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