您的位置:首页 > 运维架构 > Linux

VDSO/linux-gate.so/sysenter

2015-12-16 16:01 549 查看
背景可看看下面link:
http://blog.csdn.net/juana1/article/details/6904932
往往内核添加了一个功能,glibc要花很久才会用上。本来linux那边为这个功能是否进入内核已经吵半天了,glibc这边又要为是否使用这个内核新特性再次吵架半天(glibc不是Linux专有的,还得考虑BSD(虽然人家也不用glibc),SysV Windows(诶,这没办法),还有sun那消亡的solaris,还有,自家的Hurd。然后,总之,这样新特性让人的接受上。。。太慢了。

libc是app和内核的桥梁,libc理应快速跟上内核的接口变化,但是glibc和内核不是一块开发的,所以,这只是理想罢了。glibc还要去兼容不同版本的内核呢

而内核也要去兼容不同版本的glibc.双方都背负了太多的历史包袱。glibc至今保留Linux Threads兼容2.4版本的古老内核。Linux对已经没用,甚至有bug(接口的问题导致一些bug是必须的)的系统调用也必须保留,谁知道用户会用哪个版本的glibc呢?虽然新的glibc会使用新的调用,但是提供和老的调用一致的API来兼容,但是,用户只升级内核而不升级glibc是常有的事情。就算升级了glibc,你新版本的glibc一定就用上内核的新接口?还是再等几年等glibc的开发者吵架结束吧。

于是乎,Linux的大牛们再次使出绝招:让libc变成VDSO进驻内核。

VDSO就是Virtual Dynamic Shared Object,就是内核提供的虚拟的.so,这个.so文件不在磁盘上,而是在内核里头。内核把包含某.so的内存页在程序启动的时候映射入其内存空间,对应的程序就可以当普通的.so来使用里头的函数。比如syscall()这个函数就是在linux-vdso.so.1里头的,但是磁盘上并没有对应的文件

下面这个link是老外讲linux-gate.so的link:
http://www.trilithium.com/johan/2005/08/linux-gate/
What is linux-gate.so.1?

linux-gate.so.1 is a virtual DSO, a shared object exposed by the kernel at a fixed address in every 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]

The line marked [vdso] is the linux-gate.so.1 object in that process, a single memory page mapped at address ffffe000. A program can determine the location of the shared object in memory by examining an AT_SYSINFO entry in the ELF auxiliary vector. The auxiliary
vector (auxv) is an array of pointers passed to new processes in the same way program arguments (argv) and environment variables (envp) are.

专门搜一下elf的辅助向量。
http://www.tuicool.com/articles/MNRJVj
关于ELF的辅助向量( Auxiliary Vector )

辅助向量另外一种由内核向应用程序传递信息的方式。

辅助向量的存储位置与用户参数、环境变量类似,同样也存放在栈空间上,大致的布局结构如下:

(gdb) info auxv

32 AT_SYSINFO Special system info/entry points 0xb7fff414

33 AT_SYSINFO_EHDR System-supplied DSO's ELF header 0xb7fff000

16 AT_HWCAP Machine-dependent CPU capability hints 0xfebfbff

6 AT_PAGESZ System page size 4096

17 AT_CLKTCK Frequency of times() 100

3 AT_PHDR Program headers for program 0x8048034

4 AT_PHENT Size of program header entry 32

分析AT_SYSINFO的值,在内核中:

arch\x86\include\asm\elf.h

#define VDSO_CURRENT_BASE ((unsigned long)current->mm->context.vdso)

#define VDSO_ENTRY \

((unsigned long)VDSO32_SYMBOL(VDSO_CURRENT_BASE, vsyscall))

#define ARCH_DLINFO_IA32(vdso_enabled)\

do { \

if (vdso_enabled) {\

NEW_AUX_ENT(AT_SYSINFO,VDSO_ENTRY);
\

NEW_AUX_ENT(AT_SYSINFO_EHDR, VDSO_CURRENT_BASE);\

} \

} while (0)

arch\x86\include\asm\vdso.h

/*

* Given a pointer to the vDSO image, find the pointer to VDSO32_name

* as that symbol is defined in the vDSO sources or linker script.

*/

#define VDSO32_SYMBOL(base, name) \

