linux内核源码 http://ftp.sjtu.edu.cn/sites/ftp.kernel.org/pub/linux/kernel/v4.x/ 使用的是 linux-4.13.16
glibc源码 http://ftp.gnu.org/gnu/glibc/使用的是 glibc-2.29
glibc 对系统调用的封装
为了方便,大部分用户会选择,调用的是 glibc 里面的 open 函数 ,定义: int open(const char *pathname, int flags, mode_t mode)
glibc 的 syscal.list(sysdeps\unix) 列出 glibc 函数对应的系统调用
# File name Caller Syscall name Args Strong name Weak names
open - open Ci:siv __libc_open __open open
glibc 的脚本 make_syscall.sh(sysdeps\unix) 根据 syscal.list 生成对应的宏定义(函数映射到系统调用),例如 #define SYSCALL_NAME open。
glibc 的 syscal-template.S (sysdeps\unix)使用这些宏, 定义了系统调用的调用方式(也是通过宏)
T_PSEUDO (SYSCALL_SYMBOL, SYSCALL_NAME, SYSCALL_NARGS)
ret
T_PSEUDO_END (SYSCALL_SYMBOL)
#define T_PSEUDO(SYMBOL, NAME, N) PSEUDO (SYMBOL, NAME, N)
PSEUDO 也是一个宏,它的定义如下
#define PSEUDO(name, syscall_name, args) \
.text; \
ENTRY (name) \
DO_CALL (syscall_name, args); \
cmpl $-4095, %eax; \
jae SYSCALL_ERROR_LABEL
sysdeps\unix\sysv\linux\i386\sysdep.h
对于任何一个系统调用,会调用 DO_CALL。这也是一个宏,这个宏 32 位和 64 位的定义是不一样的。
32 位系统调用过程
32的DO_CALL
glibc源码中i386 目录下的 sysdep.h 文件
sysdeps\unix\sysv\linux\i386\sysdep.h
/* Linux takes system call arguments in registers:
syscall number %eax call-clobbered
arg 1 %ebx call-saved
arg 2 %ecx call-clobbered
arg 3 %edx call-clobbered
arg 4 %esi call-saved
arg 5 %edi call-saved
arg 6 %ebp call-saved
......
*/
#define DO_CALL(syscall_name, args) \
PUSHARGS_##args \
DOARGS_##args \
movl $SYS_ify (syscall_name), %eax; \
ENTER_KERNEL \
POPARGS_##args
将请求参数放在寄存器里面,根据系统调用的名称,得到系统调用号,放在寄存器 eax 里面,然后执行 ENTER_KERNEL。
ENTER_KERNEL 是什么呢
glibc源码
int 就是 interrupt,也就是“中断”的意思。int $0x80 就是触发一个软中断,通过它就可以陷入(trap)内核。
内核启动时的 trap_init()
在内核启动的时候,有一个 trap_init()
Linux内核源码
/arch/x86/kernel/traps.c
其中 set_system_intr_gate(IA32_SYSCALL_VECTOR, entry_INT80_32);
这是一个软中断的陷入门。当接收到一个系统调用的时候,entry_INT80_32 就被调用了
/arch/x86/entry/entry_32.S
ENTRY(entry_INT80_32)
ASM_CLAC
pushl %eax /* pt_regs->orig_ax */
SAVE_ALL pt_regs_ax=$-ENOSYS /* save rest */
movl %esp, %eax
call do_syscall_32_irqs_on
.Lsyscall_32_done:
......
.Lirq_return:
INTERRUPT_RETURN
通过 push 和 SAVE_ALL 将当前用户态的寄存器,保存在 pt_regs 结构里面
进入内核之前,保存所有的寄存器,然后调用 do_syscall_32_irqs_on
/arch/x86/entry/common.c
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);
}
将系统调用号从 eax 里面取出来,然后根据系统调用号,在系统调用表中找到相应的函数进行调用,并将寄存器中保存的参数取出来,作为函数参数。
ia32_sys_call_table
当系统调用结束之后,在 entry_INT80_32 之后,紧接着调用的是 INTERRUPT_RETURN
/arch/x86/include/asm/irqflags.h
iret 指令将原来用户态保存的现场恢复回来,包含代码段、指令指针寄存器等。这时候用户态进程恢复执行。
32 位的系统调用流程图
64 位系统调用过程
64的DO_CALL
glibc源码中x86_64 下的 sysdep.h 文件
\sysdeps\unix\sysv\linux\x86_64\sysdep.h
/* The Linux/x86-64 kernel expects the system call parameters in
registers according to the following table:
syscall number rax
arg 1 rdi
arg 2 rsi
arg 3 rdx
arg 4 r10
arg 5 r8
arg 6 r9
......
*/
#define DO_CALL(syscall_name, args) \
lea SYS_ify (syscall_name), %rax; \
syscall
和32位一样,还是将系统调用名称转换为系统调用号,放到寄存器 rax。这里是真正进行调用,不是用中断了,而是改用 syscall 指令了。
syscall 指令还使用了一种特殊的寄存器,我们叫特殊模块寄存器(Model Specific Registers,简称 MSR)。这种寄存器是 CPU 为了完成某些特殊控制功能为目的的寄存器,其中就有系统调用。
内核启动时的 trap_init->cpu_init->syscall_init
在系统初始化的时候,trap_init 除了初始化上面的中断模式,这里面还会调用 cpu_init->syscall_init。这里面有这样的代码
Linux内核源码
/arch/x86/kernel/traps.c
/arch/x86/kernel/cpu/common.c
wrmsrl(MSR_LSTAR, (unsigned long)entry_SYSCALL_64);
rdmsr 和 wrmsr 是用来读写特殊模块寄存器的。
MSR_LSTAR 就是这样一个特殊的寄存器,当 syscall 指令调用的时候,会从这个寄存器里面拿出函数地址来调用,也就是调用 entry_SYSCALL_64。
/arch/x86/entry/entry_64.S
ENTRY(entry_SYSCALL_64)
/* Construct struct pt_regs on stack */
pushq $__USER_DS /* pt_regs->ss */
pushq PER_CPU_VAR(rsp_scratch) /* pt_regs->sp */
pushq %r11 /* pt_regs->flags */
pushq $__USER_CS /* pt_regs->cs */
pushq %rcx /* pt_regs->ip */
pushq %rax /* pt_regs->orig_ax */
pushq %rdi /* pt_regs->di */
pushq %rsi /* pt_regs->si */
pushq %rdx /* pt_regs->dx */
pushq %rcx /* pt_regs->cx */
pushq $-ENOSYS /* pt_regs->ax */
pushq %r8 /* pt_regs->r8 */
pushq %r9 /* pt_regs->r9 */
pushq %r10 /* pt_regs->r10 */
pushq %r11 /* pt_regs->r11 */
sub $(6*8), %rsp /* pt_regs->bp, bx, r12-15 not saved */
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:
/* IRQs are off. */
SAVE_EXTRA_REGS
movq %rsp, %rdi
call do_syscall_64 /* returns with IRQs disabled */
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:
/* rcx and r11 are already restored (see code above) */
RESTORE_C_REGS_EXCEPT_RCX_R11
movq RSP(%rsp), %rsp
USERGS_SYSRET64
会调用do_syscall_64
/arch/x86/entry/common.c
__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 里面,从 rax 里面拿出系统调用号,然后根据系统调用号,在系统调用表 sys_call_table 中找到相应的函数进行调用,并将寄存器中保存的参数取出来,作为函数参数。
64 位的系统调用返回的时候,执行的是 USERGS_SYSRET64
/arch/x86/include/asm/irqflags.h
这里,返回用户态的指令变成了 sysretq
64 位的系统调用流程图
系统调用表
系统调用的方式,都是最终到了系统调用表
系统调用表 sys_call_table 是怎么形成的呢
32 位的系统调用表定义在 arch/x86/entry/syscalls/syscall_32.tbl 文件里
64 位的系统调用定义在另一个文件 arch/x86/entry/syscalls/syscall_64.tbl 里
第一列的数字是系统调用号。可以看出,32 位和 64 位的系统调用号是不一样的。第三列是系统调用的名字,第四列是系统调用在内核的实现函数。它们都是以 sys_ 开头。
系统调用在内核中的实现函数要有一个声明。声明在 include/linux/syscalls.h 文件中
真正的实现这个系统调用,一般在一个.c 文件里面,例如 sys_open 的实现在 fs/open.c
SYSCALL_DEFINE3 是一个宏系统调用最多六个参数,根据参数的数目选择宏
声明和实现都好了,接下来,在编译的过程中,需要根据 syscall_32.tbl 和 syscall_64.tbl 生成自己的 unistd_32.h 和 unistd_64.h。生成方式在 arch/x86/entry/syscalls/Makefile 中。
在文件 arch/x86/entry/syscall_32.c,定义了这样一个表,里面 include 了这个头文件syscalls_32.h,从而所有的 sys_ 系统调用都在这个表里面了。
同理,在文件 arch/x86/entry/syscall_64.c,定义了这样一个表,里面 include 了这个头文件syscalls_64.h,这样所有的 sys_ 系统调用就都在这个表里面了。
64 位的系统调用的完整过程
参考资料:
趣谈Linux操作系统(极客时间)链接:
http://gk.link/a/10iXZ
欢迎大家来一起交流学习