http://vm-kernel.org/cnblog/2010/04/mips-pic%E6%A6%82%E8%BF%B0/
MIPS PIC概述
PIC是一個很重要的概念,它是position independent code的簡稱.它的意思表明這些code可以被load到任何地方去執行. 這樣做的好處在於對於共用庫,我們可以在記憶體中只保留一份,然後所有其他的程式都只調用這一份共用庫中的代碼. 因此,需要共用庫的代碼是不依賴與其在記憶體中的地址的.
為了能實現這個目標,我們需要一個成為GOT(global offset table)的東西. 在MIPS中, 這個表的地址儲存在gp寄存器中.在其他架構中,會有其他的實現方法.具體可以查閱ABI手冊.這個表中存放全域的資料和要用到的外部函數(比如 printf)的地址.然後在調用這些函數的時候,會先從GOT表中取出表項,然後再跳轉.彙編語句如下:
40065c: 8f998038 lw t9,-32712(gp) ->取出GOT表項,值是要調用的函數地址
400660: 00000000 nop
400664: 0320f809 jalr t9 ->跳轉到這個函數
400668: 00000000 nop
當 我們調用printf函數的時候,由於printf函數位於libc中,因此在函數被link的時候並不知道prinf函數位於什麼地方.這個時候,需要 在GOT表中關於printf的部分填入一個stub值. 那麼這個stub值什麼時候被修改成printf函數真正的地址呢? 有兩個方法.
(1) 方法1. 當程式被載入執行的時候,parse GOT表,找出類似於printf這種還不知道函數具體地址的表項.然後去libc或者其他lib中尋找這些函數在記憶體中地址.當然如果這些lib還沒有 被載入到記憶體,必須首先把庫載入到記憶體. 這樣由於所有必需的庫都已經在記憶體中,他們的地址也就確定下來.因此,這個時候可以修改本程式的GOT表,把這些函數在記憶體中的地址寫到GOT表項中.這 樣就可以通過GOT表調用到printf這些函數.
這個方面咋聽上去不錯.但是有一個致命的問題是什麼呢?由於在程式載入的時候,需 要parse所有外部函數的地址.如果這些函數很多,那麼將會花很長很長的時間,其間可能經曆了無數次的cache miss等等. 而且有一些函數雖然在程式中被調用了,但是這些函數在實際啟動並執行時候並不會被跑到(比如在一個判斷語句後被調用的函數,而這個判斷語句的結果在啟動並執行時候 不滿足).因此,這個對於效能來說是一個很大的損失. 也許你調用一個程式後,出去倒杯水回來,程式還沒有載入完呢(當然,太極端的例子). 因此,現在實際的情況並不是採用這種方法,而是採用方法2.
(2) 方法2. 這種方法被稱為fully dynamic linking. 也就是說,把重填GOT表項的時間從載入時延遲到調用的時候.也就是說,只有這個外部函數被調用的時候,才會去重填這個表項,否則不去做. 這種方法有點類似於把事情推到非做不可的時候再做的意思,因此也被稱為lazy linking. 為了實現lazy linking,除了GOT表外, MIPS使用了稱為.MIPS.stubs的section. 在GOT表中外部函數的表項被指向了.MIPS.stubs. 當程式運行時,如果是第一次調用printf等外部函數,會跑到這個section中. 然後這個section會調用動態連接器來得到printf的真正地址,並且更新GOT表項. 下一次再調用這個函數的時候,就可以直接調用這些外部函數. 過程如:
第一次調用printf
第二次調用printf
另 外還有一個問題,對於PIC code, 它的GOT表在記憶體中的位置也是不固定的.那麼該如何得到GOT表的位置呢? 解決方案是雖然GOT表的位置是不固定的,但是呢,GOT表和當前函數的位移是固定的,並且當前函數的地址是放在寄存器t9中的. t9 + (GOT 和 當前函數的位移) 不就得到GOT表的地址了嗎. 請記住,這是有前提的.前提是 進入PIC code 的某一個函數中, t9必需等於當前函數在記憶體中的地址.這個有函數的調用者(caller)保證. 另外,在被調用函數(callee)部分,需要先儲存gp的值,然後在函數返回的時候恢複gp的值.
因此,可以通過下面彙編代碼來得到當前的GOT表:
400640: 3c1c0002 lui gp,0x2
400644: 279c82f0 addiu gp,gp,-32016 ->gp = got - 當前函數地址
400648: 0399e021 addu gp,gp,t9 -> gp + t9 = got
另外,對於動態庫函數,必需是PIC,而對於一般的應用程式來說,不需要是PIC的.並且在MIPS中,預設的應用也不是PIC的.如果需要使得產生的應用是PIC的,必須在編譯的時候叫上 -mshared 編譯參數.
下 面使用一個例子來說明MIPS 動態串連的過程. 這個例子啟動並執行環境是 debian lenny. CPU是loongson 2f. gcc的版本是gcc version 4.3.2 (Debian 4.3.2-1.1). gdb的版本是GNU gdb 6.8-debian.
源檔案:
main-pic.c test-pic.c
反組譯碼檔案:
main-pic.o.asm test-pic.o.asm test-pic.asm
編譯命令:
gcc -c -mshared -g main-pic.c
gcc -c -mshared -g test-pic.c
gcc -o test-pic main-pic.o test-pic.o
objdump -d main-pic.o > main-pic.o.asm
objdump -d test-pic.o > test-pic.o.asm
objdump -d test-pic > test-pic.asm
yajin@kill-bill:~/dev$ gdb test GNU gdb 6.8-debian Copyright (C) 2008 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "mipsel-linux-gnu"... (gdb) b main Breakpoint 1 at 0x400660: file main-pic.c, line 7. (gdb) r Starting program: /home/yajin/dev/test-pic Breakpoint 1, main () at main-pic.c:7 7 test(); (gdb) p /x $pc $1 = 0x400660 (gdb) p /x $gp $2 = 0x418930 ->gp=GOT的位置0x418930 (gdb) p /x $t9 $3 = 0x400640 ->t9=當前函數地址 (gdb) si 0x00400664 7 test(); (gdb) p /x $t9 ->t9=要調用的函數test的地址 $4 = 0x400690 (gdb) si 0x00400668 7 test(); (gdb) si test () at test.c:4 4 { (gdb) p /x $pc $5 = 0x400690 (gdb) si 0x00400694 4 { (gdb) si 0x00400698 4 { (gdb) si 0x0040069c 4 { (gdb) p /x $gp $6 = 0x418930 ->發現沒有?gp=0x418930,是GOT的位置 (gdb) p /x $pc $7 = 0x40069c (gdb) si 0x004006a0 4 { (gdb) 0x004006a4 4 { (gdb) 0x004006a8 4 { (gdb) 0x004006ac 4 { (gdb) 5 printf("hello world first /n"); (gdb) 0x004006b4 5 printf("hello world first /n"); (gdb) 0x004006b8 5 printf("hello world first /n"); (gdb) p /x $gp $8 = 0x418930 (gdb) si 0x004006bc 5 printf("hello world first /n"); (gdb) p /x $gp $9 = 0x418930 (gdb) p /x $pc $10 = 0x4006bc (gdb) si 0x004006c0 5 printf("hello world first /n"); (gdb) p /x $t9 $11 = 0x400840 ->這裡是要跳轉到的.MIPS.stubs的位置 (gdb) si 0x004006c4 5 printf("hello world first /n"); (gdb) si ->跳轉到.MIPS.stubs 0x00400840 in puts () Current language: auto; currently asm (gdb) si 0x00400844 in puts () (gdb) p /x $pc $12 = 0x400844 (gdb) p /x $t9 $13 = 0x2aabf4d0 (gdb) si 0x00400848 in puts () (gdb) si ->跳轉到動態連接器中 0x2aabf4d0 in _dl_runtime_resolve () from /lib/ld.so.1 (gdb) b *0x4006cc Breakpoint 2 at 0x4006cc: file test.c, line 5. (gdb) c Continuing. hello world first Breakpoint 2, 0x004006cc in test () at test-pic.c:5 5 printf("hello world first /n"); Current language: auto; currently c (gdb) p /x $gp $14 = 0x2ac55950 (gdb) p /x $pc $15 = 0x4006cc (gdb) si 0x004006d0 5 printf("hello world first /n"); (gdb) p /x $gp $16 = 0x418930 ->第一次printf結束,gp恢複過來了 (gdb) si 6 printf("hello world second /n"); (gdb) p /x $pc $17 = 0x4006d4 (gdb) si 0x004006d8 6 printf("hello world second /n"); (gdb) 0x004006dc 6 printf("hello world second /n"); (gdb) 0x004006e0 6 printf("hello world second /n"); (gdb) si 0x004006e4 6 printf("hello world second /n"); (gdb) p /x $t9 ->第二次直接到printf,不會到.MIPS.Stubs $18 = 0x2ab444d0 (gdb) p /x $pc $19 = 0x4006e4 (gdb) si 0x004006e8 6 printf("hello world second /n"); (gdb) p /x $pc $20 = 0x4006e8 (gdb) si |
本文中的例子是採用c來寫的.關於相關的彙編,請參考下面的串連. 另外還有一些宏 _gp_disp,__gnu_local_gp,cpload $25 等,理解了本文就不難理解了.
參考:
[1] Some new¹ tricks for better performance in MIPS-Linux
[2] Position-Independent Coding in Assembly Language
[3] PIC CODE
標籤: loongson, MIPS, PIC