计算机的基本结构和X86结构

  • CPU:中央处理器

    • 包括三个部分:运算单元、数据单元、控制单元
    • 运算单元:运算器等,做加法、做位移等
    • 数据单元:如CPU内部的缓存和寄存器组,可暂时存放数据和运算结果
    • 控制单元:统一的指挥中心

  • Bus:总线,高速通道

    • 地址总线(Address Bus)

    • 数据总线(Data Bus)

  • Memory:内存,可保存中间结果

  • I/O:输入设备和输出设备

8086结构

数据单元

  • 16位的通用寄存器:8个
    • AX、BX、CX、DX、SP、BP、SI、DI
    • 可分成两个8位使用的寄存器:AX、BX、CX、DX

控制单元

  • IP寄存器:指令指针寄存器,指向代码段中下一条指令位置
  • 16位的段寄存器:4个,指向不同进程的地址空间
    • CS、DS、SS、ES
    • CS:代码段寄存器(Code Segment Register)
    • DS:数据段寄存器
    • SS:栈寄存器
    • CS和DS两者都存储着一个段的起始地址
  • 可知,一个段的大小为\(2^{16}=64k\)

注意点:8086的地址总线地址是20位,即空间为\(1M\),而CS、DS都是16位,需要凑足20位,即“起始地址*16+偏移量

32位处理器

  • 地址总线变为32根,即内存变为\(2^{32}=4G\)
  • 扩展通用寄存器:8个16位拓展到8个32位的,保留16位和8位的使用方式

  • 重新定义段寄存器
    • 内存中某个位置存储着一个表格,表格存储着多项段描述符,而段描述符存放着段起始位置
    • CS、SS、DS、ES仍为16位,但存储的是选择子(Selector),即段描述符(Segment Descriptor)在表格中的位置
  • 实模式(Real Pattern)和保护模式(Protected Pattern)
    • 前者是8086的段寻址方式,系统刚刚启动时,CPU是处于实模式的
    • 后者是重新定义的段寻址方式,当需要更多内存时,便转为保护模式
    • 通过切换模式进行兼容

BIOS到bootloader

BIOS时期

  • 全称:Basic Input and Output System,即基本输入输出系统
    • 位于主板的ROM上,该ROM固化了一些初始化的程序
  • X86系统中,将1M空间最上面的0xF0000到0xFFFFF这64K映射给ROM,到这部分地址访问时,会访问ROM

Eagle_yFI0BCtMEA

  • 电源刚加电的时候,会进行重置工作
    • CS设置为0xFFFF
    • IP设置为0x0000
    • 第一条指令会指向0xFFFF0,该指令有一个JMP命令,跳到ROM中做初始化工作的代码,BIOS便开始进行初始化的工作
  • 首先,BIOS会检查系统的硬件
  • 然后,建立中断向量表和中断服务程序

bootloader时期

  • 操作系统安装在硬盘上,BIOS界面上有个启动盘的选项

  • 启动盘一般位于第一个扇区,占512字节,且以0xAA55结束,会在512字节内启动相关的代码

  • Linux中有个叫Grub2的工具,是搞系统启动的

    • 全称:Grand Unified Bootloader Version 2

    • 配置系统启动的选项

      1
      grub2 -mkconfig -o /boot/grub2/grub.cfg
    • 将启动程序安装到相应的位置

      1
      grub2 -install /dev/sda
  • grub2第一个安装的是boot.img,它由boot.S编译而成,有512字节,正式安装到启动盘的第一个扇区,该扇区通常称为MBR(Master Boot Record,主引导记录/扇区)

  • BIOS完成任务后,会将boot.img从硬盘加载到内存中的0x7c00来运行,boot.img会加载grub2的另一个镜像core.img

  • core.imglzma_decompress.img、diskboot.img、kernel.img和一系列模块组成,功能丰富

  • boot.img先加载的是core.img的第一个扇区,从硬盘启动的话,这个扇区存储着diskboot.img,对应代码为diskboot.S

  • boot.img将控制权交给diskboot.imgdiskboot.img的任务是将core.img的其他部分加载进来

    • 首先是解压缩程序lzma_decompress.img,对应代码是stratup_raw.S执行时,会调用real_to_prot,切换到保护模式
    • 然后是kernel.img,它是压缩过的,执行时需要解压缩
    • 最后是各个模块module对应的映像

