自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

GCC 鏈接過程中的【重定位】過程分析

系統(tǒng) Linux
所謂的安排虛擬地址,就是指定這塊內(nèi)容被加載到虛擬內(nèi)存的什么地方。當(dāng)可執(zhí)行文件被執(zhí)行的時候,加載器就把每一塊內(nèi)容復(fù)制到虛擬內(nèi)存相應(yīng)的地址處。

最近因為項目上的需要,利用動態(tài)鏈接庫來實現(xiàn)一個插件系統(tǒng),順便就復(fù)習(xí)了一下關(guān)于Linux中一些編譯、鏈接相關(guān)的內(nèi)容。

在鏈接的過程中,符號重定位是比較麻煩的事情,特別是在動態(tài)鏈接的過程中,因為需要考慮到很多不同的情況。

這篇文章作為第一篇,先來聊一聊靜態(tài)鏈接中的重定位過程。

按照慣例,還是以一個簡短的示例代碼作為載體,看一看GCC在鏈接的過程中,是如何根據(jù)目標(biāo)文件(.o文件)來進(jìn)行重定位,生成最終的可執(zhí)行文件的。

示例代碼

示例代碼很簡單,一共有2個源文件main.c和 sub.c。

在sub.c中定義了一個全局變量和一個全局函數(shù),然后在main.c中使用這個全局變量和全局函數(shù)。代碼如下:

sub.c

main.c

在一般的開發(fā)過程中,都是使用GCC工具,直接把這2個源文件編譯得到可執(zhí)行文件。

但是,為了探究編譯、鏈接過程中的一些內(nèi)部情況,我們需要把編譯、鏈接的過程拆開,從中間過程中產(chǎn)生的目標(biāo)文件(.o 文件)中,來查看一些詳細(xì)信息。

先把這2個源文件編譯成目標(biāo)文件sub.o和main.o:

$ gcc -m32 -c sub.c
$ gcc -m32 -c main.c

這樣就得到了兩個目標(biāo)文件,先來初步看一下這2個目標(biāo)文件中的一些信息。

以上這兩個編譯過程是各自獨立的,雖然main.o中使用了兩個符號(全局變量和全局函數(shù)),但是此時main.o并不知道這2個符號是在哪個文件中定義的。

當(dāng)鏈接器把所有的.o文件鏈接成可執(zhí)行文件的過程中,才能確定這2個符號是在哪里。

在Linux系統(tǒng)中,目標(biāo)文件(.o) 和可執(zhí)行文件都是ELF格式的,因此如何查看ELF格式文件的一些工具指令就非常有幫助。

sub.o 文件內(nèi)容分析

段信息

首先來簡單瞄一眼一下sub.o中的一些信息。

sub.o中的段信息如下(指令:$ readelf -S sub.o):

我們主要關(guān)心黃色的代碼段和數(shù)據(jù)段就可以了,可以看出:

  1. 代碼段(.text):地址Addr是 0x0000_0000(因為這是目標(biāo)文件,不是可執(zhí)行文件,所以不會安排地址),它在 sub.o 文件中的偏移量(Off)是 0x34,長度是 0x0C 字節(jié);
  2. 數(shù)據(jù)段(.data):地址Addr是 0x0000_0000,它在 sub.o 文件中的偏移量(Off)是 0x40,長度是 0x04 字節(jié);

簡單算一下:sub.o的開始部分是ELF的 header,通過 readelf -h sub.o 指令可以看出來header部分是52個字節(jié)(即:0x34),如下:

因此可以得到:

  1. 代碼段(.text)是緊接在 header 之后,長度是 0x0C 個字節(jié),在文件中占據(jù)著 0x34 ~ 0x3F 這部分空間(0x3F = 0x34 + 0x0C - 1);
  2. 數(shù)據(jù)段(.data)是進(jìn)階在代碼段之后,在文件中占據(jù)著 0x40 ~ 0x43 這部分空間;

符號表信息

下面再來說說符號表的事情。

簡單來說,符號表就是一個文件中定義的所有符號、引用的外部符號(在其它文件中定義),包括:變量名、函數(shù)名、段名等等,都屬于符號。

當(dāng)然了,在ELF文件中會詳細(xì)的說明每一個符號的類型、大小、可見性等信息。如果對ELF文件格式有過了解的話,一定知道每一條符號信息,都是通過一個結(jié)構(gòu)體來描述具體含義的,描述符號表的結(jié)構(gòu)體如下:

// Symbol table entries for ELF32.
struct Elf32_Sym {
Elf32_Word st_name; // Symbol name (index into string table)
Elf32_Addr st_value; // Value or address associated with the symbol
Elf32_Word st_size; // Size of the symbol
unsigned char st_info; // Symbol's type and binding attributes
unsigned char st_other; // Must be zero; reserved
Elf32_Half st_shndx; // Which section (header table index) it's defined in
};

再來看一下sub.o中的符號表,下面這張圖(指令:readelf -s sub.o):

關(guān)注上圖中黃色矩形中的兩個符號:SubData和SubFunc,很明顯它們就是sub.c中定義的兩個符號:全局變量和全局函數(shù)。

對于SubData符號來說:

  1. Size=4: 長度是 4 個字節(jié);
  2. Type=OBJECT:說明這是一個數(shù)據(jù)對象;
  3. Bind=GLOBAL:說明這個符號是全局可見的,也就是在其他文件中可以使用;
  4. Ndx=2:說明這個符號是屬于第 2 個 段中,就是數(shù)據(jù)段(.data);

同樣的道理,對于SubFunc符號來說:

  1. Size=12: 長度是 12 個字節(jié);
  2. Type=FUNC:說明這是一個函數(shù);
  3. Bind=GLOBAL:說明這個符號是全局可見的,也就是在其他文件中可以調(diào)用;
  4. Ndx=1:說明這個符號是屬于第 1 個 段中,就是代碼段(.text);

main.o 文件分析

按照上面的步驟,把main.o中的這幾個信息也查看一下。

段信息

指令:readelf -S main.o

可以看出:

  1. 代碼段(.text):地址Addr是 0x0000_0000(因為這是目標(biāo)文件,不是可執(zhí)行文件,所以不會安排地址),它在 sub.o ; 文件中的偏移量(Off)是 0x34,長度是 0x32 字節(jié);
  2. 數(shù)據(jù)段(.data):地址Addr是 0x0000_0000,它在 sub.o 文件中的偏移量(Off)是 0x66,長度是 0 個字節(jié),因為它沒有定義變量;

在文件中的布局如下所示:

符號表信息

指令:readelf -s main.o

重點看一下黃色矩形中的3個符號。

main符號:

  1. Size=50: 長度是 30 個字節(jié),也就對應(yīng)著代碼段的長度 0x32;
  2. Type=FUNC:說明這是一個函數(shù)
  3. Bind=GLOBAL:說明這個符號是全局可見的,也就是在其他文件中可以調(diào)用;
  4. Ndx=1:說明這個符號是屬于第 1 個 段中,就是代碼段(.text);

下面兩個符號SubData和SubFunc,他們的Ndx都是UND,表示這2個符號被main.o使用,但是定義在其他文件中。

我們知道,當(dāng)鏈接成可執(zhí)行文件時,所有的符號都必須有確定的地址(虛擬地址),所以鏈接器就需要在鏈接的過程中找到這2個符號在可執(zhí)行文件中的地址,然后把這兩個地址填寫到main的代碼段中。

可以先來看一下main.o的反匯編代碼:

指令:objdump -d main.o

黃色矩形框中是把數(shù)值0存儲到eax寄存器中,然后把eax 壓到棧中,然后紅色矩形框調(diào)用了一個函數(shù)。

從示例代碼(.c文件)中可知:main函數(shù)在調(diào)用sub.c中的SubFunc函數(shù)時,傳入了變量SubData。

黃色部分的00 00 00 00就應(yīng)該是符號SubData的地址,只不過此時main.o還不知道這個符號的將會被鏈接器安排在什么地址,所以只能空著(以4個字節(jié)的00來占位)。

