一步一步学linux操作系统: 06 系统调用

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

sysdeps\unix\syscal.list
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 位的系统调用流程图

图片来自极客时间趣谈linux操作系统

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);

rdmsrwrmsr 是用来读写特殊模块寄存器的。
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 位的系统调用流程图

图片来自极客时间趣谈linux操作系统

系统调用表

系统调用的方式,都是最终到了系统调用表

系统调用表 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操作系统

参考资料:

趣谈Linux操作系统(极客时间)链接:
http://gk.link/a/10iXZ
欢迎大家来一起交流学习

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

墨1024

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值