Go-main内核态细节

文摘   2024-11-21 12:46   美国  

点击上方蓝字 江湖评谈设为关注/星标




前言

上一篇:  Go-main如何被Linux内核加载执行的  分析了Go内核态和用户态的过程。但内核态似乎不太严谨,本篇继续看下这里面关键点。

内核态

内核态实际上执行了两次Go的elf可执行文件的入口(Entrypoint Address,简称:EA),第一次执行的时候因为是内核态加载的用户态EA地址,导致了出错,这点可以用以下代码确认:

我们通过Qemu打印的指令集定位到调用EA的地址如下:

IN: 0xffffffff820016c3:  OBJD-T: f6442420047502

注意反汇编参数(地址),则看到的是在函数common_interrupt_return

(lldb) di -s 0xffffffff820016c3 -c5vmlinux`common_interrupt_return:    0xffffffff820016c3 <+243>: test   byte ptr [rsp + 0x20], 0x4    0xffffffff820016c8 <+248>: jne    0xffffffff820016cc        ; <+252>    0xffffffff820016ca <+250>: iretq      0xffffffff820016cc <+252>: push   rdi    0xffffffff820016cd <+253>: swapgs 

但如果是函数,则如下:

lldb) di -n common_interrupt_return -c5vmlinux`swapgs_restore_regs_and_return_to_usermode:    0xffffffff820015d0 <+0>:  jmp    0xffffffff820015ea        ; <+26>    0xffffffff820015d2 <+2>:  mov    ecx, 0x48    0xffffffff820015d7 <+7>:  mov    rdx, qword ptr gs:[rip + 0x7e01a431] ; x86_spec_ctrl_current    0xffffffff820015df <+15>: and    edx, -0x2    0xffffffff820015e2 <+18>: mov    eax, edx

其实common_interrupt_return函数包含在了swapgs_restore_regs_and_return_to_usermode函数里面,两者皆可视作当前指令所在函数,这里认为是长名函数,便于理解。那么这里可以看做用户态的EA是被swapgs_restore_regs_and_return_to_usermode函数所调用(便于后面理解)。

EA地址(0x469600)被内核第一次加载:

(lldb) cProcess 1 resumingProcess 1 stopped* thread #1, stop reason = breakpoint 1.1    frame #0: 0x0000000000469600error: memory read failed for 0x469600

lldb提示的是当前内存读取错误,也即是说EA入口在内核态被读取的时候出错了,因为当前指令处于内核态,那么这个错误需要内核态来处理。

单步继续,看到当内核态读取EA内存错误之后,它会跳到内核vmlinux`asm_exc_page_fault(注意这里的页异常是缓存的指令集,不是标准的页异常处理函数,后面会提到)异常函数:

(lldb) siThis version of LLDB has no plugin for the language "assembler". Inspection of frame variables will be limited.Process 1 stopped* thread #1, stop reason = instruction step into    frame #0: 0xffffffff82001280 vmlinux`asm_exc_page_fault at idtentry.h:623   620   /* Raw exception entries which need extra work */   621   DECLARE_IDTENTRY_RAW(X86_TRAP_UD,    exc_invalid_op);   622   DECLARE_IDTENTRY_RAW(X86_TRAP_BP,    exc_int3);-> 623   DECLARE_IDTENTRY_RAW_ERRORCODE(X86_TRAP_PF,  exc_page_fault);   624      625   #if defined(CONFIG_IA32_EMULATION)   626   DECLARE_IDTENTRY_RAW(IA32_SYSCALL_VECTOR,  int80_emulation);(lldb) di -s $rip -c5vmlinux`asm_exc_page_fault:->  0xffffffff82001280 <+0>:  endbr64     0xffffffff82001284 <+4>:  nop    dword ptr [rax]    0xffffffff82001287 <+7>:  cld        0xffffffff82001288 <+8>:  call   0xffffffff82001920        ; error_entry    0xffffffff8200128d <+13>: mov    rsp, rax

