天天看點

Linux系統ELF檔案二進制格式分析(四)

六、重定位項

重定位是将ELF檔案中未定義的符号關聯到有效位置的過程,特别是目标檔案中這一項尤為重要。本例中引用了C語言庫函數printf和exit,連結時必須替換為該程序的虛拟位址空間中機器代碼所在位置。

每個ELF中,都有專門的類型為REL的節包含重定位項,辨別了需要進行重定位的位置。每一項都是用相同的資料結構表示的。

1.      資料結構

由于曆史原因,有兩種類型的重定位資訊,由兩種不同資料結構。第一種重定位結構稱為普通重定位,對應的節的類型為SHT_REL,結構如下:

typedef struct elf32_rel {
  Elf32_Addr	r_offset;
  Elf32_Word	r_info;
} Elf32_Rel;
           

r_offset指定需要重定位項的位置,r_info不僅提供了符号表中的位置,還包括重定位類型的資訊。其中高8位中訓示符号表中位置,低8位訓示重定位類型。在計算重定位位置時,将根據重定位類型,對該值進行不同處理。擷取相應值的宏定義如下:

#define ELF32_R_SYM(x) ((x) >> 8)
#define ELF32_R_TYPE(x) ((x) & 0xff)
           

第二種重定位結構稱為添加常數的重定位,對應節的類型為SHT_RELA,結構如下:

typedef struct elf32_rela{
  Elf32_Addr	r_offset;
  Elf32_Word	r_info;
  Elf32_Sword	r_addend;
} Elf32_Rela;
           

需要注意的是,第一種重定位結構也存在這個加數值,該加數值不在結構中存儲,而是連結器根據該值應該出現的位置擷取的。

目标檔案中這個加數值在連結成可執行檔案時會參與運算得到一個新的值,後面将會用例子進行說明如何計算得出新結果。

檢視本例中目标檔案的重定位項,結果如下:

Linux系統ELF檔案二進制格式分析(四)

2.      重定位類型

Linux存在兩種重定位類型:相對重定位(對應類型為上圖中R_386_PC32)和絕對重定位(對應類型為上圖中R_386_32)。相對重定位主要用于子例程調用。絕對重定位的重定位項指向記憶體中編譯時就已知的資料,例如字元串常數。

調用子過程是通過彙編指令call進行,X86架構32位系統中,call指令機器碼為0xe8,緊跟在call指令機器碼後的是4個位元組的位址資訊,但這個資訊不是被調用子過程的準确位址,而是上一節提到的“加數”,這個加數是一個相對位置,可以根據這個相對值确定準确位址。目标檔案和可執行檔案都存在這個加數,但由于目标檔案連結到可執行檔案時子過程位址會改變,這個加數也會改變,是以需要通過計算得到改變後的值,根據重定位類型不同,計算方法也不同。

當定位類型為相對重定位時:

           E = S – P +A

當定位類型為絕對重定位時:

           E = S + A

其中:

S表示“重定位符号位置”,P表示“重定位項的位置”,A表示“目标檔案加數值”。

E表示“可執行檔案加數值”。

下面以本例中main函數中調用add子過程為例說明計算方法。

彙編方式檢視目标檔案代碼如下,指令為objdump --disassemble ELF檔案:

Linux系統ELF檔案二進制格式分析(四)

可以看到,main函數在檔案中偏移為0x1C,0x44處調用了子過程add,0x45開始的4位元組值為0xFFFFFFFC,也就是加數,他是以補碼形式表示的,換算成十進制為-4,即A=-4。對照上一小節目标檔案重定位表可知,偏移為0x45的重定位項名字是add,彙編代碼“call 45”即表示調用了該重定位項。

彙編方式檢視可執行檔案代碼如下:

Linux系統ELF檔案二進制格式分析(四)

可執行檔案中,add子過程入口為0x8048434,即S=0x08048434;重定位項位置為0x08048479,即P=0x08048479。是以可得E為:

E=S –P + A

= 0x08048434 - 0x08048479 + (-4)

= 134513716 – 134513785 – 4

= -73

-73的二進制補碼為0xFFFFFFB7,正如可執行檔案0x08048479處的4個位元組的值一樣。

-73實際上是一個偏移量,指明了重定位符号跟目前位置的內插補點。但有的讀者會注意到,add子過程入口0x08048434和重定位項偏移0x08048479內插補點為-69,而不是-73,兩者相差4,正好是“目标檔案加數”的絕對值。這樣的結果是跟IA-32處理器工作方式有關的。在執行可執行檔案時,0x08048478處call add指令被執行後,CPU的IP寄存器即指向下一條指令的位址,即0x0804847D,前後兩條指令內插補點正好是4。但由于是調用了子過程,IP指令需要跳轉到子過程的入口,那麼如何從現在IP=0x0804847D這個位置跳轉到add指令的正确位置呢?add子過程入口0x08048434和重定位項偏移0x08048479內插補點為-69,而重定位項偏移和IP=0x0804847D內插補點為-4,則add子過程入口和IP內插補點為-69-4=-73,0x0804847D加上十進制-73,為0x08048434,正好是add子過程的入口位址。

七、動态連結

ELF檔案中,以下兩個節用于儲存動态連結器所需要的資料:

.dynsym儲存了有關符号表,包含了所有需要通過外部引用解決的符号。本例中引用外部符号如下:

Linux系統ELF檔案二進制格式分析(四)

.dynamic儲存了一個數組,數組類型為Elf32_Dyn類型,以下内容都是關于這個節的。

1.      資料結構

typedef struct dynamic{
  Elf32_Sword d_tag;
  union{
    Elf32_Sword	d_val;
    Elf32_Addr	d_ptr;
  } d_un;
} Elf32_Dyn;
           

d_tag用于區分各種指定資訊類型的标記,該結構中的聯合根據該标記進行解釋。d_un或者儲存一個虛拟位址,或者儲存一個整數。

最重要的标記如下:

DT_NEEDED指定改程式執行所需的一個動态庫,d_un指向一個字元串表項,給出庫的名稱。本例當中隻用到标準庫。

DT_STRTAB儲存了字元表的位置,其中包括了dynamic節所需的所有動态連結庫和符号名稱。

DT_SYMTAB儲存了符号表的位置,其中包含了dynamic節所需的所有資訊。

DT_INIT和DT_FINT儲存了用于初始化和結束程式的函數。

2.      動态連結資訊

用指令檢視本例動态連結資訊如下:

Linux系統ELF檔案二進制格式分析(四)

至此,關于Linux系統中ELF檔案的格式已基本分析完畢。

繼續閱讀