Linux — 系统调用过程

  1. glibc更熟悉系统调用的细节,封装成更加友好的接口,可以直接调用
  2. 用户态 进程调用glibc的open函数(函数定义如下)
int open(const char *pathname, int flags, mode_t mode)

syscalls.list

syscalls.list记录了所有glibc函数所对应的系统调用

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

make-syscalls.sh

  1. make-syscalls.sh会根据上面的配置文件,对于每个封装好的系统调用,生成一个文件F
  2. 文件F里面会定义一些宏,例如 #define SYSCALL_NAME open
    • make-syscalls.sh中对应的代码为 echo '#define SYSCALL_NAME $syscall'

syscall-template.S

syscall-template.S会使用文件F里面的宏,定义这个系统调用的 调用方式

// PSEUDO是伪代码的意思
T_PSEUDO (SYSCALL_SYMBOL, SYSCALL_NAME, SYSCALL_NARGS)
    ret
T_PSEUDO_END (SYSCALL_SYMBOL)

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

sysdep.h

PSEUDO也是一个宏,定义如下(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 ,DO_CALL也是一个宏(32位和64位的定义是不一样的)

32位系统调用

sysdep.h

sysdeps/unix/sysv/linux/i386/sysdep.h

// glibc源码
/* 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

  1. 将请求参数放在 寄存器 里面(PUSHARGS)
  2. 根据 系统调用的名称 ,得到 系统调用号 (SYS_ify (syscall_name)), 放在寄存器 %eax 里面
  3. 然后执行 ENTER_KERNEL

ENTER_KERNEL

// glibc源码
# define ENTER_KERNEL int $0x80

  1. int是 interrupt 的意思, int $0x80 就是触发一个 软中断 ,通过它可以陷入(trap)内核
  2. 在内核启动过程中,有一个 trap_init() 函数,其中有代码 SYSG(IA32_SYSCALL_VECTOR, entry_INT80_32)
    • 这是一个软中断的陷入门,当接收到一个系统调用时, entry_INT80_32 就会被调用

entry_INT80_32

// Linux源码
ENTRY(entry_INT80_32)
    ASM_CLAC
    pushl   %eax            /* pt_regs->orig_ax */
    SAVE_ALL pt_regs_ax=$-ENOSYS switch_stacks=1    /* save rest */
    movl    %esp, %eax
    call    do_int80_syscall_32
.Lsyscall_32_done:
...
.Lirq_return:
    INTERRUPT_RETURN
...
ENDPROC(entry_INT80_32)

/* Handles int $0x80 */
__visible void do_int80_syscall_32(struct pt_regs *regs)
{
    do_syscall_32_irqs_on(regs);
}

在进入内核之前,通过push和SAVE_ALL将当前 用户态的寄存器 ,保存在 pt_regs 结构里面,然后调用do_int80_syscall_32

do_syscall_32_irqs_on

// Linux源码
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 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);
}

#define ia32_sys_call_table sys_call_table

  1. 系统调用号 从寄存器 %eax 中取出,然后根据系统调用号,在 系统调用表 中找到相应的函数进行调用
  2. 将寄存器中保存的参数取出来,作为函数参数
  3. 根据宏定义, #define ia32_sys_call_table sys_call_table ,系统调用就放在这个表里面

INTERRUPT_RETURN

// Linux源码
#define INTERRUPT_RETURN       iret

iret指令将原来 用户态 保存的现场恢复回来,包括代码段、指令指针寄存器等,此时用户态进程 恢复执行

小结

64位系统调用

sysdep.h

sysdeps/unix/sysv/linux/x86_64/sysdep.h

