這次我們來看一個新的例子
a.c的內容如下:
extern int shared; int main() { int a = 10; swap(&a, &shared); }
b.c的內容如下:
int first_var = 2;
int shared = 1; void swap(int* a, int* b) { int tmp; tmp = *a; *a = *b; *b = tmp; }
gcc -c a.c -o a.o
gcc -c b.c -o b.o
回顧之前的內容,我們先看一下a.o中的符號表:
readelf -a a.o
Bind類型為LOCAL的,我們都不需要看了,重點看3個GLOBAL的,其中main的Ndx不是UND
而是1,是啥意思來着。是指這個symbol所在段在段表中的索引為1,也就是text段
看到swap和shared是NOTYPE,也即未知類型的全局符號。
然后main的size是56指的是函數指令所占的字節數,Value表示該符號相對於代碼段的起始位置的偏移量
readelf -a b.o
b.o中的shared 符號,在data段中偏移4個字節的位置,swap的size是76
我們是用鏈接器ld把a.o和b.o鏈接起來:
ld a.o b.o -e main -o ab:
-e: 表示將main做為程序入口,默認的入口是_start
鏈接完成后輸出ab, 我們執行了一下ab, 發現產生了段錯誤,crash了。
但是我用gcc a.c b.c -o ab1, 卻能夠正常運行,比較兩個輸出文件的size, ld鏈接的結果只有1224字節,但是gcc的輸出結果卻有8220字節,讓人不禁再次陷入了深思。為什么會crash, 以及鏈接后的結果為什么相差這么大,我們繼續往下分析。
ulimit -c unlimited命令,開啟core dump功能,並且不限制生成core dump文件的大小
我們這個調試的問題,后面再詳細說,這里繼續分析一下鏈接的相關問題
首先看一下38 + 4C = 0x84, 不錯,text段的確被合並了。
然后看一下ab中的text的VMA和LMA, 現在有了具體的值,變成了0x00010094,
VMA表示這個段在內存中的運行地址,LMA表示這個text段的加載地址。
也就是說這段代碼映像會被先加載到LMA, 然后運行前還要拷貝到VMA處(如果VMA不等於LMA的話)
【上述結論結合bootloader的相關知識點學習】
那么還有一個問題,為啥text段會被分配到0x00010094,data分配到0x00020118呢?
這涉及到操作系統的進程虛擬地址空間分配的規則。
就是在ld腳本中啦,ld腳本就規定程序和數據在bin文件里面是什么存儲的,以及運行時在rom和ram中是怎么存儲的文件。
這里就引入了鏈接腳本的概念,同時也是bootloader中會涉及到的概念,也就是說就是鏈接腳本中指定的這個地址。
ldd --help 中有一個-T 選項,就是讀取鏈接腳本的,也驗證了我們的說法
鏈接器在掃描和分配空間完成之后,會根據鏈接腳本中指定的位置,把對應的text和data段放上去。同時因為各個符號在段內的相對位置是固定的,這樣只要給每個符號加上一個 相對text段首大偏移 + 自己所在段的偏移就可以得到每一個符號的絕對地址
比如,swap的地址就是 main + 0x38, 也就是 10094h + 38h = 100cc h
在完成空間和地址的分配步驟以后,鏈接器就進入了符號解析與重定位的步驟。
我們先使用objdump -d a.o 看一下在編譯期間,編譯器對外部符號的處理
首先可以看到,main的起始地址是0, 只有等到空間分配完成后,各個函數才會確定自己在虛擬地址空間中的位置
然后是bl跳轉語句。關於bl指令,我目前沒有仔細研究。我們重點對比,ab中的這個地方,就知道,對應的地址被修改了。
為了輔助鏈接器完成這個功能,在elf中還有一個叫重定位表的東西,我們在前面提起過.rel.text 和 .rel.data的段就是重定位表,也叫
重定位段。
objdump -r a.o
這個命令可以查看a.o中所有需要重定位的地方。
首先指明下面的待重定位符號在text段中,偏移為0x20的地方需要對一個叫swap的符號進行重定位,偏移為0x34的地方需要對shared這個符號
重定位。 與反匯編的代碼段對照一下就可以看的更加清晰