0、前言
本文采用Linux 内核 v3.10 版本
本文不涉及调试、跟踪及异常处理的细节
一、系统调用简介
系统调用是用户空间程序与内核交互的主要机制。系统调用与普通函数调用不同,因为它调用的是内核里的代码。使用系统调用时,需要特殊指令以使处理器权限转换到内核态。另外,被调用的内核代码由系统调用号来标识,而不是函数地址。
系统调用整体流程如下图所示:
二、从 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 thesyscall
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节。
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
指令执行时,中断是禁止的。
五、系统调用编号
在示例程序中,我们使用了write
和exit
系统调用,并通过%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
直接返回一个错误码-ENOSYS
。ENOSYS
值为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 Long
或Unsigned 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 nonzero. Otherwise 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 expressione
.
2.!!(e)
: Logically negate twice:0
ife == 0
; otherwise1
.
3.-!!(e)
: Numerically negate the expression from step 2:0
if it was0
; 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_write
和SYSC_write
;然后定义了函数SyS_write
,该函数内部调用了SYSC_write
;给SyS_write
设置了一个别名sys_write
;SYSC_write
是write
系统调用的具体实现。
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)
先来看下ENTRY
、GLOBAL
、END
这三个宏,其中ENTRY
、END
定义在include/linux/linkage.h
文件中,ENTRY
宏中又引用了ALIGN
宏。ALIGN
与GLOBAL
宏一起,定义在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 calls, there 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_rcx
和save_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
指令执行完成后,内核栈的结构示意如下:
下一步,把%rax
和%rcx
寄存器的值保存到内核栈中。因为在执行syscall
时,会把返回地址存入%rcx
,所以%rcx
会被破坏,我们要把它提前保存起来。%rax
寄存器后面也会被修改,所以一起保存起来。
movq %rax,ORIG_RAX-ARGOFFSET(%rsp)
movq %rcx,RIP-ARGOFFSET(%rsp)
ORIG_RAX
、ARGOFFSET
、ARGOFFSET
这三个宏定义于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
执行完成后,内核栈示意图如下:
继续往下,是测试和跳转指令。
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_SIZE
是PAGE_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
变量与起始地址之间有两个成员变量task
和exec_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_info
。stack
是一个数组,可以看到,其包含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_info
、task_struct
和内核栈的关系见下图:
回来继续说 testl
指令,因为_TIF_WORK_SYSCALL_ENTRY
以及thread_info
里flags
变量,都是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 的值保存到%r11
;sysret
指令执行相反的操作,会用%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
,会扩展成swapgs
和sysretq
。swapgs
交换用户空间GS段和内核空间GS段的值,然后执行sysretq
返回用户空间。
// file: arch/x86/include/asm/irqflags.h
#define USERGS_SYSRET64 \
swapgs; \
sysretq;
至此,我们已经分析完了系统调用的主流程。总结一下,执行系统调用时主要有以下几个步骤:
- 在用户空间将系统调用号及参数传入指定的寄存器。
- 使用
syscall
从用户态切换到内核态,然后从入口system_call
开始执行。 - 保存用户态栈指针,然后切换到内核栈。接着,将传参用通用寄存器、返回地址及调用号存入内核栈。
- 检查系统调用号,检查通过后,在系统调用表里根据调用号找到对应的函数入口,执行函数调用;否则在
%rax
里放入错误码-ENOSYS
直接返回。 - 恢复现场,包括通用寄存器、rflags寄存器,返回地址,栈指针。
- 使用
sysret
从内核态返回到用户态。