程序锅

  • 首页
  • 分类
  • 标签
  • 归档
  • 关于

  • 搜索
基础知识 Etcd LeetCode 计算机体系结构 Kubernetes Containerd Docker 容器 云原生 Serverless 项目开发维护 ELF 深入理解程序 Tmux Vim Linux Kernel Linux numpy matplotlib 机器学习 MQTT 网络基础 Thrift RPC OS 操作系统 Clang 研途 数据结构和算法 Java 编程语言 Golang Python 个人网站搭建 Nginx 计算机通用技术 Git

Linux 系统调用过程

发表于 2021-04-14 | 分类于 Linux Kernel | 0 | 阅读次数 2352

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 函数为例进行讲解,

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

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

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

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

#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,使用上面这个宏,定义了这个系统调用的调用方式。

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 位中的定义如下所示。

// 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 指令触发一个软中断。
// 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 的宏定义是一样的。

// 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 指令以调用的方式来进行系统,而不是使用中断的方式进行系统调用。同时,传递参数的寄存器也变了。
// 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 函数。

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

内核

32 位的方式

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

// 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 结构里面。

# 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 里面取出来;
  • 然后根据系统调用号,在系统调用表中找到相应的函数进行调用,并将寄存器中保存的参数取出来,作为函数参数。
// 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 指令将原来用户态保存的现场恢复回来,包含代码段、指令指针寄存器等。这时候用户态进程恢复执行。

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

64 位的方式

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

# 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 中找到相应的函数进行调用,并将寄存器中保存的参数取出来,作为函数参数。

// 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(见上述代码)。定义如下:

// arch/x86/include/asm/irqflags.h
#define USERGS_SYSRET64				\
	swapgs;					\
	sysretq;

系统调用表

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

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

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

// 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 是一个宏定义,根据参数的数目来选择宏。具体如下所示:

#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 是这样定义的:

5	i386	open			sys_open			__ia32_compat_sys_open

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

2	common	open			__x64_sys_open

这个表定义了系统调用号(第一列)、系统调用名称(第三列)、系统调用在内核的实现函数(第四列)(可以看出,32 位和 64 位的系统调用号是不一样的)。

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 部分内容如下

#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部分内容如下

__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 进行了定义。

// 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 中的内容如下所示

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

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

// 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_ 系统调用就都在这个表里面了。

// 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操作系统》
卷死我
dawnguo 微信支付

微信支付

dawnguo 支付宝

支付宝

  • 本文作者: dawnguo
  • 本文链接: /archives/108
  • 版权声明: 本博客所有文章除特别声明外,均采用CC BY-NC-SA 3.0 许可协议。转载请注明出处!
# Linux # Linux Kernel
LeetCode 200 道高频题【按照 Tag 分类】
Java | JVM 之内存管理相关知识
  • 文章目录
  • 站点概览
dawnguo

dawnguo

215 日志
24 分类
37 标签
RSS
Creative Commons
© 2018 — 2025 程序锅
0%