Linux系统调用流程

linux系统调用流程分析

背景

在前篇中我们讲到,使用kill系统调用进行signal投递之流程,其中有个点没有提到,系统调用详细流程究竟是怎样的。

大家应该知道传统的系统调用方式,即通过int 0x80指令,以软中断的形式触发系统调用,并将系统调用号放置在rax寄存器中,系统调用其它参数也通过寄存器传递。

由于通过软中断实现系统调用比较低效,现在普遍采用syscall指令形式。

本篇主要讲解以软中断方式实现系统调用之流程,理解了这个方式,再去研究其他方式会更容易。

中断向量表

在CPU中有中断向量表,可以理解为一个数组,数组的内容为地址,也就是当对应中断发生时候的处理地址。

按照int 0x80指令的字面意思,应该是触发了中断号为0x80的中断,那么其处理函数是什么呢?

初始化中断表

x86-32位架构为例,在内核启动时候,会调用trap_init函数,其实现如下:

// /arch/x86/kernel/traps.c

void __init trap_init(void)
{
    int i;

#ifdef CONFIG_EISA
    void __iomem *p = early_ioremap(0x0FFFD9, 4);

    if (readl(p) == 'E' + ('I'<<8) + ('S'<<16) + ('A'<<24))
        EISA_bus = 1;
    early_iounmap(p, 4);
#endif

    set_intr_gate(0, &divide_error);
    set_intr_gate_ist(2, &nmi, NMI_STACK);
    /* int4 can be called from all */
    set_system_intr_gate(4, &overflow);
    set_intr_gate(5, &bounds);
    set_intr_gate(6, &invalid_op);
    set_intr_gate(7, &device_not_available);
#ifdef CONFIG_X86_32
    set_task_gate(8, GDT_ENTRY_DOUBLEFAULT_TSS);
#else
    set_intr_gate_ist(8, &double_fault, DOUBLEFAULT_STACK);
#endif
    set_intr_gate(9, &coprocessor_segment_overrun);
    set_intr_gate(10, &invalid_TSS);
    set_intr_gate(11, &segment_not_present);
    set_intr_gate_ist(12, &stack_segment, STACKFAULT_STACK);
    set_intr_gate(13, &general_protection);
    set_intr_gate(15, &spurious_interrupt_bug);
    set_intr_gate(16, &coprocessor_error);
    set_intr_gate(17, &alignment_check);
#ifdef CONFIG_X86_MCE
    set_intr_gate_ist(18, &machine_check, MCE_STACK);
#endif
    set_intr_gate(19, &simd_coprocessor_error);

    /* Reserve all the builtin and the syscall vector: */
    for (i = 0; i < FIRST_EXTERNAL_VECTOR; i++)
        set_bit(i, used_vectors);

#ifdef CONFIG_IA32_EMULATION
    set_system_intr_gate(IA32_SYSCALL_VECTOR, ia32_syscall);
    set_bit(IA32_SYSCALL_VECTOR, used_vectors);
#endif

#ifdef CONFIG_X86_32
    set_system_trap_gate(SYSCALL_VECTOR, &system_call);
    set_bit(SYSCALL_VECTOR, used_vectors);
#endif

    /*
     * Should be a barrier for any external CPU state:
     */
    cpu_init();

    x86_init.irqs.trap_init();
}

在其中进行了大量中断处理函数的设置,注意到其中有这么一句: set_system_trap_gate(SYSCALL_VECTOR, &system_call);

SYSCALL_VECTOR是多少呢?

其在/arch/x86/include/asm/irq_vectors.h中定义

#ifdef CONFIG_X86_32
# define SYSCALL_VECTOR 0x80
#endif

是熟悉的0x80,那么system_call,就是这个中断发生时候的处理函数咯。

我们再找找,看看这个函数具体内容。

system_call

其在/arch/x86/kernel/entry_32.S中以汇编的形式定义

ENTRY(system_call)
    RING0_INT_FRAME         # can't unwind into user space anyway
    pushl_cfi %eax          # save orig_eax
    SAVE_ALL
    GET_THREAD_INFO(%ebp) # system call tracing in operation / emulation
    testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags(%ebp)
    jnz syscall_trace_entry
    cmpl $(nr_syscalls), %eax
    jae syscall_badsys
syscall_call:
    call *sys_call_table(,%eax,4)
    movl %eax,PT_EAX(%esp)      # store the return value    
    ...

