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. 整體的結構:

用下面的圖來解釋:
 這張圖不清楚,貼一張真正的圖: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

相關文章
相關標籤/搜索