1. gcc編譯過程
gcc一些編譯選項
a. 預處理(Pre-Processing):gcc -E hello.c -o hello.i
1)讀取C/C++源程序,對其中的偽指令(以#開頭的指令)進行處理。
- 將所有的“#define”刪除,並且展開所有的宏定義。
- 處理所有的條件編譯指令,如:“#if”、“#ifdef”、“#elif”、“#else”、“endif”等,將那些不必要的代碼過濾掉。
- 處理“#include”預編譯指令,將被包含的文件插入到該預編譯指令的位置。
2)刪除所有的注釋。
3)添加行號和文件名標識。
b. 編譯(Compiling):gcc -S hello.i -o hello.s
將預處理完的文件進行一系列詞法分析、語法分析、語義分析及優化后,產生相應的匯編代碼文件。
c. 匯編(Assembling):gcc -c hello.s -o hello.o
將編譯完的匯編代碼文件翻譯成機器指令,並生成可重定位目標程序的.o文件。可以使用file命令來查看hello.o的文件格式。
hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
知道這是一個elf格式的文件,elf文件的格式如下圖所示,貼了三張不同角度的圖片,自行領會。
- Section:它的描述信息放在section header table。Sections是在ELF文件里頭,用以裝載內容數據的最小容器。
在ELF文件里面,每一個Sections 內都裝載了性質屬性都一樣的內容。
1) .text section:裝載了可執行代碼;
2) .data section:裝載了被初始化的數據;
3) .bss section:裝載了未被初始化的數據,由於數據是不需要初始化的,所以.bss段在文件中只占一個Section Header而沒有對應的Section,
程序加載時.bss段占多大內存空間在Section Header中描述;
4) 以 .rec 打頭的 sections:裝載了重定位條目,標識了需要進行重定位之處,重定位條目的每一項包含兩個字段,addr和val,即
需要將addr的內容(也是一個地址)改成val,等價於$(addr) = val。重定位條目在鏈接時生成全局的val。
5) .symtab 或者 .dynsym section:裝載了符號信息;
6) .strtab 或者 .dynstr section:裝載了字符串信息;
一個ELF文件中到底有哪些具體的 sections,由包含在這個ELF文件中的 section head table(SHT)決定。在SHT中,針對每一個section,都設置有一個條目,
用來描述對應的這個section,其內容主要包括該 section 的名稱、類型、大小以及在整個ELF文件中的字節偏移位置等等。
- 符號表:每個可重定位目標模塊 m 都有一個符號表,它包含了在 m 中定義和引用的符號。
1)Global symbols(模塊內部定義的全局符號):由模塊 m 定義並能被其他模塊引用的符號。例如,非static C函數和非static C全局變量。
2)External symbols(外部定義的全局符號):由其他模塊定義並被模塊 m 引用的全局符號。
3)Local symbols(僅由模塊 m 定義和引用的本地符號)。例如,在模塊 m 中定義的帶 static 的 C 函數和全局變量。
注:鏈接器的局部符號不是指程序中的局部變量(分配在棧中的臨時性變量),鏈接器不關心這種局部變量。
可以通過readelf -s查看目標文件的符號表信息,指向readelf -s hello.o,輸出如下結果:
Num: Value Size Type Bind Vis Ndx Name 310: 00000000 0 NOTYPE GLOBAL DEFAULT ABS _gp 734: 00000000 32 OBJECT GLOBAL DEFAULT 77 v 818: 00000000 496 FUNC GLOBAL DEFAULT 71 main 849: 00000000 4 OBJECT GLOBAL DEFAULT 78 phrase 955: 00000000 9 OBJECT GLOBAL DEFAULT 77 peppers 1020: 00000000 192 OBJECT GLOBAL DEFAULT 80 bins
- Num
= 符號序號
- Value
= 符號的地址,這里全都是0,是因為編譯過程中不分配地址(分配了也沒用),等到鏈接中進行重定位的時候才分配地址。
- Size
= 符號的大小
- Type
= 符號類型: Func
= Function, Object
, File
(source file name), Section
= memory section, Notype
= untyped absolute symbol or undefined
- Bind
= GLOBAL
綁定意味着該符號對外部文件的可見. LOCAL
綁定意味着該符號只在這個文件中可見. WEAK
有點像GLOBAL, 該符號可以被重載.
- Vis
= Symbols can be default, protected, hidden or internal.
- Ndx
= The section number the symbol is in. ABS means absolute: not adjusted to any section address's relocation
- Name
= symbol name
d. 鏈接(Linking):gcc hello.o -o hello
通過鏈接器將一個個目標文件(或許還會有庫文件)鏈接在一起生成一個完整的可執行程序(靜態鏈接)。
主要工作就是將有關的目標文件彼此相連接,也就是將在一個文件中引用的符號同該符號在另外一個文件中的定義連接起來,
使得所有的這些目標文件成為一個能夠被操作系統裝入執行的統一整體。
1)確定符號引用關系(符號解析):得出各個輸入模塊的代碼段和數據段的大小
2)合並相關 .o 文件(重定位):鏈接器將所有相同類型的節合並為同一類型的新的聚合節。例如來自輸入模塊的.data節全部合並成一個節,
這個節成為輸出可執行目標文件的.data節。
3)確定每個符號的地址(重定位):各個.o文件合並后,整個模塊的邏輯地址從0開始,這時需要確定代碼節和數據節中每個符號在最終的模塊中的邏輯地址。
4)在指令中填入新的地址(重定位):鏈接器依賴重定位條目,修改每個地址為最終的邏輯地址。執行的時候需要通過頁表寄存器進行尋址。
2. 程序的加載與執行
程序要運行是必然要把程序加載到內存中的。在過去的機器里都是把整個程序都加載進入物理內存中,現在一般都采用了虛擬存儲機制,即每個進程都有完整的地址空間,
給人的感覺好像每個進程都能使用完成的內存。然后由一個內存管理器把虛擬地址映射到實際的物理內存地址。
運行可執行文件時,系統使用一個被稱為加載器(loader)的程序,將可執行文件的代碼和數據從磁盤加載到內存中,然后跳轉到程序的第一條
指令(或者入口點entry point)開始執行。可執行程序的內存映像大概如下圖:
代碼段總是從地址0x08048000開始,數據段是在緊接着的一個4KB對齊的地址處,堆在數據段之后,往上增長。中間有一個共享庫保留的內存段。
然后是用戶棧,棧從最大的合法用戶地址開始,向下增長。棧之上是系統保留的內存,用戶進程不能訪問(只能通過系統調用陷入內核態訪問)。
程序從磁盤到內存可分為3步驟:
1)首先是創建虛擬地址到物理內存的映射(創建內核地址映射結構體),創建頁目錄和頁表;
2)再就是加載代碼段和數據段;
3)把可執行文件的入口地址寫到CPU的PC寄存器中。