Linux的虚拟系统调用加速


Linux中程序的内存maps中会存在以下三个区域:[vsyscall], [vdso], [vvar]. 这些内存都是为了实现虚拟函数调用以加速部分系统调用的。加速的方式是将部分安全的内核代码映射到用户空间,这使得程序可以不进入内核态[^1]直接调用系统调用。

这一功能的实现经历了两个历史过程:


vsyscall阶段

最初的vsyscall用于实现虚拟系统调用,你现在仍然能够在Linux进程空间中找到这一区域(32bit已经消失),但值得注意的是,其内容已经和最初的实现不同了

这一阶段主要是将部分内核代码放在vsyscall区域。使得程序(一般通过libc)可以直接调用time_of_day等简单的系统调用,获得加速(具体实现见后文)。

这一阶段的主要问题是在内存空间中,vsyscall的地址是固定的,并不能被地址随机化随机。这意味着如果被攻击者控制了EIP(RIP),那么这段空间的代码可以为攻击者提供不被随机化的配件,或者直接调用这些系统调用。为了解决这一问题,新的vdso实现方式出现了。


vDSO(virtual dynamic shared object)阶段

vdso区域与vsyscall的功能几乎没有区别,最主要的区别在于vDSO地址可以被ASLR随机化。新的版本的库和程序应该放弃vsyscall方式。

对vsyscall的兼容

由于vsyscall的地址空间为固定的,所以部分程序直接用硬编码地址使用了其中的系统调用。为了兼容性考虑,vsyscall中还可以实现这些功能,但是方式有所变化:

新的vsyscall在原来的入口处使用了特殊的trap指令,这些trap指令将程序捕获到内核态并模拟原来的虚拟系统调用。(是的,内核态模拟模拟内核态的虚拟系统调用,因为原来的虚拟系统调用上下文关系与直接系统调用不同。)这样的结果是其实现在使用vsyscall会使性能有部分下降,不如vdso方式迅速,但兼容了以前所有的功能,也解决了安全性问题。


内核数据的交互[vvar]

系统调用所需要读取的数据属于内核态,这一权限如何解决的呢? 内核通过映射到用户空间的一段只读区域来返回内核数据,这就是内存中的[vvar]区域。

历史上讲,可以确定的是vvar也是考虑安全性后加入的一种方式。但是其与vdso是否是同时加入的,没有继续详细的调研。


快速系统调用的支持

快速系统调用指的是sysenter/sysexit相对于int 0x80而言。简单来说,sysenter/sysexit不会像int 0x80一样维护复杂的上下文信息,没有特权级别检查和压栈的操作,所以速度快了很多。Linux对于快速系统调用的支持就体现在虚拟系统调用上。

注意:x64架构中都使用syscall指令,并没有这种区别。

简单的sysenter和sysexit概述:

调用 sysenter/sysexit 指令地址的跳转是通过设置一组特殊寄存器实现的。这些寄存器包括:

  • SYSENTER_CS_MSR - 用于指定要执行的 Ring 0 代码的代码段选择符,由它还能得出目标 Ring 0 所用堆栈段的段选择符;
  • SYSENTER_EIP_MSR - 用于指定要执行的 Ring 0 代码的起始地址;
  • SYSENTER_ESP_MSR-用于指定要执行的Ring 0代码所使用的栈指针

在 Ring3 的代码调用了 sysenter 指令之后,CPU 会做出如下的操作:

1. 将 SYSENTER_CS_MSR 的值装载到 cs 寄存器
2. 将 SYSENTER_EIP_MSR 的值装载到 eip 寄存器
3. 将 SYSENTER_CS_MSR 的值加 8(Ring0 的堆栈段描述符)装载到 ss 寄存器。
4. 将 SYSENTER_ESP_MSR 的值装载到 esp 寄存器
5. 将特权级切换到 Ring0
6. 如果 EFLAGS 寄存器的 VM 标志被置位,则清除该标志
7. 开始执行指定的 Ring0 代码

在 Ring0 代码执行完毕,调用 SYSEXIT 指令退回 Ring3 时,CPU 会做出如下操作:

1. 将 SYSENTER_CS_MSR 的值加 16(Ring3 的代码段描述符)装载到 cs 寄存器
2. 将寄存器 edx 的值装载到 eip 寄存器
3. 将 SYSENTER_CS_MSR 的值加 24(Ring3 的堆栈段描述符)装载到 ss 寄存器
4. 将寄存器 ecx 的值装载到 esp 寄存器
5. 将特权级切换到 Ring3
6. 继续执行 Ring3 的代码

由此可知,在调用 SYSENTER 进入 Ring0 之前,一定需要通过 wrmsr 指令设置好 Ring0 代码的相关信息,在调用 SYSEXIT 之前,还要保证寄存器edx、ecx 的正确性。

在 Intel 的手册中,还提到了 sysenter/sysexit 和 int n/iret 指令的一个区别,那就是 sysenter/sysexit 指令并不成对,sysenter 指令并不会把 SYSEXIT 所需的返回地址压栈,sysexit 返回的地址并不一定是 sysenter 指令的下一个指令地址。

具体参考Linux 2.6 对新型 CPU 快速系统调用的支持


细节讨论

Question:32位和64位下的区别汇总:

Answer

  • 64位下都为syscall,不同的虚拟系统调用采用不同的函数(在vdso中的路径)。
  • 32位下普通系统调用为int,快速系统调用为sysenter。
  • 32位下: 静态链接入口为_dl_sysinfo(__kernel_vsyscall),并且这一地址是加载程序后才指向vdso的。 动态链接入口为%gs:0x10
  • 64位下有[vsyscall]内存段而32位没有。

Question: 快速系统调用时内核执行的代码在用户空间么?

Answer:不在,虚拟系统调用只是使用快速系统调用的一段入口。没有实际运行的代码。样例:

#32bit 静态/动态都将指向这段代码 _dl_sysinfo
0xffffe400:     push   %ecx
0xffffe401:     push   %edx
0xffffe402:     push   %ebp
0xffffe403:     mov    %esp,%ebp
0xffffe405:     sysenter
0xffffe407:     nop
0xffffe408:     nop
0xffffe409:     nop
0xffffe40a:     nop
0xffffe40b:     nop
0xffffe40c:     nop
0xffffe40d:     nop
0xffffe40e:     jmp    0xffffe403
0xffffe410:     pop    %ebp
0xffffe411:     pop    %edx
0xffffe412:     pop    %ecx
0xffffe413:     ret

#64bit function getuid:
0x00007ffff7ada7c0 <+0>:    mov    $0x66,%eax
0x00007ffff7ada7c5 <+5>:    syscall 
0x00007ffff7ada7c7 <+7>:    retq  

Question:静态编译运行时有vdso么?

Answer:有(64/32bit)

Question:静态编译时vdso是否随机化?

Answer:是(64/32bit)

调用的路径验证(Ubuntu16.04 以getuid和gettimeofday为例,*代表系统调用号赋值):

静态32bit:

进入vdso,先准备eax

getuid() in main -> getuid* -> _dl_sysinfo in vdso

gettimeofday基本一致

静态64bit:

不进入vdso,直接syscall,进入前准备rax

getuid* -> _getuid

gettimeofday基本一致

动态32bit:

进入vdso,先准备eax

getuid() in main -> __getuid* -> call %gs:0x10(also _dl_sysinfo in vdso)

而gettimeofday则不同

gettimeofday() in main -> gettimeofday@plt -> gettimeofday in vdso* -> __kernel_vsyscall im vdso

动态64bit:

进入vdso,进入前准备rax

getuid() in main -> getuid@plt -> getuid@got -> getuid in vdso*

gettimeofday基本相同


参考:

Linux 2.6 对新型 CPU 快速系统调用的支持

On vsyscalls and the vDSO

Implementing virtual system calls

[^1]: http://lwn.net/Articles/446528/ 上"provide fast access to functionality which does not need to run in kernel mode",但具体实现中实际特权级还是发生了变化,参考http://www.ibm.com/developerworks/cn/linux/kernel/l-k26ncpu/index.html