Linux Kernel源码阅读: x86-64 系统调用实现细节(超详细)

这篇具有很好参考价值的文章主要介绍了Linux Kernel源码阅读: x86-64 系统调用实现细节(超详细)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

0、前言

本文采用Linux 内核 v3.10 版本
本文不涉及调试、跟踪及异常处理的细节

一、系统调用简介

系统调用是用户空间程序与内核交互的主要机制。系统调用与普通函数调用不同,因为它调用的是内核里的代码。使用系统调用时,需要特殊指令以使处理器权限转换到内核态。另外,被调用的内核代码由系统调用号来标识,而不是函数地址。

系统调用整体流程如下图所示:

Linux Kernel源码阅读: x86-64 系统调用实现细节(超详细)

二、从 Hello world 说起

我们以一个 Hello world 程序开始,逐步进入系统调用的学习。下面是用汇编代码写的一个简单的程序:

.section .data
msg:
    .ascii "Hello World!\n"
len = . - msg

.section .text
.globl  main
main:

    # ssize_t write(int fd, const void *buf, size_t count)
    mov $1, %rdi            # fd
    mov $msg, %rsi          # buffer
    mov $len, %rdx          # count
    mov $1, %rax            # write(2)系统调用号,64位系统为1
    syscall

    # exit(status)
    mov $0, %rdi            # status
    mov $60, %rax           # exit(2)系统调用号,64位系统为60
    syscall

编译并运行:

$ gcc -o helloworld helloworld.s 
$ ./helloworld
Hello world!
$ echo $?
0

上面这段代码,是直接从我的使用 GNU 汇编语法编写 Hello World 程序的三种方法拷贝过来的。那篇文章里还提到了使用int 0x80软中断和printf函数实现输出的方法,有兴趣的可以去看下。

三、系统调用约定

代码虽然正确运行了,但是我们得知道为什么这么写。x86-64 ABI文档 第A.2.1节,描述了调用约定:

The Linux AMD64 kernel uses internally the same calling conventions as user-level applications (see section 3.2.3 for details). User-level applications that like to call system calls should use the functions from the C library. The interface between the C library and the Linux kernel is the same as for the user-level applications with the following differences:

1. User-level applications use as integer registers for passing the sequence %rdi, %rsi, %rdx, %rcx, %r8 and %r9. The kernel interface uses %rdi, %rsi, %rdx, %r10, %r8 and %r9.

2. A system-call is done via the  syscall instruction. The kernel clobbers registers %rcx and %r11 but preserves all other registers except %rax.

3. The number of the syscall has to be passed in register %rax.

4. System-calls are limited to six arguments, no argument is passed directly on the stack.

5. Returning from the syscall, register %rax contains the result of the system-call. A value in the range between -4095 and -1 indicates an error, it is -errno.

6. Only values of class INTEGER or class MEMORY are passed to the kernel.

可以看出,系统调用约定了以下几个方面:

  • 参数相关
  • 系统调用号
  • 系统调用指令
  • 返回值及错误码

3.1 系统调用的入参

3.1.1 参数顺序

当使用 syscall进行系统调用时,参数与寄存器的对应关系如下图所示:

参数1 参数2 参数3 参数4 参数5 参数6
%rdi %rsi %rdx %r10 %r8 %r9

该对应关系也可以从 arch/x86/entry/entry_64.S 里找到。

/*
 * 64-bit SYSCALL instruction entry. Up to 6 arguments in registers.
 *
 * This is the only entry point used for 64-bit system calls.  The
 * hardware interface is reasonably well designed and the register to
 * argument mapping Linux uses fits well with the registers that are
 * available when SYSCALL is used.
 *
 * SYSCALL instructions can be found inlined in libc implementations as
 * well as some other programs and libraries.  There are also a handful
 * of SYSCALL instructions in the vDSO used, for example, as a
 * clock_gettimeofday fallback.
 *
 * 64-bit SYSCALL saves rip to rcx, clears rflags.RF, then saves rflags to r11,
 * then loads new ss, cs, and rip from previously programmed MSRs.
 * rflags gets masked by a value from another MSR (so CLD and CLAC
 * are not needed). SYSCALL does not save anything on the stack
 * and does not change rsp.
 *
 * Registers on entry:
 * rax  system call number
 * rcx  return address
 * r11  saved rflags (note: r11 is callee-clobbered register in C ABI)
 * rdi  arg0
 * rsi  arg1
 * rdx  arg2
 * r10  arg3 (needs to be moved to rcx to conform to C ABI)
 * r8   arg4
 * r9   arg5
 * (note: r12-r15, rbp, rbx are callee-preserved in C ABI)
 *
 * Only called from user space.
 *
 * When user can change pt_regs->foo always force IRET. That is because
 * it deals with uncanonical addresses better. SYSRET has trouble
 * with them due to bugs in both AMD and Intel CPUs.
 */

3.1.2 参数数量

系统调用参数限制为6个。

3.1.3 参数类型

参数类型限制为 INTEGER 和 MEMORY。这里的类型是x86-64 ABI 里定义的概念,可以在第3.2.3节 Parameter Passing看到具体的描述:

INTEGER This class consists of integral types that fifit into one of the general purpose registers.
MEMORY This class consists of types that will be passed and returned in memory via the stack.

3.2 返回值及错误码

当从系统调用返回时,%rax里保存着系统调用结果;如果是-4095 至 -1之间的值,表示调用过程中发生了错误。

3.3 系统调用号

系统调用号通过%rax传递。

3.4 系统调用指令

系统调用通过指令syscall来执行。

Intel 64 and IA-32 Architectures Software Developer Manuals(以下简称 Intel SDM ) Volume 2B 第 4.3 节对 syscall指令的描述如下:

SYSCALL invokes an OS system-call handler at privilege level 0. It does so by loading RIP from the IA32_LSTAR MSR (after saving the address of the instruction following SYSCALL into RCX). (The WRMSR instruction ensures that the IA32_LSTAR MSR always contain a canonical address.)

SYSCALL also saves RFLAGS into R11 and then masks RFLAGS using the IA32_FMASK MSR (MSR address C0000084H); specifically, the processor clears in RFLAGS every bit corresponding to a bit that is set in the IA32_FMASK MSR.

SYSCALL loads the CS and SS selectors with values derived from bits 47:32 of the IA32_STAR MSR.

根据说明,执行syscall指令时,会进行以下操作:

  • syscall指令的下一条指令(也就是返回地址)存入 %rcx 寄存,然后把指令指针寄存器 %rip 替换成IA32_LSTAR MSR寄存器里的值。
  • 把 rflags 标志寄存器的值保存到 %r11,然后把 rflags 的值与 IA32_FMASK MSR 里的值做掩码运算。
  • 把 IA32_STAR MSR寄存器里第32~47位加载到 CS 和 SS 段寄存器。

总之,就是先保存现场,然后跳转到IA32_LSTAR(Long system target address register) MSR(Model specific register)寄存器指定的地址上去。

那么这个地址是什么时候存入IA32_LSTAR MSR中去的呢?

四、系统调用初始化

在Linux启动之时,会进行一系列的初始化过程。其中,系统调用的初始化在文件arch/x86/kernel/cpu/common.c中:

// file: arch/x86/kernel/cpu/common.c
void syscall_init(void)
{
    /*
     * LSTAR and STAR live in a bit strange symbiosis.
     * They both write to the same internal register. STAR allows to
     * set CS/DS but only a 32bit target. LSTAR sets the 64bit rip.
     */
    wrmsrl(MSR_STAR,  ((u64)__USER32_CS)<<48  | ((u64)__KERNEL_CS)<<32);
    wrmsrl(MSR_LSTAR, system_call);
    wrmsrl(MSR_CSTAR, ignore_sysret);

    ......

    /* Flags to clear on syscall */
    wrmsrl(MSR_SYSCALL_MASK,
           X86_EFLAGS_TF|X86_EFLAGS_DF|X86_EFLAGS_IF|
           X86_EFLAGS_IOPL|X86_EFLAGS_AC);
}

4.1 MSRs

在64位模式下,x86 CPU 提供了以下几个寄存器来配合系统调用相关指令使用:

• IA32_KERNEL_GS_BASE — Used by SWAPGS instruction.

• IA32_LSTAR — Used by SYSCALL instruction.

• IA32_FMASK — Used by SYSCALL instruction.

• IA32_STAR — Used by SYSCALL and SYSRET instruction.

