用户态 -> 内核态:调用系统调用,进入内核,切换优先级、内核栈,执行内核代码段指令,读入通过寄存器传入的参数,读写内核数据段数据。
内核态 -> 用户态:退出系统调用,切换用户栈、优先级、用户态代码段和数据段。
下面以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