目录

Linux 系统调用过程

Glibc 对系统调用的封装

平时在编程的时候不会直接发出系统调用,而是通过 Glibc 这个库来发出系统调用。Glibc 是 Linux 下使用的开源的标准 C 库,它是 GNU 发布的 libc 库。Glibc 为程序员提供了丰富的 API,而且对系统调用的发出进行封装了。当我们调用某个库函数的时候,这个库函数将发出系统调用的过程封装进去了。

一般来说每个特定的系统调用都会对应一个 Glibc 封装的库函数。比如系统调用 sys_open 对应 Glibc 中的 open 函数。但是也有可能一个系统调用对应多个库函数,比如 Glibc 下的 malloc、calloc、free 都对应 sys_brk 的系统调用。还有可能是一个库函数对应多个系统调用,比如 printf 函数就会调用 sys_open、sys_mmap、sys_write、sys_close 等系统调用。

下面以 glibc 中的 open 函数为例进行讲解,

1
int open(const char *pathname, int flags, mode_t mode)

在 glibc 的源代码中,有个文件 syscalls.list,里面列着所有 glibc 的函数对应的系统调用,就像下面这个样子:

1
2
# File name Caller  Syscall name    Args    Strong name Weak names
open    -  open    Ci:siv  __libc_open __open open

glibc 还有一个脚本 make-syscalls.sh,可以根据上面的配置文件,对于每一个封装好的系统调用,生成一个文件。这个文件里面定义了一些宏,如下

1
2
3
4
5
6
#define SYSCALL_NAME        	syscall name
#define SYSCALL_NARGS        	number of arguments this call takes
#define SYSCALL_SYMBOL        primary symbol name
#define SYSCALL_CANCELLABLE   1 if the call is a cancelation point
#define SYSCALL_NOERRNO       1 to define a no-errno version (see below)
#define SYSCALL_ERRVAL        1 to define an error-value version (see below)

glibc 还有一个文件 syscall-template.S,使用上面这个宏,定义了这个系统调用的调用方式。

1
2
3
4
5
T_PSEUDO (SYSCALL_SYMBOL, SYSCALL_NAME, SYSCALL_NARGS)
    ret
T_PSEUDO_END (SYSCALL_SYMBOL)

#define T_PSEUDO(SYMBOL, NAME, N)    PSEUDO (SYMBOL, NAME, N)

32 位的方式

PSEUDO 也是一个宏定义,这个宏定义跟架构相关,32 位中的定义如下所示。

1
2
3
4
5
6
7
// sysdeps/unix/sysv/linux/i386/sysdep.h
#define PSEUDO(name, syscall_name, args)                      \
  .text;                                      \
  ENTRY (name)                                    \
    DO_CALL (syscall_name, args);                         \
    cmpl $-4095, %eax;                               \
    jae SYSCALL_ERROR_LABEL

这个还会调用 DO_CALL,这也是一个宏,也是跟架构相关的。

  • 在这里首先将请求参数放到寄存器中(相应寄存器也已经列出来)。
  • 之后,根据系统调用名称,得到系统调用号,放到寄存器 eax 中。可以看到 __NR##syscall_name 其实会出现在 unistd_32.h 文件中(见内核/系统调用表一节)。
  • 最后执行 ENTER_KERNEL,ENTER_KERNEL 也是一个宏定义,最终是执行 int $0x80 指令触发一个软中断。
 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
// 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


// sysdeps/unix/sysv/linux/i386/sysdep.h
/* For Linux we can use the system call table in the header file
	/usr/include/asm/unistd.h
   of the kernel.  But these symbols do not follow the SYS_* syntax
   so we have to redefine the `SYS_ify' macro here.  */
#undef SYS_ify
#define SYS_ify(syscall_name)	__NR_##syscall_name


// sysdeps/unix/sysv/linux/i386/sysdep.h
# define ENTER_KERNEL int $0x80

64 位的方式

64 位中 PSEUDO 的宏定义和 32 位中 PSEUDO 的宏定义是一样的。

