
(1)預處理,得到預處理文件hello.i,它還是一個可讀的文本文件 ,但不包含任何宏定義
$gcc –E hello.c –o hello.i $cpp hello.c > hello.i
PS:gcc命令實際上是具體程序(如ccp、cc1、as等)的包裝命令,用戶通過gcc命令來使用具體的預處理程序ccp、編譯程序ccl和匯編程序as等
處理源文件中以“#”開頭的預編譯指令,包括:
- 刪除“#define”並展開所定義的宏
- 處理所有條件預編譯指令,如“#if”,“#ifdef”, “#endif”等
- 插入頭文件到“#include”處,可以遞歸方式進行處理
- 刪除所有的注釋“//”和“/* */”
- 添加行號和文件名標識,以便編譯時編譯器產生調試用的行號信息
- 保留所有#pragma編譯指令(編譯器需要用)
(2)編譯,就是將預處理后得到的預處理文件進行詞法分析、語法分析、語義分析、優化后,生成匯編代碼文件
用來進行編譯處理的程序稱為編譯程序(編譯器,Compiler)
$gcc –S hello.i –o hello.s $gcc –S hello.c –o hello.s $ccl hello.i -o hello.s $/user/lib/gcc/i486-linux-gnu/4.1/cc1 hello.c
得到的結果是可重定位目標文件hello.o(VC下是.obj文件),其中包含的是不可讀的二進制代碼,必須用相應的工具軟件來查看其內容
(3)匯編,匯編程序(匯編器)用來將由匯編指令構成的匯編語言源程序轉換為機器指令序列(機器語言程序)
匯編指令和機器指令一一對應,前者是后者的符號表示,它們都屬於機器級指令,所構成的程序稱為機器級代碼
$gcc –c hello.s –o hello.o $gcc –c hello.c –o hello.o $as hello.s -o hello.o //as是一個匯編程序
(4)鏈接過程將多個可重定位目標文件合並以生成可執行目標文件。其包含的代碼和數據可以直接被復制到內存執行。
早期由手動完成,現在由鏈接程序——鏈接器完成。
$gcc –static –o myproc main.o test.o $ld –static –o myproc main.o test.o //static 表示靜態鏈接 //如果不指定-o選項,則可執行文件名為“a.out” //Windows中為.exe文件
高級編程語言中,子程序(函數)起始地址和變量起始地址是符號定義(definition),調用子程序(函數或過程)和使用變量即是符號的引用(reference)。一個模塊定義的符號可以被另一個模塊引用 。 最終須鏈接(即合並),合並時須在符號引用處填入定義處的地址。
鏈接本質:合並相同的節

