防止断更 请务必加首发微信:1716143665
关闭
讲堂
客户端下载
兑换中心
企业版
渠道合作
推荐作者

09 | 系统调用:公司成立好了就要开始接项目

2019-04-15 刘超(加微信:642945106 发送“赠送”领取赠送精品课程 发数字“2”获取众筹列表。)
趣谈Linux操作系统
进入课程

讲述:刘超(加微信:642945106 发送“赠送”领取赠送精品课程 发数字“2”获取众筹列表。)

时长14:24大小13.20M

上一节,系统终于进入了用户态,公司由一个“皮包公司”进入正轨,可以开始接项目了。

这一节,我们来解析 Linux 接项目的办事大厅是如何实现的,这是因为后面介绍的每一个模块,都涉及系统调用。站在系统调用的角度,层层深入下去,就能从某个系统调用的场景出发,了解内核中各个模块的实现机制。

有的时候,我们的客户觉得,直接去办事大厅还是不够方便。没问题,Linux 还提供了 glibc 这个中介。它更熟悉系统调用的细节,并且可以封装成更加友好的接口。你可以直接用。

glibc 对系统调用的封装

我们以最常用的系统调用 open,打开一个文件为线索,看看系统调用是怎么实现的。这一节我们仅仅会解析到从 glibc 如何调用到内核的 open,至于 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,可以根据上面的配置文件,对于每一个封装好的系统调用,生成一个文件。这个文件里面定义了一些宏,例如 #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。

在 Linux 的源代码注释里面,我们可以清晰地看到,这些寄存器是如何传递系统调用号和参数的。

这里面的 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
复制代码

通过 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 里面取出来,然后根据系统调用号,在系统调用表中找到相应的函数进行调用,并将寄存器中保存的参数取出来,作为函数参数。如果仔细比对,就能发现,这些参数所对应的寄存器,和 Linux 的注释是一样的。

根据宏定义,#define ia32_sys_call_table sys_call_table,系统调用就是放在这个表里面。至于这个表是如何形成的,我们后面讲。

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

#define INTERRUPT_RETURN iret
复制代码

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

这里我总结一下 32 位的系统调用是如何执行的。

64 位系统调用过程

我们再来看 64 位的情况(x86_64 下的 sysdep.h 文件)。