asm_exc_page_fault里面会把这个错误传送到Linuxkernel内核代码开始错误入口处理函数vmlinux`error_entry:

SYM_CODE_START(error_entry)        ANNOTATE_NOENDBR        UNWIND_HINT_FUNC
PUSH_AND_CLEAR_REGS save_ret=1 ENCODE_FRAME_POINTER 8
testb $3, CS+8(%rsp) jz .Lerror_kernelspace
/* * We entered from user mode or we're pretending to have entered * from user mode due to an IRET fault. */ swapgs FENCE_SWAPGS_USER_ENTRY /* We have user CR3. Change to kernel CR3. */ SWITCH_TO_KERNEL_CR3 scratch_reg=%rax IBRS_ENTER UNTRAIN_RET_FROM_CALL
leaq 8(%rsp), %rdi /* arg0 = pt_regs pointer */ /* Put us onto the real thread stack. */        jmp     sync_regs

内核码开始错误入口处理是个标准函数error_entry,它主要做了两件事情,其一:把GS寄存器从内核态转到用户态,其二:把当前内核态栈里面的数据设置为用户态的数据,比如RIP设置为EA,为用户态运行做准备。

做完这些之后,再次返回到asm_exc_page_fault

(lldb) di -s $rip -c5vmlinux`asm_exc_page_fault:->  0xffffffff8200128d <+13>: mov    rsp, rax    0xffffffff82001290 <+16>: mov    rdi, rsp    0xffffffff82001293 <+19>: mov    rsi, qword ptr [rsp + 0x78]    0xffffffff82001298 <+24>: mov    qword ptr [rsp + 0x78], -0x1    0xffffffff820012a1 <+33>: call   0xffffffff81f4c600        ; exc_page_fault at fault.c:1494:1

此时缓存指令集的asm_exc_page_fault调用真正的asm_exc_page_fault函数进行标准化的内核页异常处理,注意这里为啥有两个asm_exc_page_fault(前面略微提到过),前者是内核缓存指令集用以处理各种缓存指令,指导内核态和用户态切换。而后者则是内核自带的标准函数处理异常。

DEFINE_IDTENTRY_RAW_ERRORCODE(exc_page_fault){        irqentry_state_t state;        unsigned long address;        //中间省略,便于观看        irqentry_exit(regs, state);}

irqentry_exit

noinstr void irqentry_exit(struct pt_regs *regs, irqentry_state_t state){        lockdep_assert_irqs_disabled();        //中间省略,便于观看         if (user_mode(regs)) {                irqentry_exit_to_user_mode(regs);}

irqentry_exit_to_user_mode

noinstr void irqentry_exit_to_user_mode(struct pt_regs *regs){        instrumentation_begin();        exit_to_user_mode_prepare(regs);        instrumentation_end();        exit_to_user_mode();}

执行完成之后,又返回到了缓存的asm_exc_page_fault:

(lldb) di -s $rip -c5vmlinux`asm_exc_page_fault:->  0xffffffff820012a6 <+38>: jmp    0xffffffff82001a60        ; error_return    0xffffffff820012ab:       nop    dword ptr [rax + rax]vmlinux`asm_int80_emulation:    0xffffffff820012b0 <+0>:  endbr64     0xffffffff820012b4 <+4>:  nop    dword ptr [rax]    0xffffffff820012b7 <+7>:  cld

注意这里缓存的非标准asm_exc_page_fault每次指令都不同,此时它再次调用了Linuxkernel内核标准错误处理函数vmlinux`error_return,注意这个error_return跟上面的不同之处在于,前面的error_return函数用于处理内核态到用户态的数据切换,而这里则是直接切到用户态,准备运行用户态代码。这两个error_return的定义也不同,前者定义是内核码开始错误入口处理函数,这里定义是内核代码开始本地错误入口处理函数(注意多了本地,意思是用户态处理)。

SYM_CODE_START_LOCAL(error_return)        UNWIND_HINT_REGS        DEBUG_ENTRY_ASSERT_IRQS_OFF        testb   $3, CS(%rsp)         jz      restore_regs_and_return_to_kernel        jmp     swapgs_restore_regs_and_return_to_usermodeSYM_CODE_END(error_return)

swapgs_restore_regs_and_return_to_usermode函数,这个时候就会第二次执行EA。因为环境已经切到了用户态,所以这次直接跳到用户态的EA上进行用户态代码执行,不会再次报错误,也不需要内核态进行异常错误处理。关于这点,上一篇已经说过了,后面的处理跟上一篇是一模一样的。这就是整个的Go内核态运行的过程。

但这里依然还有疑问,第二次因为切换了内核态状态到用户态状态所以不会报错误,第一次内核态调用EA的时候就会报异常,两者代码都是通过swapgs_restore_regs_and_return_to_usermode函数调用的,问题出在哪儿呢?这个问题留待后面再说。

结尾

比之Go的用户态,总体来说,Go内核态的情况还是稍微复杂些。

往期精彩回顾

Go-main如何被Linux内核加载执行的


江湖评谈
记录,分享,自由。
 最新文章