文章目录
glibc对系统调用的封装
Linux提供了glibc这个中介,它更熟悉系统调用的细节,并且可以封装成更加友好的接口。
通过最常用的系统调用open为例,看看系统调用是如何实现的。
用户态进程里调用open函数如下:
大部分用户会选择glibc里面的open函数,定义如下
int open(const char *pathname, int flags, mode_t mode)
glibc源代码中,有个文件syscalls.list,里面列着所有glibc的函数对应的系统调用,就像下面这样
# File name Caller Syscall name Args Strong name Weak names
open - open Ci:siv __libc_open __open open
glibc源代码中,还有一个脚本make-syscall.sh,可以根据syscalls.list配置文件,对于每个封装好的系统调用,生成一个文件,文件里面定义了一些宏,例如
#define SYSCALL_NAME open
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 也是一个宏,定义如下:
#define PSEUDO(name, syscall_name, args)
.text;
ENTRY (name)
DO_CALL (syscall_name, args);
cmpl $-4095, %eax;
jae SYSCALL_ERROR_LABEL
里面对于任何一个系统调用,会调用DO_CALL,这也是一个宏,宏32位和64位的定义不一样。
32位系统调用过程
在 Linux的源代码注释里,可以看到寄存器是如何传递系统调用号和参数的。
/* 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_init(),其中有这样的代码
set_system_intr_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,实现如下
static __always_inline void do_syscall_32_irqs_on(struct pt_regs *regs)
{
struct thread_info *ti = current_thread_info();
unsigned int nr = (unsigned int)regs->orig_ax;
......
if (likely(nr < IA32_NR_syscalls)) {
regs->ax = ia32_sys_call_table[nr](
(unsigned int)regs->bx, (unsigned int)regs->cx,
(unsigned int)regs->dx, (unsigned int)regs->si,
(unsigned int)regs->di, (unsigned int)regs->bp);
}
syscall_return_slowpath(regs);
}
- 将系统调用号从eax里面取出来,然后根据系统调用号,在系统调用表中找到相应的函数进行调用
- 将寄存器中保存的参数取出来,作为函数参数。
根据宏定义 #define ia32_sys_call_table sys_call_table
,系统调用就放在这个表里面。
当系统调用结束之后,在entry_INT80_32之后,紧接着调用的是INTERRUPT_RETURN,定义如下
#define INTERRUPT_RETURN iret
iret指令将原来用户态保存的现场恢复回来,包含代码段、指令指针寄存器等,这个时候用户态恢复执行。
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指令。
- 传递参数的寄存器也变了
syscall指令还使用了一种特殊的寄存器,特殊模块寄存器(MSR),这种寄存器是CPU为了完成某些特殊控制功能为目的的寄存器,其中就有系统调用。
系统初始化的时候,trap_init 初始化了上面的中断模式,还会调用cpu_init->syscall_init,里面有这样的代码
wrmsrl(MSR_LSTAR, (unsigned long)entry_SYSCALL_64);
rdmsr和wrmsrl是用来读写特殊模块寄存器的,MSR_LSTAR是一个特殊的寄存器,当syscalll指令调用的时候,会从这个寄存器里面拿出函数地址来调用,也就是调用entry_SYSCALL_64。
在arch/x86/entry/entry_64.S 中定义了 entry_SYSCALL_64。
ENTRY(entry_SYSCALL_64)
/* Construct struct pt_regs on stack */
pushq $__USER_DS /* pt_regs->ss */
pushq PER_CPU_VAR(rsp_scratch) /* pt_regs->sp */
pushq %r11 /* pt_regs->flags */
pushq $__USER_CS /* pt_regs->cs */
pushq %rcx /* pt_regs->ip */
pushq %rax /* pt_regs->orig_ax */
pushq %rdi /* pt_regs->di */
pushq %rsi /* pt_regs->si */
pushq %rdx /* pt_regs->dx */
pushq %rcx /* pt_regs->cx */
pushq $-ENOSYS /* pt_regs->ax */
pushq %r8 /* pt_regs->r8 */
pushq %r9 /* pt_regs->r9 */
pushq %r10 /* pt_regs->r10 */
pushq %r11 /* pt_regs->r11 */
sub $(6*8), %rsp /* pt_regs->bp, bx, r12-15 not saved */
movq PER_CPU_VAR(current_task), %r11
testl $_TIF_WORK_SYSCALL_ENTRY|_TIF_ALLWORK_MASK, TASK_TI_flags(%r11)
jnz entry_SYSCALL64_slow_path
......
entry_SYSCALL64_slow_path:
/* IRQs are off. */
SAVE_EXTRA_REGS
movq %rsp, %rdi
call do_syscall_64 /* returns with IRQs disabled */
return_from_SYSCALL_64:
RESTORE_EXTRA_REGS
TRACE_IRQS_IRETQ
movq RCX(%rsp), %rcx
movq RIP(%rsp), %r11
movq R11(%rsp), %r11
......
syscall_return_via_sysret:
/* rcx and r11 are already restored (see code above) */
RESTORE_C_REGS_EXCEPT_RCX_R11
movq RSP(%rsp), %rsp
USERGS_SYSRET64
这里先保存了很多寄存器到pt_regs结构里面,例如用户态的代码段、数据段、保存参数的寄存器,然后调用entry_SYSCALL64_slow_pat->do_syscall_64
__visible void do_syscall_64(struct pt_regs *regs)
{
struct thread_info *ti = current_thread_info();
unsigned long nr = regs->orig_ax;
......
if (likely((nr & __SYSCALL_MASK) < NR_syscalls)) {
regs->ax = sys_call_table[nr & __SYSCALL_MASK](
regs->di, regs->si, regs->dx,
regs->r10, regs->r8, regs->r9);
}
syscall_return_slowpath(regs);
}
在do_syscall_64里面,从rax里面拿出系统调用号,然后根据系统调用号,在系统调用表sys_call_table中找到相应的函数进行调用,并将寄存器中保存的参数取出来,作为函数参数
系统调用返回的时候,执行的是USERGS_SYSRET64,定义如下
#define USERGS_SYSRET64 \
swapgs; \
sysretq;
返回用户态的指令变成了sysretq。å
无论是32位,还是64位,都会到系统调用表sys_call_table这里来。
系统调用表
32位的系统调用表定义在arch/x86/entry/syscalls/syscall_32.tbl 文件里,例如 open定义如下:
5 i386 open sys_open compat_sys_open
64 位的系统调用定义在另一个文件 arch/x86/entry/syscalls/syscall_64.tbl 里。例如 open 定义如下:
2 common open sys_open
- 第一列的数字是系统调用号,32和64位系统调用号是不一样的
- 第三列是系统调用的名字
- 第四列是系统调用再内核的实现函数
系统调用在内核中的实现要有一个声明,声明往往在include/linux/syscalls.h 文件中,sys_open的声明如下:
asmlinkage long sys_open(const char __user *filename,
int flags, umode_t mode);
实现这个系统调用的再.c文件中,例如sys_open的实现在fs/open.c里面
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个参数,根据参数的数目选择宏,
#define SYSCALL_DEFINE1(name, ...) SYSCALL_DEFINEx(1, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE2(name, ...) SYSCALL_DEFINEx(2, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE4(name, ...) SYSCALL_DEFINEx(4, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE5(name, ...) SYSCALL_DEFINEx(5, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE6(name, ...) SYSCALL_DEFINEx(6, _##name, __VA_ARGS__)
#define SYSCALL_DEFINEx(x, sname, ...) \
SYSCALL_METADATA(sname, x, __VA_ARGS__) \
__SYSCALL_DEFINEx(x, sname, __VA_ARGS__)
#define __PROTECT(...) asmlinkage_protect(__VA_ARGS__)
#define __SYSCALL_DEFINEx(x, name, ...) \
asmlinkage long sys##name(__MAP(x,__SC_DECL,__VA_ARGS__)) \
__attribute__((alias(__stringify(SyS##name)))); \
static inline long SYSC##name(__MAP(x,__SC_DECL,__VA_ARGS__)); \
asmlinkage long SyS##name(__MAP(x,__SC_LONG,__VA_ARGS__)); \
asmlinkage long SyS##name(__MAP(x,__SC_LONG,__VA_ARGS__)) \
{ \
long ret = SYSC##name(__MAP(x,__SC_CAST,__VA_ARGS__)); \
__MAP(x,__SC_TEST,__VA_ARGS__); \
__PROTECT(x, ret,__MAP(x,__SC_ARGS,__VA_ARGS__)); \
return ret; \
} \
static inline long SYSC##name(__MAP(x,__SC_DECL,__VA_ARGS__)
如果我们把宏展开之后,实现如下,和声明的是一样的。
asmlinkage long sys_open(const char __user * filename, int flags, int mode)
{
long ret;
if (force_o_largefile())
flags |= O_LARGEFILE;
ret = do_sys_open(AT_FDCWD, filename, flags, mode);
asmlinkage_protect(3, ret, filename, flags, mode);
return ret;
声明和实现都好了。
- 在编译的过程中,需要根据 syscall_32.tbl 和 syscall_64.tbl 生成自己的 unistd_32.h 和 unistd_64.h。生成方式在 arch/x86/entry/syscalls/Makefile 中。
- 这里面会使用两个脚本,其中第一个脚本 arch/x86/entry/syscalls/syscallhdr.sh,会在文件中生成 #define __NR_open
- 第二个脚本 arch/x86/entry/syscalls/syscalltbl.sh,会在文件中生成 __SYSCALL(__NR_open, sys_open)。
- unistd_32.h 和 unistd_64.h 是对应的系统调用号和系统调用实现函数之间的对应关系。
在文件 arch/x86/entry/syscall_32.c,定义了这样一个表,里面 include 了这个头文件,从而所有的 sys_ 系统调用都在这个表里面了。
__visible const sys_call_ptr_t ia32_sys_call_table[__NR_syscall_compat_max+1] = {
/*
* Smells like a compiler bug -- it doesn't work
* when the & below is removed.
*/
[0 ... __NR_syscall_compat_max] = &sys_ni_syscall,
#include <asm/syscalls_32.h>
};
同理,在文件 arch/x86/entry/syscall_64.c,定义了这样一个表,里面 include 了这个头文件,这样所有的 sys_ 系统调用就都在这个表里面了。
/* System call table for x86-64. */
asmlinkage const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
/*
* Smells like a compiler bug -- it doesn't work
* when the & below is removed.
*/
[0 ... __NR_syscall_max] = &sys_ni_syscall,
#include <asm/syscalls_64.h>
};