本文以最常见的系统调用 open, 打开一个文件为线索,展示 32/64 位系统调用的实现方式。
在 Linux 内核|初始化 中提到过,用户态代码无法执行更高权限的指令,要想访问核心资源,需要通过系统调用来访问。整个过程如图:
1.流程概述 再看具体的代码之前,先看一下大概的流程。这里引用这篇文章 的描述, 言简意赅。
应用程序 代码调用系统调用( xyz ),该函数是一个包装系统调用的 库函数 ;
库函数 ( xyz )负责准备向内核传递的参数,并触发 软中断 以切换到内核;(64 位是调用 寄存器 MSR_LSTAR 中的函数 entry_SYSCALL_64
进入内核的。)
CPU 被 软中断 打断后,执行 中断处理函数 ,即 系统调用处理函数 ( system_call );
系统调用处理函数 调用 系统调用服务例程 ( sys_xyz ),真正开始处理该系统调用;
应用程序:application program 库函数:libc 系统调用处理函数:system call handler 系统调用服务例程:system call service routine
2.封装系统调用的库函数glibc 2.1 什么是 glibc glibc 是 GNU 旗下的 C 标准库,后来逐渐成为了 Linux 的标准 C 库。glibc 提供很多 API,比如 open、read、write、malloc、printf 等。
2.2 fopen 到 DO_CALL 1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include <stdio.h> int main (int argc, char **argv) { FILE *fp; char buff[255 ]; fp = fopen("test.txt" , "r" ); fgets(buff, 255 , fp); printf ("%s\n" , buff); fclose(fp); return 0 ; }
上面是一个打开文件的程序,其中调用 glibc 库中的 fopen 打开文件,实际上最后调用的是 open 函数,glibc 库中的 open 函数的定义如下。
1 int open (const char *pathname, int flags, mode_t mode)
glibc 库中有个文件 syscalls.list,里面列着所有 glibc 的函数对应的系统调用。下面是 open 函数所对应的系统调用。
1 2 open - open Ci:siv __libc_open __open open
根据上述配置文件,glibc会调用脚本 make_syscall.sh 将其封装为宏,如 #define SYSCALL_NAME open
的形式。
这些宏会通过 T_PSEUDO
来调用(位于syscall-template.S)。
1 2 3 4 5 T_PSEUDO (SYSCALL_SYMBOL, SYSCALL_NAME, SYSCALL_NARGS) retT_PSEUDO_END (SYSCALL_SYMBOL) #define T_PSEUDO(SYMBOL, NAME, N) PSEUDO (SYMBOL, NAME, N)
这里的 PSEUDO 也是一个宏。从下面代码中可以看出,对于任何一个系统调用,最终会调用 DO_CALL(syscall_name, args)
。
1 2 3 4 5 6 #define PSEUDO(name, syscall_name, args) \ .text; \ ENTRY (name) \ DO_CALL (syscall_name, args); \ cmpl $-4095, %eax; \ jae SYSCALL_ERROR_LABEL
DO_CALL
这个宏 32 位和 64 位的定义是不一样的,下面我们对于 32 位和 64 位分开来讨论。
3.32 位系统调用过程 3.1 DO_CALL 到 ENTER_KERNEL 在32位系统中,DO_CALL()
位于i386 目录下的 sysdep.h 文件中,定义如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #define DO_CALL(syscall_name, args) \ PUSHARGS_##args \ DOARGS_##args \ movl $SYS_ify (syscall_name), %eax; \ ENTER_KERNEL \ POPARGS_##args
代码主要逻辑是:将请求参数放在寄存器里面,根据系统调用的名称,得到系统调用号,放在寄存器 eax 里面,然后执行 ENTER_KERNEL
。
1 #define ENTER_KERNEL int $0x80
这里面的 ENTER_KERNEL
定义如下,int 就是 interrupt,表示中断,int $0x80
表示触发一个软中断。ENTER_KERNEL
实际调用的是80软中断,以此陷入(trap)内核。
3.2 软中断 entry_INT80_32 80 软中断是一个软中断的陷入门,是在内核启动 的时候初始化的。trap_init() 通过 idt_setup_traps() 设置了很多的中断门(Interrupt Gate),用于处理各种中断,如系统调用的中断门:
1 set_system_intr_gate(IA32_SYSCALL_VECTOR, entry_INT80_32);
当接收到一个系统调用的时候,entry_INT80_32
就被调用了,从而从用户态陷入(trap)内核态。
entry_INT80_32
定义如下,进入内核之前,要保存所有的寄存器,即通过 push 和 SAVE_ALL 将当前用户态的寄存器,保存在 pt_regs 结构里面。
1 2 3 4 5 6 7 8 9 10 ENTRY(entry_INT80_32) ASM_CLAC pushl %eax SAVE_ALL pt_regs_ax=$-ENOSYS movl %esp, %eax call do_syscall_32_irqs_on .Lsyscall_32_done: ...... .Lirq_return: INTERRUPT_RETURN
保存完用户态寄存器数据后,会调用 do_syscall_32_irqs_on
。
3.3 do_syscall_32_irqs_on do_syscall_32_irqs_on
定义如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 static __always_inline void do_syscall_32_irqs_on (struct pt_regs *regs) { struct thread_info *ti = current_thread_info(); unsigned int nr = (unsigned int )regs->orig_ax; ...... if (likely(nr < IA32_NR_syscalls)) { regs->ax = ia32_sys_call_table[nr]( (unsigned int )regs->bx, (unsigned int )regs->cx, (unsigned int )regs->dx, (unsigned int )regs->si, (unsigned int )regs->di, (unsigned int )regs->bp); } syscall_return_slowpath(regs); }
其中,这个 ia32_sys_call_table
定义如下,实际上就是 sys_call_table
系统调用表,系统调用放在这个表里面。至于这个表是如何形成的,在本文后面会描述。
1 #define ia32_sys_call_table sys_call_table
3.4 返回用户态 INTERRUPT_RETURN 在 entry_INT80_32
定义中可以看到,当系统调用结束之后,紧接着调用的是 INTERRUPT_RETURN,也就是 iret 指令,定义如下:
1 #define INTERRUPT_RETURN iret
iret 指令将原来用户态保存的现场恢复回来,包含代码段、指令指针寄存器等。这时候用户态进程恢复执行。
3.5 一张图总结
4.64 位系统调用过程 4.1 DO_CALL 到 syscall 在 64 位系统中,DO_CALL
位于x86_64 目录下的 sysdep.h 文件中,定义如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #define DO_CALL(syscall_name, args) \ lea SYS_ify (syscall_name), %rax; \ syscall
和 32 位系统类似,还是将系统调用名称转换为系统调用号,放到寄存器 rax。这里是真正进行调用,不是用中断了,而是改用 syscall
指令了。(通过注释可以看到,传递参数的寄存器也变了)
4.2 syscall 到 entry_SYSCALL_64 syscall 指令使用了一种特殊的寄存器,叫特殊模块寄存器(Model Specific Registers,简称 MSR)。这种寄存器是 CPU 为了完成某些特殊控制功能为目的的寄存器,其中就有系统调用。
在系统初始化的时候,trap_init()
除了初始化上面的中断模式,还会调用 cpu_init->syscall_init()
。这里面有如下代码:
1 wrmsrl(MSR_LSTAR, (unsigned long )entry_SYSCALL_64);
4.3 entry_SYSCALL_64 在 arch/x86/entry/entry_64.S 中定义了 entry_SYSCALL_64
,定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 ENTRY(entry_SYSCALL_64) pushq $__USER_DS pushq PER_CPU_VAR (rsp_scratch) pushq %r11 pushq $__USER_CS pushq %rcx pushq %rax pushq %rdi pushq %rsi pushq %rdx pushq %rcx pushq $-ENOSYS pushq %r8 pushq %r9 pushq %r10 pushq %r11 sub $(6 *8 ) , %rsp movq PER_CPU_VAR (current_task) , %r11 testl $_TIF_WORK_SYSCALL_ENTRY|_TIF_ALLWORK_MASK, TASK_TI_flags (%r11) jnz entry_SYSCALL64_slow_path ...... entry_SYSCALL64_slow_path: SAVE_EXTRA_REGS movq %rsp, %rdi call do_syscall_64 return_from_SYSCALL_64: RESTORE_EXTRA_REGS TRACE_IRQS_IRETQ movq RCX (%rsp) , %rcx movq RIP (%rsp) , %r11 movq R11 (%rsp) , %r11 ...... syscall_return_via_sysret: RESTORE_C_REGS_EXCEPT_RCX_R11 movq RSP (%rsp) , %rsp USERGS_SYSRET64
这里先保存了很多寄存器到 pt_regs 结构里面,例如用户态的代码段、数据段、保存参数的寄存器。
最后会调用 entry_SYSCALL64_slow_path->do_syscall_64
,定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 __visible void do_syscall_64 (struct pt_regs *regs) { struct thread_info *ti = current_thread_info(); unsigned long nr = regs->orig_ax; ...... if (likely((nr & __SYSCALL_MASK) < NR_syscalls)) { regs->ax = sys_call_table[nr & __SYSCALL_MASK]( regs->di, regs->si, regs->dx, regs->r10, regs->r8, regs->r9); } syscall_return_slowpath(regs); }
在 do_syscall_64
里面,主要流程是:
4.4 返回用户态 USERGS_SYSRET64 在 entry_SYSCALL_64
的定义中可以看到,64 位的系统调用返回的时候,执行的是 USERGS_SYSRET64
。定义如下,返回用户态的指令变成了 sysretq。
1 2 3 #define USERGS_SYSRET64 \ swapgs; \ sysretq;
4.5 一张图总结
5.系统调用表 前面我们重点关注了系统调用的流程,32/64 位系统最终都需要根据系统调用号,在系统调用表 sys_call_table
中查找相应的函数。下面来看看系统调用表。
5.1 sys_call_table 的内容 32 位的系统调用表定义在面 arch/x86/entry/syscalls/syscall_32.tbl 文件里。例如 open 的定义:
1 5 i386 open sys_open compat_sys_open
64 位的系统调用定义在另一个文件 arch/x86/entry/syscalls/syscall_64.tbl 里。例如 open 的定义:
5.2 声明 syscalls.h 系统调用在内核中的实现函数要有一个声明。声明往往在 include/linux/syscalls.h 文件中。例如 sys_open
的声明:
1 2 asmlinkage long sys_open (const char __user *filename, int flags, umode_t mode) ;
5.3 实现 open.c 真正的实现这个系统调用,一般在一个.c 文件里面,例如 sys_open 的实现在 fs/open.c 里面,例如 sys_open
的实现:
1 2 3 4 5 6 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); }
SYSCALL_DEFINE3
是一个有三个参数的宏系统调用。定义如下:SYSCALL_DEFINE
最多六个参数,根据参数的数目选择不同的宏。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #define SYSCALL_DEFINE1(name, ...) SYSCALL_DEFINEx(1, _##name, __VA_ARGS__) #define SYSCALL_DEFINE2(name, ...) SYSCALL_DEFINEx(2, _##name, __VA_ARGS__) #define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__) #define SYSCALL_DEFINE4(name, ...) SYSCALL_DEFINEx(4, _##name, __VA_ARGS__) #define SYSCALL_DEFINE5(name, ...) SYSCALL_DEFINEx(5, _##name, __VA_ARGS__) #define SYSCALL_DEFINE6(name, ...) SYSCALL_DEFINEx(6, _##name, __VA_ARGS__) #define SYSCALL_DEFINEx(x, sname, ...) \ SYSCALL_METADATA(sname, x, __VA_ARGS__) \ __SYSCALL_DEFINEx(x, sname, __VA_ARGS__) #define __PROTECT(...) asmlinkage_protect(__VA_ARGS__) #define __SYSCALL_DEFINEx(x, name, ...) \ asmlinkage long sys##name(__MAP(x,__SC_DECL,__VA_ARGS__)) \ __attribute__((alias(__stringify(SyS##name)))); \ static inline long SYSC##name(__MAP(x,__SC_DECL,__VA_ARGS__)); \ asmlinkage long SyS##name(__MAP(x,__SC_LONG,__VA_ARGS__)); \ asmlinkage long SyS##name(__MAP(x,__SC_LONG,__VA_ARGS__)) \ { \ long ret = SYSC##name(__MAP(x,__SC_CAST,__VA_ARGS__)); \ __MAP(x,__SC_TEST,__VA_ARGS__); \ __PROTECT(x, ret,__MAP(x,__SC_ARGS,__VA_ARGS__)); \ return ret; \ } \ static inline long SYSC##name(__MAP(x,__SC_DECL,__VA_ARGS__)
如果我们把所有宏展开之后,实现如下,和声明的是一样的。
1 2 3 4 5 6 7 8 9 10 asmlinkage long sys_open (const char __user * filename, int flags, int mode) { long ret; if (force_o_largefile()) flags |= O_LARGEFILE; ret = do_sys_open(AT_FDCWD, filename, flags, mode); asmlinkage_protect(3 , ret, filename, flags, mode); return ret;
5.4 编译 syscalls_32/64.h 在编译的过程中,需要根据 syscall_32.tbl 和 syscall_64.tbl 生成自己的 unistd_32.h 和 unistd_64.h。生成方式在 arch/x86/entry/syscalls/Makefile 中。
这里面会使用两个脚本:
第一个脚本 arch/x86/entry/syscalls/syscallhdr.sh,会在文件中生成 #define __NR_open
。
第二个脚本 arch/x86/entry/syscalls/syscalltbl.sh,会在文件中生成 __SYSCALL(__NR_open, sys_open)
。
这样最终生成 syscalls_32.h 和 syscalls_64.h 就保存了系统调用号和系统调用实现函数之间的对应关系,如下:
1 2 3 4 5 6 __SYSCALL_COMMON(0 , sys_read, sys_read) __SYSCALL_COMMON(1 , sys_write, sys_write) __SYSCALL_COMMON(2 , sys_open, sys_open) __SYSCALL_COMMON(3 , sys_close, sys_close) __SYSCALL_COMMON(5 , sys_newfstat, sys_newfstat) ...
其中__SYSCALL_COMMON
宏定义如下,主要是将对应的数字序号和系统调用名对应。
1 2 #define __SYSCALL_COMMON(nr, sym, compat) __SYSCALL_64(nr, sym, compat) #define __SYSCALL_64(nr, sym, compat) [nr] = sym,
最终形成的表如下:
1 2 3 4 5 6 7 8 9 asmlinkage const sys_call_ptr_t sys_call_table[__NR_syscall_max +1 ] = { [0 ... __NR_syscall_max ] = &sys_ni_syscall, [0 ] = sys_read, [1 ] = sys_write, [2 ] = sys_open, ... ... ... };
5.5 使用 syscall_32/64.c 在文件 arch/x86/entry/syscall_32.c,定义了这样一个表,里面 include 了这个头文件 syscalls_32.h,从而所有的 sys_ 系统调用都在这个表里面了。
1 2 3 4 5 6 7 8 __visible const sys_call_ptr_t ia32_sys_call_table[__NR_syscall_compat_max+1 ] = { [0 ... __NR_syscall_compat_max] = &sys_ni_syscall,#include <asm/syscalls_32.h> };
同理,在文件 arch/x86/entry/syscall_64.c,定义了这样一个表,里面 include 了这个头文件syscalls_64.h,这样所有的 sys_ 系统调用就都在这个表里面了。
1 2 3 4 5 6 7 8 9 asmlinkage const sys_call_ptr_t sys_call_table[__NR_syscall_max+1 ] = { [0 ... __NR_syscall_max] = &sys_ni_syscall, #include <asm/syscalls_64.h> };
6.一张图总结 以64位系统为例
7.参考