鏈接后,程序在磁盤中,不知道程序會裝到內存何處執行,因此實際上是合並到虛擬地址空間中。程序頭表描述如何映射。
每個.o文件代碼和數據地址都是從0開始。鏈接之后讀寫數據段和只讀代碼段地址都高於0x08048000
鏈接操作的步驟:
Step 1. 符號解析(Symbol resolution)
– 程序中有定義和引用的符號 (包括變量和函數等)
– 編譯器將定義的符號存放在一個符號表( symbol table)中
– .symtab 節記錄符號表信息,是一個結構數組
– 每個表項包含符號名、長度和位置等信息
– 鏈接器將每個符號的引用都與一個確定的符號定義建立關聯
每個可重定位目標模塊都有一個符號表,它包含了在模塊中定義和引用的符號。有三種鏈接器符號:
- Global symbols(模塊內部定義的全局符號):由模塊m定義並能被其他模塊引用的符號。
例如,非static C函數和非static的C全局變量(指不帶static的全局變量)
- External symbols(外部定義的全局符號):由其他模塊定義並被模塊引用的全局符號
- Local symbols(本模塊的局部符號):僅由模塊m定義和引用的本地符號。
例如,在模塊m中定義的帶static的函數和全局變量
不是指程序中的局部變量(分配在棧中的臨時性變量),鏈接器不關心這種局部變量。他們運行時動態分配,不記錄於符號表。
ELF文件中的符號表(.symtab節)中每個表項(16B)的結構如下:
typedef struct { Elf32_Word st_name; /*符號對應字符串在.strtab節中的偏移量*/ Elf32_Addr st_value; /*在對應節(函數名在text節中,變量名在data節或bss節中)中的偏移量,或虛擬地址*/ Elf32_Word st_size; /*符號對應目標字節數,即函數大小或變量長度*/ unsigned char st_info; /*低4位指出符號的類型(Type,包括數據、函數、源文件、節、未知),高4位指定綁定屬性(Bind,包括全局符號、局部符號、弱符號) */ unsigned char st_other;/*在可重定位文件中指定可見性,定義當符號成為可執行文件或共享目標庫文件的一部分后訪問該符號的方式 */ Elf32_Word st_shndx; /*指出符號所在節在節頭表中的索引,有些符號屬於偽節(節頭表無表項,無法表示索引值):ABS表示該符號不會由於重定位發生值的變化,不該被重定位;UND表示未定義;COM表示還未被分配位置的未初始化數據(.bss),此時,st_value表示對齊要求,st_size給出最小長度 */
例如main.c
int buf[2]={1,2}; int main(){ swap(); return 0; }
和swap.c
extern int buf[]; int *bufp0 = &buf[0]; static int *bufp1; void swap(){ int temp; bufp1 = &buf[1]; temp = *bufp0; *bufp0 = *bufp1; *bufp1 = temp; }
查看main.o中的符號表中最后三個條目(共10個)
buf是main.o中第3節(.data)偏移為0的符號,是全局變量,占8B;
main是第1節(.text)偏移為0的符號,是全局函數,占33B;
swap是未定義的符號,不知道類型和大小,全局的(在其他模塊定義)

查看swap.o中的符號表中最后4個條目(共11個)
bufp1是未分配地址且未初始化的本地變量(ndx=COM), 按4B對齊且占4B

符號定義的實質就是指被分配了存儲空間。為函數名即指其代碼所在區;為變量名即指其所占的靜態數據區。所有定義符號的值就是其目標所在的首地址。
符號解析也稱符號綁定,目的是將每個模塊中引用的符號與某個目標模塊中的定義符號建立關聯。
每個定義符號在代碼段或數據段中都被分配了存儲空間,將引用符號與定義符號建立關聯后,就可在重定位時將引用符號的地址重定位為相關聯的定義符號的地址。
本地符號在本模塊內定義並引用,因此,其解析較簡單,只要與本模塊內唯一的定義符號關聯即可。
全局符號(外部定義的、內部定義的)的解析涉及多個模塊,故較復雜。
例如:int *xp=&x引用符號x對符號xp進行了定義。
函數名和已初始化的全局變量名是強符號;未初始化的全局變量名是弱符號。
符號解析時只能有一個確定的定義(即每個符號僅占一處存儲空間)
Rule 1: 強符號只能被定義一次,否則鏈接錯誤
Rule 2: 若一個符號被定義為一次強符號和多次弱符號,則按強定義為准。對弱符號的引用被解析為其強定義符號。
Rule 3: 若有多個弱符號定義,則任選其中一個。使用命令 gcc –fno-common鏈接時,會告訴鏈接器遇到多個弱定義的全局符號時輸出一條警告信息。
Step 2. 重定位
– 合並相關.o文件。將多個代碼段與數據段分別合並為一個單獨的代碼段和數據段
– 對定義符號進行重定位(確定地址),計算每個定義的符號在虛擬地址空間中的絕對地址
例如,為函數確定首地址,進而確定每條指令的地址,為變量確定首地址
完成這一步后,每條指令和每個全局或局部變量都可確定地址
– 對引用符號進行重定位(確定地址),將可執行文件中符號引用處的地址修改為重定位后的地址信息
需要用到在.rel_data和.rel_text節中保存的重定位信息
匯編器遇到引用時,生成一個重定位條目:反映符號引用的位置、綁定的定義符號名、重定位類型
數據引用的重定位條目在.rel_data節中、指令中引用的重定位條目在.rel_text節中
用readelf命令可顯示main.o中的重定位條目(表項)
$ readelf -r main.o
ELF中重定位條目格式如下:
typedef struct { int offset; /*節內偏移*/ int symbol:24, /*所綁定符號*/ type: 8; /*重定位類型*/ } Elf32_Rel;
IA-32有兩種最基本的重定位類型:
- R_386_32: 絕對地址
- R_386_PC32: PC相對地址
(1)buf的定義在.data節中偏移為0處開始,占8B。
Disassembly of section .data: 00000000 <buf>: 0: 01 00 00 00 02 00 00 00
(2)main的定義在.text節中偏移為0處開始,占0x12B(18B)
Disassembly of section .text: 00000000 <main>: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 83 e4 f0 and $0xfffffff0,%esp //用與指令調整棧頂為16的倍數 6: e8 fc ff ff ff call 7 <main+0x7> //fffffffc=-4 7: R_386_PC32 swap //匯編器同時會生成一個重定位表項。 b: b8 00 00 00 00 move $0x0,%eax 10: c9 leave 11: c3 ret
在重定位節rel_text節中有重定位條目:r_offset=0x7,r_sym=10,r_type=R_386_PC32,
被OBJDUMP工具以“7: R_386_PC32 swap”的可重定位信息顯示在需重定位的call指令的下一行。
即告訴鏈接器對.text節中偏移量為7的位置進行重定位,重定位到main.o(自身)符號表的第十個符號swap,重定位方式為PC相對地址
swap是main.o的符號表中第10項,是未定義符號,類型和大小未知,並是全局符號,故在其他模塊中定義。
假定可執行文件中main函數對應機器從0x8048380開始(0x8048000-0x8048380中間還有一些系統代碼),長0x12B
假定swap緊跟main后,機器代碼首地址按4字節邊界對齊,起始地址為0x8048394
根據call指令的機器代碼“e8 fc ff ff ff”可知,需重定位的4字節地址的初始值(init)為0xfffffffc (小端方式下是-4)。
匯編器用-4作為偏移量,得到call指令的下條指令開始處的地址PC=0x8048380+0x07-init=0x804838b,此處相對於需重定位的地址偏移為4個字節。
重定位值=轉移目標地址-PC=ADDR(r_sym) – ( ( ADDR(.text) + r_offset ) – init )
引用目標處 call指令下一條指令地址,即PC
本例中:ADDR(r_sym) = 0x8048394, ADDR(.text) = 0x8048380, r_offset=0x7,而init=-4 ,最終求得重定位值0x9。
因此,call指令實際執行了:
R[eip]=0x804838b R[esp]← R[esp]-4 M[R[esp]] ←R[eip] R[eip] ←R[eip]+0x9
(3)swap.o中.data節內容:
Disassembly of section .data 00000000 <bufp0>: 0:R_386_32 buf
在重定位節.rel.data中的重定位條目為:r_offset=0x0,r_sym=9, r_type=R_386_32,
OBJDUMP工具解釋后顯示為0:R_386_32 buf
即告訴鏈接器對.data節偏移量為0的位置進行重定位,重定位到swap.o(自身)符號表的第9個符號buf,重定位方式為絕對地址
buf是swap.o的符號表中第9項,是未定義符號,類型和大小未知,並是全局符號,故在其他模塊中定義。
重定位值初始值0,重定位后為初始值加所引用符號地址
假定buf在運行時的存儲地址ADDR(buf)=0x8049620,則重定位后,buf和bufp0同屬於.data節,故在可執行文件中它們被合並。 bufp0緊接在buf后,故地址為0x8049620+8=0x8049628 。
因是R_386_32方式,故bufp0內容為buf的絕對地址0x8049620,即“20 96 04 08”
因此,合並后的可執行目標文件中.data節的內容:
Disassembly of section .data: 08049620 <buf>: 8049620: 01 00 00 00 02 00 00 00 08049628 <bufp0>: 8049628: 20 96 04 08
(5)bufp1的地址就是鏈接合並后.bss節的首地址,假定為0x8049700
可執行文件的加載:
(1)UNIX/Linux系統中,在shell命令行提示符后輸入命令:$./hello[enter]
(2)shell命令行解釋器構造argv和envp
(3)調用fork()函數,創建一個子進程,與父進程shell完全相同(只讀/共享),包括只讀代碼段、可讀寫數據段、堆以及用戶棧等。
(4)可通過調用execve()系統調用函數來啟動加載器,在當前進程(新創建的子進程)的上下文中加載並運行hello程序。將hello中的.text節、.data節、.bss節等內容加載到當前進程的虛擬地址空間。
execve()函數的功能是在當前進程上下文中加載並運行一個新程序,用法如下:
int execve(char *filename, char *argv[], *envp[]);
filename是加載並運行的可執行文件名(如./hello),
參數列表argv和環境變量列表envp都用一個以null結尾的指針數組表示,每個數組元素都指向一個用字符串表示的參數(通常argv[0]指向可執行文件目標名,后面依次是指向命令各個參數的指針)或者環境變量串(指向的每個字符串都是一個形如"NAME= VALUE"的名-值對)。
若錯誤(如找不到指定文件filename) ,則返回-1,並將控制權交給調用程序; 若函數執行成功,則不返回 ,最終將控制權傳遞到可執行目標中的主函數main。
加載器(loader)根據可執行文件的程序(段)頭表中的信息,將可執行文件的代碼和數據從磁盤“拷貝”到存儲器中(實際上不會真正拷貝,僅建立一種映射)
(5)加載后,將PC(EIP)設定指向Entry point (即符號_start處),最終執行main函數,hello程序開始在一個進程的上下文中運行。
程序入口地址並不是0x8048000,因為前面是ELF頭和程序頭表,是在.init節。
主函數main()的原型形式如下:
int main(int argc, char **argv, char **envp); int main(int argc, char *argv[], char *envp[]);
argc指定參數個數,參數列表中第一個總是命令名(可執行文件名)
例如:命令行為
ld -o test main.o test.o
argc=6(最后有一個空)
當IA-32/Linux系統開始執行main()函數時, 在虛擬地址空間的用戶棧中具有如圖所示的組織結構:

用戶棧的棧底是一系列環境變量串, 然后是命令行參數串, 每個串以null結尾, 連續存放在棧中,每個串i由相應的envp[i]和argv[i中的指針指示。
在命令行參數串后面是指針數組envp的數組元素, 全局變量environ指向這些指針中的第一個指針envp[0]
然后是指針數組argv的數組元素。
在棧的頂部是main()函數的三個參數: envp、argv和argc。
在這三個參數所在單元的后面將生成main()函數的棧幀。
(6)總結
_start: __libc_init_first→_init→atexit(設置出口位置)→main→_exit(執行出口處的結束代碼)