从实模式切换到保护模式

  1. 启用分段
    • 在内存里面建立段描述符表,将寄存器里面的段存储器变成段选择子,指向某个段描述符
  2. 启动分页
    • 将内存分成相等大小的块
  3. 打开Gate A20
    • 即第21根地址线的控制线
    • 切换保护模式的函数DATA32 call real_to_prot会打开Gate A20
  4. 解压缩kernel.img,跳转到kernel.img开始运行
    • kernel.img对应代码为startup.S和一堆c文件
    • startup.S会调用grub_main,是grub kernel的主函数,该函数里的grub_load_config()会开始解析grub.conf文件中的配置信息
    • 正常启动时,grub_main最后会调用grub_command_execute("normal", 0, 0),最终会调用grub_normal_execute()函数,该函数中的grub_show_menu()会显示出让你选择的操作系统的列表
    • 选定启动某个操作系统后,就开始调用grub_menu_execute_entry(),解析并执行选择项
      • linux16命令,表示装载指定的内核文件,并传递内核启动参数,grub_cmd_linux() 函数会被调用,会首先读取 Linux 内核镜像头部的一些数据结构,放到内存中的数据结构来,进行检查。如果检查通过,则会读取整个 Linux 内核镜像到内存
      • initrd 命令,用于为即将启动的内核传递 init ramdisk 路径。于是grub_cmd_initrd() 函数会被调用,将 initramfs 加载到内存中来。
    • 然后,grub_command_execute("boot", 0, 0)才开始真正启动内核

内核初始化

内核的启动

  • 从入口函数strat_kernel()开始

  • init/main.c 文件中,start_kernel 相当于内核的 main 函数,里面是各种各样的初始化函数XXXX_init

  • 进程列表初始化:创始进程
    • set_task_stack_end_magic(&init_task)
    • 也称为0号进程,是唯一一个没有通过 fork 或者 kernel_thread 产生的进程,是进程列表的第一个
    • Process List(进程列表)
  • 中断初始化:trap_init()中设置了很多中断门(Interrupt Gate),用于处理各种中断,响应用户需求
    • 其中有一个set_system_intr_gate(IA32_SYSCALL_VECTOR, entry_INT80_32),是系统调用的中断门
    • 系统调用也是通过发送中断的方式进行的
    • 64位的有另外的系统调用方法
  • 内存初始化:mm_init()就是用来初始化内存管理模块的
  • 调度初始化:sched_init()是用来初始化调度模块的
  • 基于内存的文件系统rootfs的初始化:调用vfs_caches_init()
    • 在该函数里面,会调用mnt_init()->init_rootfs(),其中有一行代码,register_filesystem(&rootfs_fs_type)。在 VFS 虚拟文件系统里面注册了一种类型,我们定义为 struct file_system_type rootfs_fs_type
    • 为兼容各种各样的文件系统,将文件的相关数据结构和操作抽象出来,形成一个抽象层对上提供统一的结构,该抽象层即为VFS(Virtual File System),虚拟文件系统
  • 其他方面的初始化:调用rest_init()

初始化一号进程

  • rest_init()的第一个大工作便是,用 kernel_thread(kernel_init, NULL, CLONE_FS) 创建第二个进程,这个是1 号进程

    • 该进程是用户态所有进程的祖先
    • 1号进程将运行一个用户进程,一旦有了用户进程,资源便需要进行区分以及分配,所以x86提供了分层的权限机制,把区域分成了四个Ring,越往里权限越高,越往外权限越低

    • Ring0:存放访问关键资源的代码,也称为内核态(Kernel Mode)
    • Ring3:存放普通的代码程序,也称为用户态(User Mode)
    • 系统处于保护模式时,除了可访问空间变大,另一个功能便是保护,当处于用户态的代码想要执行更高权限的指令,这种行为是被禁止的
    • 当用户态程序要访问核心资源时,可以进行系统调用,此时会暂停用户态程序的执行,进入内核态,内核完成相应操作后,系统调用结束,暂停的程序继续运行
      • 暂停的实现:把程序运行的情况保存下来,把当前CPU寄存器的值暂存到一个地方,系统调用结束后,返回时再恢复回去

    • 整个过程:用户态 -- 系统调用 -- 保存寄存器 -- 内核态执行系统调用 -- 恢复寄存器 -- 返回用户态继续运行

    Eagle_DEOUixLW9T

从内核态到用户态

  • 在执行kernel_thread函数,即创建1号进程的时候,我们处于内核态

  • 当需要到用户态去运行一个程序时

    • kernel_thread的一个参数为一个函数kernel_init,该进程会运行这个函数
    • kernel_init里面,会调用kernel_init_freeable(),其中有段代码
    1
    2
    if(!ramdisk_execute_command)
    ramdisk_execute_command = "/init"
    • 而在kernel_init函数中,有段代码块
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    if(ramdisk_execute_command) {
    ret = run_init_process(ramdisk_execute_command);
    ...
    }
    if(!try_to_run_init_process("/sbin/init") ||
    !try_to_run_init_process("/etc/init") ||
    !try_to_run_init_process("/bin/init") ||
    !try_to_run_init_process("/bin/sh"))
    return 0;

    // run_init_process代码
    static int run_init_process(const char *init_filename) {
    argv_init[0] = init_filename;
    return do_execve(getname_kernel(init_filename),
    (const char __user *const __user *)argv_init,
    (const char __user *const __user *)envp_init);
    }
    • 这说明,1号进程运行的是一个文件run_init_process函数中调用的是do_execve

      • execve是一个系统调用,其作用是运行一个执行文件
    • 它尝试取运行ramdisk的"/init",或者普通文件系统上的"/sbin/init"、"/etc/init"、"/bin/init"、"/bin/sh"。不同版本的Linux会选择不同的文件启动,只要启动一个就可以了

    • 利用执行init文件的机会,从内核态回到用户态,在运行init时,调用了do_execve,其中,do_execve -> do_execveat_common ->exec_binprm -> search_binary_handler,这里面会调用一段代码:

    1
    2
    3
    4
    5
    6
    7
    int search_binary_handler(struct linux_binprm *bprm) {
    ......
    struct linux_binfmt *fmt;
    ......
    retval = fmt->load_binary(bprm);
    .....
    }
    • 也就是说,要运行一个程序,需要加载这个二进制文件,他是有一定格式的
    • Linux一个常用的格式是ELF(Executable and Linkable Format,可执行与可链接格式)
    1
    2
    3
    4
    5
    6
    7
    static struct linux_binfmt elf_format = {
    .module = THIS_MODULE,
    .load_binary = load_elf_binary,
    .load_shlib = load_elf_library,
    .core_dump = elf_core_dump,
    .min_coredump = ELF_EXEC_PAGESIZE,
    };
    • 其实就是先调用load_elf_binary,最后调用start_thread
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    void
    start_thread(struct pt_regs *regs, unsigned long new_ip,
    unsigned long new_sp)
    {
    set_user_gs(regs, 0);
    regs->fs = 0;
    regs->ds = __USER_DS;
    regs->es = __USER_ES;
    regs->ss = __USER_SS;
    regs->cs = __USER_CS;
    regs->ip = new_ip;
    regs->sp = new_sp;
    regs->flags = X86_EFLAGS_IF;
    force_iret();
    }
    EXPORT_SYMBOL_GPL(start_thread);
    • pt_regs便是寄存器,是在系统调用时内核用于保存用户态运行上下文的
    • 将用户态的代码段CS设置为_USER_CS,将用户态的数据段DS设置为_USER_DS,还有指令指针寄存器IP和栈指针寄存器SP,补上了系统调用中保存寄存器的步骤
    • iret是用于从系统调用中返回,这时候会恢复寄存区,CS和IP恢复了,指向用户态下一个要执行的语句,DS和SP也恢复了,指向用户态函数栈的栈顶,下一条指令便从用户态开始运行

ramdisk的作用

  • init在上面步骤中从内核态到用户态了,一开始到用户态的是ramdisk的init,后来会启动真正根文件系统上的init,成为所有用户态进程的祖先
  • Linux访问存储设备,要有驱动才能访问,如果存储系统数目很有限,那驱动可直接放到内核中
  • 但如果存储系统太多了,都放进内核,内核就太大了,因此,我们先弄一个基于内存的文件系统
    • 内存访问是不需要驱动的,这个就是ramdisk,此时它便是根文件系统
  • 运行ramdisk上的/init,等它运行完了就已经在用户态了
  • /init程序会先根据存储系统的类型来加载驱动,有了驱动就可以设置真正的根文件系统,ramdisk上的/init会启动文件系统上的init
  • 接下来便是各种系统的初始化,启动系统的服务和控制台,用户便可以登录进来。

创建2号进程

  • rest_init第二件大事情便是第三个进程--2号进程
    • 该进程统一管内核态的进程
  • kernel_thread(kthreadd, NULL, CLONE_FS|CLONE_FILES)又一次使用kernel_thread函数创建进程
  • 从内核态来看,无论是进程还是线程,我们可以统称为任务(Task),都使用相同结构
  • kthreadd函数,负责所有内核态的线程的调度和管理,是内核态所有线程运行的祖先

小结

系统调用

glibc对系统调用的封装

glibc 作用
syscalls.list 罗列所有glibc的函数对应的系统调用
make-syscall.sh 根据syscalls.list配置文件,对封装好的系统调用生成文件
syscall-template.S 使用宏,定义了系统调用的调用方式

