從底層剖析程序從編譯到運行的整個過程
三個階段
- 一、編譯階段
- 二、鏈接階段
- 三、運行階段
為了方便解釋,給出兩端示例代碼,下面圍繞代碼進行實驗:
//sum.cpp
int gdata = 10;
int sum(int a,int b)
{return a+b;
}
//main.cpp
extern int gdata;
int sum(int ,int);static int stat;int data = 20;int main()
{int a = gdata;int b = data;int ret = sum(a,b);return 0;
}
前兩個階段:
一、編譯階段
三件重要的事情:
編譯階段只關注自己模塊內的事情
編譯階段不分配虛擬空間地址,無法運行
目標文件由各個段組成
編譯階段的產物是可重定位的二進制目標文件,由各個段組成
一、符號表(.symtab段)
查看符號表命令:objdump -t main.o
1.符號表中存儲程序產生的符號,如
靜態全局變量stat
的符號為_ZL4stat
,定義在.bss
區域
全局變量data
的符號為data
,定義在.data
區域
主函數main()
的符號為main
,定義在.text
區域
外部變量gdata
的符號為gdata
,定義為UND
,表示符號的引用,不知道在哪里定義
外部函數sum(int,int)
的符號為_Z3sumii
,定義為UND
,表示符號的引用,不知道在哪里定義
2.符號表中可以看到變量的鏈接屬性
l
:表示lcoal
,符號只能在當前文件可見,內部鏈接屬性
g
:表示global
,符號可以在所有文件可見,外部鏈接屬性
所以鏈接的時候鏈接器只能看見
gloal
的符號 看不到lcoal
的符號
這就解釋了靜態全局變量/函數 和普通全局變量/函數同名的問題
在多個文件中可以定義名字相同的靜態全局變量/函數,因為local
屬性鏈接器不可見,但若多個文件中普通的全局變量/函數重名,因為具有global
屬性,鏈接的時候符號解析就會沖突
3.編譯過程中變量不分配虛擬空間地址
我們查看以下.text
段,注意需要帶有-g
輸出調試信息
g++ -c main.cpp -g
objdump -S main.o
觀察,編譯階段產生了二進制機器碼,但是不分配虛擬空間地址,所以地址先用0替代,即編譯階段指令沒法用,需要等鏈接階段分配虛擬地址補上地址才有用,這就是目標文件無法運行的原因之一
4.查看目標文件的各個段
命令:readelf -S main.o
二、鏈接階段
鏈接所有的編譯完成的目標文件(.o)和靜態庫文件(.a)
鏈接步驟:
步驟一:
將所有的目標文件的各個段進行合并
main.o的.text
段和sum.o的.text
段合并
main.o的.data
段和sum.o的.data
段合并
main.o的.bss
和sum.o的.bss
段合并
合并后進行符號解析
如鏈接階段符號為UND(符號引用)的,都需要找到該符號定義的地方,如果沒有找到=符號未定義,找到多個定義=符號重定義
UND 找到定義解析成具體 .text .data ..
段
步驟二:
符號的重定位(重定向)
符號解析之后,給所有的符號分配虛擬地址空間,成為了可執行文件
驗證
使用鏈接器自己鏈接::ld -e main sum.o main.o
查看符號表:objdump -t a.out
可以看到所有符號均有定義的段,無UND
符號引用的情況
所有符號均分配了地址(看第一列)
再看看代碼段.text
之前機器碼缺少地址的,現在也都補充上了,所以變成了可以運行的二進制機器碼(指令)
補充1:
看一下可執行文件的文件頭信息
.text段的信息
可以發現,可執行文件頭記錄了程序入口指令地址,所以CPU知道從哪個指令開始執行(這里是main函數作為入口)
補充2:
可執行文件所有的段都和二進制目標文件相同,多了一個programa headers
段,用來告訴操作系統,運行這個程序的時候,把哪些內容加載進內存(數據段 指令段),注意:不是所有的段都需要加載進內存的
查看programa headers段:readelf -l a.out