靜態鏈結 重定位

2021-09-29 15:35:14 字數 3747 閱讀 6784

在幾年前第一次學c語言時,按照書上給的示例,在vc6.0中寫了helloworld程式,然後按照書上的教程,進行編譯,鏈結,最後執行程式,就能在輸出視窗上看到helloworld。

對於乙個用ide寫**的人來說,**需要編譯鏈結才能生成可執行檔案這是乙個常識,那麼編譯鏈結到底做了乙個什麼樣的事情呢?

因為我們寫到c語言**是高階程式語言,計算機是沒法解析執行的,計算機能讀懂的只能是二進位制**,編譯就是將我們寫的高階程式語言**轉化成二進位制**。這個時候就會產生疑問,既然編譯已經生成了二進位制**,那麼鏈結有什麼用?

先說結論,鏈結主要完成兩個任務,第一步是合併目標檔案,第二步是符號解析重定位。今天我們先單獨說重定位。

在了解鏈結過程之前先簡單的鏈結一些基礎知識

簡單的說,編譯器將**轉化為可執行檔案主要分為預編譯、編譯、彙編和鏈結四個過程,前三個是編譯過程

這個目標檔案就是鏈結器所需要的檔案,鏈結器會將所有的目標檔案拼湊成乙個可執行檔案。

目標檔案的常用格式是linux平台下的elf格式和windows平台下的pe格式,可執行檔案採用的其實也是這兩種格式。

在elf格式中使用以段(section)為單位來管理資料,比如一般的elf檔案中會有存放執行語句.text段,有存放全域性變數和靜態變數區的.data段,還有存放未初始化資料的.bss段。

我們在ubuntu系統中寫了一段簡單的**:

int global_init_var=84;

int global_unint_var;

void

func1

(int i)

intmain()

儲存為******section.c檔案並使用:

gcc -c ******section.c
指令進行編譯,-c表示只編譯不鏈結。檔案中會生生成乙個******section.o檔案,這個就是乙個目標檔案。我們使用:

objdump -h ******section.o
指令來檢視這個檔案,結果如下:

這個目標檔案中共有五個段,size列代表了每個段的大小,file off段代表了這個段在檔案中的起始位置。

可以看到.text段是從40位址開始的,大小為41,其中存放的是可執行**,具體內容稍後分析。.

data段存放的是初始化過的全域性變數和靜態變數,可以看到它的大小是8個位元組,我們的**中剛好有兩個,static_var和global_init_var。

.bss段存放的是未初始化的,但是它的大小是4位元組,這是因為有些編譯器將未初始化的全域性變數當作乙個未定義的全域性變數符號,等著鏈結之後再在bss段分配空間,所有這個段中只存放了static_var2。

下面我們用指令:

objdump -d ******section.o
來觀察一下******section.o的**段,-d選項可以以反彙編的形式輸出**段內容,結果如下:

可以看到,最左邊一列是指令所在的偏移量,中間一列是二進位制指令,右邊是二進位制指令對應的彙編指令。第一行表示了第0個位元組是指令0x55,第二行指令是第1,2,3位元組是0x48,0x89,0xe5。一共41個位元組,與我們之前的觀察一致。其實這******section檔案已經是二進位制**了,而且這個檔案也不需要依賴其他任何檔案進行鏈結,但是為什麼這個目標檔案依然不能執行呢。

ld -static -e main -o ******section ******section.o
對目標檔案進行連線,-static是使用靜態鏈結,-e是選擇程式入口,鏈結後會生成乙個可執行檔案,但是執行這個程式會出現段錯誤,這個問題稍後再提。

先說一下為什麼使用這個指令進行鏈結而不使用gcc進行鏈結,其實在gcc裡面也是使用的ld鏈結器,但是gcc的鏈結會預設跟系統庫一起進行鏈結,這個問題我們留到後面討論。

先觀察一下******section可執行檔案的**段:

objdump -d ******section
會發現******section的.text中多出了很多其他函式,先不管這些函式,我們只看func1和main函式,結果如下:

