从上一集的trap_init(),到我们现在的接手项目,我们正式的开始进行系统调用了
我们利用一个系统调用,来了解内核中各个模块的实现机制
对于系统的调用,我们提供了glibc这个中介,更加熟悉系统调用的细节,封装为更友好的接口
glibc对系统调用的封装,我们以常用的系统调用open为例,走一遭,open如何实现,如何打开文件,进行一个完整流程的梳理
glibc的open函数基本如下
int open(const char *pathname, int flags, mode_t mode)
那么函数内部呢?其对应的系统调用,基本如下
# File name Caller Syscall name Args Strong name Weak names
open – open Ci:siv __libc_open __open open |
glibc利用一个脚本makr-syscall.sh 根据上面的配置文件,对每一个系统调用,生成一个文件,文件中定义了对应的宏
glibc 还有一个文件 syscall-template.S,使用上面的宏,定义了这个系统调用的调用方式
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也是一个宏
内部定义如下
里面的任何一个系统调用,都会调用DO_CALL,也是一个宏
这个宏就涉及了上章我们说的,从用户态到内核态再到用户态的流程
而这个宏的调用,分为了32位和64位的操作
1.32位系统的调用
32位系统调用如下,
/* 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内部为
# define ENTER_KERNEL int $0x80 |
int就是interrupt,就是中断的意思,int $0x80就是触发软中断事件,陷入trap内核
启动内核的时候的trap_init,和其有异曲同工之意,我们看下trap_ini()
set_system_init_gate(IA32_SYSCALL_VECTOR, entry_INT80_32);
软中断就是调用此
一个软中断触发的时候,entry_INT80_32就被调用了
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
然后在其中,我们利用系统调用号进行调用,然后传入寄存器中的参数
然后结束系统调用后,调用INTERRUPT_RETURN,就是对应的iret
将用户态的现场恢复回来,包含代码段,指令指针寄存器
2.64的调用
/* 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 |
代码如上,不过,将系统调用名称转换为系统调用名,放在寄存器rax中,然后进行真正syscall指令
传递的寄存器使用了一种特殊的寄存器,叫做特殊模块寄存器 MSR
trap_init在64位中,除了上面的中断模式,还有对应的syscall_init
wrmsrl(MSR_LSTAR, (unsigned long)entry_SYSCALL_64);
里面有读写特殊模块寄存器的,MSR_LSTAR就是这样特殊的寄存器
sys_call调用系统指令的时候,就会从这个寄存器中拿出函数地址来调用,调用entry_SYSCALL_64
内部也是保存了寄存器到pt_regs结构,比如用户态的代码段,数据段,保存参数的寄存器
然后entry_SYSCALL64_slow_pat -> do_syscall_64
do_syscall_64中, 从rax中拿到系统调用号,然后调用相关函数
然后64位系统返回调用的时候,执行的是USERGS_SYSRET64,最终一步步走指令到sysretq
调用流程如下
最终32位还是64位,都会调用到系统调用表sys_call_table中
那么系统调用表如何书写的呢?
32位和系统调用表定义和64位的定义是在不同的位置的
在不同系列中的表示不同,但是内在都是一样的,系统调用在内核中的实现函数都有一个声明,声明往往在include/linux/syscalls.h文件中
真正的实现在另外的文件中,利用面向接口的思想进行了解耦
比如sys_open实际的实现代码如下
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个参数
根据参数来选择不同的宏
然后再编译称为对应的 .h文件
这时候会生成两个脚本,对应着生成 unistd_32.h和unistd_64.h
对应着不同系统
在32系统的arch/x86/entry/syscall_32.c中,定义了一张表,里面include这个文件,这个sys系统调用都在这个表中
在64系统的arch/x86/entry/syscall_64.c中,定义了一张表,里面include这个文件,这个sys系统调用都在这个表中
整体的过程图下: