当前位置: 华文星空 > 知识

操作系统内核态和用户态切换落实到代码层面和运行层面的本质是什么?

2021-05-08知识

用户态 -> 内核态:调用系统调用,进入内核,切换优先级、内核栈,执行内核代码段指令,读入通过寄存器传入的参数,读写内核数据段数据。

内核态 -> 用户态:退出系统调用,切换用户栈、优先级、用户态代码段和数据段。

下面以x86_64中调用open函数为例描述这个过程:

#include <sys/stat.h> #include <fcntl.h> fd = open ( "file_path" , O_RDONLY );

这行代码最终会调用到内核中的sys_open函数,定义如下:

fs / open . c SYSCALL_DEFINE3 ( open , const char __user * , filename , int , flags , umode_t , mode ) { if ( force_o_largefile ()) flags |= O_LARGEFILE ; return do_sys_open ( AT_FDCWD , filename , flags , mode ); }

调用过程如下图:

其中涉及到3个关键问题:

1.internal_syscall3如何从用户态切换到内核态?

先看一下internal_syscall3在glibc中的实现:

283 # undef internal_syscall3 284 # define internal_syscall3 ( number , arg1 , arg2 , arg3 ) \ 285 ({ \ 286 unsigned long int resultvar ; \ 287 TYPEFY ( arg3 , __arg3 ) = ARGIFY ( arg3 ); \ 288 TYPEFY ( arg2 , __arg2 ) = ARGIFY ( arg2 ); \ 289 TYPEFY ( arg1 , __arg1 ) = ARGIFY ( arg1 ); \ 290 register TYPEFY ( arg3 , _a3 ) asm ( "rdx" ) = __arg3 ; \ 291 register TYPEFY ( arg2 , _a2 ) asm ( "rsi" ) = __arg2 ; \ 292 register TYPEFY ( arg1 , _a1 ) asm ( "rdi" ) = __arg1 ; \ 293 asm volatile ( \ 294 "syscall\n\t" \ 295 : "=a" ( resultvar ) \ 296 : "0" ( number ), "r" ( _a1 ), "r" ( _a2 ), "r" ( _a3 ) \ 297 : "memory" , REGISTERS_CLOBBERED_BY_SYSCALL ); \ 298 ( long int ) resultvar ; \ 299 })

286行定义存储返回结果的变量resultvar

287~292行把open传入的3个参数filename,flags,mode分别保存到rdi,rsi,rdx寄存中

293~297行是调用syscall指令真正进入内核态。其中"=a"表示系统调用的返回结果在rax寄存器中读取。296行"0"(number)表示把系统调用号存储到rax寄存器,也就是和返回结果使用相同的寄存器,"r"(_a1),"r"(_a2),"r"(_a3)表示一次通过rdi,rsi,rdx传入三个参数

298行的陈述表达式表示返回resultvar变量的值到用户进程

2. 用户态到内核态是如何传递参数的?

这涉及到x86_64调用约定 在x86_64中进行系统调用时最多只能传输6个参数,分别用rdi,rsi,rdx,r10,r8,r9进行参数传递。上面internal syscall3的定义中也可以看到sys open系统调用需要的3个参数的传递过程。

关于x86_64的调用约定请参考:

A.2.1部分。

在上图的entry_SYSCALL64函数中,内核要把所有的参数push到栈上,构建pt_regs结构体数据,然后把pt_regs的地址传入do_syscall_64函数。

87 SYM_CODE_START(entry_SYSCALL_64) 88 UNWIND_HINT_EMPTY 89 90 swapgs 91 /* tss.sp2 is scratch space. */ 92 movq %rsp, PER_CPU_VAR(cpu_tss_rw + TSS_sp2) <=== 先保存用户栈到TSS_sp2 93 SWITCH_TO_KERNEL_CR3 scratch_reg=%rsp <=== 切换到内核页表 94 movq PER_CPU_VAR(cpu_current_top_of_stack), %rsp <=== 切换到内核栈 SYM_INNER_LABEL(entry_SYSCALL_64_safe_stack, SYM_L_GLOBAL) /* Construct struct pt_regs on stack */ pushq $__USER_DS /* pt_regs->ss */ pushq PER_CPU_VAR(cpu_tss_rw + TSS_sp2) /* pt_regs->sp */ <=== 再把用户栈的地址保存到内核栈 pushq %r11 /* pt_regs->flags */ pushq $__USER_CS /* pt_regs->cs */ pushq %rcx ................................. 107 PUSH_AND_CLEAR_REGS rax=$-ENOSYS <====== push一部分寄存器的值到栈上 108 109 /* IRQs are off. */ 110 movq %rax, %rdi 111 movq %rsp, %rsi <=== 传入pt_regs数据作为第二个参数 112 call do_syscall_64 /* returns with IRQs disabled */ ................................. 217 SYM_CODE_END(entry_SYSCALL_64)

在定义系统调用时,内核会把传入的参数从pt_regs结构中再放入到对应的寄存器中。把参数push到栈上再取出,不是多此一举么?肯定不是,因为内核在执行真正的系统调用之前要做很多准备工作,需要用到那些寄存器。

3. 如何从内核态返回用户态?

执行完系统调用后,执行结果保存在regs->ax的变量中:

39 __visible noinstr void do_syscall_64(unsigned long nr, struct pt_regs *regs) 40 { 41 nr = syscall_enter_from_user_mode(regs, nr); 42 43 instrumentation_begin(); 44 if (likely(nr < NR_syscalls)) { 45 nr = array_index_nospec(nr, NR_syscalls); 46 regs->ax = sys_call_table[nr](regs); <=== 保存执行结果 54 } 55 instrumentation_end(); 56 syscall_exit_to_user_mode(regs); 57 }

我们假设要退出系统调用的时候中断是关闭的,则退出系统调用的过程为:

190 syscall_return_via_sysret: 191 /* rcx and r11 are already restored (see code above) */ 192 POP_REGS pop_rdi=0 skip_r11rcx=1 <=== 恢复用户寄存器, 其中上面保存的regs->ax的值 弹出到rax寄存器中 193 194 /* 195 * Now all regs are restored except RSP and RDI. 196 * Save old stack pointer and switch to trampoline stack. 197 */ 198 movq %rsp, %rdi 199 movq PER_CPU_VAR(cpu_tss_rw + TSS_sp0), %rsp 200 UNWIND_HINT_EMPTY 201 202 pushq RSP-RDI(%rdi) /* RSP */ <=== 保存pr_regs中的sp到栈上,sp的值就是用户 栈最终的地址 203 pushq (%rdi) /* RDI */ 204 205 /* 206 * We are on the trampoline stack. All regs except RDI are live. 207 * We can do future final exit work right here. 208 */ 209 STACKLEAK_ERASE_NOCLOBBER 210 211 SWITCH_TO_USER_CR3_STACK scratch_reg=%rdi <=== 切换到用户进程页表 212 213 popq %rdi 214 popq %rsp <=== 恢复用户进程栈 215 swapgs 216 sysretq <=== 从内核态返回用户态 217 SYM_CODE_END(entry_SYSCALL_64)

Glibc版本 2.34

Linux kernel版本 5.12.rc3