这四种MSR寄存器的说明如下,详见Intel SDM Volume 4 第2.1节

Register Address Architectural MSR Name / Bit Fields MSR/Bit Description Comment
C000_0081H IA32_STAR System Call Target Address (R/W) If CPUID.80000001:EDX.[29] = 1
C000_0082H IA32_LSTAR IA-32e Mode System Call Target Address (R/W)
Target RIP for the called procedure when SYSCALL is executed in 64-bit mode.
If CPUID.80000001:EDX.[29] = 1
C000_0083H IA32_CSTAR IA-32e Mode System Call Target Address (R/W)
Not used, as the SYSCALL instruction is not recognized in compatibility mode.
If CPUID.80000001:EDX.[29] = 1
C000_0084H IA32_FMASK System Call Flag Mask (R/W) If CPUID.80000001:EDX.[29] = 1
C000_0102H IA32_KERNEL_GS_BASE Swap Target of BASE Address of GS (R/W) If CPUID.80000001:EDX.[29] = 1

系统调用初始化时,使用了MSR_STAR、MSR_LSTAR、MSR_CSTAR、MSR_SYSCALL_MASK这四个宏,它们定义在arch/x86/include/uapi/asm/msr-index.h头文件中。可以看到,这四个宏定义的是寄存器的地址:

// file: arch/x86/include/uapi/asm/msr-index.h
/* CPU model specific register (MSR) numbers */

/* x86-64 specific MSRs */
#define MSR_STAR        0xc0000081 /* legacy mode SYSCALL target */
#define MSR_LSTAR       0xc0000082 /* long mode SYSCALL target */
#define MSR_CSTAR       0xc0000083 /* compat mode SYSCALL target */
#define MSR_SYSCALL_MASK    0xc0000084 /* EFLAGS mask for syscall */

4.2 段选择子

另外,__USER32_CS 和 __KERNEL_CS 宏定义在arch/x86/include/asm/segment.h 头文件中。其中,__USER32_CS 和 __KERNEL_CS分别为用户态代码段选择子和内核态代码段选择子。__USER32_CS宏引用了GDT_ENTRY_DEFAULT_USER32_CS宏,该宏是用户态代码段在GDT(Global Descriptor Table)中的索引。__KERNEL_CS宏引用了GDT_ENTRY_KERNEL_CS宏,该宏是内核态代码段在GDT中的索引。

// file: arch/x86/include/asm/segment.h
#define GDT_ENTRY_KERNEL_CS 2
#define GDT_ENTRY_DEFAULT_USER32_CS 4
#define __USER32_CS   (GDT_ENTRY_DEFAULT_USER32_CS*8+3)
#define __KERNEL_CS (GDT_ENTRY_KERNEL_CS*8)

可以看到,内核态段选择子等于 GDT索引*8,而用户态段选择子等于GDT索引*8+3,这是由段描述符的结构决定的。在x86架构中,段寄存器和段选择子都是16位的,但是这16位并不是全部用来存储索引值,而是由三部分组成:

  • RPL(Requested Privilege Level)位。段选择子最低2位(位0\~1)称为请求权限级别位,保存的是段权限级别;因为RPL有2位,可以有0~3四种权限,目前Linux只使用到了0和3这两个级别,其中内核程序运行在0级别,用户程序运行在3级别。
  • TI位,即表指示位(Table Indicator Flag)。段选择子的位2是TI位,TI位用来指示段的保存位置:是保存在全局描述符表GDT中,还是在本地描述符表LDT(Local Descriptor Table )中。当TI位为1时,表示在LDT中,当TI位为0时,表示在GDT中。
  • 位3\~15,才是真正保存索引的位置。

从以上分析可知,段描述符最低3位有其他用途不能用来存放索引,所以要把索引值左移3位(相当于乘以8)才能放到索引区。另外,因为用户态的权限级别为3,我们看到所有的用户段都要加3,相当于把用户态的RPL级别硬编码到程序里了。

段选择子的位分布情况见下图,详细信息请查阅Intel SDM Volume 3A:第3.42 Segment Selectors节

Linux Kernel源码阅读: x86-64 系统调用实现细节(超详细)

4.3 wrmsr指令

根据Intel SDM Volume 2D文档的描述,wrmsr指令会把%edx:%eax的值写入指定的64位 MSR 寄存器中,具体写入哪个寄存器,是通过 %ecx 指定的。%edx的值存入MSR中的高32位,%eax的值存入MSR的低32位。在64位系统中,这三个寄存器的高32位会被忽略。

wrmsrl宏是对wrmsr指令的封装,其参数msr指定了要保存的MSR寄存器,参数val是要保存的内容,其中val的高32位保存到%edx,低32位保存到%eax

4.4 初始化过程

wrmsrl(MSR_STAR,  ((u64)__USER32_CS)<<48  | ((u64)__KERNEL_CS)<<32);

这行指令把用户代码段选择子(__USER32_CS)写入MSR_STAR[48:63],把内核代码段选择子(__KERNEL_CS)写入MSR_STAR[32:47]。

其中__KERNEL_CS是给syscall指令使用的。执行syscall指令时,要从用户态切换到内核态,CPU 会根据__KERNEL_CS来更新代码段寄存器%cs和栈段寄存器%ss,伪代码如下(详细内容请参考 Intel SDM Vol. 2B 中 syscall指令):

CS.Selector := IA32_STAR[47:32] AND FFFCH (  Operating system provides CS; RPL forced to 0 )
SS.Selector := IA32_STAR[47:32] + 8; (  SS just above CS )

__USER32_CS是给sysret指令用的。执行sysret指令时,需要从内核态切换到用户态,cpu会根据__USER32_CS来更新%cs%ss,伪代码如下(详细内容请参考 Intel SDM Vol. 2B 中 sysret指令):

IF (operand size is 64-bit)
​ THEN CS.Selector := IA32_STAR[63:48]+16;
​ ELSE CS.Selector := IA32_STAR[63:48];
FI;
CS.Selector := CS.Selector OR 3; (  RPL forced to 3 )
SS.Selector := (IA32_STAR[63:48]+8) OR 3; (  RPL forced to 3 )
wrmsrl(MSR_LSTAR, system_call);

这行代码把system_call入口地址存入到MSR_LSTAR寄存器。syscall指令会把该地址加载到到%rip寄存器,从该地址开始执行。

/* Flags to clear on syscall */
    wrmsrl(MSR_SYSCALL_MASK,
           X86_EFLAGS_TF|X86_EFLAGS_DF|X86_EFLAGS_IF|
           X86_EFLAGS_IOPL|X86_EFLAGS_AC);

这几行代码,定义了EFLAGS掩码位,并把它们保存到MSR_SYSCALL_MASK寄存器。syscall指令执行时,凡是MSR_SYSCALL_MASK中置位的标志位,都会从EFALGS中清除,伪代码如下:

RFLAGS := RFLAGS AND NOT(IA32_FMASK);

特别说明一下,因为初始化时,掩码中包含中断标志位X86_EFLAGS_IF,所以syscall指令执行时,中断是禁止的。

五、系统调用编号

在示例程序中,我们使用了writeexit系统调用,并通过%rax传递了系统调用号。在Linux中,32位系统和64位系统有不同的系统调用编号。32位系统调用号定义在arch/x86/syscalls/syscall_32.tbl文件;64位系统调用号定义在arch/x86/syscalls/syscall_64.tbl文件。

下面列出了64位系统的部分系统调用及编号,可以看到,write()的系统调用编号为 1 ,exit()系统调用编号为 60。

0   common  read            sys_read
1   common  write           sys_write           # write 系统调用
2   common  open            sys_open
3   common  close           sys_close

......

59  64  execve          sys_execve
60  common  exit            sys_exit           # exit 系统调用
61  common  wait4           sys_wait4
62  common  kill            sys_kill
......

六、系统调用表及其初始化

linux内核中包含一个被称为系统调用表的数据结构。64位系统调用表定义在arch/x86/kernel/syscall_64.c文件中:

// file: arch/x86/kernel/syscall_64.c
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>
};

可以看到,sys_call_table是一个包含__NR_syscall_max+1个元素的数组。__NR_syscall_max是一个宏,在64位模式下其值为542,该宏定义于include/generated/asm-offsets.h文件,这个文件是Kbuild编译后生成的。

// file: include/generated/asm-offsets.h
#define __NR_syscall_max 542 /* sizeof(syscalls_64) - 1 # */

