unhandled write page fault at 0x7ffb0550 pc=0x1036820
unhandled write page fault at 0x7ffb0350 pc=0x1036820
unhandled write page fault at 0x7ffb0150 pc=0x1036820
……
同一個pc位置持續的pagefault,而且位址不斷減小。反彙編看一下這個pc位置:
0000000001036808 <vcpu_entry>:
1036808: 910003f3 mov x19, sp
103680c: d53bd054 mrs x20, tpidr_el0
1036810: aa0003f5 mov x21, x0
1036814: 927cee67 and x7, x19, #0xfffffffffffffff0
1036818: d10800ff sub sp, x7, #0x200
103681c: ad0007e0 stp q0, q1, [sp] pagefault位址
1036820: ad010fe2 stp q2, q3, [sp,#32]
1036824: ad0217e4 stp q4, q5, [sp,#64]
1036828: ad031fe6 stp q6, q7, [sp,#96]
103682c: ad0427e8 stp q8, q9, [sp,#128]
熟悉fiasco/L4的人應該很容易看出來,vcpu_entry就是虛拟機uvmm的入口。
pagefault發生位址位于vcpu_entry在将寄存器壓棧處,棧指針sp缺頁異常。按理說sp指針就是uvmm的堆棧指針,在啟vcpu之前就一直在使用,不應該出現缺頁異常才對。
對比啟虛拟機reset函數的列印——Starting Cpu0 @ 0x57080000 in 64Bit mode (handler @ 1036808, stack: 8000f950, task: 421000, mpidr: 80000000 (orig: 80000000)。可以發現uvmm使用的stack位址為0x8000f950,而0x7ffb0550位址确實也是未映射。這兩個位址差距如此的大到底是不是合理的呢?這首先要從fiasco的CPU虛拟化說起(本例基于armv8架構)。
參考“ARMV8對CPU虛拟化的支援及L4_fiasco中實作”文檔:https://blog.csdn.net/gaojy19881225/article/details/88889180
好了,假設已經具備了CPU虛拟化硬體和軟體的基本知識。Vcpu初始化完成後通過resume系統調用進入核心,在從核心跳轉到Uvmm的入口,整個過程使用者态堆棧并未變動。是以本文開頭的pagefault列印并不是uvmm入口的第一現場,在pagefault之前應該還有大量的動作。
眼尖的同學可以發現0x8000f950到0x7ffb0550其實間隔了若幹個0x200,正好是異常ip前面那條指令的功勞——“sub sp, x7, #0x200” 其中x7就存着sp的指針值。也就是uvmm的這個位置在不斷的重入!!
分析下這條異常指令——“stp q0, q1, [sp]”,無非就是将q0和q1兩個浮點寄存器入棧,而棧位址pagefault了。通過前面的分析,pagefault一定不是問題的第一現場(從0x8000f950到0x7ffb0550其實間隔了若幹個0x200),而且在0x7ffb0550之前是沒有pagefault但是有異常的(有異常才會循環不斷的陷入fiasco核心)。搞虛拟化的同學應該知道浮點部件是虛拟化的一個重要部分,hypervisor可以控制guest os通路浮點部件是否陷出模拟。于是需要惡補一下fiasco浮點部件的虛拟化知識“FPU虛拟化”。
參考“L4_fiasco中FPU虛拟化實作介紹”文檔:https://blog.csdn.net/gaojy19881225/article/details/88889821
好了,假設我們又具備了FPU虛拟化的相關知識。我們就在進入uvmm之前的核心中(異常路徑)添加列印,追捕pagefault異常前面的異常,按照下面的路徑倒推:
fast_return_to_user-->send_exception-->slowtrap_entry-->arm_esr_entry
arm_esr_entry函數就是fiasco異常路徑總入口了。正常情況下,從vcpu建立到reset(resume到guest),guestos第一條指令缺頁異常,fiasco核心捕捉異常,再到uvmm處理缺頁異常(做映射)。這個階段uvmm中用的都是一個堆棧,sp不會有變化(可以和正常流程做對比),而我們的問題則正是因為sp在不斷的變化,一進入uvmm就又陷入fiasco核心了。
列印出異常ec号為0(未定義指令異常),這就有點奇怪了,異常ip明明是一條浮點寄存器store指令,怎麼會未定義指令異常呢?
既然跟浮點寄存器相關,那我們就查查FPU相關寄存器設定,做做實驗!還記得前文中,我們說了v->guest_regs.cpacr沒有初始化,在調用arm_ext_vcpu_switch_to_guest的時候會利用這個沒有初始化的變量給寄存器CPACR_EL1指派,這樣就得到一個0值。0對應的是guestos(準确的說應該是EL0和EL1)中通路FPU需要陷出到EL1。這到沒有多大的問題,因為你陷不陷出我們核心都能正常處理。不過沒有初始化始終是不好的。研究了下linux中KVM代碼,這個寄存器預設初始化是3UL<<20,也就是不陷出。
那麼這麼設定有沒有問題呢?我知道作者的本意其實是想把這個控制下放到guestos(EL1的寄存器guestos的核心是有權限通路的)。我覺得作者肯定這麼想的:在arm_ext_vcpu_switch_to_host中儲存guestos配置,在arm_ext_vcpu_switch_to_guest的時候利用guest的配置恢複。可是他忽略了一個流程:從建立vcpu到resume啟動vcpu,先執行的是“arm_ext_vcpu_switch_to_guest”而不是“arm_ext_vcpu_switch_to_host”,這樣就把0值設定到CPACR_EL1寄存器了。
在異常場景下列印寄存器,發現這兩個寄存器的設定是這樣的:CPACR_EL1:0x0,CPTR_EL2:0x33ff;也就是EL2設定為不陷出,EL1設定為陷出。但是我們在進入uvmm中會調用arm_ext_vcpu_switch_to_host将CPACR_EL1設定為不陷出,并且在contex切換時會儲存全部虛拟化寄存器,這點是沒有問題的,但是switch_fpu函數會對這兩個寄存器做修改(擁有一套較完善的FPU虛拟化機制),也就是在uvmm中浮點陷出也是正常的。
關鍵來了。正是由于之前利用未初始化的0值設定了CPACR_EL1,是以在switch_fpu函數恢複寄存器時造成了“CPACR_EL1:0x0,CPTR_EL2:0x33ff”這樣的配置。這樣的配置有沒有問題呢?其實,在guestos中是沒有問題的——EL0/EL1陷出到EL1,EL2不陷出。但是出錯點在uvmm,這是關鍵,因為uvmm運作在EL0,沒有guestos,是以這時上下文中沒有人運作在EL1,這樣當uvmm中通路浮點時,CPU不知道往哪裡陷!!——未定義指令異常!
循環的“未定義指令異常”,會不斷的減小sp指針,直至缺頁——pagefault!!
到這裡,問題就解釋清楚了,解決方案也明了了,就一句代碼:
void Thread::arch_init_vcpu_state(Vcpu_state *vcpu_state, bool ext) { …… v->guest_regs.cpacr = 3UL << 20; …… } |
又解決開源一個bug,好開心!!