- 程式如下:
重定位
-
先來看這段程式碼的反彙編結果。
-
"main"的起始地址為0x00000000,這是因為在未進行空間分配之前,目標檔案程式碼段中的起始地址以0x00000000開始,等到空間分配完成以後,各個函式才會確定自己在虛擬地址空間中的位置。
-
偏移為0x18的地址上是一條mov指令,總共8個位元組,它的作用是將“shared”的地址賦值到esp暫存器+4的偏移地址中去,前面4個位元組“c7442404”是mov的指令碼,後面4個位元組是“shared”的地址。
-
偏移為0x26的地址上是一條呼叫指令,它表示對swap函式的呼叫。這條指令共5個位元組,前面的0xe8是操作碼,這是一條近址相對位移呼叫指令,後面4個位元組就是被呼叫函式的相對於呼叫指令的下一條指令的偏移量。在沒有重定位之前,相對偏移被置為0xFFFFFFFC(小端),它是常量“-4”的補碼形式。
重定位表
-
對於可重定位的ELF檔案來說,它必須包含有重定位表,用來描述如何修改相應的段裡的內容。對於每個要重定位的ELF段都有一個對應的重定位表,而一個重定位表往往就是ELF檔案中的一個段,所以其實重定位表也可以叫重定位段。
-
通過命令可以檢視目標檔案的重定位表。
-
OFFSET是重定位的入口偏移,表示該入口在要被重定位的段中的位置。“.text”表示這個重定位表示程式碼段的重定位表,所以偏移表示程式碼段中需要被調整的位置。這裡的0x1c和0x27分別就是程式碼段中“mov”指令和“call”指令的地址部分。
符號解析
-
重定位過程也伴隨著符號的解析過程,每個目標檔案都可能定義一些符號,也可能引用到定義在其他目標檔案的符號。重定位的過程中,每個重定位的入口都是對一個符號的引用,那麼當連結器需要對某個符號的引用進行重定位時,它就要確定這個符號的目標地址。這時候連結器就會去查詢由所有輸入目標檔案的符號表組成的全域性符號表,找到相應的符號後進行重定位。
-
通過命令檢視“a.o”的符號表。
-
可以看到shared和swap的型別都是“UND”,即“undefined”未定義型別,在連結器掃描完所有的輸入目標檔案後,所有這些未定義的符號都應該能夠在全域性符號表中找到,否則連結器就報符號未定義錯誤。這種一般都是連結時缺少了某些庫,或者輸入目標檔案路徑不正確或符號的宣告與定義不一樣。
指令修改方式
-
不同的處理器指令對於地址的格式和方式都不一樣。
-
對於32位x86平臺下的ELF檔案的重定位入口所修正的指令定址方式只有兩種:
- 絕對近址32位定址。
- 相對近址32位定址。
-
這兩種重定位方式指令修正方式每個被修正的位置的長度都是32位。
-
這兩種方式的定義:
-
通過前面的重定位表可以看到swap符號的型別為R_386_PC32,這是一條相對位移呼叫指令。而shared符號的型別為R_386_32,它修正的是一條傳輸指令的源,即shared的絕對地址。
-
假設在將a.o和b.o連結成最終可執行檔案後,main函式的虛擬地址為0x1000,swap函式的虛擬地址為0x2000,shared變數的虛擬地址為0x3000。
-
首先看偏移為0x18的這條mov指令的修正,它是絕對定址修正,它修正後的結果是S+A。
- S是符號shared的實際地址,即0x3000。
- A是被修正位置的值,即0x00000000。
-
所以它的修正後的地址為:0x3000+0x00000000=0x3000。
-
再來看偏移為0x26的這條call指令的修正,它是相對定址修正,它修正後的結果是S+A-P。
- S是符號swap的實際地址,即0x2000。
- A是被修正位置的值,即0xFFFFFFFC(-4)。
- P為被修正的位置,當連結成可執行檔案時,這個值應該是被修正位置的虛擬地址,即0x1000+0x27。
-
所以它的修正後的地址為0x2000+(-4)-(0x1000+0x27)=0xFD5。
-
這條相對位移呼叫指令的呼叫地址是該指令下一條指令的起始地址加上偏移量,即:0x102b+0xfd5=0x2000,剛好是swap函式的地址。
-
從這兩個例子可以看出來,絕對定址修正和相對定址修正的區別就是絕對定址修正後的地址為該符號的實際地址;相對定址修正後的地址為符號距離被修正位置的地址差。
one more thing!
C語言標準庫中的變長引數
- 變長引數是C語言的特殊引數形式,比如printf的宣告:
int printf(const char* format, ...);
複製程式碼
-
printf函式除了第一個引數型別為const char*之外,其後可以追加任意數量、任意型別的引數。
-
變長引數的實現得益於C語言預設的cdecl呼叫慣例的自右向左壓棧傳遞方式。
-
首先,看這樣一個函式。
// 第一個引數傳遞一個整數num,緊接著後面會傳遞num個整數,返回num個整數的和。
int sum(int num, ...);
複製程式碼
-
當我們呼叫:**”int n = sum(3, 16, 38, 53);“**時,引數在棧上的佈局會是這樣的。
-
在函式內部,函式可以使用名稱num來訪問數字3,當無法使用任何名稱訪問其他的幾個不定引數。當此時由於棧上其他的幾個引數實際恰好依序排列在引數num的高地址方向,因此可以簡單地通過num的地址計算出其他引數的地址。
// sum的實現
int sum(int num, ...) {
int *p = &num + 1;
int ret = 0;
while (num--)
ret += *p++;
return ret;
}
複製程式碼
- printf的不定引數比sum要複雜很多,因為printf的引數不僅數量不定,而且型別也不定。所以printf需要在格式字串中註明引數型別。printf裡的格式字串如果將型別描述錯誤,因為不同引數的大小不同,不僅可能導致這個引數的輸出出錯,還有可能導致其後的一系列引數錯誤。