系统调用表的元素类型为sys_call_ptr_t,这是通过typedef定义的函数指针。

// file: arch/x86/kernel/syscall_64.c
typedef void (*sys_call_ptr_t)(void);

sys_ni_syscall表示一个未实现的系统调用,其定义如下:

// file: kernel/sys_ni.c
asmlinkage long sys_ni_syscall(void)
{
    return -ENOSYS;
}

sys_ni_syscall直接返回一个错误码-ENOSYSENOSYS值为38,表示调用了一个未实现的函数。

// file: include/uapi/asm-generic/errno.h
#define ENOSYS      38  /* Function not implemented */

符号 ...是GCC编译器的的一个扩展--Designated Initializers,该扩展允许我们以任意顺序初始化成员元素。正如我们看到的,sys_call_table先用sys_ni_syscall进行初始化,然后再用<asm/syscalls_64.h>头文件中的内容对数组进行填充。该头文件是使用arch/x86/syscalls/syscalltbl.sh脚本读取syscall_64.tbl后生成的,它包含以下宏:

// file: arch/x86/include/generated/asm/syscalls_64.h
__SYSCALL_COMMON(0, sys_read, sys_read)
__SYSCALL_COMMON(1, sys_write, sys_write)
__SYSCALL_COMMON(2, sys_open, sys_open)

......

__SYSCALL_X32(540, compat_sys_process_vm_writev, compat_sys_process_vm_writev)
__SYSCALL_X32(541, compat_sys_setsockopt, compat_sys_setsockopt)
__SYSCALL_X32(542, compat_sys_getsockopt, compat_sys_getsockopt)

__SYSCALL_COMMON宏定义如下,该宏被扩展成__SYSCALL_64宏,最终被扩展成函数定义。

// file: arch/x86/kernel/syscall_64.c
#define __SYSCALL_COMMON(nr, sym, compat) __SYSCALL_64(nr, sym, compat)
#define __SYSCALL_64(nr, sym, compat) [nr] = sym,

最终,sys_call_table被扩展成了下面的格式,各系统调用号关联的函数指针被填充到该数组中;其它所有未实现的系统调用号都指向了sys_ni_syscall函数,该函数只是简单返回一个错误码-ENOSYS

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,
    [0] = sys_read,
    [1] = sys_write,
    [2] = sys_open,
    ......
};

七、系统调用的定义

下面我们以示例程序中使用的write系统调用为例,来看看系统调用是如何定义的。

write系统调用函数原型如下,可以通过 man 2 write命令查看。

ssize_t write(int fd, const void *buf, size_t count);

在linux内核中,write系统调用定义在fs/read_write.c文件中。由于write有3个参数,所以是用SYSCALL_DEFINE3宏定义的。

// file: fs/read_write.c
SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
        size_t, count)
{
    struct fd f = fdget(fd);
    ssize_t ret = -EBADF;

    if (f.file) {
        loff_t pos = file_pos_read(f.file);
        ret = vfs_write(f.file, buf, count, &pos);
        file_pos_write(f.file, pos);
        fdput(f);
    }

    return ret;
}

SYSCALL_DEFINE3宏定义在 include/linux/syscalls.h中。可以看到,linux 内核一共定义了7个宏,每个宏后面都有一个数字,表示入参数量。

// file: include/linux/syscalls.h
#define SYSCALL_DEFINE0(sname)                  \
    SYSCALL_METADATA(_##sname, 0);              \
    asmlinkage long sys_##sname(void)

#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__)

SYSCALL_DEFINE3被扩展成了SYSCALL_DEFINEx宏,该宏又扩展成了SYSCALL_METADATA__SYSCALL_DEFINEx

write为例,看下扩展过程:

SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf, size_t, count)

扩展成:

SYSCALL_DEFINEx(3, _write, unsigned int, fd, const char *, buf, size_t, count)

注意,扩展后,函数名前面多个了下划线”_“。”##“是连接操作符,在宏扩展时,可以把2个符号合并成一个,具体使用见 gcc 文档 3.5 Concatenation。

继续扩展:

SYSCALL_METADATA(_write, 3, unsigned int, fd, const char *, buf, size_t, count)         \
__SYSCALL_DEFINEx(3, _write, unsigned int, fd, const char *, buf, size_t, count)

SYSCALL_METADATA宏的实现,由Kbuild时配置的选项CONFIG_FTRACE_SYSCALLS来决定,只有设置CONFIG_FTRACE_SYSCALLS选项时,该宏才有实际意义。从选项名称就能够看出来,它主要是用来对系统调用过程进行追踪的。 关于调试和追踪方面的细节,本文暂不涉及,我们主要来看下__SYSCALL_DEFINEx宏的实现。

7.1 __SYSCALL_DEFINEx

__SYSCALL_DEFINEx宏定义于 include/linux/syscalls.h文件:

// file: include/linux/syscalls.h
#define __SYSCALL_DEFINEx(x, name, ...)                 \
    asmlinkage long sys##name(__MAP(x,__SC_DECL,__VA_ARGS__));  \
    static inline long SYSC##name(__MAP(x,__SC_DECL,__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;                     \
    }                               \
    SYSCALL_ALIAS(sys##name, SyS##name);                \
    static inline long SYSC##name(__MAP(x,__SC_DECL,__VA_ARGS__))

7.1.1 __MAP

__MAP宏会根据参数数量和映射函数做适当的扩展。其中n表示参数数量,m代表映射函数,其它参数都是成对出现的,t表示参数类型,a表示参数值。从注释中也可以看到,__MAP(n, m, t1, a1, t2, a2, ..., tn, an)会被扩展成m(t1, a1), m(t2, a2), ..., m(tn, an)

// file: include/linux/syscalls.h
/*
 * __MAP - apply a macro to syscall arguments
 * __MAP(n, m, t1, a1, t2, a2, ..., tn, an) will expand to
 *    m(t1, a1), m(t2, a2), ..., m(tn, an)
 * The first argument must be equal to the amount of type/name
 * pairs given.  Note that this list of pairs (i.e. the arguments
 * of __MAP starting at the third one) is in the same format as
 * for SYSCALL_DEFINE<n>/COMPAT_SYSCALL_DEFINE<n>
 */
#define __MAP0(m,...)
#define __MAP1(m,t,a) m(t,a)
#define __MAP2(m,t,a,...) m(t,a), __MAP1(m,__VA_ARGS__)
#define __MAP3(m,t,a,...) m(t,a), __MAP2(m,__VA_ARGS__)
#define __MAP4(m,t,a,...) m(t,a), __MAP3(m,__VA_ARGS__)
#define __MAP5(m,t,a,...) m(t,a), __MAP4(m,__VA_ARGS__)
#define __MAP6(m,t,a,...) m(t,a), __MAP5(m,__VA_ARGS__)
#define __MAP(n,...) __MAP##n(__VA_ARGS__)

7.1.2 __SC_DECL、__SC_LONG、__SC_CAST、__SC_TEST、__SC_ARGS

这些宏是作为__MAP宏的映射函数存在的,这些宏中的t表示参数类型(type),a表示参数值(argument)。其中__SC_DECL__SC_CAST__SC_ARGS这三个宏比较简单,就不做说明了,重点说说其它宏。

// file: include/linux/syscalls.h
#define __SC_DECL(t, a) t a
#define __SC_CAST(t, a) (t) a
#define __SC_ARGS(t, a) a

#define __SC_LONG(t, a) __typeof(__builtin_choose_expr(__TYPE_IS_LL(t), 0LL, 0L)) a
#define __SC_TEST(t, a) (void)BUILD_BUG_ON_ZERO(!__TYPE_IS_LL(t) && sizeof(t) > sizeof(long))
#define __TYPE_IS_LL(t) (__same_type((t)0, 0LL) || __same_type((t)0, 0ULL))

7.1.2.1 __SC_LONG

7.1.2.1.1 __TYPE_IS_LL

__SC_LONG宏中引用了__TYPE_IS_LL宏,而__TYPE_IS_LL宏又引用了__same_type函数。__same_type函数定义如下:

// file: include/linux/compiler.h
/* Are two types/vars the same type (ignoring qualifiers)? */
#ifndef __same_type
# define __same_type(a, b) __builtin_types_compatible_p(typeof(a), typeof(b))
#endif

__same_type函数通过gcc 内建函数__builtin_types_compatible_p来判断2个入参的类型是否一致,如果一致,返回1,否则返回0。__builtin_types_compatible_p函数说明如下:

You can use the built-in function  __builtin_types_compatible_p to determine whether two types are the same.
This built-in function returns 1 if the unqualified versions of the types type1 and type2 (which are types, not expressions) are compatible, 0 otherwise. The result of this built-in function can be used in integer constant expressions.

综上所述,__TYPE_IS_LL(t)的作用是判断给定的类型t是否是Logg LongUnsigned Long Long类型,如果是其值为1,否则为0。

7.1.2.1. __builtin_choose_expr

__builtin_choose_expr也是一个gcc 内建函数,该函数有3个参数,第一个参数是一个常量表达式(const_exp)。其作用类似于三元操作符”?:“,如果第一参数非0,则返回第2个参数,否则返回第3个参数。

Built-in Function:  type  __builtin_choose_expr  (const_exp, exp1, exp2)
You can use the built-in function  __builtin_choose_expr to evaluate code depending on the value of a constant expression. This built-in function  returns exp1 if const_exp, which is an integer constant expression,  is nonzeroOtherwise it returns exp2.

7.1.2.1.3 结论

经过以上分析,宏__SC_LONG(t, a)的作用就是把”LL“或”ULL“类型的参数,转换为”LL“类型;其它类型的参数,转换成”L“类型

7.1.2.2 __SC_TEST

#define __SC_TEST(t, a) (void)BUILD_BUG_ON_ZERO(!__TYPE_IS_LL(t) && sizeof(t) > sizeof(long))

从名称也可以看到,宏__SC_TEST(t, a)主要用于测试目的。该宏又引用了BUILD_BUG_ON_ZERO,其定义如下。

// file: include/linux/bug.h
/* Force a compilation error if condition is true, but also produce a
   result (of value 0 and type size_t), so the expression can be used
   e.g. in a structure initializer (or where-ever else comma expressions
   aren't permitted). */
/* sizeof(struct { int:-!!(e); } 用法参考: https://stackoverflow.com/questions/9229601/what-is-in-c-code */
#define BUILD_BUG_ON_ZERO(e) (sizeof(struct { int:-!!(e); }))

这是一种使用技巧,它主要用来进行编译时检查。

sizeof(struct { int: -!!(e); }))

