热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

【Linux】系统调用

【Linux】系统调用本文以open为例,解析从glibc如何调用到内核的open。NOTE:glibc是Linux提供给用户友好的系统调用接口。首先&




【Linux】系统调用

本文以 open 为例,解析从 glibc 如何调用到内核的 open


NOTE:glibc 是 Linux 提供给用户友好的系统调用接口。


首先,我们在用户态里调用 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,可以根据上面的配置文件,对于每一个封装好的系统调用,生成一个文件。这个文件里面定义了一些宏,例如 #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 位系统调用过程

先来看 32 位的情况(i386 目录下的 sysdep.h 文件)。

/* 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(),其中有这样的代码:

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

通过 pushSAVE_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();
## 1、将系统调用号从 eax 里面取出来
unsigned int nr = (unsigned int)regs->orig_ax;
......
if (likely(nr < IA32_NR_syscalls)) {
## 2、根据系统调用号&#xff0c;在系统调用表中找到相应的函数进行调用
regs->ax &#61; ia32_sys_call_table[nr](
## 3、将寄存器中保存的参数取出来&#xff0c;作为函数参数
(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);
}

在这里&#xff0c;我们看到&#xff0c;将系统调用号从 eax 里面取出来&#xff0c;然后根据系统调用号&#xff0c;在系统调用表中找到相应的函数进行调用&#xff0c;并将寄存器中保存的参数取出来&#xff0c;作为函数参数。如果仔细比对&#xff0c;就能发现&#xff0c;这些参数所对应的寄存器&#xff0c;和 Linux 的注释是一样的。

根据宏定义&#xff0c;#define ia32_sys_call_table sys_call_table&#xff0c;系统调用就是放在这个表里面

当系统调用结束之后&#xff0c;在 entry_INT80_32 之后&#xff0c;紧接着调用的是 INTERRUPT_RETURN&#xff0c;我们能够找到它的定义&#xff0c;也就是 iret

#define INTERRUPT_RETURN iret

iret 指令将原来用户态保存的现场恢复回来&#xff0c;包含代码段、指令指针寄存器等。这时候用户态进程恢复执行。

image-20221105210914116


64 位系统调用过程

再来看 64 位的情况&#xff08;x86_64 下的 sysdep.h 文件&#xff09;。

/* 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

和之前一样&#xff0c;还是将系统调用名称转换为系统调用号&#xff0c;放到寄存器 rax。这里是真正进行调用&#xff0c;不是用中断了&#xff0c;而是改用 syscall 指令了。并且&#xff0c;通过注释我们也可以知道&#xff0c;传递参数的寄存器也变了。

syscall 指令还使用了一种特殊的寄存器&#xff0c;叫 特殊模块寄存器&#xff08;Model Specific Registers&#xff0c;简称 MSR&#xff09;。这种寄存器是 CPU 为了完成某些特殊控制功能为目的的寄存器&#xff0c;其中就有系统调用。

在系统初始化的时候&#xff0c;trap_init 除了初始化上面的中断模式&#xff0c;这里面还会调用 cpu_init->syscall_init。这里面有这样的代码&#xff1a;

wrmsrl(MSR_LSTAR, (unsigned long)entry_SYSCALL_64);

rdmsrwrmsr 是用来 读写特殊模块寄存器的MSR_LSTAR 就是这样一个特殊的寄存器&#xff0c;当 syscall 指令调用的时候&#xff0c;会从这个寄存器里面拿出函数地址来调用&#xff0c;也就是调用 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 结构里面&#xff0c;例如用户态的代码段、数据段、保存参数的寄存器&#xff0c;然后调用 entry_SYSCALL64_slow_path->do_syscall_64

__visible void do_syscall_64(struct pt_regs *regs)
{
struct thread_info *ti &#61; current_thread_info();
## 1、将系统调用号从 rax 里面取出来
unsigned long nr &#61; regs->orig_ax;
......
if (likely((nr & __SYSCALL_MASK) < NR_syscalls)) {
## 2、根据系统调用号查系统调用表
regs->ax &#61; sys_call_table[nr & __SYSCALL_MASK](
## 3、将寄存器中保存的参数取出来&#xff0c;作为函数参数
regs->di, regs->si, regs->dx,
regs->r10, regs->r8, regs->r9);
}
syscall_return_slowpath(regs);
}

do_syscall_64 里面&#xff0c;从 rax 里面拿出系统调用号&#xff0c;然后根据系统调用号&#xff0c;在系统调用表 sys_call_table 中找到相应的函数进行调用&#xff0c;并将寄存器中保存的参数取出来&#xff0c;作为函数参数。如果仔细比对&#xff0c;你就能发现&#xff0c;这些参数所对应的寄存器&#xff0c;和 Linux 的注释又是一样的。

所以&#xff0c;无论是 32 位&#xff0c;还是 64 位&#xff0c;都会到系统调用表 sys_call_table 这里来。

在研究系统调用表之前&#xff0c;我们看 64 位的系统调用返回的时候&#xff0c;执行的是 USERGS_SYSRET64。定义如下&#xff1a;

#define USERGS_SYSRET64 \
swapgs; \
sysretq;

这里&#xff0c;返回用户态的指令变成了 sysretq

image-20221105212234625


系统调用表

前面我们重点关注了系统调用的方式&#xff0c;都是最终到了系统调用表&#xff0c;但是到底调用内核的什么函数&#xff0c;还没有解读。

现在我们再来看&#xff0c;系统调用表 sys_call_table 是怎么形成的呢&#xff1f;

32 位的系统调用表定义在面 arch/x86/entry/syscalls/syscall_32.tbl 文件里。例如 open 是这样定义的&#xff1a;

5 i386 open sys_open compat_sys_open

64 位的系统调用定义在另一个文件 arch/x86/entry/syscalls/syscall_64.tbl 里。例如 open 是这样定义的&#xff1a;

2 common open sys_open

第一列的数字是系统调用号。可以看出&#xff0c;32 位和 64 位的系统调用号是不一样的。第三列是系统调用的名字&#xff0c;第四列是系统调用在内核的实现函数。不过&#xff0c;它们都是以 sys_ 开头。

**系统调用在内核中的实现函数要有一个声明。**声明往往在 include/linux/syscalls.h 文件中。例如 sys_open 是这样声明的&#xff1a;

asmlinkage long sys_open(const char __user *filename,
int flags, umode_t mode);

系统调用的真正实现&#xff0c;一般在一个 .c 文件里面&#xff0c;例如 sys_open 的实现在 fs/open.c 里面&#xff0c;但是你会发现样子很奇怪。

SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)
{
if (force_o_largefile())
flags |&#61; O_LARGEFILE;
return do_sys_open(AT_FDCWD, filename, flags, mode);
}

SYSCALL_DEFINE3 是一个 宏系统调用&#xff0c;最多六个参数&#xff0c;它根据参数的数目选择宏。具体是这样定义的&#xff1a;

#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 &#61; 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__)

如果我们把宏展开之后&#xff0c;实现如下&#xff0c;和声明的是一样的。

asmlinkage long sys_open(const char __user * filename, int flags, int mode)
{
long ret;


if (force_o_largefile())
flags |&#61; O_LARGEFILE;


ret &#61; do_sys_open(AT_FDCWD, filename, flags, mode);
asmlinkage_protect(3, ret, filename, flags, mode);
return ret;

声明和实现都好了。接下来&#xff0c;在编译的过程中&#xff0c;需要根据 syscall_32.tblsyscall_64.tbl 生成自己的 unistd_32.hunistd_64.h。生成方式在 arch/x86/entry/syscalls/Makefile 中。

之后&#xff0c;unistd_32.hunistd_64.h对应的系统调用号和系统调用实现函数之间的对应关系

在文件 arch/x86/entry/syscall_32.c&#xff0c;定义了这样一个表&#xff0c;里面 include 了这个头文件&#xff0c;从而所有的 sys_ 系统调用都在这个表里面了。

__visible const sys_call_ptr_t ia32_sys_call_table[__NR_syscall_compat_max&#43;1] &#61; {
/*
* Smells like a compiler bug -- it doesn&#39;t work
* when the & below is removed.
*/

[0 ... __NR_syscall_compat_max] &#61; &sys_ni_syscall,
#include
};

同理&#xff0c;在文件 arch/x86/entry/syscall_64.c&#xff0c;定义了这样一个表&#xff0c;里面 include 了这个头文件&#xff0c;这样所有的 sys_ 系统调用就都在这个表里面了。

/* System call table for x86-64. */
asmlinkage const sys_call_ptr_t sys_call_table[__NR_syscall_max&#43;1] &#61; {
/*
* Smells like a compiler bug -- it doesn&#39;t work
* when the & below is removed.
*/
[0 ... __NR_syscall_max] &#61; &sys_ni_syscall,
#include
};

X86 的总结

image-20221105213413038







推荐阅读
author-avatar
zerosmall
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有