紅色部分的調(diào)用(call)地址為什么是fc ff ff ff?

按照小端格式計算一下:0xfffffffc,十進(jìn)制的值就是-4,為什么設(shè)置成-4呢?

對于x86平臺的ELF格式來說,對地址進(jìn)行修正的方式有2種:絕對尋址和相對尋址。

絕對尋址

對于SubData符號就是絕對尋址,在鏈接成可執(zhí)行文件時,這個地址在代碼段中偏移0x12個字節(jié)(黃色矩形框指令碼偏移0x11個字節(jié),跨過一個字節(jié)的指令碼a1就是0x12個字節(jié)),這個地方4個字節(jié)的當(dāng)前值是 00 00 00 00。

鏈接器在修正的時候(就是鏈接成可執(zhí)行文件的時候),會把這4個字節(jié)修改為SubData變量在可執(zhí)行文件中的實際地址(虛擬地址)。

相對尋址

紅色矩形框中的函數(shù)調(diào)用(SubFunc符號),就是相對尋址,就是說:當(dāng)CPU執(zhí)行到這條指令的時候,把PC寄存中的值加上這個偏移地址,就是被調(diào)用對象的實際地址。

鏈接器在重定位的時候,目的就是計算出相對地址,然后替換掉fc ff ff ff這四個字節(jié)。

PC寄存器中的值是確定的,當(dāng)call這條指令被CPU取到之后,PC寄存器被自動增加,指向下一條指令的開始地址(偏移0x1f地址處)。

實際地址 = PC值 + xxxx_xxxx,所以得到:xxxx_xxxx = 實際地址 - PC值。

而PC值與 xxxx_xxxx 所在的地址之間是有關(guān)系的:PC值 + (-4)就得到 xxxx_xxxx 所在的地址,因此在main.o中預(yù)先在這個地址處填 fc ff ff ff(-4)。

問題來了,鏈接器怎么知道m(xù)ain.o中代碼段的這兩個地方,需要進(jìn)行地址修正?

這就是下面介紹的重定位表的作用了!

重定位表信息

指令:objdump -r main.o

重定位表就表示: 該目標(biāo)文件中,有哪些符號需要在鏈接的時候進(jìn)行地址重定位。

從圖中黃色矩形框可以看出:main.o中代碼段(.text)的 SubData和SubFunc這 2 個符號都需要鏈接器對它進(jìn)行重定位。

TYPE列:R_386_32表示絕對尋址, R_386_PC32 表示相對尋址; OFFSET列表示需要重定位的符號在main.o文件代碼段中的偏移位置。

剛才已經(jīng)看了main.o的反匯編代碼,可以看到偏移0x12 和 0x1b的地方,就是需要進(jìn)行地址重定位的兩個符號。

可執(zhí)行程序 main

有了 2 個目標(biāo)文件:sub.o和main.o,就可以鏈接得到可執(zhí)行程序了:

$ ld -m elf_i386 main.o sub.o -e main -o main

段信息

使用readelf工具來看一下main可執(zhí)行文件中的段信息(指令:readelf -S main):

  1. 紅色矩形框是代碼段(.text),鏈接器把它放在虛擬地址 0x0804_8094;
  2. 黃色矩形框是數(shù)據(jù)段(.data),鏈接器把它放在虛擬地址 0x0804_9138;

從段信息中可以看到main文件中代碼段和數(shù)據(jù)段的布局如下:

可執(zhí)行程序main是由main.o和sub.o這兩個目標(biāo)文件組成的,所以main中的代碼段是由main.o中的代碼段和sub.o中的代碼段組合得到的;對于數(shù)據(jù)段,由于 main.o中數(shù)據(jù)段的長度為0,所以main中的數(shù)據(jù)段就是sub.o中的數(shù)據(jù)段(長度為4),如下圖所示:

符號表信息

指令:readelf -s main

黃色矩形框中的SubData屬于數(shù)據(jù)段,長度是 4 個字節(jié),虛擬地址是 0x0804_9138,與段信息中的值是一致的。

