0%

Linux vdso&vsyscall

基础知识

随便用 GDB 调试一个 ELF 文件,使用 vmmap 命令就可以找到它们:

1
2
3
4
5
6
7
8
9
10
    0x7ffff7fc9000     0x7ffff7fcd000 r--p     4000 0      [vvar]
0x7ffff7fcd000 0x7ffff7fcf000 r-xp 2000 0 [vdso]
0x7ffff7fcf000 0x7ffff7fd0000 r--p 1000 0 /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x7ffff7fd0000 0x7ffff7ff3000 r-xp 23000 1000 /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x7ffff7ff3000 0x7ffff7ffb000 r--p 8000 24000 /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x7ffff7ffc000 0x7ffff7ffd000 r--p 1000 2c000 /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x7ffff7ffd000 0x7ffff7ffe000 rw-p 1000 2d000 /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x7ffff7ffe000 0x7ffff7fff000 rw-p 1000 0 [anon_7ffff7ffe]
0x7ffffffde000 0x7ffffffff000 rw-p 21000 0 [stack]
0xffffffffff600000 0xffffffffff601000 --xp 1000 0 [vsyscall]
  • vsyscall:第一种也是最古老的一种用于加快系统调用的机制
    • 它提供了一种在用户空间下快速执行系统调用的方法
    • Linux 内核在用户空间映射一个包含一些变量及一些系统调用的实现的内存页,对特定的系统调用使用函数调用进行代替(不必切换到内核态)
  • vdso(virtual dynamic shared object):vdso 是用来代替 vsyscall 的
    • vsyscall 区域太小了,而且映射区域固定,有安全问题(为了兼容性考虑,vsyscall 还是存在)
    • vdso 其实是一个动态库,它由内核提供,映射到每个进程的地址空间,它将提供一些函数调用来替代系统调用
    • 本质上 vdso 是一段内核空间的代码,映射给用户态使其更快地调用系统调用
  • vvar:存放数据的地方,vdso 中的函数会使用 vvar 中的数据

vsyscall

  • Intel 最先实现了专门的快速系统调用指令 sysenter 和系统调用返回指令 sysexit
  • AMD 针锋相对地实现了另一组专门的快速系统调用指令 syscall 和系统调用返回指令 sysret

vsyscall 机制的核心就在于:通过调用 __kernel_vsyscall 来确定到底应该执行 syscall/sysret 指令还是 sysenter/sysexit 指令

  • __kernel_vsyscall 是一个特殊的页,其位于内核地址空间,但也是唯一允许用户访问的区域,该区域的地址固定为 0xffffffffff600000(64位系统),大小固定为4K(所有的进程都共享内核映射)
  • __kernel_vsyscall 属于内核数据,用户态程序只能通过 ELF 辅助向量来获取其基地址(具体来说是 AT_SYSINFO)
  • 在ELF辅助向量中找到 AT_SYSINFO 后,就会像传统系统调用一样,将系统调用号和参数写入寄存器中,调用 __kernel_vsyscall 函数(由它来判断执行 syscall 还是 sysenter)

vsyscall 还可以对特定的系统调用使用函数调用进行代替,在 Linux 路径 /usr/include/asm/vsyscall.h 中可以看到如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
#ifndef _ASM_X86_VSYSCALL_H
#define _ASM_X86_VSYSCALL_H

enum vsyscall_num {
__NR_vgettimeofday,
__NR_vtime,
__NR_vgetcpu,
};

#define VSYSCALL_ADDR (-10UL << 20)

#endif /* _ASM_X86_VSYSCALL_H */

vsyscall 机制支持的系统调用有3个:

  • gettimeofday():把时间包装为一个结构体返回,包括秒,微妙,时区等信息
  • time():获取当前的系统时间,返回一个大整数
  • getcpu():获取CPU信息

这些函数都有一个特点:Root 用户和普通用户都会获得相同的结果(不存在安全问题)

  • 在内核与用户态之间建立一段共享内存区域,由内核定期“推送”最新值到该共享内存区域
  • 当用户态程序在调用这些系统调用的时候,库函数并不真正执行系统调用,而是通过 vsyscall page(我们在GDB中看到的就是这个)来读取该数据的最新值
  • 将系统调用改造成了函数调用,直接提升了执行性能(减少了内核的开销)

vdso

vdso 是用来代替 vsyscall 的,它们的区别如下:

  • vdso 本质上是一个ELF共享目标文件,而 vsyscall 只是一段内存代码和数据
  • vsyscall 位于内核地址空间,采用静态地址映射方式,而 vdso 借助共享目标文件天生具有的 PIC 特性,可以以进程为粒度动态映射到进程地址空间中

通过 ldd 命令就可以轻松找到 vdso:

1
2
3
4
➜  exp ldd /bin/sh
linux-vdso.so.1 (0x00007ffe04dee000) # target
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f70dd181000)
/lib64/ld-linux-x86-64.so.2 (0x00007f70dd3ad000)
  • vdso mapping 的本体是一个ELF共享目标文件
  • 源码位于 Linux 内核,路径为 /arch/x86/entry/vdso
  • 其中包括一小段汇编代码,一些C源文件和一个链接器脚本

vdso 同样也拥有替代系统调用的能力,相关代码如下:(在上述路径的 vdso.lds.S 文件中)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
* 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 机制支持的系统调用有4个:

  • gettimeofday():把时间包装为一个结构体返回,包括秒,微妙,时区等信息
  • time():获取当前的系统时间,返回一个大整数
  • getcpu():获取CPU信息
  • clock_gettime():用于计算精度和纳秒

其实现原理和 vsyscall 有所不同:

  • 当一个程序被加载时,动态链接器和加载器便会加载程序依赖的动态链接对象,也包括 vdso
  • 当 glibc 解析ELF头部时,会存储有关于 vdso 的一些位置信息,也包括简短的 stub 函数,用来在真正执行系统调用前搜索 vdso 中的符号名
  • 在 glibc 中的代码会在 vdso 中搜索对应的函数并且返回其地址,真正发挥作用的代码就被包装在 vdso 而不是在内核里,当然也就不需要系统调用了
  • vdso 需要用到的内核信息由 vvar mapping 提供,vdso 本身的地址则依靠 ELF 辅助向量进行传递(具体来说是 AT_SYSINFO_EHDR)

参考: