文章是转载,但是我在后面的案例加了不少注解并debug了,如有疑问,留言交流 。(其实我也不懂)
原文链接: blog.packagecloud.io
https://zcfy.cc/article/the-definitive-guide-to-linux-system-calls-670.html?t=new
这篇blog解释linux程序如何调用linux内核函数。
这篇文章概述不同的几个做系统调用的方法,如何自己写系统调用(包含例子),系统调用的内核入口,内核出口,glibc封装器,bugs等等。
当你运行的程序调用了open, fork, read, write等等,你就做了系统调用 。
系统调用就是程序如何进入内核执行任务。程序使用系统调用执行一系列的操作诸如:创建进程,网络和文件IO等等。
你可以在man page for syscalls(2)里面看到系统调用的列表。 用户程序做系统调用有不同的方法,CPU架构不同做系统调用的底层指令也不同。
作为应用开发者,你不需要经常思考系统调用如何正确执行。你只需要把头文件引入,然后像普通功能一样调用。
glibc作为装饰器,抽象组装你传递的参数然后进入内核的细节。
在我们详细研究系统调用如何实现之前,需要定义一些后面将会出现的条款和核心概念。
前提信息
本文做如下假设:
你使用的是Intel或者AMD的32位或者64位CPU。本文讨论的方法可能对其他系统也有用,但是例子中的代码包含一些CPU专用代码。
你对3.13.0版本的Linux内核感兴趣。其他版本内核是相似的,但是代码准确的行数,代码的组织和文件路径是不一样的。建议从GitHub上链接3.13.0版本内核源码树。
你对glibc或者由glibc得到的libc实现感兴趣。 本文所指的x86-64是基于x86架构的64位Intel和AMDCPU。
原来是x86-64的CPU有一个权限等级概念。权限等级是个复杂的题目适合单独一片博客来阐述。在这片博客中,我们简单地把权限等级概念解释为:
1. 权限等级意味着访问控制。当前权限等级决定了那些CPU指令和IO操作可以执行。
2. 内核运行在最高权限等级,叫做“Ring 0”。用户程序运行在较低等级,叫做“Ring 3”。
用户程序为了要执行某些高权限操作,必须修改权限等级(从“Ring 3”到“Ring 0”),所以由内核执行。
这里有一些方法可以改变权限等级,触发内核执行操作。
先介绍一个内核调用的普通方法:中断。
中断
你可以认为中断时由硬件或者软件产生的事件。
一个硬件中断时由硬件设备产生的通知内核有特殊事件发生了。这种中断较常见的例子是网卡收到包产生的中断。
**一个软件中断是执行某条代码的时候产生的。**在x86-64系统中,执行int指令可以产生一个软件中断。
中断一般有一个分配的中断号。有些中断号有特殊意义。
你可以想象CPU存储器中有一个数组。数组中的每一个条目都指向一个中断号。每个条目都包含一个函数的入口地址,当某个操作产生中断的时候,CPU可以通过入口地址执行这个函数。
Intel CPU指南里面这张图展示了数组中各个条目的布局:
Screenshot of Interrupt Descriptor Table entry diagram for x86_64 CPUs
如果你仔细看这个图,会发现2bit字段DPL(Descriptor Privilege Level)。这个字段的值决定了CPU执行程序的权限等级。
这就CPU是如何知道需要执行哪个地址的指令以及这个指令的权限等级(当一个特殊类型事件发生的时候)。
实际上x86-64系统有很多种方法可以处理中断。如果你对这方面感兴趣可以读8259 Programmable Interrupt Controller, Advanced Interrupt Controllers, 和 IO Advanced Interrupt Controllers.
处理硬件/软件中断还要处理一些其它复杂的事情,比如中断号冲突和重映射。
讨论系统调用的时候我们不需要关心这些细节。
Model Specific Registers (MSRs)
特殊模块寄存器
你可以分别使用rdmsr 和 wrmsr来读写MSRs。
也有命令行工具可以读写MSRs,但是不推荐因为改变MSRs值是危险的(特别是当操作系统正在运行的时候),除非你真的很小心。
如果你不介意系统的崩溃或者数据的不可逆失效风险,可以安装msr-tools然后加载msr内核模块来读写MSRs。 (干活前,打个快照!!!)
% sudo apt-get install msr-tools
% sudo modprobe msr
% sudo rdmsr
稍后我们将会看到一些系统调用使用MSRs。
用汇编做系统调用是坏主意
自己写汇编代码执行系统调用不是个好办法。
其中的一个原因是在有些系统调用前/调用后,glibc要执行一些额外的代码。
下面的例子我们会使用 exit 系统调用。使用 atexit 注册函数,当程序调用 exit 时就会执行你注册的函数。
那些代码是通过glibc调用的而不是内核。所以,如果你写汇编语言像下面那样执行exit,你注册的函数不会被执行因为绕过了glibc。
然而,用汇编语言做系统调用有利于学习经验。
有两个需要预先准备的知识:
我们可以通过生成软中断触发内核调用。
我们可以用汇编指令int生成软中断。
结合这两个概念让我们看Linux传统系统调用接口。
用户空间程序可以取到Linux内核软中断号,这样就可以进入内核和执行系统调用。
Linux内核给128(0x80)中断注册了名为 ia32_syscall 的中断执行程序。让我们看看具体做这件事的代码。
内核3.13.0,arch/x86/kernel/traps.c源码中的trap_init函数
void __init trap_init(void) {
/* ..... other code ... */
set_system_intr_gate(IA32_SYSCALL_VECTOR, ia32_syscall);
IA32_SYSCALL_VECTOR在arch/x86/include/asm/irq_vectors.h中定义的,值是0x80。
但是,虽然用户空间程序可以发出内核系统存储的软中断信号来触发内核,内核如何知道需要执行哪个系统调用?
用户空间程序会把系统调用号放在eax寄存器中,系统调用的参数放在其它通用寄存器中。
arch/x86/ia32/ia32entry.S注释中有它的说明。
* Emulated IA32 system calls via int 0x80.
*
* Arguments:
* %eax System call number.
* %ebx Arg1
* %ecx Arg2
* %edx Arg3
* %esi Arg4
* %edi Arg5
* %ebp Arg6 [note: not saved in the stack frame, should not be touched]
*
我们已经知道如何做系统调用和参数存在哪里,现在我们通过写内联汇编做一个系统调用。
用汇编做传统系统调用
你可以写一小段内联汇编做传统系统调用。虽然以学习的观点来看这很有趣,但我还是建议读者永远不要手动写汇编函数做系统调用。
在这个例子中,我们试着做exit系统调用,这个调用有一个参数:退出状态。
首先,我们先找到exit的系统调用号。Linux内核包含一个文件,这个文件在一个表格中列出了各个系统调用。这个文件在构建阶段被不同的脚本加工然后生成可以被用户程序使用的头文件。
让我们看看这个在 arch/x86/syscalls/syscall_32.tbl 发现的表格:
1 i386 exit sys_exit
exit系统调用号是1。根据上面的接口描述,我们只需要把系统调用号放到eax寄存器,第一个参数(推出状态)放到ebx寄存器。
这里是一段含有内联汇编代码的C语言程序。我们把退出状态设置成“42”:
(这段代码可以被简化,但我想这样写可以让那些不知道GCC内联汇编的人理解和参考。)
int
main(int argc, char *argv[])
{
unsigned int syscall_nr = 1;
int exit_status = 42;
asm ("movl %0, %%eax\n"
"movl %1, %%ebx\n"
"int $0x80"
: /* output parameters, we aren't outputting anything, no none */
/* (none) */
: /* input parameters mapped to %0 and %1, repsectively */
"m" (syscall_nr), "m" (exit_status)
: /* registers that we are "clobbering", unneeded since we are calling exit */
"eax", "ebx");
}
下一步,编译,执行,然后检查退出状态:
$ gcc -o test test.c
$ ./test
$ echo $?
42
成功了!我们使用传统系统调用方法通过发出软中断来执行exit。
内核内部:int $0x80入口
我们已经看了用户空间程序如何触发系统调用,接下来看看内核如何使用系统调用号执行系统调用代码。
上一章我们提到,内核注册了一个系统调用执行函数叫做 ia32_syscall 。
这个函数是在 arch/x86/ia32/ia32entry.S 中用汇编实现的,
ia32_do_call:
IA32_ARG_FIXUP
call *ia32_sys_call_table(,%rax,8) # xxx: rip relative
IA32_ARG_FIXUP 是一个宏,它重新排列了传统参数好让当前系统调用层恰当理解。
ia32_sys_call_table 标示符引用了 arch/x86/ia32/syscall_ia32.c 中定义的表格。注意代码结尾处的#include行。
const sys_call_ptr_t ia32_sys_call_table[__NR_ia32_syscall_max+1] = {
/*
* Smells like a compiler bug -- it doesn't work
* when the & below is removed.
*/
[0 ... __NR_ia32_syscall_max] = &compat_ni_syscall,
#include
};
回忆我们之前看到的在arch/x86/syscalls/syscall_32.tbl中定义的表格。
编译期有几个脚本会取这个表格并且生成syscalls_32.h文件。生成的头文件由有效的C语言组成,就是上面用#include插入的代码,它把根据系统调用号得到的函数地址索引写进ia32_sys_call_table。
这就是你如何通过传统系统调用进入内核的。
用iret从传统系统调用返回
我们已经看到用软中断如何进入内核,但是内核是如何返回用户空间的,并且内核结束执行之后如何丢弃权限等级的?
我们可以在这个文档(注意:大PDF)Intel Software Developer’s Manual 看到一副有用的图解说明了当权限级别改变的时候程序栈是如何安排的。
如图:
Screenshot of the Stack Usage on Transfers to Interrupt and Exception-Handling Routines
当用户程序触发软中断,程序转移到内核函数ia32_syscall的时候权限级别发生改变。结果就是当进入ia32_syscall 的时候程序栈就会想上面图例一样。
这就意味着返回地址,译成权限等级等的CPU标志,还有很多在ia32_syscall执行前都被保存在程序栈中。
所以,为了恢复执行,内核只需要把程序栈中的值拷贝回寄存器,这样程序又恢复到了用户空间。
好的,你该怎么做?
只有很少的方法可以做到,但是最简单的方法是用iret指令。
在Intel指令集手册的解释是:iret指令把返回地址和存贮的寄存器值从栈中压出。
随着实地址模式中断的返回,IRET指令从栈中分别弹出返回指令指针,返回代码段选择器,EFLAGS镜像到EIP, CS, 和 EFLAGS 寄存器,然后恢复执行被中断的程序或进程。
在内核里面找到这段代码有点困难,因为它隐藏在一些宏代码之下,并且处理信号和 ptrace 退出跟踪需要特别小心。
最终在内核中挖出的汇编语言宏代码揭露了iret如何从系统调用返回用户程序。
从 arch/x86/kernel/entry_64.S中的irq_return :
irq_return:
INTERRUPT_RETURN
INTERRUPT_RETURN在arch/x86/include/asm/irqflags.h中定义为iretq。
你现在知道了传统系统调用时如何工作了。
传统方法看起来非常合理,但是现在有新的方法触发系统调用,不需要包含软中断并且比使用软中断要快得多。
两个快速方法都是由两个指令组成。一个进入内核,一个离开内核。两个方法都在Intel CPU文档中“快速系统调用”里介绍。
不幸的是,当CPU在32位或64位模式的时候,在哪个方法有效的问题上,Intel和AMD的实现是不一致的。
为了最大的兼容Intel和AMD的CPU:
在32位系统使用: sysenter 和 sysexit.
在64位系统使用: syscall 和 sysret.
32-bit fast system calls
32位快速系统调用
sysenter/sysexit
使用sysenter做系统调用比传统通断方法更复杂并且在用户空间和内核之间要做更多适配(通过glibc)。
我们一步一步的做并挑出其中的细节。首先我们看看Intel指令集参考文档(注意大文件PDF)对sysenter的介绍和怎么使用。
我们看一看:
执行SYSENTER指令前,软件必须通过把值写入下面的MSRs中来指定权限等级0的代码段和代码入口,并且指定权限等级0的堆栈段和堆栈指针。
IA32_SYSENTER_CS (MSR address 174H) — MSR的低16位是权限等级0代码段的段选择器。这个值也用来决定权限等级0堆栈段的段选择器(见Operation章)。这个值不能指示一个空选择器。
IA32_SYSENTER_EIP (MSR address 176H) — MSR的这个值加载到RIP(这样,这个值就指向了被选择的操作程序或常规程序的第一条指令的地方)。在保护模式,只有31:0位会被加载。
IA32_SYSENTER_ESP (MSR address 175H) — MSR的这个值加载到RSP(这样,这个值包含了权限等级0栈的栈指针)。这个值不能表示一个不按规则的地址。在保护模式,只有31:0位会被加载。
换句话说:为了让内核收到 sysenter 系统调用,内核必须设置3个特殊模块寄存器(MSRs)。我们最需要关注的MSR是IA32_SYSENTER_EIP(含有0x176地址)。当用户程序执行sysenter指令,这个MSR就是内核指定的将要执行的程序的地址。
我们可以在内核中arch/x86/vdso/vdso32-setup.c找到写MSR的代码:
void enable_sep_cpu(void)
{
/* ... other code ... */
wrmsr(MSR_IA32_SYSENTER_EIP, (unsigned long) ia32_sysenter_target, 0);
MSR_IA32_SYSENTER_EIP在arch/x86/include/uapi/asm/msr-index.h中赋值为0x00000176。
就像传统软中断系统调用,这是用 sysenter 做系统调用的惯例。
arch/x86/ia32/ia32entry.S中的注释做了说明:
* 32bit SYSENTER instruction entry.
*
* Arguments:
* %eax System call number.
* %ebx Arg1
* %ecx Arg2
* %edx Arg3
* %esi Arg4
* %edi Arg5
* %ebp user stack
* 0(%ebp) Arg6
回忆传统系统调用方法对返回被中断的用户空间程序有个一机制:iret指令。
理解让sysenter正确执行的逻辑是复杂的,因为不像软中断,sysenter没有保存返回地址。
确切的讲,在执行 sysenter 指令之前,内核存储地址和其它数据是如何实现的。(你将会在下面的Bugs那章看到它确实实现了)。
为了避免未来的变化,用户程序原打算调用__kernel_vsyscall函数,这个函数是内核实现的,但是在进程开始的时候映射到了各个用户进程。
这有点旧了;这段代码来自内核,却在用户空间执行。
__kernel_vsyscall 被证明是虚拟动态共享对象(vDSO)的一部分,vDSO是用来让程序在用户空间执行内核代码。
接下来我们将会深度的检查vDSO是什么,有什么功能,和如何工作的。
我们开始检测__kernel_vsyscall的内部构件。
__kernel_vsyscall internals
__kernel_vsyscall 内部构件
__kernel_vsyscall 函数封装了sysenter调用惯例,可以在 arch/x86/vdso/vdso32/sysenter.S中看到:
__kernel_vsyscall:
.LSTART_vsyscall:
push %ecx
.Lpush_ecx:
push %edx
.Lpush_edx:
push %ebp
.Lenter_kernel:
movl %esp,%ebp
sysenter
__kernel_vsyscall 是动态共享对象(也被叫做共享库)的一部分。用户程序在运行时如何找到动态共享函数的地址的?
__kernel_vsyscall 函数地址写在 ELF 辅助向量,这个向量在用户程序或者库(特别是glibc)可以找到和使用的地方。
有一些方法可以找到ELF辅助向量:
第一种方法最简单,但是 glibc 的2.16版本之后才有。下面例子的代码对第二种方法做了解释。
就像我们上面看到的代码,__kernel_vsyscall 在 sysenter 调用前做了一些记账。
所以,我们手动用 sysenter 进入内核需要做的全部事情是:
找到 AT_SYSINFO 的 ELF辅助向量,找到 __kernel_vsyscall 地址。
就像传统系统调用一样把系统调用号和参数放倒寄存器中。
调用 __kernel_vsyscall 函数。
你永远也不应该自己写 sysenter 封装函数因为内核使用 sysenter 进入和退出系统调用的??传统会变化,你的代码会被中断。
你应该一直用 __kernel_vsyscall 来执行 sysenter 系统调用。
好的,让我们这么做。
写汇编使用sysenter系统调用
就像之前我们的传统系统调用例子,我们将会执行退出状态是 42 的 exit 。
exit系统调用号是1。根据上面的接口描述,我们只需要把系统调用号传入eax寄存器,把第一个参数(退出状态码)传入ebx。
(这段代码可以被简化,但我想这样写可以让那些不知道GCC内联汇编的人理解和参考。)
#include
#include
int main(int argc, char* argv[], char* envp[])
{
unsigned int syscall_nr = 1;
int exit_status = 42;
Elf32_auxv_t *auxv;
/*typedef struct
{
uint32_t a_type; /* Entry type */
union
{
uint32_t a_val; /* Integer value */
/* We use to have pointer elements added here. We cannot do that,
though, since it does not work when using 32-bit definitions
on 64-bit platforms and vice versa. */
} a_un;
} Elf32_auxv_t;
*/
/* auxilliary vectors are located after the end of the environment
* variables
*
* check this helpful diagram: https://static.lwn.net/images/2012/auxvec.png
*/
while(*envp++ != NULL);
/* envp is now pointed at the auxilliary vectors, since we've iterated
* through the environment variables.
*/
// #define AT_SYSINFO 32
// #define AT_NULL 0
for (auxv = (Elf32_auxv_t *)envp; auxv->a_type != AT_NULL; auxv++)
{
if( auxv->a_type == AT_SYSINFO)
{
break;
}
}
/* NOTE: in glibc 2.16 and higher you can replace the above code with
* a call to getauxval(3): getauxval(AT_SYSINFO)
*/
asm(
"movl %0, %%eax \n"
"movl %1, %%ebx \n"
"call *%2 \n"
: /* output parameters, we aren't outputting anything, no none */
/* (none) */
: /* input parameters mapped to %0 and %1, repsectively */
"m" (syscall_nr), "m" (exit_status), "m" (auxv->a_un.a_val)
: /* registers that we are "clobbering", unneeded since we are calling exit */
"eax", "ebx");
}
下一步,编译,执行,然后检查退出状态:
Dump of assembler code for function main:
0x0804843b <+0>: lea ecx,[esp+0x4]
0x0804843f <+4>: and esp,0xfffffff0
0x08048442 <+7>: push DWORD PTR [ecx-0x4]
0x08048445 <+10>: push ebp
0x08048446 <+11>: mov ebp,esp
0x08048448 <+13>: push ebx
0x08048449 <+14>: push ecx
0x0804844a <+15>: sub esp,0x20
0x0804844d <+18>: mov eax,ecx
0x0804844f <+20>: mov edx,DWORD PTR [eax+0x4]
0x08048452 <+23>: mov DWORD PTR [ebp-0x1c],edx
0x08048455 <+26>: mov eax,DWORD PTR [eax+0x8]
0x08048458 <+29>: mov DWORD PTR [ebp-0x20],eax
0x0804845b <+32>: mov eax,gs:0x14
0x08048461 <+38>: mov DWORD PTR [ebp-0xc],eax
0x08048464 <+41>: xor eax,eax
0x08048466 <+43>: mov DWORD PTR [ebp-0x18],0x1
0x0804846d <+50>: mov DWORD PTR [ebp-0x14],0x2a
0x08048474 <+57>: nop
0x08048475 <+58>: mov eax,DWORD PTR [ebp-0x20]
0x08048478 <+61>: lea edx,[eax+0x4]
0x0804847b <+64>: mov DWORD PTR [ebp-0x20],edx
0x0804847e <+67>: mov eax,DWORD PTR [eax]
0x08048480 <+69>: test eax,eax
0x08048482 <+71>: jne 0x8048475
0x08048484 <+73>: mov eax,DWORD PTR [ebp-0x20]
0x08048487 <+76>: mov DWORD PTR [ebp-0x10],eax
0x0804848a <+79>: jmp 0x804849a
0x0804848c <+81>: mov eax,DWORD PTR [ebp-0x10]
0x0804848f <+84>: mov eax,DWORD PTR [eax]
0x08048491 <+86>: cmp eax,0x20
0x08048494 <+89>: je 0x80484a5
0x08048496 <+91>: add DWORD PTR [ebp-0x10],0x8
0x0804849a <+95>: mov eax,DWORD PTR [ebp-0x10]
0x0804849d <+98>: mov eax,DWORD PTR [eax]
0x0804849f <+100>: test eax,eax
0x080484a1 <+102>: jne 0x804848c
0x080484a3 <+104>: jmp 0x80484a6
0x080484a5 <+106>: nop
0x080484a6 <+107>: mov edx,DWORD PTR [ebp-0x10]
0x080484a9 <+110>: mov eax,DWORD PTR [ebp-0x18]
0x080484ac <+113>: mov ebx,DWORD PTR [ebp-0x14]
0x080484af <+116>: call DWORD PTR [edx+0x4]
0x080484b2 <+119>: mov eax,0x0
0x080484b7 <+124>: mov ebx,DWORD PTR [ebp-0xc]
0x080484ba <+127>: xor ebx,DWORD PTR gs:0x14
0x080484c1 <+134>: je 0x80484c8
0x080484c3 <+136>: call 0x8048310 <__stack_chk_fail@plt>
0x080484c8 <+141>: add esp,0x20
0x080484cb <+144>: pop ecx
0x080484cc <+145>: pop ebx
0x080484cd <+146>: pop ebp
0x080484ce <+147>: lea esp,[ecx-0x4]
0x080484d1 <+150>: ret
End of assembler dump.
debug
► 0x80484af call dword ptr [edx + 4] <0xf7fd8be0>
0x80484b2 mov eax, 0
0x80484b7 mov ebx, dword ptr [ebp - 0xc]
0x80484ba xor ebx, dword ptr gs:[0x14]
0x80484c1 je main+141 <0x80484c8>
pwndbg> x/10i 0xf7fd8be0
0xf7fd8be0 <__kernel_vsyscall>: push ecx
0xf7fd8be1 <__kernel_vsyscall+1>: push edx
pwndbg> p/x auxv->a_un.a_val 实际上就是 __kernel_vsyscall 地址,get it!!!
$2 = 0xf7fd8be0
pwndbg> x/2xw $edx+4
0xffffd180: 0xf7fd8be0 0x00000021
$ gcc -m32 -o demo demo.c
$ $ echo $?
42
成功了!我们在没有生成软中断的情况下使用传统的 sysenter 方法做了 exit 系统调用。怎么理解这里的 sysenter 呢? 看下面的代码自明 。
pwndbg> x/12i 0xf7fd8be0
0xf7fd8be0 <__kernel_vsyscall>: push ecx
0xf7fd8be1 <__kernel_vsyscall+1>: push edx
0xf7fd8be2 <__kernel_vsyscall+2>: push ebp
0xf7fd8be3 <__kernel_vsyscall+3>: mov ebp,esp
0xf7fd8be5 <__kernel_vsyscall+5>: sysenter
0xf7fd8be7 <__kernel_vsyscall+7>: int 0x80
0xf7fd8be9 <__kernel_vsyscall+9>: pop ebp
0xf7fd8bea <__kernel_vsyscall+10>: pop edx
0xf7fd8beb <__kernel_vsyscall+11>: pop ecx
0xf7fd8bec <__kernel_vsyscall+12>: ret
check:
0xf7fd8000 0xf7fd9000 r-xp 1000 0 [vdso]
内核侧:sysenter入口
好的,我们已经看到用户空间程序是如何用**__kernel_vsyscall** 做 sysenter来触发系统调用的,让我们看看内核如何使用系统调用号执行系统调用代码的。
回忆上一章内核注册了ia32_sysenter_target系统调用执行函数。
这个功能是arch/x86/ia32/ia32entry.S里面用汇编实现的。让我们看看为了执行系统调用eax寄存器中的值是在哪里被使用的。
sysenter_dispatch:
call *ia32_sys_call_table(,%rax,8)
和我们看到了和传统系统调用模式一样的代码:一个叫ia32_sys_call_table的表格,里面有系统调用号。
在所有需要的记录存储以后,传统系统调用模式和sysenter系统调用模式使用了相同的机制和系统调用表格来分发系统调用。
相关 int $0x80 入口章来学习 ia32_sys_call_table 是在哪定义的和如何构造的。
这就是你如何通过sysenter系统调用进入内核。
用sysexit从sysenter调用中返回
内核可以用sysexit恢复用户程序执行。
使用这个这个指令不像iret那么直接。调用着需要把返回的地址放入rdx寄存器,把程序栈指针放入rcx寄存器。
就是说你的软件必须计算程序恢复执行的地址,保存这个值,在执行sysexit之前恢复这个值 。
我们可以看到这么做的代码:arch/x86/ia32/ia32entry.S:
sysexit_from_sys_call:
andl $~TS_COMPAT,TI_status+THREAD_INFO(%rsp,RIP-ARGOFFSET)
/* clear IF, that popfq doesn't enable interrupts early */
andl $~0x200,EFLAGS-R11(%rsp)
movl RIP-R11(%rsp),%edx /* User %eip */
CFI_REGISTER rip,rdx
RESTORE_ARGS 0,24,0,0,0,0
xorq %r8,%r8
xorq %r9,%r9
xorq %r10,%r10
xorq %r11,%r11
popfq_cfi
/*CFI_RESTORE rflags*/
popq_cfi %rcx /* User %esp */
CFI_REGISTER rsp,rcx
TRACE_IRQS_ON
ENABLE_INTERRUPTS_SYSEXIT32
ENABLE_INTERRUPTS_SYSEXIT32是arch/x86/include/asm/irqflags.h 里定义的宏,包含sysexit指令。
现在你知道了32位快速系统调用如何工作的了。
64位快速系统调用
旅程的下一步是64位快速系统调用。这些系统调用分别使用syscall 和sysret指令进入和返回。
SYSCALL触发一个权限等级0的操作系统系统调用执行程序。它通过从IA32_LSTAR MSR加载RIP 来实现(把 SYSCALL 后面指令的地址保存到RCX之后)。
换句话说:为了让内核收到接下来的系统调用,必须把系统调用发生时候执行的代码地址储存在IA32_LSTAR` MSR 中。
我们可以在arch/x86/kernel/cpu/common.c里面看到这段代码:
void syscall_init(void)
{
/* ... other code ... */
wrmsrl(MSR_LSTAR, system_call);
MSR_LSTAR值在arch/x86/include/uapi/asm/msr-index.h中定义为0xc0000082。
就像传统软中断系统调用,有一个惯例是用syscall做系统调用。
用户空间程序需要把系统调用号存入rax寄存器。syscall的参数存入其它通用寄存器中。
x86-64 ABI文档第A.2.1章写到:
arch/x86/kernel/entry_64.S中的注释也做了说明。
现在我们知道如何做系统调用,如何传递参数,下面我们通过写内联汇编程序实现一个。
自己写汇编做 syscall系统调用
继续前面的例子,我们用内联汇编的C程序执行exit系统调用,退出状态码为42 。
首先,我们需要找到 exit 系统调用号。在这个例子里我们需要在arch/x86/syscalls/syscall_64.tbl里找:
60 common exit sys_exit
exit 系统调用号是60。根据上面的接口描述,我们只需要把60传入rax寄存器,把第一个参数(退出状态码)传入rdi寄存器。
这里是一段有内联汇编的C代码实现。就像上面的例子,这个例子不是最简单实现,是为了方便说明:
int
main(int argc, char *argv[]) {
unsigned long syscall_nr = 60;
long exit_status = 42;
asm ("movq %0, %%rax\n"
"movq %1, %%rdi\n"
"syscall"
: /* output parameters, we aren't outputting anything, no none */
/* (none) */
: /* input parameters mapped to %0 and %1, repsectively */
"m" (syscall_nr), "m" (exit_status)
: /* registers that we are "clobbering", unneeded since we are calling exit */
"rax", "rdi");
}
下面,编译,执行,然后检查退出状态码:
$ gcc -o test test.c
$ ./test
$ echo $?
42
成功了!我们用syscall执行了exit系统调用。我们没有生成软中断并且(如果我们做计时)它执行的更快。
内核侧:系统调用入口
现在我们已经看了如何从用户空间触发系统调用,让我们看看内核如何用系统调用号执行系统调用代码。
回忆上一章我们看到了system_call函数的地址被写入了LSTAR MSR。
让我们看看这个函数的代码和它使用rax值如何准确传递执行到系统调用。arch/x86/kernel/entry_64.S:
`call *sys_call_table(,%rax,8) # XXX: rip relative`
非常像传统系统调用方法,sys_call_table是定义在C文件中的表格,这个C文件是脚本生成的,在C代码中用#include引入。
arch/x86/kernel/syscall_64.c,底部的#include:
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
};