/* 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 指令还使用了一种特殊的寄存器,我们叫特殊模块寄存器(Model Specific Registers,简称 MSR)。这种寄存器是 CPU 为了完成某些特殊控制功能为目的的寄存器,其中就有系统调用。

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

wrmsrl(MSR_LSTAR, (unsigned long)entry_SYSCALL_64);
复制代码

rdmsr 和 wrmsr 是用来读写特殊模块寄存器的。MSR_LSTAR 就是这样一个特殊的寄存器,当 syscall 指令调用的时候,会从这个寄存器里面拿出函数地址来调用,也就是调用 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 中找到相应的函数进行调用,并将寄存器中保存的参数取出来,作为函数参数。如果仔细比对,你就能发现,这些参数所对应的寄存器,和 Linux 的注释又是一样的。

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

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

#define USERGS_SYSRET64 \
swapgs; \
sysretq;
复制代码

这里,返回用户态的指令变成了 sysretq。

我们这里总结一下 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 位的系统调用号是不一样的。第三列是系统调用的名字,第四列是系统调用在内核的实现函数。不过,它们都是以 sys_ 开头。

系统调用在内核中的实现函数要有一个声明。声明往往在 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 是一个宏系统调用最多六个参数,根据参数的数目选择宏。具体是这样定义的:

#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>
};
复制代码

总结时刻

系统调用的过程还是挺复杂的吧?如果加上上一节的内核态和用户态的模式切换,就更复杂了。这里我们重点分析 64 位的系统调用,我将整个完整的过程画了一张图,帮你总结、梳理一下。

课堂练习

请你根据这一节的分析,看一下与 open 这个系统调用相关的文件都有哪些,在每个文件里面都做了什么?如果你要自己实现一个系统调用,能不能照着 open 来一个呢?

欢迎留言和我分享你的疑惑和见解,也欢迎你收藏本节内容,反复研读。你也可以把今天的内容分享给你的朋友,和他一起学习、进步。

© 加微信:642945106 发送“赠送”领取赠送精品课程 发数字“2”获取众筹列表。
上一篇
08 | 内核初始化:生意做大了就得成立公司
下一篇
10 | 进程:公司接这么多项目,如何管?
 写留言

1716143665 拼课微信(50)

  • 孟晓冬
    2019-04-15
    106
    这个专栏要有一定的知识储备才能学习,起码要熟悉c,数据结构,linux系统管理,否则只会一脸懵逼的进来,一脸懵逼的出去
  • why
    2019-04-15
    33
    - glibc 将系统调用封装成更友好的接口
    - 本节解析 glibc 函数如何调用到内核的 open
    ---
    - 用户进程调用 open 函数
        - glibc 的 syscal.list 列出 glibc 函数对应的系统调用
        - glibc 的脚本 make_syscall.sh 根据 syscal.list 生成对应的宏定义(函数映射到系统调用)
        - glibc 的 syscal-template.S 使用这些宏, 定义了系统调用的调用方式(也是通过宏)
        - 其中会调用 DO_CALL (也是一个宏), 32位与 64位实现不同
    ---
    - 32位 DO_CALL (位于 i386 目录下 sysdep.h)
        - 将调用参数放入寄存器中, 由系统调用名得到系统调用号, 放入 eax
        - 执行 ENTER_KERNEL(一个宏), 对应 int $0x80 触发软中断, 进入内核
        - 调用软中断处理函数 entry_INT80_32(内核启动时, 由 trap_init() 配置)
        - entry_INT80_32 将用户态寄存器存入 pt_regs 中(保存现场以及系统调用参数), 调用 do_syscall_32_iraq_on
        - do_syscall_32_iraq_on 从 pt_regs 中取系统调用号(eax), 从系统调用表得到对应实现函数, 取 pt_regs 中存储的参数, 调用系统调用
        - entry_INT80_32 调用 INTERRUPT_RUTURN(一个宏)对应 iret 指令, 系统调用结果存在 pt_regs 的 eax 位置, 根据 pt_regs 恢复用户态进程
    ---
    - 64位 DO_CALL (位于 x86_64 目录下 sysdep.h)
        - 通过系统调用名得到系统调用号, 存入 rax; 不同中断, 执行 syscall 指令
        - MSR(特殊模块寄存器), 辅助完成某些功能(包括系统调用)
        - trap_init() 会调用 cpu_init->syscall_init 设置该寄存器
        - syscall 从 MSR 寄存器中, 拿出函数地址进行调用, 即调用 entry_SYSCALL_64
        - entry_SYSCALL_64 先保存用户态寄存器到 pt_regs 中
        - 调用 entry_SYSCALL64_slow_pat->do_syscall_64
        - do_syscall_64 从 rax 取系统调用号, 从系统调用表得到对应实现函数, 取 pt_regs 中存储的参数, 调用系统调用
        - 返回执行 USERGS_SYSRET64(一个宏), 对应执行 swapgs 和 sysretq 指令; 系统调用结果存在 pt_regs 的 ax 位置, 根据 pt_regs 恢复用户态进程
    ---
    - 系统调用表 sys_call_table
        - 32位 定义在 arch/x86/entry/syscalls/syscall_32.tbl
        - 64位 定义在 arch/x86/entry/syscalls/syscall_64.tbl
        - syscall_*.tbl 内容包括: 系统调用号, 系统调用名, 内核实现函数名(以 sys 开头)
        - 内核实现函数的声明: include/linux/syscall.h
        - 内核实现函数的实现: 某个 .c 文件, 例如 sys_open 的实现在 fs/open.c
            - .c 文件中, 以宏的方式替代函数名, 用多层宏构建函数头
        - 编译过程中, 通过 syscall_*.tbl 生成 unistd_*.h 文件
            - unistd_*.h 包含系统调用与实现函数的对应关系
        - syscall_*.h include 了 unistd_*.h 头文件, 并定义了系统调用表(数组)
    展开
  • weihebuken
    2019-04-15
    7
    我想问,想看懂这篇,我先需要看哪些书,或者贮备哪些知识先,真的很懵。。。
  • William
    2019-04-15
    6
    大家可以参考glibc的源码理解,https://www.gnu.org/software/libc/started.html。 主要过程是CPU上下文切换的过程。
  • 阿恺
    2019-04-15
    3
    老师,请教个问题,对于64位,DO_CALL在两个地方有地址,sysdeps/unix/sysv/linux/x86_64/sysdep.h:179和sysdeps/unix/x86_64/sysdep.h:26,我采用的最新的glibc的git下载。看到的和您给的代码不一样,您采用了前者的注释,后者的代码,两者使用的寄存器不一样。如何知道是通过哪个入口。sysdeps/unix/sysv/linux/x86_64/sysdep.h:179中注释写到,将系统调用号放在rax,后面的代码中的是eax,这里没有看懂。
    展开
  • 刘強
    2019-04-15
    3
    这个专栏,源码是linux哪个版本的?
    展开
  • 春明
    2019-04-15
    3
    开始吃力了,只能排除细节,先了解几个重要阶段了。
    展开
  • kdb_reboot
    2019-04-15
    3
    参数如果超过6个存在哪里?(32/64两种情况
    展开

    作者回复: 不许超过,系统调用可以查一下,没这么多参数

  • 望天
    2019-05-23
    2
    这些东西我觉得不必要深入每一行代码,大概过一遍,知道整体流程,宏观流程就OK了(比如上面图片的概括)。反正很多细节过一段时间也会忘。

    作者回复: 对的

  • Sharry
    2019-05-15
    1
    什么是系统调用?

    系统调用是操作系统提供给程序设计人员使用系统服务的接口

    系统调用流程

    Linux 提供了 glibc 库, 它封装了系统调用接口, 对上层更友好的提供服务, 系统调用最终都会通过 DO_CALL 发起, 这是一个宏定义, 其 32 位和 64 位的定义是不同的
    - 32 位系统调用
       - 用户态
          - 将请求参数保存到寄存器
          - 将系统调用名称转为系统调用号保存到寄存器 eax 中
          - 通过软中断 ENTER_KERNEL 进入内核态
       - 内核态
          - 将用户态的寄存器保存到 pt_regs 中
          - 在系统调用函数表 sys_call_table 中根据调用号找到对应的函数
          - 执行函数实现, 将返回值写入 pt_regs 的 ax 位置
          - 通过 INTERRUPT_RETURN 根据 pt_regs 恢复用户态进程

    - 64 位系统调用
       - 用户态
          - 将请求参数保存到寄存器
          - 将系统调用名称转为系统调用号保存到寄存器 rax 中
          - **通过 syscall 进入内核态**
       - 内核态
          - 将用户态的寄存器保存到 pt_regs 中
          - 在系统调用函数表 sys_call_table 中根据调用号找到对应的函数
          - 执行函数实现, 将返回值写入 pt_regs 的 ax 位置
          - **通过 sysretq 返回用户态**
    展开
  • 逆流的鱼
    2019-04-21
    1
    系统调用都会导致用户态切换内核态?而纯计算的不会?
    展开

    作者回复: 系统调用都会,纯计算看算什么了,算加法不用进内核

  • 陈锴
    2019-04-17
    1
    有个小问题,64位内核是不是已经取消使用cs 代码寄存器 和 ds数据段寄存器了(或者说默认设为0了),也就是只采用分页而不采用分段了

    作者回复: 分段还是有,只不过是残废的状态,就像你说的一样,到了内存那一节会详细说这个问题

  • 青石
    2019-04-16
    1
    完全不懂C,内容看起来真的吃力,以下是个人理解,不知道对不对,还请指正。

    文中有一部分代码是在glibc源码中的,git clone git://sourceware.org/git/glibc.git

    32位系统调用过程:

    1. 系统调用最多6个参数;
    2. syscall number 对应的是%eax,所有参数存放在寄存器里;
    3. 执行系统调用时执行int $0x80触发软中断陷入内核;
    4. 陷入内核态之前需要保存用户态,这里面有个pushl %eax(和2中寄存器地址相同),将%eax存入pt_regs结构中,保存完用户态现场应该就进入内核态了;
    5. 进入内核态后调用do_syscall_32_irqs_on,取出syscall number及参数,do_syscall_32_irqs_on中的ax、bx、cx、dx、si、di、bp对应system call arguments中的%eax、%ebx、%ecx、%edx、%esi、%edi、%ebp,这些值是06节讲到的32位系统通用寄存器的数据单元。在内核态执行系统调用;
    6. 完成后调用INTERRUPT_RETURN恢复用户态保存的现场。


    64位系统调用:

    1. 同样最多6个参数,但是与32位寄存器地址不同,64位系统的寄存器地址有16个,以r开始,32位系统的寄存器地址有8个,以e开始;
    2. syscall_name转为syscall number存入寄存器%rax;
    3. 调用syscall执行系统调用,期间的过程同样会陷入内核态;
    4. 调用MSR_LSTAR -> entry_SYSCALL_64保存用户态现场,具体参数如何传进去的看不懂;
    5. 调用do_syscall_64,取出系统调用号和参数(和32位没区别),在内核态执行系统调用;
    6. 完成后调用USERGS_SYSRET64返回,sysretq恢复用户态现场。
    展开
  • 小颜
    2019-04-16
    1
    此处仅展示32位系统调用:

    glibc大部分使用脚本封装生成代码,用到三种文件:
        1.make-syscall.sh:读取syscalls.list文件的内容,对文件的每一行进行解析。根据每一行的内容生成一个.S汇编文件,汇编文件封装了一个系统调用。文件路径:sysdeps/unix/make-syscall.sh
        2.syscall-template.S:是系统调用封装代码的模板文件。生成的.S汇编文件都调用它。文件路径:sysdeps/unix/syscall-template.S
        3.syscalls.list:是数据文件,定义了全部的系统调用信息。文件路径有多个:sysdeps/unix/syscalls.list,sysdeps/unix/sysv/linux/syscalls.list,sysdeps/unix/sysv/linux/generic/syscalls.list,sysdeps/unix/sysv/linux/i386/syscalls.list

    1.syscall-template.S生成汇编文件
        在syscall-template.S生成汇编文件中在78行有调用T_PSEUDO (SYSCALL_SYMBOL, SYSCALL_NAME, SYSCALL_NARGS)方法,
    2.T_PSEUDO是一个宏定义,此文件会引用#include <sysdep.h>
    3.在sysdep.h文件中175行定义了函数PSEUDO:
        /* 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

         The stack layout upon entering the function is:

            24(%esp)    Arg# 6
            20(%esp)    Arg# 5
            16(%esp)    Arg# 4
            12(%esp)    Arg# 3
             8(%esp)    Arg# 2
             4(%esp)    Arg# 1
             (%esp)    Return address */
        #define DO_CALL(syscall_name, args)                      \
            PUSHARGS_##args                             \
            DOARGS_##args                             \
            movl $SYS_ify (syscall_name), %eax;                     \
            ENTER_KERNEL                             \
            POPARGS_##args
        在此函数中调用了DO_CALL:将 系统调用 号保存在eax寄存器,其他参数分别保存至其他寄存器
    4。然后调用ENTER_KERNEL,该宏定义在在125行:# define ENTER_KERNEL int $0x80,
    5.int $0x80是一个软中断,将会触发软中断触发函数entry_INT80_32,
    6.entry_INT80_32将用户态的一些信息保存在pt_regs,最终调用do_syscall_32_irqs_on,
    7.do_syscall_32_irqs_on函数将从eax寄存器取出系统调用号,然后根据系统调用号从系统调用表中取出索引,最终取出对应函数,参数从pt_regs中,最终调用系统调用
    8.在函数最后调用INTERRUPT_RETURN iret最终返回数据保存在pt_regs的eax中,并将pt_regs的用户态数据恢复
    展开
  • 时间是最真...
    2019-04-15
    1
    想问一下,java开发的,会一些基础的linux命令,怎么学好这个专栏?感觉看的一头雾水,消化不了,有什么建议吗
  • 安排
    2019-04-15
    1
    进入内核之前,保存所有的寄存器,然后调用 do_syscall_32_irqs_on。

    进入entry_INT80_32的时候已经是内核态了吧?怎么这里说进入内核之前?
    展开

    作者回复: 是的,中断完了就在内核了

  • garlic
    2019-06-03
    1 用户态glibc 32位置
      sysdeps\unix\syscall.list
      sysdeps\unix\syscall-tempate.S
      sysdeps\unix\make-syscalls.sh
      sysdeps\unix\sysv\linux\i386\sysdep.h (32)
      sysdeps\unix\sysv\linux\x86_64\sysdep.h (64)
      生成用户接口

    2. 内核态: X86_32
       /init/main.c
              start_kernel ->trap_init
      /arch/x86/kernel/traps.c
               trap_init -> idt_setup_traps
       /arch/x86/kernel/idt.c
               idt_setup_traps->idt_setup_from_table
              idt_setup_from_table->entry_INT80_32
       /arch/x86/entry/entry_32.S
               entry_INT80_32->do_int80_syscall_32
       /arch/x86/entry/common.c
               do_int80_syscall_32->do_syscall_32_irqs_on
       /arch/x86/entry/syscall_32.c
                ia32_sys_call_table[__NR_syscall_compat_max+1]
       /arch/x86/entry/entry_32.S
                entry_INT80_32->INTERRUPT_RETURN
      /arch/x86/include/asm/irqflags.h
               swapgs, sysretl

      内核态: X86_64
       /init/main.c
              start_kernel ->trap_init
      /arch/x86/kernel/traps.c
               trap_init -> idt_setup_traps
       /arch/x86/kernel/idt.c
               idt_setup_traps->idt_setup_from_table
              idt_setup_from_table->entry_INT80_32
        /arch/x86/kernel/cpu/common.c
              cpu_init->syscall_init
        /arch/x86/entry/entry_64.S
               entry_SYSCALL_64->USERGS_SYSRET64        
       /arch/x86/entry/common.c
               do_syscall_64->do_syscall_32_irqs_on
       /arch/x86/entry/syscall_64.c
              sys_call_table[__NR_syscall_compat_max+1]
      arch/x86/entry/entry_64.S
               entry_SYSCALL_64-> USERGS_SYSRET64
      /arch/x86/include/asm/irqflags.h
              swapgs; sysretq;

    3. 增加一个系统调用
        linux-5.2-rc2/arch/x86/entry/syscalls/syscall_64.tbl
               新增编号
        linux-5.2-rc2/include/linux/syscalls.h
               增加声明
        kernel/linux-5.2-rc2/fs/iadd_test.c
               增加定义目录可选
         kernel/linux-5.2-rc2/fs/Makefile
               修改makefile加入新增源文件。
       作业笔记:https://garlicspace.com/2019/06/02/linux下实现一个系统调用/
         
    展开
  • edwjn
    2019-05-31
    我理解这个中断机制就是一个巧妙的指令跳转,这个指令的指针是指向了do_syscall_32_irqs_on这个方法,在这个方法里会把控制单元里属于用户态的cs、ds保存下来,把调用系统方法需要的参数放到对应的寄存器里,然后切换控制单元为内核态的cs、ds,再根据系统调用表执行对应的系统方法,并拿到结果,把结果放到寄存器里,最后把控制单元里的cs、ds恢复回用户态。这就完成了一个系统调用。
    想知道的一点是,一次系统调用的开销与一次进程切换(进程A->系统调度服务->进程A)的开销是不是一样的?
    展开
  • Akay
    2019-05-30
    这几章看得好困难,有些懵懵懂懂~~~
    展开
  • weiguozhi...
    2019-05-28
    有个问题:首先没有说在这个文章中自己使用的glibc的版本是哪个版本的。其次:make-syscall.sh是错的,应该是make-syscalls.sh

    作者回复: 谢谢,是./sysdeps/unix/make-syscalls.sh。glibc的版本是glibc-2.26

收藏