VDSO(Virtual Dynamically-lined Shared Object),这是一个由内核提供的虚拟.so文件,它不在磁盘上,而在内核里,内核将其映射到一个地址空间中,被所有程序共享,正文段大小为一个页面。
$ ldd /bin/lsmod linux-vdso.so.1 => (0x00007ffff7fdf000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ffff7c42000) /lib64/ld-linux-x86-64.so.2 (0x00007ffff7fe0000)
这里的linux-vdso.so.1就是vdso文件,现在的内核映射提供了进程随机地址化功能,vdso文件以及其他.so文件的映射地址会有不同的offset,通过
$ echo "0" > /proc/sys/kernel/randomize_va_space (root环境) 或
$ sudo sysctl -w kernel.randomize_va_space=0
可以关闭随机化地址功能,接下来把vdso的内容从/proc/self/mem中dd出来反汇编研究一下
$ cat /proc/self/maps 00400000-0040c000 r-xp 00000000 08:01 1430947 /bin/cat 0060c000-0060d000 rw-p 0000c000 08:01 1430947 /bin/cat 0060d000-0062e000 rw-p 00000000 00:00 0 [heap] 7ffff7a59000-7ffff7bd3000 r-xp 00000000 08:01 801437 /lib/x86_64-linux-gnu/libc-2.13.so 7ffff7bd3000-7ffff7dd3000 ---p 0017a000 08:01 801437 /lib/x86_64-linux-gnu/libc-2.13.so 7ffff7dd3000-7ffff7dd7000 r--p 0017a000 08:01 801437 /lib/x86_64-linux-gnu/libc-2.13.so 7ffff7dd7000-7ffff7dd8000 rw-p 0017e000 08:01 801437 /lib/x86_64-linux-gnu/libc-2.13.so 7ffff7dd8000-7ffff7ddd000 rw-p 00000000 00:00 0 7ffff7ddd000-7ffff7dfc000 r-xp 00000000 08:01 801438 /lib/x86_64-linux-gnu/ld-2.13.so 7ffff7e68000-7ffff7fdf000 r--p 00000000 08:01 1832159 /usr/lib/locale/locale-archive 7ffff7fdf000-7ffff7fe2000 rw-p 00000000 00:00 0 7ffff7ff9000-7ffff7ffb000 rw-p 00000000 00:00 0 7ffff7ffb000-7ffff7ffc000 r-xp 00000000 00:00 0 [vdso] 7ffff7ffc000-7ffff7ffd000 r--p 0001f000 08:01 801438 /lib/x86_64-linux-gnu/ld-2.13.so 7ffff7ffd000-7ffff7ffe000 rw-p 00020000 08:01 801438 /lib/x86_64-linux-gnu/ld-2.13.so 7ffff7ffe000-7ffff7fff000 rw-p 00000000 00:00 0 7ffffffde000-7ffffffff000 rw-p 00000000 00:00 0 [stack] ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
$ dd if=/proc/self/mem of=vdso.so bs=4096 skip=$[0x7ffff7ffb] count=1 dd: `/proc/self/mem': cannot skip to specified offset 1+0 records in 1+0 records out 4096 bytes (4.1 kB) copied, 0.00105878 s, 3.9 MB/s $ file vdso.so vdso.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, stripped
vdso是将内核态的调用映射到用户态的地址空间中,使得调用开销更小,路径更好。传统的系统调用方式是通过软中断指令int 0x80实现的,在x86保护模式中,处理INT中断指令时,CPU首先从中断描述符表IDT中取出对应的门描述符,判断门描述符的种类,然后检查门描述符的级别DPL和INT指令调用者的级别CPL,当CPL<=DPL时,即INT调用者级别高于描述符指定级别时,才能成功调用,最后再根据描述符的内容,进行压栈、跳转、权限级别提升。内核代码执行完毕之后,调用IRET指令返回,IRET指令恢复用户栈,并跳转回低级别代码。
其实,在发生系统调用,从用户层进入内核层的这个过程浪费了不少CPU周期,例如,系统调用必然需要由Ring3进入Ring0(由内核调用INT指令的方式除外,这多半属于Hacker的内核模块所为),权限提升之前和之后的级别是固定的,CPL肯定是3,而INT 0x80的DPL肯定也是3,这样CPU检查门描述符的DPL和调用者的CPL就是完全没有必要的,正是由此,Intel x86 CPU从PII 300(Family 6, Model 3, Stepping 3)之后,开始支持新的系统调用指令sysenter/sysexit。sysenter指令用于由Ring3进入Ring0(sysenter指令可用于3、2、1特权级),sysexit指令用于由Ring0返回Ring3(sysexit只能在0级特权使用),由于没有特权级别检查的处理,也就没有了压栈操作,所以执行速度比INT n/IRET快了不少。
系统调用多被封装成库函数提供给应用程序使用,应用程序调用库函数后,由glibc库负责进入内核调用系统调用函数,在老版glibc中,库函数就是通过int指令来完成系统调用的,而2.6之前的内核提供的系统调用接口也很简单只要在IDT中提供INT 0x80的入口,库就可以完成中断调用。
在2.6中,内核代码包含了对int 0x80中断方式和sysenter指令方式调用的支持,因此内核会给用户空间提供一段入口代码,内核启动时根据CPU类型,决定这段代码采取哪种系统调用方式,对于glibc来说,无需考虑系统调用方式,直接调用这段入口代码, 即可完成系统调用,这样做还可以尽量减少对glibc的改动,在glibc的源码中,只需将"int $0x80"指令替换成"call入口地址"即可,这个地址便是vsyscall的首地址。
至于在新版内核中,据观察系统调用都是利用syscall()函数实现的
SYSCALL_DEFINE0(getpid) { return task_tgid_vnr(current); } #define SYSCALL_DEFINE0(name) asmlinkage long sys_##name(void) #define SYSCALL_DEFINE1(name, ...) SYSCALL_DEFINEx(1, _##name, __VA_ARGS__) #define SYSCALL_DEFINE2(name, ...) SYSCALL_DEFINEx(2, _##name, __VA_ARGS__) #define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__) #define SYSCALL_DEFINE4(name, ...) SYSCALL_DEFINEx(4, _##name, __VA_ARGS__) #define SYSCALL_DEFINE5(name, ...) SYSCALL_DEFINEx(5, _##name, __VA_ARGS__) #define SYSCALL_DEFINE6(name, ...) SYSCALL_DEFINEx(6, _##name, __VA_ARGS__) #define __SYSCALL_DEFINEx(x, name, ...) asmlinkage long sys##name(__SC_DECL##x(__VA_ARGS__)) static inline int sys_perf_event_open(struct perf_event_attr *attr, pid_t pid, int cpu, int group_fd, unsigned long flags) { attr->size = sizeof(*attr); return syscall(__NR_perf_event_open, attr, pid, cpu, group_fd, flags); }
查看vdso.ld.S文件,可以看到
/* Linker script for 64-bit vDSO. We #include the file to define the layout details. Here we only choose the prelinked virtual address. This file defines the version script giving the user-exported symbols in the DSO. We can define local symbols here called VDSO* to make their values visible using the asm-x86/vdso.h macros from the kernel proper. */ #define VDSO_PRELINK 0xffffffffff700000 #include "vdso-layout.lds.S" /* 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: *; }; } VDSO64_PRELINK = VDSO_PRELINK;
$ cat vdso.dump | grep ">:" ffffffffff700700 <__vdso_clock_gettime-0x220>: ffffffffff700920 <__vdso_clock_gettime>: ffffffffff7009a0 <__vdso_gettimeofday>: ffffffffff700a30 <__vdso_time>: ffffffffff700a60 <__vdso_getcpu>:
这样看来,只有在调用clock_gettime、gettimeofday、getcpu、time这些系统调用时,才会使用vdso,其他系统调用是通过syscall实现的,原因是:快速系统调用指令比起中断指令来说,其消耗时间必然会少一些,但是随着CPU设计的发展,将来应该不会再出现类似Intel pentium4这样悬殊的差距,而快速系统调用指令比起中断方式的系统调用,还存在一定局限,例如无法在一个系统调用处理过程中再通过快速系统调用指令调用别的系统调用,因此,并不一定每个系统调用都需要通过快速系统调用指令来实现,比如,对于复杂的系统调用例如fork,两种系统调用方式的时间差和系统调用本身运行消耗的时间来比,可以忽略不计,此处采取快速系统调用指令方式就没有什么必要了,而真正应该使用快速系统调用指令方式的,是那些本身运行时间很短,对时间精确性要求高的系统调用,例如getcpu、gettimeofday等,因此,采取灵活的手段,针对不同的系统调用采取不同的方式,才能得到最优化的性能和实现最完美的功能。
References:
1. Linux 2.6 对新型 CPU 快速系统调用的支持http://www.ibm.com/developerworks/cn/linux/kernel/l-k26ncpu/index.html
2. linux下的vdso与vsyscall
3. Linux下的VDSO