其中ENTRYinclude/linux/linkage.h中定义

#ifndef ENTRY
#define ENTRY(name) \
  .globl name; \
  ALIGN; \
  name:
#endif

system_call中经过一系列处理,最终调用 call *sys_call_table(,%eax,4) 也就是说系统调用号保存在%rax中,并根据其值在系统调用表sys_call_table中进行查找,然后调用其值。

sys_call_table 32系统的初始化

32位系统的初始化比较直接,其在arch/x86/kernel/syscall_table_32.S直接定义

ENTRY(sys_call_table)
    .long sys_restart_syscall   /* 0 - old "setup()" system call, used for restarting */
    .long sys_exit
    .long ptregs_fork
    .long sys_read
    .long sys_write
    .long sys_open      /* 5 */
    .long sys_close
    .long sys_waitpid
    .long sys_creat

sys_call_table 64系统的初始化

64位系统的初始化稍微复杂,其定义在arch/x86/kernel/syscall_64.c

/* System call table for x86-64. */

#include <linux/linkage.h>
#include <linux/sys.h>
#include <linux/cache.h>
#include <asm/asm-offsets.h>

#define __NO_STUBS

#define __SYSCALL(nr, sym) extern asmlinkage void sym(void) ;
#undef _ASM_X86_UNISTD_64_H
#include <asm/unistd_64.h>

#undef __SYSCALL
#define __SYSCALL(nr, sym) [nr] = sym,
#undef _ASM_X86_UNISTD_64_H

typedef void (*sys_call_ptr_t)(void);

extern void sys_ni_syscall(void);

const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
    /*
    *Smells like a like a compiler bug -- it doesn't work
    *when the & below is removed.
    */
    [0 ... __NR_syscall_max] = &sys_ni_syscall,
#include <asm/unistd_64.h>
};

可见,sys_call_table是一个数组,其值为typedef void (*sys_call_ptr_t)(void);类型的函数指针。

在其中#include <asm/unistd_64.h>两次,有什么作用,又有什么区别呢?

直接看看asm/unistd_64.h文件内容

...

#ifndef __SYSCALL
#define __SYSCALL(a, b)
#endif

/*
 * This file contains the system call numbers.
 *
 * Note: holes are not allowed.
 */

/* at least 8 syscall per cacheline */
#define __NR_read               0
__SYSCALL(__NR_read, sys_read)
#define __NR_write              1
__SYSCALL(__NR_write, sys_write)
#define __NR_open               2
__SYSCALL(__NR_open, sys_open)
#define __NR_close              3
__SYSCALL(__NR_close, sys_close)
#define __NR_stat               4
__SYSCALL(__NR_stat, sys_newstat)
#define __NR_fstat              5
__SYSCALL(__NR_fstat, sys_newfstat)
#define __NR_lstat              6
__SYSCALL(__NR_lstat, sys_newlstat)
#define __NR_poll               7
__SYSCALL(__NR_poll, sys_poll)

...

在其中定义了每个系统调用对应的调用号

那么再回头看看arch/x86/kernel/syscall_64.c文件内容,其首先对__SYSCALL进行重新定义

#define __SYSCALL(nr, sym) extern asmlinkage void sym(void) ;
#undef _ASM_X86_UNISTD_64_H
#include <asm/unistd_64.h>

这样就可以对每个系统调用原型进行声明,其展开为:

...
extern asmlinkage void sys_read(void) ;
extern asmlinkage void sys_write(void) ;
...

继续往下看,其对__SYSCALL又进行重定义

#undef __SYSCALL
#define __SYSCALL(nr, sym) [nr] = sym,

所以,sys_call_table展开为

const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
    /*
    *Smells like a like a compiler bug -- it doesn't work
    *when the & below is removed.
    */
    [0 ... __NR_syscall_max] = &sys_ni_syscall,

[0] = sys_read,
[1] = sys_write,

...
};

如此,根据系统调用号,即可在sys_call_table中找到对应的系统调用函数。

至此,sys_call_table中保存了系统调用的处理地址,执行call *sys_call_table(,%rax,4)即可转入对应系统调用函数进行真正处理。

精简流程为:

int 0x80触发软中断 -> 在中断向量表中查找处理函数,system_call -> 根据%rax值(系统调用号)查找系统调用处理函数 -> 执行系统调用

发布于 2019-01-18