當前位置: 華文星空 > 知識

作業系統內核態和使用者態切換落實到程式碼層面和執行層面的本質是什麽?

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