以系统调用open为例子,看系统调用是如何实现的,解析从glibc中是如何调用到内核的open

  • 我们开始在用户态进程里面调用open函数,为了方便,大部分用户会选择使用中介,即调用的是glibc里面的open函数
1
2
\\ open函数的定义
int open(const char *pathname, int flags, mode_t mode)
  • 在glibc的源代码中,有个文件syscalls.list,里面列着所有glibc的函数对应的系统调用
1
2
# File name Caller  Syscall name    Args    Strong name    Weak names
open - open Ci:siv __libc_open __open ope
  • glibc还有一个脚本make-syscall.sh,可根据上面的配置文件,对于每一个封装好的系统调用,生成一个文件
    • 该文件里面定义了一些宏,例如#define SYSCALL_NAME open
  • glibc还有一个文件syscall-template.S,使用上面这个宏,定义了这个系统调用的调用方式
1
2
3
4
5
6
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也是一个宏,定义如下:

1
2
3
4
5
6
#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位系统调用过程

sysdep.h文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
* 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
1
# define ENTER_KERNEL int $0x80
  • int就是interrupt,中断的意思,int $0x80就是触发一个软中断,通过它可以陷入(trap)内核

  • 内核启动的时候,有一个trap_init(),其中有这样的代码

    1
    set_system_intr_gate(IA32_SYSCALL_VECTOR, entry_INT80_32)
    • 这是一个软中断的陷入门,当接收到一个系统调用的时候,entry_INT80_32就被调用了
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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(即DO_CALL),实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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,找到它的定义,也就是iret

1
#define INTERRUPT_RETURN   iret
  • iret指令将原来用户态保存的现场恢复回来,包含代码段、指令指针寄存器等,此时用户态进程恢复执行

小结

64位系统调用过程

x86_64的sysdep.h文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* 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
  • 跟32位的步骤一样,将系统调用名称转换为系统调用号,放到寄存器rax。但是这里是真正进行调用,不是用中断了,改用syscall指令了,传递参数的寄存器也变了
  • syscall指令使用了一种特殊的寄存器,叫特殊模块寄存器(Model Specific Registers,简称MSR)
    • 这种寄存器是CPU为完成某些特殊控制功能为目的的寄存器,其中就有系统调用
  • 系统初始化的时候,trap_init除了初始化上面的中断模式,还会调用cpu_init->syscall_init,其中有段代码:
1
wrmsrl(MSR_LSTAR, (unsigned long)entry_SYSCALL_64);
  • rdmsrwrmsr是用来读写特殊模块寄存器的
    • MSR_LSTAR是一个特殊的寄存器,当syscall指令调用的时候,会从寄存器里面拿出函数地址来调用,也就是调用entry_SYSCALL_64
    • 在arch/x86/entry/entry_64.S中定义了entry_SYSCALL_64
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
 ENTER(entry_SYSCALL_64)  
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
1
2
3
4
5
6
7
8
9
10
11
__visible void do_syscall_64(strcut pt_regs *regs) {
strcut 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
);
}
}

在do_syscall_64里面,从rax里面拿出系统调用号,然后根据系统调用号,在系统调用表sys_call_table中找到相应的函数进行调用,并将寄存器中保存的参数取出来,作为函数参数

  • 无论是32位还是64位,都会到系统调用表sys_call_table这里来
  • 系统调用返回时,执行的是USERGS_SYSRET64
1
2
3
#define USERGS_SYSRET64
swapgs;
sysretq;

返回用户态的指令变成了sysretq

小结

p8fS7nVqpr

系统调用表

32位的系统调用表定义在arch/x86/entry/syscalls/syscall_32.tbl

以open的定义为例

1
5 i386 open  sys_open  compat_sys_open

64位的系统调用定义在另一个文件arch/x86/entry/syscalls/syscall_64.tbl里

1
2 common open  sys_open
  • 第一列数字是系统调用号
    • 可看出,32位和64位的系统调用号是不一样的
  • 第三列是系统调用的名字
  • 第四列是系统调用在内核的实现函数

系统调用在内核中的实现函数要有一个声明,往往在include/linux/syscall.h文件中

1
2
3
// sys_open的声明
asmlinkage long sys_open(const char __user *filename,
int flag, umode_t mode);
  • 真正实现这个系统调用的一般在一个.c文件里面,例如sys_open的实现在fs/open.c里面
1
2
3
4
5
6
7
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个参数,根据参数的数目选择宏,具体定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#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_

把宏展开之后,实现如下,和声明是一样的

1
2
3
4
5
6
7
8
9
10
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_系统调用都在这个表里面了
1
2
3
4
5
6
7
8
__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_系统调用都在这个表里面了

1
2
3
4
5
6
7
8
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>
};

小结

Eagle_xkjph5ZaQg