紅色矩形框中的SubFunc屬于代碼段,長度是 12 個字節(jié),虛擬地址是 0x0804_80c6。

因為main中的代碼段包括 2 部分內(nèi)容:

  1. main.o 中的代碼段 main 函數(shù);
  2. sub.o 中的代碼段 SubFunc 函數(shù);

所以,可執(zhí)行文件main中的代碼段,先存放的是main函數(shù),虛擬地址:0x0804_8094,長度是0x32(50 個字節(jié));

緊接著存放的是SubFunc函數(shù),虛擬地址:0x0804_80c6,長度是0x0c(12 個字節(jié))。

如下圖所示:

鏈接器在第一遍掃描所有的目標(biāo)文件時,把所有相同類型的段進(jìn)行合并,安排到相應(yīng)的虛擬地址,如上圖所示。

所謂的安排虛擬地址,就是指定這塊內(nèi)容被加載到虛擬內(nèi)存的什么地方。當(dāng)可執(zhí)行文件被執(zhí)行的時候,加載器就把每一塊內(nèi)容復(fù)制到虛擬內(nèi)存相應(yīng)的地址處。

同時,鏈接器還會建立一個全局符號表,把每一個目標(biāo)文件中的符號信息都復(fù)制到這個全局符號表中。

對于我們的實例程序,全局符號表中包括:

SubData: 屬于 sub.o 文件,數(shù)據(jù)段,安排在虛擬地址 0x0804_9138;

SubFunc: 屬于 sub.o 文件,代碼段,安排在虛擬地址 0x0804_80c6;

其它符號信息...

絕對地址重定位

然后,鏈接器第二遍掃描所有的目標(biāo)文件,檢查哪些目標(biāo)文件中的符號需要進(jìn)行重定位。

對于我們的示例程序,首先來看一下main.o中使用的外部變量SubData的重定位。

從main.o的重定位表中可知:SubData符號需要進(jìn)行重定位,需要把這個符號在執(zhí)行時刻的絕對尋址(虛擬地址),寫入到 main可執(zhí)行文件中代碼段中偏移0x12字節(jié)處。

也就是說需要解決 2 個問題:

  1. 需要計算出在執(zhí)行文件 main 中的什么位置來填寫絕對地址(虛擬地址);
  2. 填寫的絕對地址(虛擬地址)的值是多少;

首先來解決第一個問題。

從可執(zhí)行文件的段表中可以看出:目標(biāo)文件main.o和sub.o中的代碼段被存放到可執(zhí)行文件main中代碼段的開始位置,先放main.o代碼段,再放sub.o代碼段。

代碼段的開始地址距離文件開始的偏移量是0x94,再加上偏移量0x12,結(jié)果就是0xa6。

也就是說:需要在main文件中偏移0xa6處填入SubData在執(zhí)行時刻的絕對地址(虛擬地址)。

再來解決第二個問題。

鏈接器從全局符號表中發(fā)現(xiàn):SubData符號屬于sub.o文件,已經(jīng)被安排在虛擬地址0x0804_9138處,因此只需要把0x0804_9138填寫到可執(zhí)行文件main中偏移0xa6的地方。

我們來讀取main文件,驗證一下這個位置處的虛擬地址是否正確:

指令:od -Ax -t x1 -j 166 -N 4 main

-Ax: 顯示地址的時候,用十六進(jìn)制來表示。如果使用 -Ad,意思就是用十進(jìn)制來顯示地址;

-t -x1: 顯示字節(jié)碼內(nèi)容的時候,使用十六進(jìn)制(x),每次顯示一個字節(jié)(1);

-j 166: 跨過 166 個字節(jié)(十六進(jìn)制 0xa6);

-N 4:只需要讀取 4 個字節(jié);

注意:顯示的是小端格式。

相對地址重定位

從上面描述的重定位表中看出:main.o代碼段中的SubFunc符號也需要重定位,而且是相對尋址。

