官方文档
水平有限, 如果发现错误请指出, 我会尽快修改
NAME
vsdo - overview of the virtual ELF dynamic shared object
SYNOPSIS
void *vdso = (uintptr_t) getauxval(AT_SYSINFO_EHDR); //TODO
DESCRIPTION
"vDSO"(虚拟动态共享对象)是一个内核会自动映射到每个用户进程的地址空间的小型共享库. 用户的程序通常不需要关心vdso的内部细节, 因为这个库主要是提供给c语言库使用的. 你只需要使用c语言库提供的函数, 而c语言库会在某些必要的函数中使用到vdso提供的功能.
所以究竟为什么vdso会存在呢? 有些内核提供的系统调用会被用户非常频繁地调用, 以至于这些系统调用的性能会对程序的整体性能有决定性的影响. 因为频繁地系统调用同时也会导致用户空间和内核空间之间的频繁切换.
这个文档剩下的部分主要针对的是C语言库的开发者, 如果你试图在你自己的程序中使用vdso而不是使用c语言库那么你很有可能出错了.
Example background
进行系统调用可以很慢, 在x86架构的32位系统中, 你可以通过触发一个软中断(int 0x80)来告诉系统你要进行一个进行系统调用, 然而这个指令(int) 的代价非常大: 它需要经过一个完成的中断处理过程.(译注:原文为 it goes through the full interrupt-handling paths in the processor's microcode as well as in the kernel., 因为不清楚就不擅自翻译了)新的处理器提供了更快(同时也是向后兼容)的指令来进行系统调用. c语言库可以使用vdso中内核提供的函数, 而不是在运行时判断这项功能是否可以使用.
需要注意的是这儿有个属于可能会比较容易引起误会. 在x86系统中, vDSO中用来进行系统调用的函数被命名为__kernel_vsyscall, 但是在x86-64中, vsyscall还有这样的意思:一个过时的方法来询问内核当前的时间或者调用者所使用的cpu的信息.
一个常用的系统调用就是gettimeofday. 这个系统调用既会被用户直接调用, 也可能被c语言库调用. 比如polling, timing loops 或者timestamps , 这些情况下都需要通过频繁地调用gettimeofday来获知当前的时间. 而且时间这个信息不是需要保密的-任何运行在任何权限的程序(root 或者guest)都会得到相同的答案. 因此内核将获得答案(时间)所需要的信息放置在程序可以访问的到的内存空间中. 现在调用gettimeofday
从需要进行系统调用变成了普通的函数调用和一些内存访问.
Finding the vDSO
内核通过ELF辅助向量(译注:关于什么是elf辅助向量可以参考这篇文章)中标签为AT_SYSINFO_EHDR的项来把vDSO的基地址传递给每个进程(参考 getauxval(3))
每个新创建的进程中vdso映射的地址是随机的, 这么做是出于安全性的考虑, 为了避免return2libc
攻击
某些架构的elf辅助向量中还有一个标签为AT_SYSINFO的项. 这一项的作用就是用来确定vsyscall
的入口地址而且经常被省略或者被置为0(意味着不可用). 从vdso的作用的角度看, 这个标签是倒退的表现.(参考下面的History部分) 所以应该避免使用它.
File format
既然vdso是一个完成的elf镜像, 所以你可以在其中查找符号(译注:原文为you can do symbol lookups on it). 这也意味着随着新的内核的发布, 新的符号也可以被添加进去, 同时也允许C语言库运行在不同版本的内核中时侦查可用的功能. 通常情况下c语言库会在第一次使用某个功能是进行查询并将结果缓存供之后调用使用.
所有的符号都有版本信息(译注:All symbols are also versioned (using the GNU version format))这样可以允许内核更新函数的签名而避免和之前的内核不兼容. 这意味着函数的参数和返回值都会被修改(译注:原文This means changing the arguments that the function accepts as well as the return value.不懂什么意思, 是update的过程中可以修改参数和返回值吗). 因此, 当你在vdso中查找某个符号的时候, 你必须把版本信息也带上.
通常情况下vsdo符合这样的命名规范: 每个符号都有__vdso_
或者__kernel_
前缀. 用以将这些符号和标准符号区分开来. for example: gettimeofday
函数就被命名为__vdso_gettimeofday
调用vdso中的函数的过程就和调用c语言库调用中的函数是完全相同的, 你完全不需要考虑寄存器或者栈的行为.
NOTES
source
当你编译内核的时候, 它会自动编译并链接vdso的代码. 你会经常在architecture-specific目录中找到它
find arch/$ARCH/ -name '*vdso*.so*' -o -name '*gate*.so*'
vDSO names
vdso的命名在不同架构中可能不同. 我们通常可以通过类似glibc的ldd(1)的工具的输出来得到答案:
$ ldd /bin/ls
linux-vdso.so.1 (0x00007ffcc3563000)
libselinux.so.1 => /lib64/libselinux.so.1 (0x00007f87e5459000)
libcap.so.2 => /lib64/libcap.so.2 (0x00007f87e5254000)
libc.so.6 => /lib64/libc.so.6 (0x00007f87e4e92000)
libpcre.so.1 => /lib64/libpcre.so.1 (0x00007f87e4c22000)
libdl.so.2 => /lib64/libdl.so.2 (0x00007f87e4a1e000)
/lib64/ld-linux-x86-64.so.2 (0x00005574bf12e000)
libattr.so.1 => /lib64/libattr.so.1 (0x00007f87e4817000)
libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f87e45fa000)
命名并不会对其中的代码产生影响. 所以不要对其进行硬编码(译注: The exact name should not matter to any code, so do not hardcode it.)
user ABI | vDSO name |
---|---|
aarch64 | linux-vdso.so.1 |
arm | linux-vdso.so.1 |
ia64 | linux-gate.so.1 |
mips | linux-vdso.so.1 |
ppc/32 | linux-vdso32.so.1 |
ppc/64 | linux-vdso64.so.1 |
s390 | linux-vdso32.so.1 |
s390x | linux-vdso64.so.1 |
sh | linux-gate.so.1 |
i386 | linux-gate.so.1 |
x86-64 | linux-vdso.so.1 |
x86/x32 | linux-vdso.so.1 |
(译注: 1. 我经过查询并未获知i386和x86的区别, 参考SO, 2. 关于x32可以参考这篇回答, 3. user ABI可能值得是用户空间的ABI, 而不是内核空间的ABI, 可以参考下面ARCHITECTURE-SPECIFIC NOTES的开头部分内容)
strace(1), seccomp(2), and the vDSO
当使用strace(1)追踪系统调用的时候, vdso 导出(export)的系统调用的符号不会再trace的输出中显示. 那些系统调用可能会对seccomp(2)过滤器不可见.
ARCHITECTURE-SPECIFIC NOTES
注意具体使用哪种vdso取决于用户空间的ABI, 而不是内核空间的ABI. 因此, for example, 当你运行一个i386 32位 elf 2进制文件, 不管你是在i386 32位内核空间还是x64 64位内核空间, 都会得到相同的vsdo, 因此需要根据用户空间的ABI来决定参考下面的哪个section(译注: 我这儿就贴一些我觉得常用的架构了)
ARM functions
下面的表列出了vdso导出的符号
symbol version
────────────────────────────────────────────────────────────
__vdso_gettimeofday LINUX_2.6 (exported since Linux 4.1)
__vdso_clock_gettime LINUX_2.6 (exported since Linux 4.1)
i386 functions
下面的表列出了vdso导出的符号
symbol version
──────────────────────────────────────────────────────────────
__kernel_sigreturn LINUX_2.5
__kernel_rt_sigreturn LINUX_2.5
__kernel_vsyscall LINUX_2.5
__vdso_clock_gettime LINUX_2.6 (exported since Linux 3.15)
__vdso_gettimeofday LINUX_2.6 (exported since Linux 3.15)
__vdso_time LINUX_2.6 (exported since Linux 3.15)
x86-64 functions
下面的表列出了vdso导出的符号, 所有这些符号去掉__vdso_
前缀也都是可以找到的.
symbol version
─────────────────────────────────
__vdso_clock_gettime LINUX_2.6
__vdso_getcpu LINUX_2.6
__vdso_gettimeofday LINUX_2.6
__vdso_time LINUX_2.6
x86/x32 functions
下面的表列出了vdso导出的符号
symbol version
─────────────────────────────────
__vdso_clock_gettime LINUX_2.6
__vdso_getcpu LINUX_2.6
__vdso_gettimeofday LINUX_2.6
__vdso_time LINUX_2.6
History
vdso最初只包含一个函数:vsyscall
, 在老版本的内核中, 你可能会在进程的memory map中看到vsyscall
而不是vdso
. 经过一段时间之后, 意识到可以使用这种机制给用户空间提供更多的功能, 所以它在新的版本中被重新构思(reconceive)为vdso