从上一集的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系统调用都在这个表中

整体的过程图下:

图片

发表评论

邮箱地址不会被公开。 必填项已用*标注