Linux2.6對新型CPU的支援(2)(轉)

gugu99發表於2007-08-10
Linux2.6對新型CPU的支援(2)(轉)[@more@]

  由使用者態經庫函式進入核心態

  為了配合核心使用新的系統呼叫方式,glibc中要做一定的修改。新的glibc-2.3.2(及其以後版本中)中已經包含了這個改動,在glibc原始碼的sysdeps/unix/sysv/linux/i386/sysdep.h檔案中,處理系統呼叫的宏INTERNAL_SYSCALL在不同的編譯選項下有不同的結果。在開啟支援sysenter/sysexit指令的選項I386_USE_SYSENTER下,系統呼叫會有兩種方式,在靜態連結(編譯時加上-static選項)情況下,採用"call *_dl_sysinfo"指令;在動態連結情況下,採用"call *%gs:0x10"指令。這兩種情況由glibc庫採用哪種方法連結,實際上最終都相當於呼叫某個固定地址的程式碼。下面我們透過一個小小的程式,配合gdb來驗證。

  首先是一個靜態編譯的程式,程式碼很簡單:

  main()

{

  getuid();

}

  將程式碼加上static選項用gcc靜態編譯,然後用gdb裝載並反編譯main函式。

[root@test opt]# gcc test.c -o ./static -static

[root@test opt]# gdb ./static

(gdb) disassemble main

0x08048204
:  push  %ebp

0x08048205
:  mov  %esp,%ebp

0x08048207
:  sub  $0x8,%esp

0x0804820a
:  and  $0xfffffff0,%esp

0x0804820d
:  mov  $0x0,%eax

0x08048212
:  sub  %eax,%esp

0x08048214
:  call  0x804cb20 <__getuid>

0x08048219
:  leave

0x0804821a
:  ret

  可以看出,main函式中呼叫了__getuid函式,接著反編譯__getuid函式。

(gdb) disassemble 0x804cb20

0x0804cb20 <__getuid>:    push  %ebp

0x0804cb21 <__getuid>:    mov  0x80aa028,%eax

0x0804cb26 <__getuid>:    mov  %esp,%ebp

0x0804cb28 <__getuid>:    test  %eax,%eax

0x0804cb2a <__getuid>:    jle  0x804cb40 <__getuid>

0x0804cb2c <__getuid>:    mov  $0x18,%eax

0x0804cb31 <__getuid>:    call  *0x80aa054

0x0804cb37 <__getuid>:    pop  %ebp

0x0804cb38 <__getuid>:    ret

  上面只是__getuid函式的一部分。可以看到__getuid將eax暫存器賦值為getuid系統呼叫的功能號0x18然後呼叫了另一個函式,這個函式的入口在哪裡呢?接著檢視位於地址0x80aa054的值。

  (gdb) X 0x80aa054

  0x80aa054 <_dl_sysinfo>:    0x0804d7f6

  看起來不像是指向核心對映頁面內的程式碼,但是,可以確認,__dl_sysinfo指標的指向的地址就是0x80aa054。下面,我們試著啟動這個程式,然後停在程式第一條語句,再檢視這個地方的值。

(gdb) b main

Breakpoint 1 at 0x804820a

(gdb) r

Starting program: /opt/static

Breakpoint 1, 0x0804820a in main ()

(gdb) X 0x80aa054

0x80aa054 <_dl_sysinfo>:    0xffffe400

  可以看到,_dl_sysinfo指標指向的數值已經發生了變化,指向了0xffffe400,如果我們繼續執行程式,__getuid函式將會呼叫地址0xffffe400處的程式碼。

  接下來,我們將上面的程式碼編譯成動態連結的方式,即預設方式,用gdb裝載並反編譯main函式

[root@test opt]# gcc test.c -o ./dynamic

[root@test opt]# gdb ./dynamic

(gdb) disassemble main

0x08048204
:  push  %ebp

0x08048205
:  mov  %esp,%ebp

0x08048207
:  sub  $0x8,%esp

0x0804820a
:  and  $0xfffffff0,%esp

0x0804820d
:  mov  $0x0,%eax

0x08048212
:  sub  %eax,%esp

0x08048214
:  call  0x8048288

0x08048219
:  leave

0x0804821a
:  ret

  由於libc庫是在程式初始化時才被裝載,所以我們先啟動程式,並停在main第一條語句,然後反彙編getuid庫函式。

  (gdb) b main

Breakpoint 1 at 0x804820a

(gdb) r

Starting program: /opt/dynamic

Breakpoint 1, 0x0804820a in main ()

(gdb) disassemble getuid

Dump of assembler code for function getuid:

0x40219e50 <__getuid>:    push  %ebp

0x40219e51 <__getuid>:    mov  %esp,%ebp

0x40219e53 <__getuid>:    push  %ebx

0x40219e54 <__getuid>:    call  0x40219e59 <__getuid>

0x40219e59 <__getuid>:    pop  %ebx

0x40219e5a <__getuid>:    add  $0x84b0f,%ebx

0x40219e60 <__getuid>:    mov  0xffffd87c(%ebx),%eax

0x40219e66 <__getuid>:    test  %eax,%eax

0x40219e68 <__getuid>:    jle  0x40219e80 <__getuid>

0x40219e6a <__getuid>:    mov  $0x18,%eax

0x40219e6f <__getuid>:    call  *%gs:0x10

0x40219e76 <__getuid>:    pop  %ebx

0x40219e77 <__getuid>:    pop  %ebp

0x40219e78 <__getuid>:    ret

  可以看出,庫函式getuid將eax暫存器設定為getuid系統呼叫的呼叫號0x18,然後呼叫%gs:0x10所指向的函式。在gdb中,無法檢視非DS段的資料內容,所以無法檢視%gs:0x10所儲存的實際數值,不過我們可以透過程式設計的辦法,內嵌彙編將%gs:0x10的值賦予某個區域性變數來得到這個數值,而這個數值也是0xffffe400,具體程式碼這裡就不再贅述。

  由此可見,無論是靜態還是動態方式,最終我們都來到了0xffffe400這裡的一段程式碼,這裡就是核心為我們對映的系統呼叫入口程式碼。在gdb中,我們可以直接反彙編來檢視這裡的程式碼

  (gdb) disassemble 0xffffe400 0xffffe414

Dump of assembler code from 0xffffe400 to 0xffffe414:

0xffffe400:   push  %ecx

0xffffe401:   push  %edx

0xffffe402:   push  %ebp

0xffffe403:   mov  %esp,%ebp

0xffffe405:   sysenter

0xffffe407:   nop

0xffffe408:   nop

0xffffe409:   nop

0xffffe40a:   nop

0xffffe40b:   nop

0xffffe40c:   nop

0xffffe40d:   nop

0xffffe40e:   jmp  0xffffe403

0xffffe410:   pop  %ebp

0xffffe411:   pop  %edx

0xffffe412:   pop  %ecx

0xffffe413:   ret

End of assembler dump.

  這段程式碼正是arch/i386/kernel/vsyscall-sysenter.S檔案中的程式碼。其中,在sysenter之前的是入口程式碼,在0xffffe410開始的是核心返回處理程式碼(後面提到的SYSENTER_RETURN即指向這裡)。在入口程式碼中,首先是儲存當前的ecx,edx(由於sysexit指令需要使用這兩個暫存器)以及ebp。然後呼叫sysenter指令,跳轉到核心Ring 0程式碼,也就是sysenter_entry入口處。

  核心中的處理和返回

  sysenter_entry整個的實現可以參見arch/i386/kernel/entry.S。核心處理SYSENTER的程式碼和處理INT的程式碼不太一樣。透過sysenter指令進入Ring 0之後,由於當前的ESP並非指向正確的核心棧,而是當前CPU的TSS結構中的一個緩衝區(參見上文),所以首先要解決的是修復ESP,幸運的是,TSS結構中ESP0成員本身就儲存有Ring 0狀態的ESP值,所以在這裡將TSS結構中ESP0的值賦予ESP暫存器。將ESP恢復成指向正確的堆疊之後,由於SYSENTER不是透過呼叫門進入Ring 0,所以在堆疊中的上下文和使用INT指令的不一樣,INT指令進入Ring 0後棧中會儲存如下的值。

  低地址

  返回使用者態的EIP

  使用者態的CS

  使用者態的EFLAGS

  使用者態的ESP

  使用者態的SS(和DS相同)

  高地址

  因此,為了簡化和重用程式碼,核心會用pushl指令往棧中放入上述各值,值得注意的是,核心在棧中放入的相對應使用者態EIP的值,是一個程式碼標籤SYSENTER_RETURN,在vsyscall-sysenter.S可以看到,它就在sysenter指令的後面(在它們之間,有一段NOP,是核心返回出錯時的處理程式碼)。接下來,處理系統呼叫的程式碼就和中斷方式的處理程式碼一模一樣了,核心儲存所有的暫存器,然後系統呼叫表找到對應系統呼叫的入口,完成呼叫。最後,核心從棧中取出前面存入的使用者態的EIP和ESP,存入edx和ecx暫存器,呼叫SYSEXIT指令返回使用者態。返回使用者態之後,從棧中取出ESP,edx,ecx,最終返回glibc庫。

  其它作業系統以及其它硬體平臺的支援

  值得一提的是,從 Windows XP 開始,Windows 的系統呼叫方式也從軟中斷 int 0x2e 轉換到採用 sysenter 方式,由於完全不再支援 int 方式,因此 Windows XP 的對 CPU 的最低配置要求是 PentiumII 300MHz。在其它的作業系統例如 *BSD 系列,目前並沒有提供對 sysenter 指令的支援。

  在 CPU 方面,AMD 的 CPU 支援一套與之對應的指令 SYSCALL/SYSRET。在純 32 位的 AMD CPU 上,還沒有支援 sysenter 指令,而在 AMD 推出的 AMD64 系列 CPU 上,處於某些模式的情況下,CPU 能夠支援 sysenter/sysexit 指令。在 Linux 核心針對 AMD64 架構的程式碼中,採用的還是 SYSCALL/SYSRET 指令。至於這兩種指令最終誰將成為標準,目前還無法得出結論。

  未來

  我們將 Intel 的 sysenter/sysexit 指令,AMD 的 SYSCALL/SYSRET 指令統稱為"快速系統呼叫指令"。"快速系統呼叫指令"比起中斷指令來說,其消耗時間必然會少一些,但是隨著 CPU 設計的發展,將來應該不會再出現類似 Intel Pentium4 這樣懸殊的差距。而"快速系統呼叫指令"比起中斷方式的系統呼叫方式,還存在一定侷限,例如無法在一個系統呼叫處理過程中再透過"快速系統呼叫指令"呼叫別的系統呼叫。因此,並不一定每個系統呼叫都需要透過"快速系統呼叫指令"來實現。比如,對於複雜的系統呼叫例如 fork,兩種系統呼叫方式的時間差和系統呼叫本身執行消耗的時間來比,可以忽略不計,此處採取"快速系統呼叫指令"方式沒有什麼必要。而真正應該使用"快速系統呼叫指令"方式的,是那些本身執行時間很短,對時間精確性要求高的系統呼叫,例如 getuid、gettimeofday 等等。因此,採取靈活的手段,針對不同的系統呼叫採取不同的方式,才能得到最最佳化的效能和實現最完美的功能。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10748419/viewspace-940031/,如需轉載,請註明出處,否則將追究法律責任。

相關文章