Linux 系统调用过程详细分析

内核版本:Linux-4.19

操作系统通过系统调用为运行于其上的进程提供服务。

那么,在应用程序内,调用一个系统调用的流程是怎样的呢?

我们以一个假设的系统调用 xyz() 为例,介绍一次系统调用的所有环节。

Linux 系统调用过程详细分析

如上图所示,系统调用执行的流程如下:

1. 应用程序代码调用 xyz(),该函数是一个包装系统调用的库函数;  2. 库函数 xyz() 负责准备向内核传递的参数,并触发软中断以切换到内核;  3. CPU 被软中断打断后,执行中断处理函数,即系统调用处理函数(system_call);  4. 系统调用处理函数调用系统调用服务例程(sys_xyz ),真正开始处理该系统调用。

系统调用的实现来自于Glibc,几乎所有 C 程序都要调用 Glibc 的动态链接库 libc.so 中的库函数。这些库函数的源码是不可见的,可通过 objdump 或 gdb 等工具对代码进行汇编反编译,摸清大体的过程。

我们可不必太过纠结,知道原理就好。

下面继续分析在内核中的实现过程。

Pure EABI user space always put syscall number into scno (r7).

当从用户态转为内核态时,系统会将 syscall number 存储在寄存器 R7 中,利用 R7 来传参。

在 entry-header.S 文件中,有如下代码:

scno    .req    r7      @ syscall number  tbl .req    r8      @ syscall table pointer  why .req    r8      @ Linux syscall (!= 0)  tsk .req    r9      @ current thread_info

类似于给寄存器起了个“别名”。

最后通过

invoke_syscall tbl, scno, r10, __ret_fast_syscall

代码成功调用 syscall table 中的服务程序。

invoke_syscall 定义如下:

    .macro  invoke_syscall, table, nr, tmp, ret, reload=0  #ifdef CONFIG_CPU_SPECTRE      mov tmp, nr      cmp tmp, #NR_syscalls      @ check upper syscall limit      movcs   tmp, #0      csdb      badr    lr, ret            @ return address      .if reload      add r1, sp, #S_R0 + S_OFF       @ pointer to regs      ldmccia r1, {r0 - r6}           @ reload r0-r6      stmccia sp, {r4, r5}            @ update stack arguments      .endif      ldrcc   pc, [table, tmp, lsl #2]  @ call sys_* routine  #else      cmp nr, #NR_syscalls       @ check upper syscall limit      badr    lr, ret            @ return address      .if reload      add r1, sp, #S_R0 + S_OFF       @ pointer to regs      ldmccia r1, {r0 - r6}           @ reload r0-r6      stmccia sp, {r4, r5}            @ update stack arguments      .endif      ldrcc   pc, [table, nr, lsl #2]   @ call sys_* routine  #endif      .endm

回看

invoke_syscall tbl, scno, r10, __ret_fast_syscall

这段代码。tbl 是指向的何处呢?

接下来,就简单的介绍一下 syscall table 这个表是怎样形成的。

查看代码我们发现,tbl 表示 sys_call_table 的地址:

adr tbl, sys_call_table @ load syscall table pointer

entry-common.S 中有这样一段代码:

    syscall_table_start sys_call_table        #define COMPAT(nr, native, compat) syscall nr, native  #ifdef CONFIG_AEABI  #include <calls-eabi.S>  #else  #include <calls-oabi.S>  #endif  #undef COMPAT        syscall_table_end sys_call_table

calls-eabi.S 文件内容如下:

NATIVE(0, sys_restart_syscall)  NATIVE(1, sys_exit)  NATIVE(2, sys_fork)  NATIVE(3, sys_read)  NATIVE(4, sys_write)  NATIVE(5, sys_open)  NATIVE(6, sys_close)  NATIVE(8, sys_creat)  NATIVE(9, sys_link)  NATIVE(10, sys_unlink)  NATIVE(11, sys_execve)  NATIVE(12, sys_chdir)  NATIVE(14, sys_mknod)  NATIVE(15, sys_chmod)  NATIVE(16, sys_lchown16)  NATIVE(19, sys_lseek)  NATIVE(20, sys_getpid)      ...

以上代码中宏的定义如下:

    /* 定义 sys_call_table,并将 __sys_nr 清 0 */      .macro  syscall_table_start, sym      .equ    __sys_nr, 0      .type   sym, #object  ENTRY(sym)      .endm        /* 检查序号错误,并利用 sys_ni_syscall 填充缺少的序号 */      .macro  syscall, nr, func      .ifgt   __sys_nr - nr      .error  "Duplicated/unorded system call entry"      .endif      .rept   nr - __sys_nr      .long   sys_ni_syscall      .endr      .long   func      .equ    __sys_nr, nr + 1      .endm        /* 检查序号是否超过了 __NR_syscalls,如果不足的话,用 sys_ni_syscall 来填充 */      .macro  syscall_table_end, sym      .ifgt   __sys_nr - __NR_syscalls      .error  "System call table too big"      .endif      .rept   __NR_syscalls - __sys_nr      .long   sys_ni_syscall      .endr      .size   sym, . - sym      .endm        /* NATIVE 宏定义 */  #define NATIVE(nr, func) syscall nr, func

到这里应该分析完了系统调用的大概过程,感谢大家花费宝贵的时间浏览,如果有什么问题欢迎探讨,后期会进行修改和补充!

© 版权声明
THE END
点赞0
抢沙发
头像
提交
头像

昵称

取消
昵称
一言一语