/
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触发系统调用时也定义了一些规范。
* 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(virtual Dynamic Shared Object)的一部分,而vDSO允许程序在用户层中执行内核代码。
后文中会具体介绍什么是vDSO,vDSO做了什么以及工作原理是什么。
我们先来了解_ _kernel_vsyscall的一些细节。
_ _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既然是vDSO(也被称为共享库)的一部分,那用户程序是如何在运行时定位该函数地址的呢?
_ _kernel_vsyscall函数的地址被写入到 ELF 辅助向量, 用户程序以及库函数(主要指glibc)可以在这里找到其地址并使用。
以下方法可用来查找ELF辅助向量:
使用getauxval函数,带有AT_SYSINFO参数.
迭代搜索环境变量,然后从内存中解析这些变量.
第一种方法较为简单,但glibc 2.16前的版本不支持。 下文例子中介绍的代码中实现的是第二种方法。
从有关__kernel_vsyscall的代码中可以看出,在执行sysenter指令之前__kernel_vsyscall会做一些记录工作。
因此,我们需要做的就是手动利用sysenter进入到内核中:
. 在ELF辅助向量中找到AT_SYSINFO,也就是_ _kernel_vsyscall被写入的地址 . 像传统系统调用一样,将系统调用号和参数写入寄存器中 . 调用_ _kernel_vsyscall函数
你可能从来没写过属于你自己的sysenter封装函数,那是因为内核利用sysenter进入和退出系统调用的规范是会变化的,到那时候你的代码就会崩溃了。
在开始用sysenter触发系统调用前,你都要调用_ _kernel_vsyscall函数。
那么,让我们开始吧。
自己写汇编使用sysenter
继续利用前面传统系统调用的例子,我们调用exit,退出状态是42.
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;
/* 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.
*/
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");
}
接下来,编译,执行,然后检查退出状态:
$ gcc -m32 -o test test.c
$ ./test
$ echo $?
42
成功了! 我们利用 sysenter触发了exit系统调用,而不需要引发软件中断。
内核层面: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(内核层面:int $0x80入口这一章节),可以了解到ia32_sys_call_table 是如何定义和构造的。
以上内容就是如何通过sysenter系统调用进入内核的全部过程。
sysexit: 从sysenter中返回
内核利用sysexit指令将执行环境恢复到用户程序。
sysexit指令的使用不像iret那么直接。调用者需要将返回地址写入rdx寄存器中,并将栈指针写入rcx寄存器。
这就意味着你的代码中需要计算执行环境要返回的地址,保存地址值,并在调用sysexit前能恢复。
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/sysret
Intel指令集参考指南解释了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).
换句话说:为了让内核接收到系统调用,内核必须向IA32_LSTAR MSR注册当系统调用触发时要执行的代码地址。
void syscall_init(void)
{
/* ... other code ... */
wrmsrl(MSR_LSTAR, system_call);
类似传统软件中断型的系统调用, 使用syscall触发系统调用时也定义了一些规范。
用户程序需要将系统调用号写入rax寄存器中。系统调用的参数要c传入通用寄存器中。
x86-64 ABI章节A.2.1 对此有所描述:
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 destroys registers %rcx and %r11.
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.
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写入eax寄存器,以及第一个参数(退出状态)写入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系统调用。避免了软件中断,如果我们要用micro-benchmark 来计算时间的话,这种方法执行起来要快许多。
内核层面:syscall入口
现在我们已经知道了如何从用户程序中触发系统调用。接下来就要介绍内核如何利用系统调用号执行系统调用中的代码。
回想前文,我们知道如何将system_call函数的地址写入LSTAR MSR中。
下面就来看看这个函数中的代码是如何利用rax寄存器将执行环境切换到系统调用中的。可以参考arch/x86/kernel/entry_64.S:
call *sys_call_table(,%rax,8) # XXX: rip relative
类似传统系统调用,sys_call_table一张在C文件中定义的表,其利用#include将脚本产生的C代码包含进来。
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
};
从前文我们知道,系统调用表定义在arch/x86/syscalls/syscall_64.tbl。与传统中断模式一样,脚本在内核编译的时候执行并通过syscall_64.tbl文件中的表生成syscalls_64.h文件。
上述代码包括能生成系统调用号索引的函数指针数组的C代码。
以上就是如何通过syscall系统调用进入内核的全过程。
sysret: 从syscall中返回
内核利用sysret指令将执行环境恢复到用户程序执行syscall的地方。
sysret比起sysexit来要简单一些,因为当执行syscall时,需要被恢复执行的地址保存在rcx寄存器中。
只要能将这个地址保存起来,并在执行sysret前将其恢复到rcx寄存器中,执行环境就能在触发syscall的地方恢复。
这种机制比较方便,因为sysenter却要求你自己在代码中计算这个地址,并将其重写到寄存器中。
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
其中USERGS_SYSRET64是定义在arch/x86/include/asm/irqflags.h中的宏,其中包含sysret指令。
好了,目前为止,你已经知道64位系统调用是如何工作的了。
syscall(2)半手动调用syscall
太棒了,针对不同系统调用模型,我们已经知道如何编写汇编代码去触发这些系统调用了。
通常来说,你没必要自己写汇编代码。glibc提供的封装器函数已经为你处理好了所有的汇编代码。
当然,也有一些系统调用,glibc并没有为其做好封装器。其中一个例子就是futex–快速用户层上锁系统调用。
等等,为什么futex没有系统调用封装器呢?
futex是为库函数调用准备的,而不是应用程序。因此,要想调用futex,你必须这样做:
为你想支持的平台生成汇编存根(stub)
使用glibc提供的syscall封装器
有些时候,如果你想执行那些没有封装器的系统调用,你别无选择,只能利用第二种方法:使用glibc提供的syscall函数。
我们试试利用glibc提供的syscall调用exit,其退出状态是42:
#include
int
main(int argc, char *argv[])
{
unsigned long syscall_nr = 60;
long exit_status = 42;
syscall(syscall_nr, exit_status);
}
接下来,编译,执行,然后检查退出状态:
$ gcc -o test test.c
$ ./test
$ echo $?
42
成功了!我们利用glibc提供的syscall封装器成功的调用了exit.
glibc syscall封装器内幕
来看看前面例子中syscall封装器在glibc中是如何工作的
/* Usage: long syscall (syscall_number, arg1, arg2, arg3, arg4, arg5, arg6)
We need to do some arg shifting, the syscall_number will be in
rax. */
.text
ENTRY (syscall)
movq %rdi, %rax /* Syscall number -> rax. */
movq %rsi, %rdi /* shift arg1 - arg5. */
movq %rdx, %rsi
movq %rcx, %rdx
movq %r8, %r10
movq %r9, %r8
movq 8(%rsp),%r9 /* arg6 is on the stack. */
syscall /* Do the system call. */
cmpq $-4095, %rax /* Check %rax for error. */
jae SYSCALL_ERROR_LABEL /* Jump to error handler if error. */
L(pseudo_end):
ret /* Return to caller. */
前面我们给出了x86_64 ABI文档的参考链接,其描述了用户层和内核层的调用规范。
这段汇编stud很酷,因为它同时遵守两种调用规范。传递到这个函数的参数遵守用户层调用规范,但是在转移到另一组不同的寄存器利用syscall进入内核之前,又遵守内核层规范。
以上就是在你要执行默认没有封装器的系统调用时,glibc提供的syscall封装器的工作方式。
虚拟系统调用
到目前为止,我们已经介绍了所有进入内核触发系统调用的方法,并演示了如何手动(或者半手动)触发系统调用将系统从用户层切换到内核层。
倘若程序能触发一些系统调用,而不需要进入到内核呢?
这就是Linux vDSO存在的原因。Linux vDSO是内核代码的一部分,但是却被映射到用户程序地址空间中在用户层执行。
这也就是一些系统调用不用进入到内核就能被执行的原因。举个这样的例子: gettimeofday系统调用。
程序调用gettimeofday并不会真正进入到内核。而是简单的调用了内核提供的一小段代码,然后在用户层执行。
没有软件中断,也不需要复杂的sysenter或者syscall的记录工作。gettimeofday只是一个普通的函数调用。
当你执行ldd命令时,可以看到vDSO出现在第一个条目中:
$ ldd `which bash`
linux-vdso.so.1 => (0x00007fff667ff000)
libtinfo.so.5 => /lib/x86_64-linux-gnu/libtinfo.so.5 (0x00007f623df7d000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f623dd79000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f623d9ba000)
/lib64/ld-linux-x86-64.so.2 (0x00007f623e1ae000)
下面来看看vDSO在内核中是如何设置的。
内核中的vDSO
可以在 arch/x86/vdso/中找到vDSO的源码。 其中包括一小段汇编代码、一些C源文件和一个链接器脚本
此链接器脚本是个很酷的东西,可以具体去了解一下。
/*
* This controls what userland symbols we export from the vDSO.
*/
VERSION {
LINUX_2.6 {
global:
clock_gettime;
__vdso_clock_gettime;
gettimeofday;
__vdso_gettimeofday;
getcpu;
__vdso_getcpu;
time;
__vdso_time;
local: *;
};
}
链接器脚本很有用处,但并不被大家所熟知。链接器脚本会处理vDSO要导出的符号表。
可以看到,vDSO导出了4个不同的函数,每个函数都有两个名字。 可以在此文件夹下的C文件中找到函数的定义。
int gettimeofday(struct timeval *, struct timezone *)
__attribute__((weak, alias("__vdso_gettimeofday")));
这段代码将gettimeofday作为__vdso_gettimeofday 的弱别名(weak alias)。
同文件中的__vdso_gettimeofday函数中包含了当用户程序执行gettimeofday系统调用时真正在用户层执行的源代码。
在内存中定为vDSO
由于地址空间布局随机化(ASLR)的原因,当程序开始执行时,vDSO会被加载到随机的地址空间中。
如果vDSO加载到随机地址空间中,用户程序是如何找到它的呢?
回想前文提到的sysenter系统调用方法,用户程序要调用_ _kernel_vsyscall函数,而不是自己写sysenter汇编代码。
而_ _kernel_vsyscall函数也是vDSO的一部分。
提供的代码样例中通过搜索ELF辅助头文件找到和AT_SYSINFO匹配的头文件,头文件中含有_ _kernel_vsyscall函数的地址。
类似的,要定位到vDSO, 用户程序可以搜索ELF辅助头文件找到和AT_SYSINFO_EHDR匹配的头文件. 里面包含由链接器脚本生成的vDSO的ELF头的起始地址。
两个例子中,程序被加载时内核都会将其地址写入到ELF头中。这也就是为何正确的地址总是出现AT_SYSINFO_EHDR和AT_SYSINFO中。
一旦定为到ELF头部信息,用户程序就能解析ELF对象了(可以用libelf),并且可以根据需要调用ELF对象中的函数。
这样很酷,因为这就意味着vDSO能充分利用ELF有用的特性,比如symbol versioning。
内核文档 Documentation/vDSO/中有解析vDSO和调用其中函数的例子。
glibc中的vDSO
大多数情况下,大家都会访问vDSO,但并不会意识到。那是因为glibc利用前面章节介绍的接口对其进行了封装抽象。
当一个程序被加载时,动态链接器和加载器便会加载程序依赖的DSOs,也包括vDSO.
当glibc解析被加载程序的ELF头部时,会存储有关于vDSO的一些位置信息。也包括简短的stub函数,用来在真正执行系统调用前搜索vDSO中的符号名。
void *gettimeofday_ifunc (void) __asm__ ("__gettimeofday");
void *
gettimeofday_ifunc (void)
{
PREPARE_VERSION (linux26, "LINUX_2.6", 61765110);
/* If the vDSO is not available we fall back on the old vsyscall. */
return (_dl_vdso_vsym ("gettimeofday", &linux26)
?: (void *) VSYSCALL_ADDR_vgettimeofday);
}
__asm (".type __gettimeofday, %gnu_indirect_function");
这段glibc中的代码会在vDSO中搜索gettimeofday函数并且返回其地址。这些工作都通过间接函数封装好了。
这就是程序如何通过glibc调用gettimeofday并访问vDSO却完全不用切换到内核、引发特权级转变以及触发软件中断的全过程。
其中也总结了Linux 32/64位 Intel/AMD系统的每种系统调用方法的优势。
glibc 系统调用封装器
在讨论系统调用的时候,简单的提提glibc是如何处理系统调用是很有意义的。
对于许多系统调用来说,glibc只需要一个简单的封装函数将参数传入合适的寄存器中,然后执行syscall或者int $0x80指令,或者调用_ _kernel_vsyscall。
这些都是利用一系列在文本文件中定义的表完成的,这些表会经过脚本处理并输出C代码。
access - access i:si __access access acct - acct i:S acct chdir - chdir i:s __chdir chdir chmod - chmod i:si __chmod chmod
要了解每一栏目的含义,可以查看处理此文件的脚本注释: sysdeps/unix/make-syscalls.sh.
对于更复杂的系统调用,比如exit,其会触发那些有真正C代码或汇编代码实现的处理函数,并不会出现在类似这样的文本文件中。
以后的博文中会具体介绍glibc的实现以及linux内核中一些重要的系统调用。
重要的syscall相关bugs
很荣幸能利用这次机会提两个与linux系统调用相关的神奇bugs.
一起来瞧一瞧吧!
CVE-2010-3301
这个安全利用能让本地用户获取root权限。
引起这个漏洞的原因就在于汇编代码中的一个小bug,其允许用户程序触发x86_64系统中的传统系统调用。
此利用代码相当聪明:在一个特定的地址下,利用mmap生成一块内存区域,并利用一个整数让代码产生溢出:
(还记得上面章节中提到的传统中断方式中的这段代码吗?)
call *ia32_sys_call_table(,%rax,8)
此代码可以将执行环境切换到一块任意的地址中,在那执行内核代码,从而可以将运行的进程权限提升到root.
Android sysenter ABI 破坏
还记得前面说过不要在应用程序代码中硬编码sysenter ABI吗?
不幸的是,用android-x86的一些人就容易犯这些错误。只要内核的ABI一改变,andorid-x86就瞬间无法工作。
工作于内核的人最终用一些陈旧的sysenter硬编码序列来恢复sysenter ABI,以避免对Android设备的破坏。
这里是提交给Linux 内核的patch. 你也可以在提交信息中找到向android源码提交的攻击代码的链接
记住:千万不要自己编写汇编代码使用sysenter. 如果你因为某种原因要自己实现,可以使用前面例子中的一些代码,起码要仔细检查一下 __kernel_vsyscall函数。
结论
Linux内核中的系统调用机制是及其复杂的。触发系统调用有许多中方案,各都有其优缺点。
自己编写汇编代码来触发系统调用可不是个好主意,因为在你代码下层的ABI可能会崩溃。系统中的内核以及libc的实现会(可能会)选择最快的方式来触发系统调用。
如果你不能使用glibc提供的封装器(或者那个封装器不存在),你起码应该用syscall封装器函数,或者仔细检查vDSO提供的_ _kernel_vsyscall.
继续关注将来研究单个系统调用及其实现的相关文章。
相关博文
如果你喜欢这篇博客,你可能对以下底层技术的相关博客也感兴趣。
Acknowledgement
此文是一篇完整的译文,尽量在原文的基础上做到通俗易懂
仅为学习使用,未经博主同意,请勿转载。
作者[新浪微博:@diting0x]