// glibc源码
/* 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

  1. 与32位的系统调用类似,首先将 系统调用名称 转换为 系统调用号 放在寄存器 %rax
  2. 在这里是 进行真正调用 ,而不是采用 中断 模式,改用 syscall 指令(传递参数的寄存器也改变了)

syscall

  1. syscall指令使用了一种特殊的寄存器,称为 特殊模块寄存器 (Model Specific Registers, MSR
  2. MSR是CPU为了完成某些 特殊控制功能 为目的的寄存器,例如 系统调用
  3. 在Linux系统初始化时,trap_init除了初始化上面的 中断模式 外,还会调用cpu_init(),而cpu_init()会调用 syscall_init()

syscall_init()

// Linux源码
void syscall_init(void)
{
    wrmsrl(MSR_LSTAR, (unsigned long)entry_SYSCALL_64);
}

  1. rdmsr和wrmsr是用来读写特殊模块寄存器的,MSR_LSTAR就是一个特殊模块寄存器
  2. 当syscall指令调用的时候,会从MSR_LSTAR寄存器里取出函数地址来调用,即调用entry_SYSCALL_64

entry_SYSCALL_64

arch/x86/entry/entry_64.S

// Linux源码
ENTRY(entry_SYSCALL_64)
    /* Construct struct pt_regs on stack */
    pushq   $__USER_DS              /* pt_regs->ss */
    pushq   PER_CPU_VAR(cpu_tss_rw + TSS_sp2)   /* pt_regs->sp */
    pushq   %r11                    /* pt_regs->flags */
    pushq   $__USER_CS              /* pt_regs->cs */
    pushq   %rcx                    /* pt_regs->ip */
GLOBAL(entry_SYSCALL_64_after_hwframe)
    pushq   %rax                    /* pt_regs->orig_ax */
    PUSH_AND_CLEAR_REGS rax=$-ENOSYS
    TRACE_IRQS_OFF
    /* IRQs are off. */
    movq    %rax, %rdi
    movq    %rsp, %rsi
    call    do_syscall_64       /* returns with IRQs disabled */
...
    cmpq    %rcx, %r11  /* SYSRET requires RCX == RIP */
    jne swapgs_restore_regs_and_return_to_usermode
...
syscall_return_via_sysret:
    ...
    USERGS_SYSRET64
END(entry_SYSCALL_64)

首先保存很多寄存器到 pt_regs 结构里面,例如用户态的代码段、数据段、保存参数的寄存器,然后调用do_syscall_64

do_syscall_64

// Linux源码
__visible void do_syscall_64(unsigned long nr, struct pt_regs *regs)
{
    struct thread_info *ti;
    ...
    ti = current_thread_info();
    if (READ_ONCE(ti->flags) & _TIF_WORK_SYSCALL_ENTRY)
        nr = syscall_trace_enter(regs);
    ...
    nr &= __SYSCALL_MASK;
    if (likely(nr ax = sys_call_table[nr](regs);
    }

    syscall_return_slowpath(regs);
}

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

USERGS_SYSRET64

// Linux源码
#define USERGS_SYSRET64                \
    swapgs;                 \
    sysretq;

返回用户态的指令变成了sysretq

小结

系统调用表

32位 VS 64位

// Linux源码 -- arch/x86/entry/syscalls/syscall_32.tbl
5   i386    open            sys_open            __ia32_compat_sys_open

// Linux源码 -- arch/x86/entry/syscalls/syscall_64.tbl
2   common  open            __x64_sys_open

  1. 第1列的数字是 系统调用号 ,32位和64位的系统调用号是不一样的
  2. 第3列是 系统调用名称
  3. 第4列是系统调用在 内核中的实现函数

实现函数

声明

系统调用在内核中的实现函数需要有一个 声明 ,该声明一般在 include/linux/syscalls.h 文件中

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

实现

系统调用的真正实现,一般在.c文件中,sys_open的实现在 fs/open.c 里面,但里面只有SYSCALL_DEFINE3

// Linux源码
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是一个宏, 系统调用最多6个参数 ,根据参数的数量选择宏,具体的宏定义如下

// Linux源码
#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 __PROTECT(...) asmlinkage_protect(__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__))

宏展开后,实现如下,与声明的是一致的

// Linux源码
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;
}

转载请注明出处:http://zhongmingmao.me/2019/04/20/linux-system-call-process/

访问原文「 Linux — 系统调用过程 」获取最佳阅读体验并参与讨论