鏈接器需要把SunFunc符號在執(zhí)行時刻的絕對地址(虛擬地址),減去call指令的下一條指令(PC 寄存器) 之后的差值,填寫到執(zhí)行文件main中的main.o代碼段偏移0x1b的地方。

同樣的道理,需要解決 2 個問題:

  1. 需要計算出在執(zhí)行文件 main 中的什么位置來填寫相對地址;
  2. 填寫的相對地址的值是多少;

首先來解決第一個問題。

從main.o的重定位表中可知:需要修正的位置距離main.o中代碼段的偏移量是0x1b字節(jié)。

可執(zhí)行文件main中代碼段的開始地址距離文件開始的偏移量是0x94,再加上偏移量0x1b就是0xaf。

也就是說:需要在main文件中0xaf偏移處填入一個相對地址,這個相對地址的值就是SubFunc在執(zhí)行時刻的絕對地址(虛擬地址)、距離call指令的下一條指令的偏移量。

再來解決第二個問題。

鏈接器在第一遍掃描的時候,已經(jīng)把sub.o中的符號SubFunc記錄到全局符號表中了,知道SubFunc函數(shù)被安排在虛擬地址0x0804_80c6的地方。

但是不能把這個絕對地址直接填寫進(jìn)去,因為 call 指令需要的是相對地址(偏移地址)。

鏈接器把main代碼段起始位置安排在 0x0804_8094,那么偏移0x1b處的虛擬地址就是:0x0804_80af,然后還需要再跨過4個字節(jié)(因為執(zhí)行call指令時,PC的值自動增加到下一條指令的開始地址)才是此刻PC寄存器的值,即:0x0804_80b3,如下圖中紅色部分:

兩個虛擬地址都知道了,計算一下差值就可以了:0x0804_80c6 - 0x0804_80b3 = 0x13。

也就是說:在可執(zhí)行文件main中偏移為0xaf的地方,填入相對地址0x0000_0013就完成了SubFunc符號的重定位。

還是用od指令來讀取main文件的內(nèi)容來驗證一下:

指令:od -Ax -t x1 -j 175 -N 4 main

總結(jié)

經(jīng)過以上兩個重定位操作,main.c中使用的兩個外部符號就解決了地址重定位問題。

再來看一下可執(zhí)行文件main的反匯編代碼:

從黃色和紅色的矩形框可以看出,二進(jìn)制指令中的地址值與上面的分析是一致的。

以上就是靜態(tài)鏈接過程中地址重定位的基本過程,與動態(tài)鏈接相比,靜態(tài)鏈接還是相對簡單很多。

以后有機(jī)會的話,我們再繼續(xù)聊一下動態(tài)鏈接中的一些操作,謝謝!

責(zé)任編輯:武曉燕 來源: IOT物聯(lián)網(wǎng)小鎮(zhèn)
相關(guān)推薦

2022-03-21 07:56:25

動態(tài)鏈接Linux操作系統(tǒng)

2010-09-28 09:27:27

2009-11-26 13:50:16

無線路由器

2011-05-03 10:31:59

噴墨打印機(jī)注墨誤區(qū)

2009-07-23 14:10:38

Hibernate J

2010-01-13 17:03:01

配置交換機(jī)

2013-08-15 12:26:40

阿里云飛天

2010-07-01 14:05:43

SNMPMIB

2011-04-11 17:28:50

oracle存儲select語句

2021-08-05 15:36:34

NFV網(wǎng)絡(luò)設(shè)備

2011-06-14 18:39:01

SEO

2010-04-15 16:54:31

Oracle存儲過程

2019-08-13 15:01:04

變更運維項目經(jīng)理

2010-05-27 17:45:13

MySQL存儲過程

2010-05-31 16:57:09

2023-02-28 16:26:46

推薦系統(tǒng)模塊

2010-11-12 09:18:13

SQL Server存

2010-03-04 09:54:24

Android開發(fā)

2016-09-07 20:28:17

MySQL存儲數(shù)據(jù)庫

2012-01-17 16:41:08

JavaSwing
點贊
收藏

51CTO技術(shù)棧公眾號