请稍侯

Linux系统调用执行流程

Linux 的提供的编程接口可以分成系统调用和应用编程接口(API),而 API 的大部分功能都是依赖于系统提供的标准系统调用。使用系统调用也有两种途径:通过 API 和使用软件中断汇编指令进入内核空间,而实质上前者也是通过后者实现功能的。所以 API 的执行流程可以分为:

  1. 应用调用 API;
  2. API 根据应用参数进入内核空间调用系统调用;
  3. 内核根据传入的系统调用号(即软件中断传入的立即数)在系统调用表中定位到具体的 syscall;
  4. 执行 syscall 或者处理未定义的系统调用;
  5. 返回用户空间, API 返回操作结果。

1. 系统调用的 C 库阶段

以 ARM 和 musl c 库为例,它的 socket API 定义如下:(其他系统调用同理)

int socket(int domain, int type, int protocol)
{
    int s = socketcall(socket, domain, type, protocol, 0, 0, 0);
...
    return s;
}

socketcall 是一个宏,展开后最终调用的是 __syscall

__syscall:
    mov ip,sp
    stmfd sp!,{r4,r5,r6,r7}
    mov r7,r0
    mov r0,r1
    mov r1,r2
    mov r2,r3
    ldmfd ip,{r3,r4,r5,r6}
    svc 0
    ldmfd sp!,{r4,r5,r6,r7}
    bx lr

其中最重要的命令就是 svc 0,通过这条指令切换到 svc 模式(svc 替代了以前的 swi 指令,是 arm 提供的系统调用指令),进入到软件中断处理函数( SWI handler )。

2. 系统调用的 kernel 阶段

SWI handler 在 arch/arm/kernel/entry-common.S 定义:

/*=============================================================================
 * SWI handler
 *-----------------------------------------------------------------------------
 */
ENTRY(vector_swi)
...

    /*
     * Get the system call number.
     */
    /*
     - Pure EABI user space always put syscall number into scno (r7).
     */
    adr tbl, sys_call_table     @ load syscall table pointer
...
    bic scno, scno, #0xff000000     @ mask off SWI op-code
    eor scno, scno, #__NR_SYSCALL_BASE  @ check OS number
...

    cmp scno, #NR_syscalls      @ check upper syscall limit
    adr lr, BSYM(ret_fast_syscall)  @ return address
    ldrcc   pc, [tbl, scno, lsl #2]     @ call sys_* routine

    add r1, sp, #S_OFF
2:  cmp scno, #(__ARM_NR_BASE - __NR_SYSCALL_BASE)
    eor r0, scno, #__NR_SYSCALL_BASE    @ put OS number back
    bcs arm_syscall
    mov why, #0             @ no longer a real syscall
    b   sys_ni_syscall          @ not private func

...
ENDPROC(vector_swi) 

主要步骤包括:

  1. 获取系统调用号(scno);
  2. 在系统调用表(sys_call_table) 中找到系统调用的入口,并调用系统调(ldrcc pc, [tbl, scno, lsl #2] @ call sys_* routine);
  3. 使用预定义的函数处理不支持的系统调用(bcs arm_syscallb sys_ni_syscall)。

其中 NR_syscalls 为 arm linux 系统调用的总数, sys_call_table 是系统调用表的基地址,系统调用表定义在 calls.S , 然后被包含在 entry-common.S:

    .type   sys_call_table, #object
ENTRY(sys_call_table)
#include "calls.S"
#undef ABI
#undef OBSOLETE

3. ppc 和 x86

PPC 和 x86 的处理流程也一样,不同只是底层汇编不同: x86 使用 syscall 、 PPC 使用 sc 进入软件中断处理函数。

x86

__syscall:
    movq %rdi,%rax
    movq %rsi,%rdi
    movq %rdx,%rsi
    movq %rcx,%rdx
    movq %r8,%r10
    movq %r9,%r8
    movq 8(%rsp),%r9
    syscall
    ret

ppc

__syscall:
    mr      0, 3                  # Save the system call number
    mr      3, 4                  # Shift the arguments: arg1
    mr      4, 5                  # arg2
    mr      5, 6                  # arg3
    mr      6, 7                  # arg4
    mr      7, 8                  # arg5
    mr      8, 9                  # arg6
    sc
    bnslr+ # return if not summary overflow
    #else error:
    # return negated value.
    neg 3, 3
    blr

进入内核后,ppc 的 swi handler 位于 arch/powerpc/kernel/entry_32.S

/*
 * Handle a system call.
 */
    .stabs  "arch/powerpc/kernel/",N_SO,0,0,0f
    .stabs  "entry_32.S",N_SO,0,0,0f
0:
_GLOBAL(DoSyscall)

...

x86 的 swi handler 位于 arch/x86/kernel/entry_32.S

/*
 * syscall stub including irq exit should be protected against kprobes
 */
    .pushsection .kprobes.text, "ax"
    # system call handler stub
ENTRY(system_call)

...