MIPS 動態符號的延遲綁定

  1. 概述
MIPS平臺中動態庫的裝載與解析原理,網上資料並不多,而且很多講解都是錯誤的,甚至MIPS寶書《see mips run》中也沒有提供詳細的方案。因此本人只好通過代碼走讀和運行驗證的方法來詳細瞭解其原理。之所以要了解mips動態符號的解析調用,是因爲之前在實現mips backtrace時,對一些調整gp的指令存在疑惑,顧而深入研究一下。

先上結論:MIPS動態庫中的外部符號調用,是依賴.got段和.MIPS.stubs段來共同實現的。
.got中保存着外部符號的地址,但由於採用了延遲綁定技術,某外部符號在第一次被調用前,其地址是未知的,ld.so在裝載動態庫階段並沒有立刻將外部函數解析並填充到.got中,目的是爲了加快啓動速度,也是爲了不必要的時間浪費,因爲在進程運行期間,某些外部符號可能永遠都不會被調用到,所以裝載階段解析所有符號是一個浪費時間的行爲。
因此,.got中的未決議符號項內,先指向了.MIPS.stubs中的一段指令(每個符號都對應一段單獨的指令),這段指令的作用是跳轉至符號解析函數_dl_runtime_resolve。
.got段的第一項比較特殊,它保存的符號正是函數"_dl_runtime_resolve",用objdump查看時此項爲0,但運行起來之後,會被ld.so填充進_dl_runtime_resolve的地址。

下面簡單舉例說明。
hello.c:
#include <stdio.h>
#include "hello.h"
int hello(int i)
{
printf("hello %d \n",i);
return 0;
}

main.c:
#include <stdio.h>
#include "hello.h"
int main(int argc, char** argv)
{
int i = 0;

hello(i);
i++;

hello(i);
return 0;
}

mipsel-linux-gcc hello.c -g -fPIC -rdynamic -fasynchronous-unwind-tables -shared -o libhello.so
mipsel-linux-gcc main.c -g -fPIC -rdynamic -fasynchronous-unwind-tables -L./ -lhello -o main.elf
  1. 流程圖
  1. main首次調用hello
mips_lazy_banding.png
圖1 main首次調hello
  1. main再次調用hello
mips_after_banding.png

圖2 main再次調hello
  1. 彙編代碼走讀
main函數彙編代碼走讀:
00400810 <main>:
400810: 3c1c0002 lui gp,0x2 //gp = 0x20000
400814: 279c81b0 addiu gp,gp,-32336 //gp = gp -32336;
400818: 0399e021 addu gp,gp,t9 //gp == &_gp,此時gp寄存器指向符號_gp(_gp_disp),也就是 &_gp = 4189c0 關於_gp的詳細解釋請見附錄,t9存的是本函數地址
40081c: 27bdffd8 addiu sp,sp,-40 //調整棧頂指針
400820: afbf0024 sw ra,36(sp) //保存返回地址
400824: afbe0020 sw s8,32(sp) //保存caller fp
400828: 03a0f021 move s8,sp //調整fp
40082c: afbc0010 sw gp,16(sp) //保存本函數gp
400830: afc40028 sw a0,40(s8) //保存入參
400834: afc5002c sw a1,44(s8) //保存入參
400838: afc00018 sw zero,24(s8) //本地變量初始化
40083c: 8fc40018 lw a0,24(s8) //準備將本地變量作爲入參傳給子函數
400840: 8f828018 lw v0,-32744(gp) //v0 = *(&_gp - 32744) = *(4109d8) = 4008f0,也就是.got內第3項存的是.MIPS.stub的地址,稍後經過binding之後,此處會got第3項變成hello的地址
400844: 0040c821 move t9,v0 //經計算,此處t9寄存器的值是4008f0,也即.MIPS.stubs的地址
400848: 0320f809 jalr t9 //跳轉到.MIPS.stubs處執行,返回地址已自動存入ra寄存器
40084c: 00000000 nop
...

004008f0 <.MIPS.stubs>:
4008f0: 8f998010 lw t9,-32752(gp) //t9 = *(4109d0),爲 .got的首地址
4008f4: 03e07821 move t7,ra //將返回地址存到t7,此處的返回地址顯然是main中的指令 "jalr t9"的下下條指令。_dl_runtime_resolve中會用t7寄存器來返回到main。
4008f8: 0320f809 jalr t9 //跳轉執行.got首地址的指令,也就是_dl_runtime_resolve函數地址
4008fc: 24180015 li t8,21 //t8=21 此處是將hello的符號表(.dynsym)的索引21存入t8
...

