点击上方蓝字 江湖评谈设为关注/星标
前言
上一篇: Go-main如何被Linux内核加载执行的 分析了Go内核态和用户态的过程。但内核态似乎不太严谨,本篇继续看下这里面关键点。
内核态
内核态实际上执行了两次Go的elf可执行文件的入口(Entrypoint Address,简称:EA),第一次执行的时候因为是内核态加载的用户态EA地址,导致了出错,这点可以用以下代码确认:
我们通过Qemu打印的指令集定位到调用EA的地址如下:
IN:
0xffffffff820016c3:
OBJD-T: f6442420047502
注意反汇编参数(地址),则看到的是在函数common_interrupt_return
(lldb) di -s 0xffffffff820016c3 -c5
vmlinux`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 -c5
vmlinux`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) c
Process 1 resuming
Process 1 stopped
* thread #1, stop reason = breakpoint 1.1
frame #0: 0x0000000000469600
error: memory read failed for 0x469600
lldb提示的是当前内存读取错误,也即是说EA入口在内核态被读取的时候出错了,因为当前指令处于内核态,那么这个错误需要内核态来处理。
单步继续,看到当内核态读取EA内存错误之后,它会跳到内核vmlinux`asm_exc_page_fault(注意这里的页异常是缓存的指令集,不是标准的页异常处理函数,后面会提到)页异常函数:
si
This 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);
di -s $rip -c5
:
0xffffffff82001280 <+0>: endbr64
0xffffffff82001284 <+4>: nop dword ptr [rax]
0xffffffff82001287 <+7>: cld
0xffffffff82001288 <+8>: call 0xffffffff82001920 ; error_entry
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 -c5
vmlinux`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 -c5
vmlinux`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_usermode
SYM_CODE_END(error_return)
swapgs_restore_regs_and_return_to_usermode函数,这个时候就会第二次执行EA。因为环境已经切到了用户态,所以这次直接跳到用户态的EA上进行用户态代码执行,不会再次报错误,也不需要内核态进行异常错误处理。关于这点,上一篇已经说过了,后面的处理跟上一篇是一模一样的。这就是整个的Go内核态运行的过程。
但这里依然还有疑问,第二次因为切换了内核态状态到用户态状态所以不会报错误,第一次内核态调用EA的时候就会报异常,两者代码都是通过swapgs_restore_regs_and_return_to_usermode函数调用的,问题出在哪儿呢?这个问题留待后面再说。
结尾
比之Go的用户态,总体来说,Go内核态的情况还是稍微复杂些。
往期精彩回顾