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. 整體的結構:
用下面的圖來解釋:
這張圖不清楚,貼一張真正的圖:html
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,徵我的告訴我。
linux
下面link也不錯,可對照參考一下;程序員
http://www.ibm.com/developerworks/cn/linux/kernel/l-k26ncpu/index.html
Linux 2.6 對新型 CPU 快速系統調用的支持api