执行流程如下,详见What is ":-!!" in C code?:

1.  (e): Compute expression  e.
2. !!(e): Logically negate twice:  0 if  e == 0; otherwise  1.
3.  -!!(e): Numerically negate the expression from step 2:  0 if it was  0; otherwise  -1.
4. struct{int: -!!(0);} --> struct{int: 0;}: If it was zero, then we declare a struct with an anonymous integer bitfield that has width zero. Everything is fine and we proceed as normal.
5.  struct{int: -!!(1);} --> struct{int: -1;}: On the other hand, if it  isn't zero, then it will be some negative number. Declaring any bitfield with  negative width is a compilation error.

综上,__SC_TEST(t, a)的作用就是当参数类型t不是LL类型,但其类型大小却超过L类型时,强制编译器报错。说白了就是进行类型检测。

7.1.3 SYSCALL_ALIAS

SYSCALL_ALIAS宏定义如下:

// file: include/linux/linkage.h
#ifndef SYSCALL_ALIAS
#define SYSCALL_ALIAS(alias, name) asm(         \
    ".globl " VMLINUX_SYMBOL_STR(alias) "\n\t"  \
    ".set   " VMLINUX_SYMBOL_STR(alias) ","     \
          VMLINUX_SYMBOL_STR(name))
#endif

VMLINUX_SYMBOL_STR定义如下:

// file: include/linux/export.h
/*
 * Export symbols from the kernel to modules.  Forked from module.h
 * to reduce the amount of pointless cruft we feed to gcc when only
 * exporting a simple symbol or two.
 *
 * Try not to add #includes here.  It slows compilation and makes kernel
 * hackers place grumpy comments in header files.
 */
/* Indirect, so macros are expanded before pasting. */
#define VMLINUX_SYMBOL(x) __VMLINUX_SYMBOL(x)
#define VMLINUX_SYMBOL_STR(x) __VMLINUX_SYMBOL_STR(x)

#define __VMLINUX_SYMBOL(x) x
#define __VMLINUX_SYMBOL_STR(x) #x

实际效果是给name设置了个别名alias,本例中是给SyS_write设置了别名sys_write

7.1.4 最终扩展

我们继续往下分析,刚才分析到了如下代码:

__SYSCALL_DEFINEx(3, _write, unsigned int, fd, const char *, buf, size_t, count)

所以我们知道,在宏内部x值为3,__VA_ARGS__参数类型和值列表。

根据__MAP__SC_DECL宏定义,__MAP(x,__SC_DECL,__VA_ARGS__)被扩展成为:

unsigned int fd, const char * buf, size_t count

根据__MAP__SC_LONG宏定义,__MAP(x,__SC_LONG,__VA_ARGS__)被扩展成:

long fd, long buf, long count

__MAP(x,__SC_CAST,__VA_ARGS__)被扩展成:

(unsigned int) fd, (const char *) buf, (size_t) count

__MAP(x,__SC_ARGS,__VA_ARGS__) 被扩展成:

fd, buf, count

所以,__SYSCALL_DEFINEx(3, _write, unsigned int, fd, const char *, buf, size_t, count)最终扩展如下:

asmlinkage long sys_write(unsigned int fd, const char * buf, size_t count); \
    static inline long SYSC_write(unsigned int fd, const char * buf, size_t count); \
    asmlinkage long SyS_write(long fd, long buf, long count)    \
    {                               \
        long ret = SYSC_write((unsigned int) fd, (const char *) buf, (size_t) count);   \
        __MAP(x,__SC_TEST,__VA_ARGS__);             \   # 用于测试,不涉及
        __PROTECT(x, ret, fd, buf, count);  \
        return ret;                     \
    }                               \
    SYSCALL_ALIAS(sys_write, SyS_write);                \
    static inline long SYSC_write(unsigned int fd, const char * buf, size_t count)

再结合write函数具体实现,完整的write系统调用扩展如下:

asmlinkage long sys_write(unsigned int fd, const char * buf, size_t count); \
    static inline long SYSC_write(unsigned int fd, const char * buf, size_t count); \
    asmlinkage long SyS_write(long fd, long buf, long count)    \
    {                               \
        long ret = SYSC_write((unsigned int) fd, (const char *) buf, (size_t) count);   \
        __MAP(x,__SC_TEST,__VA_ARGS__);             \   # 用于测试,不涉及
        __PROTECT(x, ret, fd, buf, count);  \
        return ret;                     \
    }                               \
    SYSCALL_ALIAS(sys_write, SyS_write);                \
    static inline long SYSC_write(unsigned int fd, const char * buf, size_t count)  
    {
        struct fd f = fdget(fd);
        ssize_t ret = -EBADF;

        if (f.file) {
            loff_t pos = file_pos_read(f.file);
            ret = vfs_write(f.file, buf, count, &pos);
            file_pos_write(f.file, pos);
            fdput(f);
        }

        return ret;
    }

这段代码先声明了2个入参相同的函数sys_writeSYSC_write;然后定义了函数SyS_write,该函数内部调用了SYSC_write;给SyS_write设置了一个别名sys_writeSYSC_writewrite系统调用的具体实现。

7.1.5 总结

总结一下实现流程:

  • 内部实现函数为SYSC_write
  • SyS_write函数对SYSC_write进行了封装,增加了编译时类型检查及参数保护;
  • SyS_write设置了别名sys_write

八、系统调用处理程序

本文只会讲解正常的系统调用流程,涉及到调试、追踪及异常相关的处理,并没有涉及。另外代码比较长,全贴上去是为了让大家有一个全局视角,下面我们会逐句来分析。

/*
 * Register setup:
 * rax  system call number
 * rdi  arg0
 * rcx  return address for syscall/sysret, C arg3
 * rsi  arg1
 * rdx  arg2
 * r10  arg3    (--> moved to rcx for C)
 * r8   arg4
 * r9   arg5
 * r11  eflags for syscall/sysret, temporary for C
 * r12-r15,rbp,rbx saved by C code, not touched.
 *
 * Interrupts are off on entry.
 * Only called from user space.
 *
 * XXX  if we had a free scratch register we could save the RSP into the stack frame
 *      and report it properly in ps. Unfortunately we haven't.
 *
 * When user can change the frames always force IRET. That is because
 * it deals with uncanonical addresses better. SYSRET has trouble
 * with them due to bugs in both AMD and Intel CPUs.
 */

ENTRY(system_call)
    CFI_STARTPROC   simple
    CFI_SIGNAL_FRAME
    CFI_DEF_CFA rsp,KERNEL_STACK_OFFSET
    CFI_REGISTER    rip,rcx
    /*CFI_REGISTER  rflags,r11*/
    SWAPGS_UNSAFE_STACK
    /*
     * A hypervisor implementation might want to use a label
     * after the swapgs, so that it can do the swapgs
     * for the guest and jump here on syscall.
     */
GLOBAL(system_call_after_swapgs)

    movq    %rsp,PER_CPU_VAR(old_rsp)
    movq    PER_CPU_VAR(kernel_stack),%rsp
    /*
     * No need to follow this irqs off/on section - it's straight
     * and short:
     */
    ENABLE_INTERRUPTS(CLBR_NONE)
    SAVE_ARGS 8,0
    movq  %rax,ORIG_RAX-ARGOFFSET(%rsp)
    movq  %rcx,RIP-ARGOFFSET(%rsp)
    CFI_REL_OFFSET rip,RIP-ARGOFFSET
    testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags+THREAD_INFO(%rsp,RIP-ARGOFFSET)
    jnz tracesys
system_call_fastpath:
#if __SYSCALL_MASK == ~0
    cmpq $__NR_syscall_max,%rax
#else
    andl $__SYSCALL_MASK,%eax
    cmpl $__NR_syscall_max,%eax
#endif
    ja badsys
    movq %r10,%rcx
    call *sys_call_table(,%rax,8)  # XXX:    rip relative
    movq %rax,RAX-ARGOFFSET(%rsp)
/*
 * Syscall return path ending with SYSRET (fast path)
 * Has incomplete stack frame and undefined top of stack.
 */
ret_from_sys_call:
    movl $_TIF_ALLWORK_MASK,%edi
    /* edi: flagmask */
sysret_check:
    LOCKDEP_SYS_EXIT
    DISABLE_INTERRUPTS(CLBR_NONE)
    TRACE_IRQS_OFF
    movl TI_flags+THREAD_INFO(%rsp,RIP-ARGOFFSET),%edx
    andl %edi,%edx
    jnz  sysret_careful
    CFI_REMEMBER_STATE
    /*
     * sysretq will re-enable interrupts:
     */
    TRACE_IRQS_ON
    movq RIP-ARGOFFSET(%rsp),%rcx
    CFI_REGISTER    rip,rcx
    RESTORE_ARGS 1,-ARG_SKIP,0
    /*CFI_REGISTER  rflags,r11*/
    movq    PER_CPU_VAR(old_rsp), %rsp
    USERGS_SYSRET64
END(system_call)

先来看下ENTRYGLOBALEND这三个宏,其中ENTRYEND定义在include/linux/linkage.h文件中,ENTRY宏中又引用了ALIGN宏。ALIGNGLOBAL宏一起,定义在arch/x86/include/asm/linkage.h文件中。

// file: include/linux/linkage.h
#define ALIGN __ALIGN
#define ALIGN_STR __ALIGN_STR

#define ENTRY(name) \
  .globl name; \
  ALIGN; \
  name:
#endif

#define END(name) \
  .size name, .-name
// file: arch/x86/include/asm/linkage.h
#define GLOBAL(name)    \
    .globl name;    \
    name:

#if defined(CONFIG_X86_64) || defined(CONFIG_X86_ALIGNMENT_16)
#define __ALIGN     .p2align 4, 0x90
#define __ALIGN_STR __stringify(__ALIGN)
#endif

解释一下这些宏的内容:

  • ENTRY宏定义了一个全局符号,并标明了地址对齐方式符号的起始地址
  • GLOBAL宏的作用与ENTRY宏类似,只不过没有标明地址对齐方式。
  • END宏定义了一个符号的字节大小。
  • ALIGN宏定义了对齐方式,最终被扩展成.p2align 4, 0x90指令。该指令指示编译器按照16字节对齐(2^4),对齐产生的空洞使用字节0x90来填充。
  • .size.p2align等伪指令的详细说明,请参考 gas 官方文档。

继续往下看,会遇到一堆以CFI_开头的宏。这些宏,最后都会扩展到 cfi 相关的指令,这部分指令主要是用来调试、追踪用的,我们在本文中不会涉及到这些指令的细节,下文中遇到这些指令也会直接跳过。大家有兴趣的话,可以查阅 gas官方文档中 CFI-directives 这一节。

下一步是SWAPGS_UNSAFE_STACK,该宏定义在arch/x86/include/asm/irqflags.h头文件中,会扩展成swapgs指令:

// file: arch/x86/include/asm/irqflags.h
#define SWAPGS_UNSAFE_STACK swapgs

我们来看一下 Intel SDM Volume 2B 中对swapgs指令的说明:

SWAPGS exchanges the current GS base register value with the value contained in MSR address C0000102H (IA32_KERNEL_GS_BASE). The SWAPGS instruction is a privileged instruction intended for use by system software.
When using SYSCALL to implement system callsthere is no kernel stack at the OS entry point. Neither is there a straightforward method to obtain a pointer to kernel structures from which the kernel stack pointer could be read. Thus, the kernel cannot save general purpose registers or reference memory.
By design, SWAPGS does not require any general purpose registers or memory operands. No registers need to be saved before using the instruction.  SWAPGS exchanges the CPL 0 data pointer from the IA32_KERNEL_GS_BASE MSR with the GS base register. The kernel can then use the GS prefix on normal memory references to access kernel data structures. Similarly, when the OS kernel is entered using an interrupt or exception (where the kernel stack is already set up), SWAPGS can be used to quickly get a pointer to the kernel data structures.

该指令会交换当前 GS 基址寄存器和 IA32_KERNEL_GS_BASE 寄存器的值,交换后 GS 基址寄存器会指向内核的数据结构。

接下来,我们用GLOBAL定义了一个全局符号system_call_after_swapgs。从名字上也能看出来,它表示的是swapgs之后的系统调用执行过程。

GLOBAL(system_call_after_swapgs)

再往后的两条指令,先把用户空间的栈指针保存起来,然后用内核栈指针填充%rsp寄存器。之后,%rsp指向内核栈的栈顶位置,我们就可以访问内核栈了。

movq    %rsp,PER_CPU_VAR(old_rsp)
    movq    PER_CPU_VAR(kernel_stack),%rsp

再往下,使用ENABLE_INTERRUPTS打开中断,该宏定义在arch/x86/include/asm/irqflags.h文件中。与其一起定义的还有DISABLE_INTERRUPTS宏,该宏会禁止中断,我们下文中会遇到。

// file: arch/x86/include/asm/irqflags.h
#define ENABLE_INTERRUPTS(x)    sti
#define DISABLE_INTERRUPTS(x)   cli

接下来,SAVE_ARGS 8,0会将部分通用寄存器保存到内核栈中,其中SAVE_ARGS宏定义于arch/x86/include/asm/calling.h文件中。该宏有三个参数addskip、 save_rcxsave_r891011,其中addskip表示跳过的字节数,save_rcx指示是否保存 %rcx寄存器,save_r891011指示是否保存r8~r11这四个寄存器。从调用指令中可以看到,入参addskip为 8、save_rcx为 0,save_r891011参数未指定,按默认值1处理。

SAVE_ARGS宏中又引入了movq_cfi宏,该宏定义于arch/x86/include/asm/dwarf2.h文件中,其功能是把指定寄存器的值复制到栈中指定的偏移地址处。

// file:arch/x86/include/asm/dwarf2.h
    .macro movq_cfi reg offset=0
    movq %\reg, \offset(%rsp)
    CFI_REL_OFFSET \reg, \offset
    .endm

SAVE_ARGS 8,0执行时,%rsp指针先向下移动9*8 + 8 = 80个字节,然后按地址从高到低依次填充%rdi%rsi ...... %r11寄存器的值。根据入参要求,有的寄存器值可以不保存,但空间会预留出来。

