这篇blog是关于在《一个64位操作系统的设计与实现》中,第4.8节创建init进程产生的异常。会出现什么异常?如何解决?是这篇blog的重点。
助记符 | 描述 | 触发源 |
---|---|---|
#GP(general protection) | 通用保护异常 | 任何内存引用和保护检测 |
#PF(page fault) | 页错误 | 任何内存引用 |
#UD(undefined opcode) | 无效/未定义的机器码 | UD2指令或保留机器码 |
先说结论:这个问题是示例代码的问题。
#GP异常通常会打印出出现异常的RIP地址,通过objdump -D system
反汇编源代码,根据RIP地址定位到__switch_to
函数的retq指令,当__switch_to函数执行完毕后,会跳转到kernel_thread_func模块,通过打印该模块的函数地址,发现地址异常,示例代码是这样写的:
extern void kernel_thread_func(void); __asm__ ( "kernel_thread_func: \n\t" " popq %r15 \n\t" " popq %r14 \n\t" " popq %r13 \n\t" // ... );
猜想,这样定义函数时,函数地址的位置并不是我们想要的'正确地址'
,所以我通过另外一种方式定义该函数:
extern void kernel_thread_func(void); void kernel_thread_func() { __asm__ ( " popq %r15 \n\t" " popq %r14 \n\t" " popq %r13 \n\t" // ... ); }
成功解决该问题,但随之出现另外一个问题 #PF异常 。
先说结论:作者在书籍中遗漏了一些注释代码,即有一小段代码需要注释,但是在书籍中未提到,源码中该段代码被注释,这段代码是:
// ~ kernel/memory.c // PML4页表中的前10个页表项清零(第一项清零就可以了),不会立即生效,需要调用flush_tlb函数 // for(int i = 0;i < 10;i++) { // *(Phy_To_Virt(Global_CR3) + i) = 0UL; // }
还有另一小段在书籍中未提到的也需要更改:
// ~ kernel/head.S 原句: MOVQ $0xffff800000007E00, %RSP 更改后: MOVQ _stack_start(%rip), %RSP
同样,#PF异常打印的异常信息得知CR2 = 0x101000, 以及RIP信息。通过全局搜索0x101000这个地址,最初是在head.S中赋值给CR3控制寄存器,并且通过输出测试以及更改CR3值,发现的确是关于CR3控制寄存器而出现的异常;
CR3值获取,是在Get_gdt函数中,在内存管理memory.c/init_memory()函数中,通过对比源码,发现原来不用不用注释的代码在新的一节中被注释;注释掉该段代码后,该异常消失,但又出现了 #UD异常。
先说结论:为什么出现这个异常,我还没有找到具体原因,只理清大部分头绪,该异常可能会复现,但至少在init进程运行时已正常。导致该异常的原因是:栈不平衡?虚拟机问题?代码问题?
这个异常最难调试,相比较前两者而言,这个异常提供的信息少之又少;我是通过输出调试以及debug调试才找到原因所在;在调试过程中,有许多错误猜想,虽然对得到结果没什么用处,但是对理解创建init进程以及切换进程更加熟悉,直接说最后的正确步骤。
通过输出测试,定位到kernel_thread_func函数,debug反汇编代码,发现本来要执行init进程
的指令跳到了0x00000000地址,而产生异常;init进程入口地址的地址信息存储在哪?RBX寄存器(在kernel_thread函数中赋值)。然后再次debug代码,通过调试(先break __switch_to的retq指令, 即在bochs执行b 0xa9de
,0xa9de
是retq指令的物理地址,再一步步执行),发现pop rbx时,rbx是0x00000000,猜想,什么导致栈不平衡,没有正确返回?
通过在bochs调试r
指令打印寄存器信息,发现RCX存储的就是init进程的入口地址!为什么会到了RCX而不是RBX?而函数参数原本存储在RDX中,结果发现函数参数存储在RDI中?为什么会这样。奇奇怪怪的存放顺序。书中提到,应用程序在进入内核层时已将进程的执行现场(所有通用寄存器值)保存起来,在哪里保存的?以何种形式保存?按道理来讲,即使压栈,压栈的顺序跟弹出的顺序一致(逆序),暂时无法解决该问题。
解决方式就是将kernel_thread_func函数中callq %rbx
改成callq %rcx
:
" addq $0x38, %rsp \n\t" " movq %rdx, %rdi \n\t" " callq *%rcx \n\t" " movq %rax, %rdi \n\t" " callq do_exit \n\t"
此上,解决了书籍中4.8节遗留的问题,我试着运行了源代码,发现同样出现了异常。