使用者態 -> 內核態:呼叫系統呼叫,進入內核,切換優先級、內核棧,執行內核程式碼段指令,讀入透過寄存器傳入的參數,讀寫內核數據段數據。
內核態 -> 使用者態:結束系統呼叫,切換使用者棧、優先級、使用者態程式碼段和數據段。
下面以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