1
2
3
4
5
6
7
// sysdeps/unix/sysv/linux/x86_64/sysdep.h
# define PSEUDO(name, syscall_name, args)				      \
  .text;								      \
  ENTRY (name)								      \
    DO_CALL (syscall_name, args);					      \
    cmpq $-4095, %rax;							      \
    jae SYSCALL_ERROR_LABEL

但是,DO_CALL 的宏定义是不一样,

  • 首先是将系统调用名称转换为系统调用号,存到 eax 寄存器中。
  • 与 32 位不同的是,这边使用 syscall 指令以调用的方式来进行系统,而不是使用中断的方式进行系统调用。同时,传递参数的寄存器也变了。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 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)		\
    DOARGS_##args				\
    movl $SYS_ify (syscall_name), %eax;		\
    syscall;

syscall 指令还使用了一种特殊的寄存器,叫做特殊模块寄存器(Model Specific Registers,简称 MSR),这种 寄存器就是 CPU 为了完成某种特殊控制功能为目的的寄存器,其中就有系统调用。在系统初始化的时候,会进行如下的初始化。其中 wrmsrl 和 rdmsr 都是用来读写特殊模块寄存器的;MSR_LSTAR 就是一个特殊的寄存器,这里将 entry_SYSCALL_64 这个函数的地址填到这个寄存器中,那么当 syscall 指令调用的时候,会从这个寄存器中拿出函数地址来调用(所以说 64 位下面使用的是调用的方式而不是中断的方式),也就是调用 entry_SYSCALL_64 函数。

1
2
// arch/x86/kernel/cpu/common.c(Linux Kernel 4.19)
wrmsrl(MSR_LSTAR, (unsigned long)entry_SYSCALL_64);

内核

32 位的方式

在内核启动的时候,有一个 trap_init(),其中有如下这样的代码。这是一个软中断的陷入门。当接受到一个系统调用的时候,entry_INT80_32 就被调用了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// arch/x86/kernel/traps.c (Linux Kernel 4.19)
void __init trap_init(void) {
......
	idt_setup_traps();
......
}

// arch/x86/kernel/idt.c (Linux Kernel 4.19)
void __init idt_setup_traps(void) {
	idt_setup_from_table(idt_table, def_idts, ARRAY_SIZE(def_idts), true);
}

// arch/x86/kernel/idt.c (Linux Kernel 4.19)
static const __initconst struct idt_data def_idts[] = {
	SYSG(IA32_SYSCALL_VECTOR,	entry_INT80_32)
}

entry_INT80_32 代码如下所示,通过 pushl 和 SAVE_ALL 将当前用户态的寄存器,保存在 pt_regs 结构里面。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# arch/x86/entry/entry_32.S (Linux Kernel 4.19)
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_int80_syscall_32
.Lsyscall_32_done:
......
.Lirq_return:
  INTERRUPT_RETURN
......
ENDPROC(entry_INT80_32)

之后调用 do_int80_syscall_32,实现如下。

  • 首先将系统调用号从 eax 里面取出来;
  • 然后根据系统调用号,在系统调用表中找到相应的函数进行调用,并将寄存器中保存的参数取出来,作为函数参数。
 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
// arch/x86/entry/common.c (Linux Kernel 4.19)
__visible void do_int80_syscall_32(struct pt_regs *regs) {
	enter_from_user_mode();
	local_irq_enable();
	do_syscall_32_irqs_on(regs);
}

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)) {
		nr = array_index_nospec(nr, IA32_NR_syscalls);
#ifdef CONFIG_IA32_EMULATION
		regs->ax = ia32_sys_call_table[nr](regs);
#else
		/*
		 * It's possible that a 32-bit syscall implementation
		 * takes a 64-bit parameter but nonetheless assumes that
		 * the high bits are zero.  Make sure we zero-extend all
		 * of the args.
		 */
		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);
#endif /* CONFIG_IA32_EMULATION */
	}

	syscall_return_slowpath(regs);
}

