原文见:
http://www.trilithium.com/johan/2005/08/linux-gate/ ldd /bin/sh
linux-gate.so.1 => (0xffffe000)
libdl.so.2 => /lib/libdl.so.2 (0xb7fb2000)
libc.so.6 => /lib/libc.so.6 (0xb7e7c000)
/lib/ld-linux.so.2 (0xb7fba000)
它到底是什么文件呢?仅仅是一个动态载入库(dynamically loaded library),对吗?
对于广义的动态载入库定义来说,有那么几分意思。输出缺少文件名暗示了ldd不能找到这个文件的位置。事实上,任何试图找到这个文件的做法——无论是手工查找还是利用自动载入和分析这种库文件的软件——都不会成功。
一些用户时常因为寻找并不存在的系统文件而晕头转向并屡屡受挫。对于这种根本就不会有结果的寻找,你可以很自信的告诉用户:linux-gate.so.1文件目前在文件系统中根本就不被支持;它只是一个虚拟的DSO(译者注virtual DSO:dynamically shared object),一个在每个进程的存储空间(process’ memory)指定的地址点被内核暴露出来的共享对象:
cat /proc/self/maps
08048000-0804c000 r-xp 00000000 08:03 7971106 /bin/cat
0804c000-0804d000 rwxp 00003000 08:03 7971106 /bin/cat
0804d000-0806e000 rwxp 0804d000 00:00 0 [heap]
b7e88000-b7e89000 rwxp b7e88000 00:00 0
b7e89000-b7fb8000 r-xp 00000000 08:03 8856588 /lib/libc-2.3.5.so
b7fb8000-b7fb9000 r-xp 0012e000 08:03 8856588 /lib/libc-2.3.5.so
b7fb9000-b7fbc000 rwxp 0012f000 08:03 8856588 /lib/libc-2.3.5.so
b7fbc000-b7fbe000 rwxp b7fbc000 00:00 0
b7fc2000-b7fd9000 r-xp 00000000 08:03 8856915 /lib/ld-2.3.5.so
b7fd9000-b7fdb000 rwxp 00016000 08:03 8856915 /lib/ld-2.3.5.so
bfac3000-bfad9000 rw-p bfac3000 00:00 0 [stack]
ffffe000-fffff000 ---p 00000000 00:00 0 [vdso]
这里,cat输出了它自己的内存映射。标记[vdso]的行是该进程的linux-gate.so.1对象,一个映射到地址为ffffe000的单独的内存页面。程序可以通过查询ELF辅助向量中的AT_SYSINFO入口点来获取内存中共享对象的位置。和程序参数(argv)和环境变量(envp)类似,辅助向量(auxv)是一个指向新进程的指针数组。
理论上进程之间的地址可能是不同的,但是,据我所知linux内核总是把它映射到一个固定的位置。上面例子的输出来自一个x86 box,那里进程存在于无格式老的32位地址空间,空间页面大小为4096字节,使得ffffe000为倒数第二个页面。真正的最后一个页面被保存用来捕捉通过无效指针访问,例如,对一个消耗掉的NULL指针或者从mmap返回的MAP_FAILED指针进行解除参照。
因为所有的进程在相同的位置共享相同的对象,所以如果我们想进一步查看,很容易的就可以摘录出它的一个副本。例如,我们可以简单的使用dd来从它自己的空间中得到这个页面(谨慎的选择一个与linux-gate.so.1不同的输出名以免创建已存在的文件):
dd if=/proc/self/mem of=linux-gate.dso bs=4096 skip=1048574 count=1
1+0 records in
1+0 records out
我们跳过1048574个页面是因为总共有220=1048576个页面,我们想要的是与最后一个页面相邻的那个页面。和其它的共享ELF对象文件类似,结果如下:
file -b linux-gate.dso
ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), stripped
objdump -T linux-gate.dso
linux-gate.dso: file format elf32-i386
DYNAMIC SYMBOL TABLE:
ffffe400 l d .text 00000000
ffffe460 l d .eh_frame_hdr 00000000
ffffe484 l d .eh_frame 00000000
ffffe608 l d .useless 00000000
ffffe400 g DF .text 00000014 LINUX_2.5 __kernel_vsyscall
00000000 g DO *ABS* 00000000 LINUX_2.5 LINUX_2.5
ffffe440 g DF .text 00000007 LINUX_2.5 __kernel_rt_sigreturn
ffffe420 g DF .text 00000008 LINUX_2.5 __kernel_sigreturn
这些符号是rt_sigreturn/sigreturn函数的入口点,也是为了虚拟系统调用(virtual system call)用而设置。在x86平台上,linux-gate.so.1一开始的名字是linux-vsyscall.so.1,但是在开发过程中,为了使用一个通俗的名字而被修改了,从而精确的反映了在跨平台下的目的:在用户和内核空间之间起到网关(gateway)的作用。并不是所有的系统都需要虚拟系统调用,但是对x86它们一定同等重要,从而来保证这个精细的机制。
传统中,x86系统调用通过中断来实现。你可能还记得在不方便的老的MS-DOS时代,请求操作系统函数的方式是通过中断33(21h)。Windows系统调用隐藏在用户模式API层,但在一些地方它们同样深入到了int ox2e。在linux和其它的*nix内核中类似的实现使用int 0x80。
然而,在很多x86处理器家族中的新成员中,通过使用中断来调用系统调用的效率被证明是“相当”(^_^)低的。一个0x80系统调用单从命令来说,在2GHz Pentium上要比在一个850MHz Pentium Ⅲ上慢很多很多。因为这个原因而产生的在执行上的影响是明显的,至少对于执行大量系统调用的应用程序是这样的。
Intel在早期就注意到了这个问题,并且引进了一个更有效的以sysenter和sysexit的形式的系统调用接口。快速系统调用特色最初在Pentium Pro处理器中出现,但是由于硬件上的bug,它实际上被早期的大部分CPU所抛弃(broken)。这就是你可能看到PentiumⅡ甚至Pentium Ⅲ引入了sysenter的声明的原因了。
硬件的问题也可以帮助解释为什么在操作系统开始支持快速系统调用之前经历了很长时间。如果我们忽略早期的实验性质的片段(patches),Linux对sysenter的支持出现在2002年12月,那时内核2.5正在开发中。距离sysenter的提出已经有10年了!Micorsoft在Windows XP中才刚刚使用sysenter。
如果你的Linux机器在系统调用上使用了sysenter指令,你可以通过反汇编__kernel_vsyscall找到:
objdump -d --start-address=0xffffe400 --stop-address=0xffffe414 linux-gate.dso
linux-gate.dso: file format elf32-i386
Disassembly of section .text:
ffffe400 <__kernel_vsyscall>:
ffffe400: 51 push %ecx
ffffe401: 52 push %edx
ffffe402: 55 push %ebp
ffffe403: 89 e5 mov %esp,%ebp
ffffe405: 0f 34 sysenter
ffffe407: 90 nop
ffffe408: 90 nop
ffffe409: 90 nop
ffffe40a: 90 nop
ffffe40b: 90 nop
ffffe40c: 90 nop
ffffe40d: 90 nop
ffffe40e: eb f3 jmp ffffe403 <__kernel_vsyscall+0x3>
ffffe410: 5d pop %ebp
ffffe411: 5a pop %edx
ffffe412: 59 pop %ecx
ffffe413: c3 ret
系统调用的首选调用策略在启动时由内核确定,很明显这个box使用了sysenter。在老式机器上你可能会看到int 0x80被使用。如果你正努力地想搞清楚这个跳转(jump)的含义(象我第一次看到它一样),你可能会兴趣很浓的去学习以至于认为其原因是:Linus Torvalds is a disgusting pig and proud of it.(使用六个参数来处理重起系统调用是一个小窍门)。