Disassembly of section .got:
004109d0 <_GLOBAL_OFFSET_TABLE_>:
4109d0: 00000000 nop //此處保存的是_dl_runtime_resolve,編譯後值爲0,但運行後會被ld.so填充上_dl_runtime_resolve的地址,已用gdb驗證
4109d4: 80000000 lb zero,0(zero)
4109d8: 004008f0 tge v0,zero,0x23


ld-uClibc.so中的_dl_runtime_resolve函數:
00007f50 <_dl_runtime_resolve>:
7f50: 03801821 move v1,gp
7f54: 27bdffd8 addiu sp,sp,-40
7f58: 2739000c addiu t9,t9,12
7f5c: 3c1c0002 lui gp,0x2
7f60: 279ca0b4 addiu gp,gp,-24396
7f64: 0399e021 addu gp,gp,t9
7f68: 03e01021 move v0,ra
7f6c: afbc0020 sw gp,32(sp)
7f70: afaf0024 sw t7,36(sp) //將.MIPS.stubs中保存的main中的返回地址保存起來
7f74: afa40010 sw a0,16(sp)
7f78: afa50014 sw a1,20(sp)
7f7c: afa60018 sw a2,24(sp)
7f80: afa7001c sw a3,28(sp)
7f84: 03002021 move a0,t8 //將t8的值做爲入參傳給函數<__dl_runtime_resolve>,從其函數原型看,t8存的是hello在符號表中的index,__dl_runtime_resolve會根據這個index來更新got(通過對hello在symtab中的index進行一定的計算和轉換,能得到hello在got表中的位置)
7f88: 00602821 move a1,v1
7f8c: 8f99814c lw t9,-32436(gp)
7f90: 0411ec02 bal 2f9c <__dl_runtime_resolve>
7f94: 00000000 nop
7f98: 8fbc0020 lw gp,32(sp)
7f9c: 8fbf0024 lw ra,36(sp) //此處將main中調用hello的返回地址加載到ra
7fa0: 8fa40010 lw a0,16(sp)
7fa4: 8fa50014 lw a1,20(sp)
7fa8: 8fa60018 lw a2,24(sp)
7fac: 8fa7001c lw a3,28(sp)
7fb0: 27bd0028 addiu sp,sp,40
7fb4: 0040c821 move t9,v0 //v0做爲__dl_runtime_resolve的返回值,保存的是hello的實際地址
7fb8: 03200008 jr t9 //執行hello函數,由於此時ra存的是main調hello的返回地址,因此hello函數返回時正好跳轉到main中。這也是此處使用jr而不是jal的原因,因爲jal會自動更新ra寄存器!
7fbc: 00000000 nop


uclinux-rootfs/lib/uClibc/ldso/ldso/mips/elfinterp.c:
unsigned long __dl_runtime_resolve(unsigned long sym_index,
unsigned long old_gpreg)
{
......
return new_addr;
}
  1. 運行驗證
# ./gdb ./main.elf
Reading symbols from /mnt/usb/main.elf...done.
(gdb) b main
Breakpoint 1 at 0x400838: file ./main.c, line 8.
(gdb) r
Starting program: /mnt/usb/main.elf
[Thread debugging using libthread_db enabled]

Breakpoint 1, main (argc=1, argv=0x7fff6e04) at ./main.c:8
8 ./main.c: No such file or directory.
in ./main.c
(gdb) info address main
Symbol "main" is a function at address 0x400810.
(gdb) x/xw &main
0x400810 <main>: 0x3c1c0002 //此處main的地址還是0x400810,說明main.elf雖然以fPIC編譯的,但是做爲主程序,依然被加載到了默認地址。
(gdb) x/32xw &main
0x400810 <main>: 0x3c1c0002 0x279c81b0 0x0399e021 0x27bdffd8
0x400820 <main+16>: 0xafbf0024 0xafbe0020 0x03a0f021 0xafbc0010
0x400830 <main+32>: 0xafc40028 0xafc5002c 0xafc00018 0x8fc40018
0x400840 <main+48>: 0x8f828018 0x0040c821 0x0320f809 0x00000000
0x400850 <main+64>: 0x8fdc0010 0x8fc20018 0x24420001 0xafc20018
0x400860 <main+80>: 0x8fc40018 0x8f828018 0x0040c821 0x0320f809
0x400870 <main+96>: 0x00000000 0x8fdc0010 0x00001021 0x03c0e821
0x400880 <main+112>: 0x8fbf0024 0x8fbe0020 0x27bd0028 0x03e00008
(gdb) x/32xw 0x4109d0 //既然main的地址沒發生偏移,那麼說明.got的地址也沒變,依然是編譯後的0x4109d0,打印顯示.got的第一項是0x77fe6780
0x4109d0 <_GLOBAL_OFFSET_TABLE_>: 0x77fe6780 0x77ff5028 0x004008f0 0x00000000 //.got第3項是4008f0,也即.MIPS.stubs內的地址,此時還沒真正調用到hello()
0x4109e0 <completed.5347>: 0x00000000 0x00000000 0xffffffff 0x00000000
0x4109f0 <object.5359+8>: 0x00000000 0x0041095c 0x000007f8 0x77fba898
0x410a00: 0x00000000 0x00000000 0x00000000 0x00000000
0x410a10: 0x00000000 0x00000000 0x00000000 0x00000000
0x410a20: 0x00000000 0x00000000 0x00000000 0x00000000
0x410a30: 0x00000000 0x00000000 0x00000000 0x00000000
0x410a40: 0x00000000 0x00000000 0x00000000 0x00000000
(gdb) info symbol 0x77fe6780 //此地址真的是_dl_runtime_resolve!猜想得以驗證!
_dl_runtime_resolve in section .text of /lib/ld-uClibc.so.0
(gdb) info address hello
Symbol "hello" is a function at address 0x77faa690.
(gdb) b main.c:11
Breakpoint 2 at 0x400854: file ./main.c, line 11.
(gdb) c
Continuing.
hello,world, i=0

Breakpoint 2, main (argc=1, argv=0x7fff6e04) at ./main.c:11
11 in ./main.c
(gdb) x/32xw 0x4109d0 //此時已經調用過一次hello函數了,再次查看.got的第3項:已經變成hello的地址了!!
0x4109d0 <_GLOBAL_OFFSET_TABLE_>: 0x77fe6780 0x77ff5028 0x77faa690 0x00000000
0x4109e0 <completed.5347>: 0x00000000 0x00000000 0xffffffff 0x00000000
0x4109f0 <object.5359+8>: 0x00000000 0x0041095c 0x000007f8 0x77fba898
0x410a00: 0x00000000 0x00000000 0x00000000 0x00000000
0x410a10: 0x00000000 0x00000000 0x00000000 0x00000000
0x410a20: 0x00000000 0x00000000 0x00000000 0x00000000
0x410a30: 0x00000000 0x00000000 0x00000000 0x00000000
0x410a40: 0x00000000 0x00000000 0x00000000 0x00000000
(gdb) info symbol 0x77faa690
hello in section .text of /mnt/usb/libhello.so
 附錄 關於寄存器gp與符號_gp
我們知道PIC的函數入口處,首先會調整gp寄存器,很多資料上都說gp指向的是.got,實際上並不是,gp寄存器指向的是符號_gp,這個_gp絕不是got段的地址,而是一個常數區的中間位置,它是在編譯階段生成的,它的身份是做爲gp相對尋址的“基準”位置。
《see mips run》p260 9.4.1 相對於gp尋址:
要求compiler,assembler,linker以及運行時激活代碼(runtime startup code)偕同配合,把程序中的’小’變量和常數彙集到一個獨立的內存區間;然後設置register $28(通常稱爲全局量指針global pointer或簡寫爲gp)指向該區間的中央(linker生成一個特殊符號_gp,其地址爲該區間的中央,激活代碼負責將_gp的地址加載到gp寄存 器,這一動作在第一個load/store指令運行之前完成).只要這些全局變量\靜態編量\常量加起來不佔用超過64k大小的空間,這些資料相對該區間 的中點也就不超過正負32k(偏移量15bit+符號位1bit,參見mips機器碼格式),那幺我們就可以在一條指令中完成對它們的 load/store操作:
lw $2, addr ---> lw $2, addr-_gp(at)
在同一個動態模塊中,每個函數入口處的gp寄存器調整,都將使gp指向符號_gp的地址。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章