天天看點

一個pagefault引發的“血案”

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,好開心!!