// file: arch/x86/include/asm/calling.h
    .macro SAVE_ARGS addskip=0, save_rcx=1, save_r891011=1
    subq  $9*8+\addskip, %rsp
    CFI_ADJUST_CFA_OFFSET   9*8+\addskip
    movq_cfi rdi, 8*8
    movq_cfi rsi, 7*8
    movq_cfi rdx, 6*8

    .if \save_rcx
    movq_cfi rcx, 5*8
    .endif

    movq_cfi rax, 4*8

    .if \save_r891011
    movq_cfi r8,  3*8
    movq_cfi r9,  2*8
    movq_cfi r10, 1*8
    movq_cfi r11, 0*8
    .endif

    .endm

SAVE_ARGS 8,0指令执行完成后,内核栈的结构示意如下:

Linux Kernel源码阅读: x86-64 系统调用实现细节(超详细)

下一步,把%rax%rcx寄存器的值保存到内核栈中。因为在执行syscall时,会把返回地址存入%rcx,所以%rcx会被破坏,我们要把它提前保存起来。%rax寄存器后面也会被修改,所以一起保存起来。

movq  %rax,ORIG_RAX-ARGOFFSET(%rsp)
    movq  %rcx,RIP-ARGOFFSET(%rsp)

ORIG_RAXARGOFFSETARGOFFSET这三个宏定义于arch/x86/include/asm/calling.h头文件中。这个文件主要是根据 x86 函数调用习惯,使用宏定义了调用时各通用寄存器在栈中的偏移量以及一些寄存器操作,比如刚才我们用到的SAVE_ARGS宏。

可以看到,该文件开头就描述了x86-64 函数调用习惯。在x86-64 函数调用中,%rdi%rsi%rdx%rcx%r8%r9是作为参数传递用的,属于调用者保存的寄存器;另外%r10%r11也是调用者保存的寄存器。%rbx%rbp%r12~%r15这6个寄存器是被调用者保存的。%rax%rdx这两个寄存器是存放函数返回值的。所谓调用者保存,就是说在调用发生时,被调用方有权利破坏这些寄存器而不通知调用方。所以调用方为了保证调用返回后能顺利执行,就要自己来保存这些值。所谓被调用方保存,是指这些寄存器你可以随便用,但有一个前提,就是在返回前要把这些值复原。另外,也可以看到,在把寄存器值复制到内核栈时,其顺序和偏移量跟文件中定义的值是对应的。

ARGOFFSET宏定义值为48,与R11以一致,表示的是最后入栈的参数的偏移量。ORIG_RAX宏定义值为120,表示的是原%rax的保存位置。RIP宏为128,是指返回地址的偏移量。ORIG_RAX-ARGOFFSET计算后为72,所以movq %rax,ORIG_RAX-ARGOFFSET(%rsp)会把原始 %rax的值存入到 %rsp+72所指向的地址,也就是上图中的保留位置。RIP-ARGOFFSET计算结果为80,所以movq %rcx,RIP-ARGOFFSET(%rsp)会把原始%rcx的值存入到%rsp+80所指向的的地址。

// file: arch/x86/include/asm/calling.h
/*
 x86 function call convention, 64-bit:
 -------------------------------------
  arguments           |  callee-saved      | extra caller-saved | return
 [callee-clobbered]   |                    | [callee-clobbered] |
 ---------------------------------------------------------------------------
 rdi rsi rdx rcx r8-9 | rbx rbp [*] r12-15 | r10-11             | rax, rdx [**]
 */

/*
 * 64-bit system call stack frame layout defines and helpers,
 * for assembly code:
 */

#define R15       0
#define R14       8
#define R13      16
#define R12      24
#define RBP      32
#define RBX      40

/* arguments: interrupts/non tracing syscalls only save up to here: */
#define R11      48
#define R10      56
#define R9       64
#define R8       72
#define RAX      80
#define RCX      88
#define RDX      96
#define RSI     104
#define RDI     112
#define ORIG_RAX    120       /* + error_code */
/* end of arguments */

/* cpu exception frame or undefined in case of fast syscall: */
#define RIP     128
#define CS      136
#define EFLAGS      144
#define RSP     152
#define SS      160

#define ARGOFFSET   R11
#define SWFRAME     ORIG_RAX

执行完成后,内核栈示意图如下:

Linux Kernel源码阅读: x86-64 系统调用实现细节(超详细)

继续往下,是测试和跳转指令。

testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags+THREAD_INFO(%rsp,RIP-ARGOFFSET)
    jnz tracesys

_TIF_WORK_SYSCALL_ENTRY宏(TIF 是 Thread Info Flag的缩写)和THREAD_INFO宏定义于arch/x86/include/asm/thread_info.h文件中。该文件定义了线程中使用到的标志位及基本操作。

// file: arch/x86/include/asm/thread_info.h

#define KERNEL_STACK_OFFSET (5*8)

/* work to do in syscall_trace_enter() */
#define _TIF_WORK_SYSCALL_ENTRY \
    (_TIF_SYSCALL_TRACE | _TIF_SYSCALL_EMU | _TIF_SYSCALL_AUDIT |   \
     _TIF_SECCOMP | _TIF_SINGLESTEP | _TIF_SYSCALL_TRACEPOINT | \
     _TIF_NOHZ)

/*
 * Same if PER_CPU_VAR(kernel_stack) is, perhaps with some offset, already in
 * a certain register (to be used in assembler memory operands).
 */
#define THREAD_INFO(reg, off) KERNEL_STACK_OFFSET+(off)-THREAD_SIZE(reg)

TI_flags宏定义在include/generated/asm-offsets.h文件中,这个文件是由Kbuild自动生成的,定义了一些偏移常量。

// file: include/generated/asm-offsets.h
#define TI_flags 16 /* offsetof(struct thread_info, flags)  # */

THREAD_INFO宏中,又引用了THREAD_SIZE宏,THREAD_SIZE定义的是线程内核栈的大小,在文件arch/x86/include/asm/page_64_types.h中。可以看到,THREAD_SIZE是把PAGE_SIZE左移一位得到的,也就是说,THREAD_SIZEPAGE_SIZE的2倍。PAGE_SIZE表示的是内存页的大小,该宏定义在arch/x86/include/asm/page_types.h文件中,其值通过计算为4096。所以,THREAD_SIZE的值为8192。到目前为止,我们计算出了内核栈的大小。

// file: arch/x86/include/asm/page_64_types.h
#define THREAD_SIZE_ORDER   1
#define THREAD_SIZE  (PAGE_SIZE << THREAD_SIZE_ORDER)
#define CURRENT_MASK (~(THREAD_SIZE - 1))
// file: arch/x86/include/asm/page_types.h
/* PAGE_SHIFT determines the page size */
#define PAGE_SHIFT  12
#define PAGE_SIZE   (_AC(1,UL) << PAGE_SHIFT)
#define PAGE_MASK   (~(PAGE_SIZE-1))

总结一下:

  • _TIF_WORK_SYSCALL_ENTRY宏表示的是在进入系统调用追踪时,有哪些状态位要置位。
  • THREAD_INFO宏根据传入的寄存器和偏移量,计算出 thread_info结构体的地址。
  • TI_flags+THREAD_INFO(%rsp,RIP-ARGOFFSET)计算出线程状态

写到这里,可能会有同学带有疑问:为什么TI_flags+THREAD_INFO(%rsp,RIP-ARGOFFSET)能够计算出线程的状态呢。这涉及到进程(线程)的数据结构。Linux内核中使用thread_info结构体来存储线程的相关信息,thread_info结构体定义在arch/x86/include/asm/thread_info.h文件中。thread_info有个成员变量flags,表示的是线程的标志位。系统利用这些标志位来做一些特殊处理。thread_info符号本身表示结构体起始的地址,flags变量与起始地址之间有两个成员变量taskexec_domain,这两个变量都是8字节的指针,所以flags变量相对thread_info的偏移量为16,跟我们看到的的TI_flags宏定义是一致的。所以TI_flags+THREAD_INFO(%rsp,RIP-ARGOFFSET)表示的是变量flags的地址。

// file: arch/x86/include/asm/thread_info.h
struct thread_info {
    struct task_struct  *task;      /* main task structure */
    struct exec_domain  *exec_domain;   /* execution domain */
    __u32           flags;      /* low level flags */
    __u32           status;     /* thread synchronous flags */
    __u32           cpu;        /* current CPU */
    int         preempt_count;  /* 0 => preemptable,
                           <0 => BUG */
    mm_segment_t        addr_limit;
    struct restart_block    restart_block;
    void __user     *sysenter_return;
#ifdef CONFIG_X86_32
    unsigned long           previous_esp;   /* ESP of the previous stack in
                           case of nested (IRQ) stacks
                        */
    __u8            supervisor_stack[0];
#endif
    unsigned int        sig_on_uaccess_error:1;
    unsigned int        uaccess_err:1;  /* uaccess failed */
};

