《程序員的自我修養》番外筆記——符號解析與重定位

  • 程序以下:

重定位

  • 先來看這段代碼的反彙編結果。

  • "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。spa

    • 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裏的格式字符串若是將類型描述錯誤,由於不一樣參數的大小不一樣,不只可能致使這個參數的輸出出錯,還有可能致使其後的一系列參數錯誤。
相關文章
相關標籤/搜索