天天看点

一个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,好开心!!