thread_info并不孤单,它跟内核栈是共生的关系,这点从thread_union结构体中可以看到。thread_union结构体定义在include/linux/sched.h文件中,包含两个成员变量,一个是stack,一个就是thread_infostack是一个数组,可以看到,其包含THREAD_SIZE个字节,也就是8192字节。另外,Linux内核中,用task_struct结构体来表示进程。task_struct结构体中,有一个成员变量stack,该变量是一个指针,会指向内核栈。

// file: include/linux/sched.h
struct task_struct {
    ......
    void *stack;
    ......
}
union thread_union {
    struct thread_info thread_info;
    unsigned long stack[THREAD_SIZE/sizeof(long)];
};

thread_infotask_struct和内核栈的关系见下图:

Linux Kernel源码阅读: x86-64 系统调用实现细节(超详细)

回来继续说 testl指令,因为_TIF_WORK_SYSCALL_ENTRY以及thread_infoflags变量,都是32位整数,所以使用了带 l 后缀的的testl指令。 该指令会会对两个操作数做逻辑与(AND)运算,然后把执行结果丢弃,但会根据执行结果设置 SF、 ZF 和 PF 状态位。 所以这行代码通过testl指令判断线程状态信息里有没有设置跟踪、调试相关的状态位,有的话执行结果非0,ZF 位被清除,接下来的jnz tracesys会跳到tracesys去执行。在本文中,我们不关心追踪调试相关的流程,所以我们跳过这一部分。

testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags+THREAD_INFO(%rsp,RIP-ARGOFFSET)
jnz tracesys

继续往下走,进入system_call_fastpath标签。进入该标签后,我们会执行以下几行代码。这段代码的作用,是判断系统调用号是否超出了最大值,如果超出了,跳转到badsys执行。

#if __SYSCALL_MASK == ~0
    cmpq $__NR_syscall_max,%rax
#else
    andl $__SYSCALL_MASK,%eax
    cmpl $__NR_syscall_max,%eax
#endif
ja badsys

__SYSCALL_MASK宏定义在arch/x86/include/asm/unistd.h文件中,根据系统配置选项CONFIG_X86_X32_ABI来决定。如果系统不支x32 ABI,那么该宏被扩展为(~0);否则被扩展(~(__X32_SYSCALL_BIT))__X32_SYSCALL_BIT定义在arch/x86/include/uapi/asm/unistd.h中,其值为0x40000000__NR_syscall_max宏定义于include/generated/asm-offsets.h文件中,这是一个Kbuild编译时动态生成的文件。

// file: arch/x86/include/asm/unistd.h
# ifdef CONFIG_X86_X32_ABI
#  define __SYSCALL_MASK (~(__X32_SYSCALL_BIT))
# else
#  define __SYSCALL_MASK (~0)
# endif
/* x32 syscall flag bit */
#define __X32_SYSCALL_BIT   0x40000000
// file: include/generated/asm-offsets.h
#define __NR_syscall_max 542 /* sizeof(syscalls_64) - 1 # */

我们已经知道,在x86-64系统中,系统调用号是通过%rax寄存器来传递的(32位系统通过%eax来传递)。cmp指令会用第二个操作数减去第一个操作数,计算结果丢弃,但会根据计算结果设置 CF、OF、SF、ZF、AF 和 PF 状态位。ja指令会检查比较后的 ZF 和 CF 状态位,如果全为 0 就会执行跳转。同样的,对于异常处理本文不做解析。

继续往下,就会执行到下面两行代码:

movq %r10,%rcx
    call *sys_call_table(,%rax,8)  # XXX:    rip relative

虽然在执行syscall指令时,第四个参数要求使用%r10来传递。但是根据x86_64 ABI,按照 C 调用习惯,使用call指令进行函数调用时,第四个参数需要使用%rcx来传递,所以需要把第四个参数从 %r10复制到%rcx,然后才能发起函数调用。sys_call_table是系统调用表的地址,我们已经知道,系统调用表是一个数组,数组的每个元素都是一个8字节的指针,保存的是函数地址;另外,%rax里保存的是系统调用号;所以sys_call_table(,%rax,8)表示该系统调用号对应的函数入口地址。call *Operand是一个间接调用,表示操作数是从寄存器或内存中读出的。最终,call *sys_call_table(,%rax,8)会会切换到系统调用号对应的函数去执行。

函数调用完成后,程序会返回到arch/x86/kernel/entry_64.S继续执行。此时,被调用函数的执行结果已经保存到 %rax寄存器。接下来,会把 %rax的值 保存到内核栈对应的位置。

movq %rax,RAX-ARGOFFSET(%rsp)

接下来,进入ret_from_sys_call标签。

ret_from_sys_call:
    movl $_TIF_ALLWORK_MASK,%edi

_TIF_ALLWORK_MASKh宏定义在arch/x86/include/asm/thread_info.h文件中,表示在返回用户空间时需要处理的一些工作,比如说有信号等待处理(TIF_SIGPENDING)或者需要重新调度(TIF_NEED_RESCHED)等。

// file: arch/x86/include/asm/thread_info.h
/* work to do on any return to user space */
#define _TIF_ALLWORK_MASK                       \
    ((0x0000FFFF & ~_TIF_SECCOMP) | _TIF_SYSCALL_TRACEPOINT |   \
    _TIF_NOHZ)

然后进入sysret_check标签,表示系统调用返回前要做的一些检查工作。

sysret_check:
    LOCKDEP_SYS_EXIT
    DISABLE_INTERRUPTS(CLBR_NONE)
    TRACE_IRQS_OFF

这三个宏都定义在arch/x86/include/asm/irqflags.h文件中,LOCKDEP_SYS_EXIT宏的具体实现依赖于内核配置选项CONFIG_DEBUG_LOCK_ALLOC,它允许我们从系统调用返回时调试锁信息。本文不会涉及到调试相关的细节,所以略过。DISABLE_INTERRUPTS宏被直接扩展成cli指令,禁止中断。TRACE_IRQS_OFF宏的具体实现依赖于内核配置选项CONFIG_TRACE_IRQFLAGS,该宏跟中断追踪有关,本文暂略过。

// file: arch/x86/include/asm/irqflags.h
#define DISABLE_INTERRUPTS(x)   cli

#ifdef CONFIG_TRACE_IRQFLAGS
#  define TRACE_IRQS_ON     call trace_hardirqs_on_thunk;
#  define TRACE_IRQS_OFF    call trace_hardirqs_off_thunk;
#else
#  define TRACE_IRQS_ON
#  define TRACE_IRQS_OFF
#endif

#ifdef CONFIG_DEBUG_LOCK_ALLOC
#  define LOCKDEP_SYS_EXIT  ARCH_LOCKDEP_SYS_EXIT
#  define LOCKDEP_SYS_EXIT_IRQ  ARCH_LOCKDEP_SYS_EXIT_IRQ
# else
#  define LOCKDEP_SYS_EXIT
#  define LOCKDEP_SYS_EXIT_IRQ
# endif

再接下来,会判断系统调用返回前,有没有需要处理的工作。movl把线程当前的标志位信息复制到%edx,然后与%edi进行逻辑与操作。%edi里保存的是返回前需要处理的标志位组合。andl指令执行后,如果结果为0,eflags里的ZF 位为1,表示没有额外的工作要处理;如果不为0,会清除ZF位, 说明有工作要处理。jnz指令会判断 ZF 标志位的值,ZF为0时,跳转到sysret_careful执行。sysret_careful处的执行流程本文不涉及。

movl TI_flags+THREAD_INFO(%rsp,RIP-ARGOFFSET),%edx
    andl %edi,%edx
    jnz  sysret_careful

检查通过之后,会对寄存器和栈进行恢复。syscall指令会把返回地址保存到 %rcx,把 rflags 的值保存到%r11sysret指令执行相反的操作,会用%rcx%r11的值恢复%rip和rflags。所以,调用sysret之前,我们要先用保存在栈中的值去恢复%rcx%r11;还要把%rsp恢复成用户空间的栈指针。

