引言這一節主要使用下面兩個源**」a.c「和」b.c「作為例子講解:
/* a.c */
extern
int shared;
intmain()
/* b.c */
int shared =1;
void
swap
(int
* a ,
int* b )
假設程式只有這兩個模組,使用gcc將兩個模組編譯成.o目標檔案:
$ gcc -c a.c b.c -m32
從**中可以看到,b.c定義了兩個全域性符號,乙個是變數」shared「,另外乙個是函式」swap「。a.c中定義了乙個全域性符號函式」main「。模組a.c中引用到了b.c中的」swap「和」shared「,接下來要做的就是把a.o和b.o兩個目標檔案鏈結在一起,並最終形成乙個可執行檔案」ab「
乙個最簡單的方案就是將輸入的目標檔案按照次序疊加起來(按序疊加):
乙個更實際的方法是將相同性質的段合併到一起(相似段合併):
空間分配起始只關注與虛擬空間的分配。
在第一步掃瞄和空間分配階段,鏈結器按照前面的方法進行分配,這個時候輸入檔案中的各個段在鏈結後的虛擬位址就已經確定了,比如.text段起始位址為0x08048094,.data段的起始位址為0x08049108。
在完成上一步之後,鏈結器開始計算各個符號的虛擬位址。因為各個符號在段內的相對位址是固定的,比如此時」main「、」shared「和」swap「的位址已經確定,但是鏈結器還需要給每個符號加上偏移量。
為什麼要加上偏移量?舉個栗子:假設a.o中的main函式相對於a.o的.text段的偏移是x,但是經過鏈結合併後a.o的.text段位於虛擬位址0x08048094,那麼main的位址應該是0x08048094+x。
在分析符號解析和重定位之前,首先看一下a.o裡面是怎麼使用兩個外部符號的,就是在a.c的源程式裡面使用了」shared「變數和」swap「函式。當原始碼」a.c「在被編譯成目標檔案時,編譯器並不知道」shared「和」swap「的位址,因為他們定義在其他目標檔案中。所以這時候有兩種情況:
在elf檔案中,有乙個叫重定位表(relocation table)的結構專門用來儲存這些重定位相關的資訊,在elf檔案中往往是乙個或多個段。對於每個要被重定位的elf段都有乙個對應的重定位表,而乙個重定位表往往就是elf檔案中的乙個段,所以其實重定位表也可以叫重定位段,也可以統稱為重定位表。
每個要被重定位的地方叫乙個重定位入口(relocation entry),重定位入口的偏移(offset)表示該入口在要被重定位的段中的位置。
對於32位的intel x86系列處理器來說,重定位表的結構是乙個elf32_rel結構的陣列,每個陣列元素對應乙個重定位入口:
typedef
struct
elf32_rel;
成員
含義r_offset
重定位入口的偏移。對於可重定位檔案來說,這個值是該重定位入口所要修正的位置的第乙個位元組相對於段起始的偏移;對於可執行檔案或共享物件檔案來說,這個值是該重定位入口所要修正的位置的第乙個位元組的虛擬位址
r_info
衝行為入口的型別和符號,這個成員的低8位表示重定位入口的型別,高24位表示重定位入口的符號在符號表中的下標
因為各種處理器的指令格式不一樣,所以重定位所修正的指令位址格式也不一樣,每種處理器都有自己一套重定位入口的型別,對於可執行檔案和共享目標檔案來說,他們的重定位入口是動態鏈結型別的
在重定位的過程中,每個重定位的入口都是對乙個符號的引用,那麼當鏈結器需要對某個符號的引用進行重定位時,他就要確定這個符號的目標位址。這個時候鏈結器就會去找所有輸入目標檔案的符號表組成的全域性符號表,找到相應的符號後進行重定位。在聯結器掃瞄完所有輸入目標檔案後,所有未定義的符號都應該能夠在全域性符號表中找到,否則鏈結器會報符號位定義錯誤
直至2023年為止,intel x86系列cpu的jimp指令有11中定址模式;call指令有10種;mov指令有34種,這些定址方式有如下幾方面的區別:
這兩種重定位方式指令修正方式每個被修正的位置的長度都為32位,即4個位元組。而且都是近址定址,不用考慮段間遠址定址。唯一區別就是相對定址和絕對定址。重定位入口的r_info成員低8位表示重定位入口型別:
巨集定義值
重定位修正方法
r_386_32
1絕對修正s + a
r_386_pc32
2相對定址修正s + a - p
a = 儲存在被修正位置的值
p = 被修正的位置(相對於段開始的偏移量或者虛擬位址),該值可通過r_offset計算得到
s = 符號的實際位址,即由r_info的高24位指定的符號的實際位址
對照前面a.o的重定位資訊,第乙個重定位入口是對swap符號的引用,型別為r_386_pc32,是一條相對位移呼叫指令,根據原文例子得出偏移為0x26
而shared是r_386_32型別的,是一條傳輸指令的源,該傳輸指令的源是乙個立即數,即shared的絕對位址,根據原文例子得出偏移為0x18
這兩種重定位入口分別代表了兩種不同的重定位位址修正方式。
假設a.o和b.o鏈結成最終可執行檔案後,main函式的虛擬位址為0x1000,swap函式的虛擬位址為0x2000,shared變數的虛擬位址為0x3000,接下來我們模擬一下修正這兩個重定位入口:
絕對定址修正
先看a.o的第乙個重定位入口,即偏移為0x18這條mov指令的修正,它的修正方式是r_386_32,即絕對位址修正。對於這個重定位入口,他修正後的結果應該是s + a
相對定址修正
再看第二個重定位入口,即偏移為0x26的call指令的修正,它的指令修正方式為r_386_pc32,即相對定址修正,它修正後的結果應該是s + a - p
這兩個例子可以看出來,絕對定址修正和相對定址修正的區別就是絕對定址修正後的位址為該符號位址,相對定址修正後的位址為符號距離被修正位置的位址差
起始靜態庫可以看成一組目標檔案的集合,就像很多目標檔案經過壓縮打包後形成的乙個檔案,在linux中最常用的c語言靜態庫libc位於/usr/lib/libc.a,屬於glibc專案的一部分。
glibc本身是c語言開發的,所以會有很多c語言源**,編譯完成後有同樣數量的目標檔案。使用者使用這麼多零散的檔案會非常費勁,那麼就是用」ar「壓縮程式將這些目標檔案壓縮到一起,並對其進行編號和索引,以便於查詢和檢索,這就形成了libc.a的靜態庫
那麼靜態庫是怎麼鏈結的呢,拿helloworld程式舉例,程式中會使用到printf函式,那麼很顯然printf函式並不在原有的源**當中,它在靜態庫中,這個時候鏈結器就派上大用場了,他會自動尋找所需要的符號及他們在靜態庫中的目標檔案,再講這些目標檔案從」libc.a「中」解壓「出來,最終將他們鏈結在一起成為乙個可執行檔案。理論上可以認為將」hello.o「和」libc.a「鏈結起來:
個人Python學習心得第四章
這章的內容是 函式 說起來,這一章理解起來有點難度 ps 掉了好幾根頭髮 還是和c語言進行對比 簡單的函式建立和呼叫和c語言大同小異 較為明顯感受來說,python的函式更為靈活 曾記得不知道在 看到的一句話 靈活既強大 表示贊同?第一節1.函式的建立 2.函式引數 3.返回值 這三點是最基本的,與...
程式設計師修煉之道(通俗版) 第四章
程式設計師修煉之道 這本書中的內容挺不錯,裡面包含了很多精華,但一些句子很拗口,所以我就根據國人的閱讀習慣,在不改變原意的情況下對詞句稍加修改,標題中的 通俗版 就是這麼來的。1 繼承和多型是物件導向語言的基石,是合約可以真正閃耀的領域。說到這裡,你或許會想到黎克特制替換原則 子類必須要能通過基類的...
《道德經》程式設計師版第四章
道衝而用之或不盈。淵兮,似萬物之宗。挫其銳,解其紛,和其光,同其塵。湛兮,似或存。吾不知誰之子,象帝之先。程式的執行過程雖然是虛的。程式設計師秉承這個思路開發出各種程式,並且永遠不會感到滿足。程式的執行過程就像黑洞,它是各種程式的 新手程式設計師會有種銳氣,看到漂亮的效果,神奇的功能,就想直接cop...