({ \

extern const char VDSO32_##name[];\

(void __user *)(VDSO32_##name + (unsigned long)(base));\

})

VDSO32_vsyscall can be found in arch/x86/vdso/vdso32/vdso32.lds.S

VDSO32_vsyscall = __kernel_vsyscall;

VDSO32_sigreturn = __kernel_sigreturn;

VDSO32_rt_sigreturn = __kernel_rt_sigreturn;

VDSO32_clock_gettime = clock_gettime;

VDSO32_gettimeofday = gettimeofday;

VDSO32_time = time;

这样AT_SYSINFO就与__kernel_vsyscall联系上了。后面的文章可验证这一点。

再回到前面的vdso

dd if=/proc/self/mem of=linux-gate.dso bs=4096 skip=1048574 count=1

可参考下面link来获取这个dso文件: http://blog.csdn.net/guoshaobei/article/details/5694023
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

These symbols are entry points for the rt_sigreturn/sigreturn functions and for making virtual system calls.

Traditionally, x86 system calls have been done with interrupts. syscall implementations in Linux and other *nix kernels have been using int 0x80.

It turns out, though, that system calls invoked via interrupts are remarkably slow on the more recent members of the x86 processor family. An int 0x80 system call can be as much as an order of magnitude slower on a 2 GHz Pentium 4 than on an 850 MHz Pentium
III. The impact on performance resulting from this could easily be significant, at least for applications that do a lot of system calls.

Intel recognized this problem early on and introduced a more efficient system call interface in the form of sysenter and sysexit instructions. This fast system call feature first appeared in the Pentium Pro processor, but due to hardware bugs it's actually
broken in most of the early CPUs. That's why you may see claims that sysenter was introduced with Pentium II or even Pentium III.

The preferred way of invoking a system call is determined by the kernel at boot time(sysenter/int80/syscall, http://stackoverflow.com/questions/12905799/having-trouble-finding-the-method-kernel-vsyscall-within-the-linux-kernel), and evidently this box uses
sysenter.

再转smth上一个牛人的文章,对照看一下就清楚了:
http://www.newsmth.net/bbsanc.php?path=%2Fgroups%2Fcomp.faq%2FKernelTech%2Finnovate%2Fsolofox%2FM.1222336489.G0
linux-gate.so技术细节

1. linux-gate.so是什么

参考这里:http://www.trilithium.com/johan/2005/08/linux-gate/

简而言之,linux-gate.so是为了实现用户程序使用sysenter/sysexit进行

系统调用的辅助机制。为什么我们需要这么一种机制来完成sysenter/sysexit?

按照我们使用int 80进行系统调用的思维,我们期待sysenter/sysexit是这样的

一个过程:

user app: kernel:

/*things*/

/*setup parameters*/

movl $__NR_getpid, %eax

sysenter ------>

movl current->pid, %eax

sysexit

<------

/*%eax=pid*/

/*other things*/

我们编写一个例子试试上面的想法:

[root@w237 vdso.d]# cat pid.c

#include <stdio.h>

#include <sys/types.h>

#include <unistd.h>

#include <sys/syscall.h>

#define STRINGFY_(x) #x

#define STRINGFY(x) STRINGFY_(x)

int main()

{

pid_t pid;

__asm__ volatile("movl $"STRINGFY(__NR_getpid)", %%eax\n"

"sysenter\n"

: "=a"(pid));

printf("pid=%u\n", pid);

return 0;

}

编译,gdb调试:

[root@w237 vdso.d]# gcc -g -o pid pid.c

[root@w237 vdso.d]# gdb -q ./pid

Using host libthread_db library "/lib/tls/libthread_db.so.1".

(gdb) disassemble main

Dump of assembler code for function main:

0x08048368 <main+0>: push %ebp

0x08048369 <main+1>: mov %esp,%ebp

0x0804836b <main+3>: sub $0x8,%esp

0x0804836e <main+6>: and $0xfffffff0,%esp

0x08048371 <main+9>: mov $0x0,%eax

0x08048376 <main+14>: add $0xf,%eax

0x08048379 <main+17>: add $0xf,%eax

0x0804837c <main+20>: shr $0x4,%eax

0x0804837f <main+23>: shl $0x4,%eax

0x08048382 <main+26>: sub %eax,%esp

0x08048384 <main+28>: mov $0x14,%eax

0x08048389 <main+33>: sysenter

0x0804838b <main+35>: mov %eax,0xfffffffc(%ebp)

0x0804838e <main+38>: sub $0x8,%esp

0x08048391 <main+41>: pushl 0xfffffffc(%ebp)

0x08048394 <main+44>: push $0x8048488

0x08048399 <main+49>: call 0x80482b0

0x0804839e <main+54>: add $0x10,%esp

0x080483a1 <main+57>: mov $0x0,%eax

0x080483a6 <main+62>: leave

0x080483a7 <main+63>: ret

End of assembler dump.

(gdb)

我们在sysenter一行设置断点,并且运行跟踪:

(gdb) b *0x8048389

Breakpoint 1 at 0x8048389: file pid.c, line 13.

(gdb) r

Starting program: /home/wensg/vdso.d/pid

Reading symbols from shared object read from target memory...done.

Loaded system supplied DSO at 0xffffe000

Breakpoint 1, 0x08048389 in main () at pid.c:13

13 __asm__ volatile("movl $"STRINGFY(__NR_getpid)", %%eax\n"

这时候gdb中断在sysenter这一行,用stepi单步运行这条指令:

(gdb) stepi

0xffffe424 in __kernel_vsyscall ()

看见了么?当sysenter执行完毕(也就是sysexit的结果)以后,程序是停在了0xffffe424这一行,

这个地址位于函数__kernel_vsyscall中!!为什么不是sysenter的下一行0x804838b???

2. sysenter/sysexit指令

参考IA32的文档。

sysenter/sysexit被冠以“Fast System Call facility”。至于是否如此,我现在不关心。

sysenter调用的过程为:

设置下面寄存器值(%msr[SYSENTER_CS]表示名为SYSENTER_CS的msr值,model specific

register,一组特别的寄存器组):

%cs = %msr[SYSENTER_CS]

%eip = %msr[SYSENTER_EIP]

%ss = %msr[SYSENTER_SS] + 8

%esp = %msr[SYSENTER_ESP]

%CPL = 0

然后从%cs:%eip继续执行。

sysexit调用过程为:

设置下面寄存器值:

%cs = %msr[SYSENTER_CS] + 16

%eip = %edx

%ss = %msr[SYSENTER_CS] + 24

%esp = %ecx

%CPL = 3

然后从%cs:%eip继续执行。

我们看到sysenter调用进入内核时,CPU不会保存用户堆栈,返回地址和其它的寄存器,

那么sysexit怎么返回到正确的用户空间呢?

一种办法就是调用前把%eip, %esp(因为%cs, %ss只是内核用来糊弄MMU的,我们先不管了)

保存在别的寄存器中,不过这样需要2个寄存器才能完成任务。

另外一种办法就是sysexit总是返回到用户进程某个固定的地址!vdso就是作为

sysenter/sysexit的存根(stub)的。sysenter只会在某个固定的位置被调用,而sysexit

也只需要返回到调用sysenter+2的位置(sysenter的机器码占2个字节)。不过%esp还是

需要保存的。

这就是为什么我们在例子1中观察到了sysenter指令会跳转到了__kernel_vsyscall()函数中,

sysexit返回的固定地址就在这个__kernel_vsyscall中。

让我们看看__kernel_vsyscall的汇编代码:

(gdb) disassemble __kernel_vsyscall

Dump of assembler code for function __kernel_vsyscall:

0xffffe414 <__kernel_vsyscall+0>: push %ecx

0xffffe415 <__kernel_vsyscall+1>: push %edx

0xffffe416 <__kernel_vsyscall+2>: push %ebp

0xffffe417 <__kernel_vsyscall+3>: mov %esp,%ebp

0xffffe419 <__kernel_vsyscall+5>: sysenter

0xffffe41b <__kernel_vsyscall+7>: nop

0xffffe41c <__kernel_vsyscall+8>: nop

0xffffe41d <__kernel_vsyscall+9>: nop

0xffffe41e <__kernel_vsyscall+10>: nop

0xffffe41f <__kernel_vsyscall+11>: nop

0xffffe420 <__kernel_vsyscall+12>: nop

0xffffe421 <__kernel_vsyscall+13>: nop

0xffffe422 <__kernel_vsyscall+14>: jmp 0xffffe417 <__kernel_vsyscall+3>

0xffffe424 <__kernel_vsyscall+16>: pop %ebp ; sysexit返回到这里

0xffffe425 <__kernel_vsyscall+17>: pop %edx

0xffffe426 <__kernel_vsyscall+18>: pop %ecx

0xffffe427 <__kernel_vsyscall+19>: ret

End of assembler dump.

(gdb)

看到没有,在0xffffe424这一行的上方有一个sysenter指令。Linux的设计是:进程只应当

从一个地方调用sysenter, sysexit返回到这个调用下面的某个地方,这两个地址都是固定的。

__kernel_vsyscall的sysenter到sysexit返回的地址0xffffe424中间有数个nop和jmp指令

的作用,下面再解释。

3. 如何使用sysenter

从例1的例子来看,我们是无法直接使用sysenter的,因为我们无法知道这个返回地址和

调用的协议。实际上,这样的指令对于普通的程序员来说,完全是透明的。vdso是C库的开发

者关心的问题。

__kernel_vsyscall的设计目标是代替int 80, 也就是下面两种方式应该是等价的:

/* int80 */ /* __kernel_vsyscall */

movl $__NR_getpid, %eax movl $__NR_getpid, %eax

int $0x80 call __kernel_vsyscall

/* %eax=getpid() */ /* %eax=getpid() %/

C库有怎么知道有__kernel_vsyscall呢?很简单,kernel告诉C库,kernel中存在

__kernel_vsyscall。至于C库选择int80,还是sysenter进行系统调用,那就是C库管了,

kernel已经提供了这样的一种机制,策略就不管是它管的了。

kernel告诉C库__kernel_vsyscall的位置,则是通过elf的interpreter的auxiliary vector

这个的具体细节看以参考elf的技术文档,我们可以通过下面的手段观察auxiliary vector

[root@w237 vdso.d]# LD_SHOW_AUXV=1 /bin/ls

AT_SYSINFO: 0xffffe414

AT_SYSINFO_EHDR: 0xffffe000

AT_HWCAP: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe

AT_PAGESZ: 4096

AT_CLKTCK: 100

AT_PHDR: 0x8048034

AT_PHENT: 32

AT_PHNUM: 8

AT_BASE: 0x0

AT_FLAGS: 0x0

AT_ENTRY: 0x8049cf0

AT_UID: 0

AT_EUID: 0

AT_GID: 0

AT_EGID: 0

AT_SECURE: 0

AT_PLATFORM: i686

AT_SYSINFO就是__kernel_vsyscall函数的地址,AT_SYSINFO_EHDR是vdso加载的位置。

4. 总体的结构:

用下面的图来解释:

这张图不清楚,贴一张真正的图:



linux-gate.so(vdso)是内核镜像中的特定页,它是一个完整的elf share object,

因此在磁盘的任何位置都找不到一个它。它是由内核的某些文件编译生成的。

当使用exec()执行新的镜像时,内核把linux-gate.so的页面映射到程序的进程空间中。

内核把__kernel_vsyscall的地址以auxiliary vector的形式告诉interpreter

(C库)。

当C库要进入内核时,它就可以选择使用__kernel_vsyscall或者int80来进行系统调用。

5. 内核的细节

让我们想想内核需要做那些工作:

5.1 生成vdso,并链接到内核中。

5.2 设置MSR,以便sysenter能进入内核的正确位置,sysexit能返回到用户程序的正确位置。

5.3 exec()时,将vdso映射到用户程序的地址空间中,找到__kernel_vsyscall的地址,

传给interpreter。

5.4 调用时,正确传递参数。

5.5 sysenter的响应函数要正确解析参数,调用相应的系统函数完成服务;设置%ecx, %edx,

用sysexit返回

5.6 当程序exit时,解除vdso的映射。

当你理解上面的内容之后,理解内核的细节不过是把它们找出来而已。自己去翻内核看,也是理解

上面内容的一个很好的途径。

6. __kernel_vsyscall

前面还遗留了一个问题,那7个nop和jmp是干什么的呢?让我们再看看它的代码:

0xffffe414 <__kernel_vsyscall+0>: push %ecx

0xffffe415 <__kernel_vsyscall+1>: push %edx

0xffffe416 <__kernel_vsyscall+2>: push %ebp

0xffffe417 <__kernel_vsyscall+3>: mov %esp,%ebp

0xffffe419 <__kernel_vsyscall+5>: sysenter

0xffffe41b <__kernel_vsyscall+7>: nop

0xffffe41c <__kernel_vsyscall+8>: nop

0xffffe41d <__kernel_vsyscall+9>: nop

0xffffe41e <__kernel_vsyscall+10>: nop

0xffffe41f <__kernel_vsyscall+11>: nop

0xffffe420 <__kernel_vsyscall+12>: nop

0xffffe421 <__kernel_vsyscall+13>: nop

0xffffe422 <__kernel_vsyscall+14>: jmp 0xffffe417 <__kernel_vsyscall+3>

0xffffe424 <__kernel_vsyscall+16>: pop %ebp ; sysexit返回到这里

0xffffe425 <__kernel_vsyscall+17>: pop %edx

0xffffe426 <__kernel_vsyscall+18>: pop %ecx

0xffffe427 <__kernel_vsyscall+19>: ret

前面连个push %ecx和%edx是因为sysexit返回时,要用这两个寄存器来制定返回的eip和esp,因此先保存起来。

然后我们要把%esp的值保存在%ebp中,否则我们就无法获得当前的堆栈指针了,在覆盖%ebp前,先保存%ebp,

这是系统调用的第六个参数。

然后使用sysenter

然后一堆的nop和一个jmp,这里完全是一个死循环。这是干什么的?正常的sysexit又不会执行这里(直接到

jmp之后了)

这个问题linus在这封mail中讨论了:
http://lkml.org/lkml/2002/12/18/218
他的意思是jmp的设计是用来支持restarted system call的,如果一个system call需要restart,它只需要返

回到某个nop中,然后jmp到重新初始化%ebp的代码中,从而是sysenter再次执行。

不过什么情况下会使一个system call restart,征个人告诉我。

下面link也不错,可对照参考一下;
http://www.ibm.com/developerworks/cn/linux/kernel/l-k26ncpu/index.html
Linux 2.6 对新型 CPU 快速系统调用的支持
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: