鏈接符號引用重定位
簡介
我們知道一個.c文件可以被編譯爲.o文件,即目標文件,而假如一個.c中引用了別的.c中的函數或者是變量,這時候的.o其實是不知道引用函數實際的內存位置的,也就無法跳轉,這就需要【重定位】的操作了,而針對函數名(也是符號)的重定位
例子
我們編寫兩個.c文件,分別是main.c和func.c,main.c引用了func.c中的func函數
// main.c
int func(int x, int y);
int main()
{
int c = func(1, 2);
return 0;
}
// func.c
int func(int x, int y)
{
return 2*x + y;
}
我們使用gcc來編譯出兩個.o文件,使用如下命令將會生成main.o與func.o
gcc -c main.c func.c
然後我們對main.o與func.o
反彙編,使用如下命令可以生成main_dump.txt與func_dump.txt
的反彙編文件
objdump -d main.o > main_dump.txt
objdump -d func.o > func_dump.txt
因爲main.c調用func.c而func.c並未調用其他函數,所以我們查看main.o反彙編結果,如下圖藍色區域,因爲在鏈接之前每個.c相對是獨立的,並不知道如何跳轉到其他文件的函數,所以無法正確的寫成跳轉指令
但是我們將兩個.o文件進一步編譯成可執行文件,並查看其反彙編。執行以下命令做進一步的編譯,將main.o和func.o鏈接進而生成exe可執行的二進制文件
gcc -o exe main.o func.o
如圖
我們對exe反彙編,得到exe的彙編代碼
objdump -d exe > exe_dump.txt
發現可以正確的跳轉到func所指定的地址,這是我們想要的,也是鏈接中重定位所做的功勞
重定位條目
簡介
----引用自《深入理解計算機系統》
但是書上講的很抽象,而且沒有例子,接下來簡單解釋以下這些條目中各個符號的意義
offset
因爲在鏈接之前,每個.c相當於一個獨立的部分,我們稱之爲【節】,而offset表明這個符號引用相對於【節】起始處的偏移(其實就是相對地址)
值得注意的是,這個offset指向的是callq指令中需要被修改的操作數,而不是callq指令本身
如圖 main.c 的節
type
是何種重定位方式,因爲編譯的時候可以有很多種重定位方式,但是接下來只討論書上描述的比較難懂的【重定位PC相對引用】
symbol
即這個引用指向的是哪個符號,比如main.c引用func函數那麼就指向func函數
addend
在PC+偏移跳轉計算實際地址時用到的計算偏移,待會會講解
重定位PC相對引用
這裏是書上講的比較晦澀的地方,書上直接給出了公式
這個公式很難懂,但是我們可以翻譯一下。仍然以main.c引用func.c爲例子
符號 | 意義 |
---|---|
ADDR(s) | main節在實際的可執行文件中,位於內存的實際地址 |
ADDR(r.symbol) | func函數在實際可執行文件中,位於內存的實際地址 |
r.offset | 上文提到的offset,計算出需要修改的引用的操作數相對main節的偏移 |
r.addend | 上文提到的addend,計算實際callq語句操作數時需要用到的計算偏移 |
結合exe可執行文件的反彙編代碼,我們進一步理解:
現在再來看這個公式,其中offset爲0x13,根據上面offset定義推得
refaddr = ADDR(s) + r.offset
要修改的引用的實際地址 = main函數實際地址 + 要修改的引用的相對地址
0x60d = 0x5fa + 0x13
addend是由命令查看精靈 ELF readelf -r main.o
查詢得的
*refptr = ADDR(r.symbol) + r.addend - refaddr
修改後的內容x = func函數實際地址 + 計算偏移 - 要修改的引用的實際地址
0xa = 0x61b + -0x4 - 0x60d
注:
因爲是【PC+偏移】尋址,修改後的內容x就是call的操作數
要想使跳轉到func函數,就需要【PC+x = func函數實際地址】
PC + 0xa = 0x611 + 0xa = 0x61b 確實是func函數的地址,達到效果
可以看到編譯後的結果,也就是鏈接的時候修改後的結果,確實是0xa(小端表示牢記於心)
現在再看書上的公式是否變簡單了?
重定位PC絕對引用
這個簡單,沒有那麼多做差和偏移,直接將實際的func函數的地址賦值給操作數,但是一般用於外部變量數據的引用中