当系统调用结束之后,紧接着调用的是 INTERRUPT_RETURN,我们能够找到它的定义,也就是 iret。iret 指令将原来用户态保存的现场恢复回来,包含代码段、指令指针寄存器等。这时候用户态进程恢复执行。

1
2
// arch/x86/include/asm/irqflags.h
#define INTERRUPT_RETURN                iret

https://img.dawnguo.cn/All/566299fe7411161bae25b62e7fe20506.jpg

64 位的方式

entry_SYSCALL_64 这个函数中会先保存很多寄存器的值到 pt_regs 结构里面,例如用户态的代码段、数据段、保存参数的寄存器,然后调用 do_syscall_64。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# arch/x86/entry/entry_64.S (Linux Kernel)
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 */
......
      	/* IRQs are off. */
        movq	%rax, %rdi
        movq	%rsp, %rsi
      	call	do_syscall_64		/* returns with IRQs disabled */
......
        popq	%rdi
        popq	%rsp
        USERGS_SYSRET64
END(entry_SYSCALL_64)

在 do_syscall_64 里面,先从 rax 里面拿出系统调用号,然后根据系统调用号,在系统调用表 sys_call_table 中找到相应的函数进行调用,并将寄存器中保存的参数取出来,作为函数参数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// arch/x86/entry/common.c
__visible void do_syscall_64(struct pt_regs *regs) {
  struct thread_info *ti;

  local_irq_enable();
  ti = current_thread_info();
  if (READ_ONCE(ti->flags) & _TIF_WORK_SYSCALL_ENTRY)
    nr = syscall_trace_enter(regs);
  
  nr &= __SYSCALL_MASK;
  if (likely(nr < NR_syscalls)) {
    nr = array_index_nospec(nr, NR_syscalls);
    regs->ax = sys_call_table[nr](regs);
  }

  syscall_return_slowpath(regs);
}

64 位的系统调用返回的时候,执行的是 USERGS_SYSRET64(见上述代码)。定义如下:

1
2
3
4
// arch/x86/include/asm/irqflags.h
#define USERGS_SYSRET64				\
	swapgs;					\
	sysretq;

https://img.dawnguo.cn/All/1fc62ab8406c218de6e0b8c7e01fdbd7.jpg

系统调用表

系统调用在内核中的实现函数要有一个声明,声明往往在 include/linux/syscalls.h 文件中。例如 sys_open 是这样声明的:

1
asmlinkage long sys_open(const char __user *filename, int flags, umode_t mode);

真正的实现这个系统调用的一般在一个 c 文件中,例如 sys_open 的实现就在 fs/open.c 这个文件中。

1
2
3
4
5
6
7
// fs/open.c (Linux Kernerl 4.19)
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 是一个宏定义,根据参数的数目来选择宏。具体如下所示:

 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
#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 __SYSCALL_DEFINEx(x, name, ...)					\
	__diag_push();							\
	__diag_ignore(GCC, 8, "-Wattribute-alias",			\
		      "Type aliasing is used to sanitize syscall arguments");\
	asmlinkage long sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))	\
		__attribute__((alias(__stringify(__se_sys##name))));	\
	ALLOW_ERROR_INJECTION(sys##name, ERRNO);			\
	static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__));\
	asmlinkage long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__));	\
	asmlinkage long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__))	\
	{								\
		long ret = __do_sys##name(__MAP(x,__SC_CAST,__VA_ARGS__));\
		__MAP(x,__SC_TEST,__VA_ARGS__);				\
		__PROTECT(x, ret,__MAP(x,__SC_ARGS,__VA_ARGS__));	\
		return ret;						\
	}								\
	__diag_pop();							\
	static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))

声明和实现都好了之后,接下去需要生成相应的系统调用表。


32 位的系统调用表定义在 arch/x86/entry/syscalls/syscall_32.tbl 文件里。例如 open 是这样定义的:

1
5	i386	open			sys_open			__ia32_compat_sys_open

64 位的系统调用定义在另一个文件 arch/x86/entry/syscalls/syscall_64.tbl 里。例如 open 是这样定义的:

1
2	common	open			__x64_sys_open

这个表定义了系统调用号(第一列)、系统调用名称(第三列)、系统调用在内核的实现函数(第四列)(可以看出,32 位和 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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
out := arch/$(SRCARCH)/include/generated/asm
uapi := arch/$(SRCARCH)/include/generated/uapi/asm

# Create output directory if not already present
_dummy := $(shell [ -d '$(out)' ] || mkdir -p '$(out)') \
	  $(shell [ -d '$(uapi)' ] || mkdir -p '$(uapi)')

syscall32 := $(srctree)/$(src)/syscall_32.tbl
syscall64 := $(srctree)/$(src)/syscall_64.tbl

syshdr := $(srctree)/$(src)/syscallhdr.sh
systbl := $(srctree)/$(src)/syscalltbl.sh

quiet_cmd_syshdr = SYSHDR  $@
      cmd_syshdr = $(CONFIG_SHELL) '$(syshdr)' '$<' '$@' \
		   '$(syshdr_abi_$(basetarget))' \
		   '$(syshdr_pfx_$(basetarget))' \
		   '$(syshdr_offset_$(basetarget))'
quiet_cmd_systbl = SYSTBL  $@
      cmd_systbl = $(CONFIG_SHELL) '$(systbl)' $< $@

quiet_cmd_hypercalls = HYPERCALLS $@
      cmd_hypercalls = $(CONFIG_SHELL) '$<' $@ $(filter-out $<,$^)

syshdr_abi_unistd_32 := i386
$(uapi)/unistd_32.h: $(syscall32) $(syshdr)
	$(call if_changed,syshdr)

syshdr_abi_unistd_32_ia32 := i386
syshdr_pfx_unistd_32_ia32 := ia32_
$(out)/unistd_32_ia32.h: $(syscall32) $(syshdr)
	$(call if_changed,syshdr)

syshdr_abi_unistd_x32 := common,x32
syshdr_offset_unistd_x32 := __X32_SYSCALL_BIT
$(uapi)/unistd_x32.h: $(syscall64) $(syshdr)
	$(call if_changed,syshdr)

syshdr_abi_unistd_64 := common,64
$(uapi)/unistd_64.h: $(syscall64) $(syshdr)
	$(call if_changed,syshdr)

syshdr_abi_unistd_64_x32 := x32
syshdr_pfx_unistd_64_x32 := x32_
$(out)/unistd_64_x32.h: $(syscall64) $(syshdr)
	$(call if_changed,syshdr)

$(out)/syscalls_32.h: $(syscall32) $(systbl)
	$(call if_changed,systbl)
$(out)/syscalls_64.h: $(syscall64) $(systbl)
	$(call if_changed,systbl)

$(out)/xen-hypercalls.h: $(srctree)/scripts/xen-hypercalls.sh
	$(call if_changed,hypercalls)

$(out)/xen-hypercalls.h: $(srctree)/include/xen/interface/xen*.h

uapisyshdr-y			+= unistd_32.h unistd_64.h unistd_x32.h
syshdr-y			+= syscalls_32.h
syshdr-$(CONFIG_X86_64)		+= unistd_32_ia32.h unistd_64_x32.h
syshdr-$(CONFIG_X86_64)		+= syscalls_64.h
syshdr-$(CONFIG_XEN)		+= xen-hypercalls.h

targets	+= $(uapisyshdr-y) $(syshdr-y)

PHONY += all
all: $(addprefix $(uapi)/,$(uapisyshdr-y))
all: $(addprefix $(out)/,$(syshdr-y))
	@:

这两个文件在编译的过程中会用到,具体是 arch/x86/entry/syscalls/Makefile 这个文件会用到。这个文件会根据 syscall_32.tbl 和 syscall_64.tbl 生成相应的 unistd_32.h、unistd_64.h、syscalls_32.h、syscalls_64.h 等文件。这个文件中其实还用到下面这两个脚本,

  • 第一个脚本 arch/x86/entry/syscalls/syscallhdr.sh,会在 unistd_32.h、unistd_64.h 等文件中生成 #define __NR_open 5
  • 第二个脚本 arch/x86/entry/syscalls/syscalltbl.sh,会在 syscalls_32.h、syscalls_64.h 文件中生成 __SYSCALL(__NR_open, sys_open)

其中 unistd_32.h 部分内容如下

1
2
3
4
5
6
#define __NR_restart_syscall 0
#define __NR_exit 1
#define __NR_fork 2
#define __NR_read 3
#define __NR_write 4
#define __NR_open 5

syscalls_32.h部分内容如下

1
2
3
4
5
6
7
8
9
__SYSCALL_I386(0, sys_restart_syscall, )
__SYSCALL_I386(1, sys_exit, )
#ifdef CONFIG_X86_32
__SYSCALL_I386(2, sys_fork, )
#else
__SYSCALL_I386(2, sys_fork, )
#endif
__SYSCALL_I386(3, sys_read, )
__SYSCALL_I386(4, sys_write, )

在文件 arch/x86/entry/syscall_32.c 中对 __SYSCALL_I386 进行了定义。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// arch/x86/entry/syscall_32.c (Linux Kernel 4.19)

/* System call table for i386. */
#ifdef CONFIG_IA32_EMULATION
/* On X86_64, we use struct pt_regs * to pass parameters to syscalls */
#define __SYSCALL_I386(nr, sym, qual) extern asmlinkage long sym(const struct pt_regs *);

/* this is a lie, but it does not hurt as sys_ni_syscall just returns -EINVAL */
extern asmlinkage long sys_ni_syscall(const struct pt_regs *);

#else /* CONFIG_IA32_EMULATION */
#define __SYSCALL_I386(nr, sym, qual) extern asmlinkage long sym(unsigned long, unsigned long, unsigned long, unsigned long, unsigned long, unsigned long);
extern asmlinkage long sys_ni_syscall(unsigned long, unsigned long, unsigned long, unsigned long, unsigned long, unsigned long);
#endif /* CONFIG_IA32_EMULATION */

#include <asm/syscalls_32.h>
#undef __SYSCALL_I386

#define __SYSCALL_I386(nr, sym, qual) [nr] = sym,

__SYSCALL_I386 还进行了两次定义,我们重点关注第二次定义。那么,syscalls_32.h 中的内容如下所示

1
2
3
4
[0] = sys_restart_syscall,
[1] = sys_exit,
[2] = sys_fork,
...

之后,还是在文件 arch/x86/entry/syscall_32.c 中,定义了这样一个表,里面 include 了这个头文件,从而所有的 sys_ 系统调用都在这个表里面了。

1
2
3
4
5
6
7
8
9
// arch/x86/entry/syscall_32.c (Linux Kernel 4.19)
__visible const sys_call_ptr_t ia32_sys_call_table[__NR_syscall_compat_max+1] = {
	/*
	 * Smells like a compiler bug -- it doesn't work
	 * when the & below is removed.
	 */
	[0 ... __NR_syscall_compat_max] = &sys_ni_syscall,
#include <asm/syscalls_32.h>
};

同理,在文件 arch/x86/entry/syscall_64.c,定义了这样一个表,里面 include 了这个头文件,这样所有的 sys_ 系统调用就都在这个表里面了。

1
2
3
4
5
6
7
8
9
// arch/x86/entry/syscall_64.c (Linux Kernel 4.19)
asmlinkage const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
	/*
	 * Smells like a compiler bug -- it doesn't work
	 * when the & below is removed.
	 */
	[0 ... __NR_syscall_max] = &sys_ni_syscall,
#include <asm/syscalls_64.h>
};

巨人的肩膀

  1. 关于系统调用过程 sys_call_table 的生成更详细的其实可以看这里:https://github.com/chenpengcong/blog/issues/6
  2. http://blog.csdn.net/gatieme/article/details/50779184
  3. https://zhuanlan.zhihu.com/p/28984642
  4. 极客时间-《趣谈Linux操作系统》