movq RIP-ARGOFFSET(%rsp),%rcx
    RESTORE_ARGS 1,-ARG_SKIP,0
    /*CFI_REGISTER  rflags,r11*/
    movq    PER_CPU_VAR(old_rsp), %rsp

movq RIP-ARGOFFSET(%rsp),%rcx恢复%rcx的值;RESTORE_ARGS 1,-ARG_SKIP,0使用保存到内核栈中值恢复各寄存器,其中%rcx的值在上一指令已经恢复过了,此处忽略未恢复;movq PER_CPU_VAR(old_rsp), %rsp恢复用户空间的栈指针。

RESTORE_ARGS宏定义于arch/x86/include/asm/calling.h文件,其又引入了movq_cfi_restore宏。movq_cfi_restore宏定义在arch/x86/include/asm/dwarf2.h文件中。

// file: arch/x86/include/asm/calling.h
#define ARG_SKIP    (9*8)

    .macro RESTORE_ARGS rstor_rax=1, addskip=0, rstor_rcx=1, rstor_r11=1, \
                rstor_r8910=1, rstor_rdx=1
    .if \rstor_r11
    movq_cfi_restore 0*8, r11
    .endif

    .if \rstor_r8910
    movq_cfi_restore 1*8, r10
    movq_cfi_restore 2*8, r9
    movq_cfi_restore 3*8, r8
    .endif

    .if \rstor_rax
    movq_cfi_restore 4*8, rax
    .endif

    .if \rstor_rcx
    movq_cfi_restore 5*8, rcx
    .endif

    .if \rstor_rdx
    movq_cfi_restore 6*8, rdx
    .endif

    movq_cfi_restore 7*8, rsi
    movq_cfi_restore 8*8, rdi

    .if ARG_SKIP+\addskip > 0
    addq $ARG_SKIP+\addskip, %rsp
    CFI_ADJUST_CFA_OFFSET   -(ARG_SKIP+\addskip)
    .endif
    .endm
// file: arch/x86/include/asm/dwarf2.h
    .macro movq_cfi_restore offset reg
    movq \offset(%rsp), %\reg
    CFI_RESTORE \reg
    .endm

主流程最后一步,执行到USERGS_SYSRET64

USERGS_SYSRET64

该宏定义于arch/x86/include/asm/irqflags.h,会扩展成swapgssysretqswapgs交换用户空间GS段和内核空间GS段的值,然后执行sysretq返回用户空间。

// file: arch/x86/include/asm/irqflags.h
#define USERGS_SYSRET64             \
    swapgs;                 \
    sysretq;

至此,我们已经分析完了系统调用的主流程。总结一下,执行系统调用时主要有以下几个步骤:文章来源地址https://www.toymoban.com/news/detail-460379.html

  • 在用户空间将系统调用号及参数传入指定的寄存器。
  • 使用syscall从用户态切换到内核态,然后从入口system_call开始执行。
  • 保存用户态栈指针,然后切换到内核栈。接着,将传参用通用寄存器、返回地址及调用号存入内核栈。
  • 检查系统调用号,检查通过后,在系统调用表里根据调用号找到对应的函数入口,执行函数调用;否则在%rax里放入错误码-ENOSYS直接返回。
  • 恢复现场,包括通用寄存器、rflags寄存器,返回地址,栈指针。
  • 使用sysret从内核态返回到用户态。

到了这里,关于Linux Kernel源码阅读: x86-64 系统调用实现细节(超详细)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Linux系统下cpython-37m-x86_64-linux-gnu.so文件ImportError

    ) 在查找答案的过程中遇到了几种回答,大多评论都表示有用 库的版本不兼容 多数遇到的是这种,但因为库比较难发现哪个不匹配等等原因,继续寻找其它回答…(当然,如果出现numpy或是pillow库的问题,还是及时解决比较好) 我运行的是UA-CMDet,如果有类似的可以作为简单

    2024年02月19日
    浏览(41)
  • X86_64函数调用汇编程序分析

    %rdi, %rsi, %rdx, %rcx, %r8, %r9分别用于函数调用过程中的前6个参数,对于6的参数存放在栈中传递 %rsp用做栈指针寄存器,指向栈顶 %rbp用作栈框寄存器,指向栈底 %rax用做函数返回值的第一个寄存器 2.1.1 main的C代码实现 2.1.2 main函数对应汇编及其分析 这段汇编代码实现了一个简单的

    2024年02月09日
    浏览(40)
  • 基于aarch64分析kernel源码 一:环境搭建

    功能 工具 操作系统 ubuntu 22.04 编译工具 gcc-12-aarch64-linux-gnu 调试工具 gdb-multiarch 模拟器 qemu 6.2.0 busybox busybox-1.36.1 kernel linux-6.4.1 编辑器 vscode 1、查找ubuntu仓库中aarch64编译器 2、安装gcc 修改gcc名字 1、源码 2、配置busybox 3、编译 4、安装 详细内容见:qemu搭建arm嵌入式linux开发环

    2024年02月13日
    浏览(39)
  • vscode 阅读 android以及kernel 源码

    在Ubuntu系统中安装vscode 参考文档: https://blog.csdn.net/m0_57368670/article/details/127184424 1, 下载vscode https://code.visualstudio.com 2, 安装vscode $ sudo dpkg -i code_1.78.1-1683194560_amd64.deb 3, 打开vscode $ code vscode 阅读 android以及kernel 源码 参考文档: https://blog.csdn.net/lyndon_li/article/details/127955889 sudo

    2024年02月05日
    浏览(48)
  • [linux kernel]slub内存管理分析(4) 细节操作以及安全加固

    前情回顾 关于slab几个结构体的关系和初始化和内存分配的逻辑请见: [linux kernel]slub内存管理分析(0) 导读 [linux kernel]slub内存管理分析(1) 结构体 [linux kernel]slub内存管理分析(2) 初始化 [linux kernel]slub内存管理分析(2.5) slab重用 [linux kernel]slub内存管理分析(3) kmalloc 描述方法约定

    2023年04月09日
    浏览(50)
  • 基于aarch64分析kernel源码 三:启动代码分析

    这段代码是主要的入口点函数 primary_entry ,在 启动过程 中执行 一系列操作 后 将控制权切换到内核 。 以下是对每行代码的解释: 这行代码表示这是一个 本地符号 ,标识 primary_entry 函数的 开始位置 。 这三行代码调用了三个不同的函数。首先调用 record_mmu_state 函数来 记录

    2024年02月15日
    浏览(42)
  • 在Windows上使用VScode阅读kernel源码

    有一说一,在Windows上使用Source Inside阅读kernel源码真的很舒服,但是有时候带着轻薄本出去,又不想往轻薄本上安装很多的软件,就使用VS code临时阅读kernel源码。如果不能进行跳转,阅读kernel源码就很难受,今天就记录一下,如何配置VS code,可以方便地阅读kernel源码。 首先

    2024年01月18日
    浏览(47)
  • Linux操作系统下Docker和Docker Compose的安装教程(包含x86和arm64平台离线一键安装资源包)

    本文章将详细介绍Linux下Docker和Docker Compose的安装教程。 目录3为x86和arm64平台Docker离线安装资源包,包含Docker Compose、一键安装脚本使用教程。 相关文章参考: Docker常用基础命令 Docker批量清理删除镜像和容器常用命令 版本如下 名称 版本 CentOS 7.6+ openEuler等其他操作系统可以使

    2024年02月06日
    浏览(46)
  • Linux上x86_64架构的动态链接器 ld-linux-x86-64.so.2

    /lib64/ld-linux-x86-64.so.2 是Linux操作系统上x86_64架构的动态链接器(也称为动态链接编辑器)。它负责加载和链接在运行时(即程序启动时或之后)被引用的动态库。现在,我们来深入了解其作用和重要性: 动态链接器的作用 : 当运行一个可执行程序时,该程序可能依赖于多个动

    2024年02月02日
    浏览(49)
  • linux系统上nodejs 报错:node: /lib/x86_64-linux-gnu/libm.so.6: version `GLIBC_2.27‘ not found

    原因:因为当前系统不支持GLIBC_2.27,而且node的版本过高,但是后来降低了版本还是报这个错误,后来发现低版本的软链接在/usr/bin,而高版本的软链接在/usr/local/bin,一直使用了高版本所在的软链接 解决方法:删除/usr/local/bin中node的软链接,然后再安装一个比较低版本的no

    2024年02月16日
    浏览(53)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包