會發現,我嘞個去,居然和******section.o檔案裡面的內容是一樣的,但是仔細看,最左邊一列是不一樣的,函式和指令的位址出現了變化,這就是鏈結器做的其中一件事情,重定位,鏈結器會為目標檔案中的符號進行重定位,給其分配對應的虛擬位址,只有擁有了這個虛擬位址,這個檔案才能被作業系統裝載然後執行。

其實可以聯想到c語言的記憶體分割槽中有**段用來存放執行**,資料段存放全域性變數和資料,還有堆區和棧區。作業系統能夠將可執行檔案裝載進記憶體進行執行就是因為鏈結器為其分配了虛擬位址,而目標檔案則沒有分配虛擬位址,沒法裝載執行。當然,堆區和棧區是執行時才有的,在elf檔案中是沒有的,區域性變數都是在堆和棧中的,所以elf檔案中不會存放區域性變數。

為什麼不使用gcc進行鏈結。

為什麼******section程式執行會出現段錯誤。

先說問題1,其實gcc也是使用的ld鏈結器,不過gcc在鏈結過程中會將我們的目標檔案和crt1.o, crti.o, crtbegin.o, crtend.o, crtn.o這些系統庫檔案進行一起鏈結,這些系統庫檔案會為我們的程式做一些初始化和掃尾工作,具體的可以參考:

crt1.o, crti.o, crtbegin.o, crtend.o, crtn.o

再說問題2,執行這個程式會出現段錯誤的原因就是因為沒有和系統庫鏈結,在main程式執行完成後沒辦法退出,會繼續執行後續的其他**,然後導致了段錯誤。

和系統庫一起進行鏈結後可執行檔案中會出現其他的**段,影響我們觀察檔案本身。

現在我們重新寫一段**:

#include

void

func1

(int i)

intmain()

然後使用gcc addr.c -o addr執行生成可執行檔案,觀察一下可執行檔案的.text段,裡面多出了其他的許多函式,這就是系統庫中的函式,然後我們再來看一下func1函式:

可以看到func1函式在鏈結重定位之後的位址是0x400526,現在執行addr,可以多執行幾次:

輸出了func1在虛擬記憶體中的位址,4195622,即0x400526。也即是之前我們所說的,系統將可執行檔案裝載進虛擬記憶體是按照可執行檔案中的虛擬位址裝載的,也就是說**中函式和全域性變數靜態變數的位址在鏈結的時候就已經確定了。

學完了之後來練習一下,這是乙個計算機考研真題:

在虛擬記憶體管理中,位址變換機構將邏輯位址轉換為實體地址,形成邏輯位址的階段是()

a. 編輯

b. 編譯

c. 鏈結

d. 裝載

重定位和鏈結

鏈結和重定位是嵌入式c中很重要的部分,對於這一塊掌握的越精細越好。指令分為兩種 在程式設計編譯鏈結過程會給程式乙個執行位址,而且必須給編譯聯結器指定這個位址,最後得到的二進位制程式是和指定的鏈結位址相關的,這個位址叫做 鏈結位址 所以我們在程式編譯時其實就已經知道程式將來執行時的位址,這個位址叫做 ...

重定位和鏈結

指令分為兩種 在程式設計編譯鏈結過程會給程式乙個執行位址,而且必須給編譯聯結器指定這個位址,最後得到的二進位制程式是和指定的鏈結位址相關的,這個位址叫做 鏈結位址 所以我們在程式編譯時其實就已經知道程式將來執行時的位址,這個位址叫做 執行位址 執行位址和鏈結位址相關,但是不一定是同乙個,程式執行時必...

重定位 與 鏈結

動態重定向 現代技術機基本都用這種技術。裝入程式把裝入模組裝入記憶體後,並不會立即把邏輯位址轉換為實體地址,而是把位址轉換推遲到程式真正執行時才發生。這種方式需要乙個重定位暫存器的支援。並且支援換入換出 每次位址會不同 靜態鏈結 將幾個目標模組鏈結裝配成乙個裝入模組時,即將每個模組中所用的外部呼叫符...