編譯
一猖任、系統(tǒng)環(huán)境
CPU:Intel(R) Core(TM) i5-5200U CPU @ 2.20GHz
操作系統(tǒng):Ubuntu 18.04.2 LTS
內(nèi)核版本:Linux version 4.18.0-25-generic
-
GNU GCC版本:gcc version 7.4.0 (Ubuntu 7.4.0-1ubuntu1~18.04.1)
- C standard revision:C11
-
GNU Compiled BY
- GMP version: 6.1.2
- MPFR version :4.0.1
- MPC version : 1.1.0
- isl version : isl-0.19-GMP
GNU 匯編器版本:2.30 (x86_64-linux-gnu) using BFD version (GNU Binutils for Ubuntu) 2.30
-
鏈接器版本:
-
collect2 version:7.4.0
- gcc一般是collect2,而不是ld遵班,collect2 是ld鏈接器的一個(gè)封裝堤撵,最終還是調(diào)用ld來(lái)完成鏈接工作
- collect2通過(guò)第一次鏈接程序查看鏈接器輸出文件來(lái)查找具有特定名稱表明是構(gòu)造函數(shù)的符號(hào),如果找得到則會(huì)創(chuàng)建一個(gè)新的臨時(shí)‘.c’文件包含這些符號(hào),然后編譯這個(gè)文件并第二次鏈接程序.The program collect2 works by linking the program once and looking through the linker output file for symbols with particular names indicating they are constructor functions. If it finds any, it creates a new temporary ‘.c’ file containing a table of them, compiles it, and links the program a second time including that file.)
- GNU ld (GNU Binutils for Ubuntu):2.30
-
collect2 version:7.4.0
二驯击、GCC編譯過(guò)程
2.1 GCC編譯過(guò)程
-
預(yù)處理
- 刪除所有的#define,展開所有的宏定義
- 處理所有的條件預(yù)編譯指令<#if,#endif,#ifdef,#ifndef,#elif,#else>
- 處理#include預(yù)編譯指令屋谭,將包含的文件插入到include的位置(遞歸進(jìn)行)
- 刪除所有的注釋
- 添加行號(hào)和文件名標(biāo)識(shí)(調(diào)試時(shí)使用)
- 保留所有的#pragma編譯器指令(編譯器需要使用這些指令)
# 單獨(dú)產(chǎn)生預(yù)處理后的文件(本模塊假設(shè)hello.c是源代碼程序,hello.i是hello.c預(yù)處理后的文件,hello.s是hello.c編譯后的文件,hello.o是hello.c匯編后的文件龟糕,hello是hello.c最終的可執(zhí)行程序) # 使用gcc命令產(chǎn)生預(yù)處理文件 $ gcc -E hello.c -o hello.i # 使用cpp命令產(chǎn)生預(yù)處理文件 $ cpp hello.c > hello.i
-
編譯:將預(yù)處理完的文件進(jìn)行一系列的詞法分析桐磁、語(yǔ)法分析、語(yǔ)義分析讲岁、中間代碼生成我擂、目標(biāo)代碼生成與優(yōu)化之后產(chǎn)生相應(yīng)的匯編代碼文件
- 詞法分析:掃描器運(yùn)行類似于有限狀態(tài)機(jī)的算法將代碼的字符序列分割成一系列的記號(hào)
- 語(yǔ)法分析:語(yǔ)法分析器對(duì)掃描器產(chǎn)生的記號(hào)進(jìn)行語(yǔ)法分析,從而產(chǎn)生語(yǔ)法樹(以表達(dá)式為節(jié)點(diǎn)的樹)
- 語(yǔ)義分析:語(yǔ)義分析器確定語(yǔ)句的意義(比如兩個(gè)指針做乘法是沒(méi)有意義的)缓艳,編譯器只能分析靜態(tài)語(yǔ)義(在編譯時(shí)能夠確定的語(yǔ)義校摩,通常包括聲明和類型的匹配,類型的轉(zhuǎn)換阶淘;與之相對(duì)的動(dòng)態(tài)語(yǔ)義是在運(yùn)行時(shí)才能確定的語(yǔ)義衙吩,例如將0作為除數(shù)是一個(gè)運(yùn)行期語(yǔ)義錯(cuò)誤)
# 編譯預(yù)處理后的文件產(chǎn)生匯編代碼文件 $ gcc -S hello.i -o hello.s # 編譯源文件產(chǎn)生匯編代碼文件 $ gcc -S hello.c -o hello.s # 現(xiàn)在的gcc編譯器將預(yù)處理和編譯兩個(gè)步驟合成了一個(gè)步驟,使用一個(gè)叫cc1的程序來(lái)完成這個(gè)過(guò)程 $ /usr/lib/gcc/x86_64-linux-gnu/7/cc1 hello.c -o hello.s
-
匯編:將匯編代碼轉(zhuǎn)變成機(jī)器可以執(zhí)行的指令(根據(jù)匯編指令和機(jī)器指令的對(duì)照表一一翻譯)
# 使用as處理匯編文件產(chǎn)生目標(biāo)文件 $ as hello.s -o hello.o # 使用gcc處理匯編文件產(chǎn)生目標(biāo)文件 $ gcc -c hello.s -o hello.o # 使用gcc處理源文件產(chǎn)生目標(biāo)文件 $ gcc -c hello.c -o hello.o
-
鏈接:將目標(biāo)文件鏈接到一起形成可執(zhí)行文件,主要包括地址和空間分配溪窒,符號(hào)決議坤塞,和重定位等步驟
符號(hào)決議:也叫做符號(hào)綁定、名稱綁定澈蚌、名稱決議等等摹芙。從細(xì)節(jié)上來(lái)講,決議更傾向于靜態(tài)鏈接宛瞄,綁定更傾向與動(dòng)態(tài)鏈接
重定位:編譯一個(gè)文件時(shí)不知道一個(gè)要調(diào)用的函數(shù)或者需要操作的一個(gè)變量的地址浮禾,就會(huì)把這些調(diào)用函數(shù)或者操作變量的指令目標(biāo)地址擱置,等到最后鏈接的時(shí)候由鏈接器去將這些指令的目標(biāo)地址修正份汗,這個(gè)地址修正的過(guò)程也被叫做重定位伐厌,每一個(gè)需要修正的地方叫做重定位入口。
2.2 實(shí)際編譯過(guò)程
-
使用如下樣例裸影,包含hello.c和func.c兩個(gè)源文件(之后也是用這兩個(gè)文件進(jìn)行分析)
/* hello.c:主測(cè)試程序挣轨,包括全局靜態(tài)變量,局部靜態(tài)變量轩猩,全局變量卷扮,局部變量,基本的函數(shù)調(diào)用 */ // export var extern int export_func_var; // global var int global_uninit_var; int global_init_var_0 = 0; int global_init_var_1 = 1; // const var const char *const_string_var = "const string"; // static global var static int static_global_uninit_var; static int static_global_init_var_0 = 0; static int static_global_init_var_1 = 1; // func header void func_call_test(int num); int main(void){ // local var int local_uninit_var; int local_init_var_0 = 0; int local_init_var_1 = 1; // static local var static int static_local_uninit_var; static int static_local_init_var_0 = 0; static int static_local_init_var_1 = 1; // call func func_call_test(8); // export var op export_func_var = export_func_var * 2; return 0; }
/* func.c:包含一個(gè)簡(jiǎn)單的被調(diào)用函數(shù)和一個(gè)全局變量 */ int export_func_var = 666; void func_call_test(int num){ int double_num = num * 2; }
-
使用
gcc -v hello.c func.c
編譯生成可執(zhí)行文件a.out均践,產(chǎn)生如下輸出(簡(jiǎn)化版本)[delta@delta: code ]$ gcc -v func.c hello.c # 對(duì)func.c的預(yù)處理和編譯過(guò)程 /usr/lib/gcc/x86_64-linux-gnu/7/cc1 func.c -o /tmp/ccfC6J5E.s # 對(duì)func.c產(chǎn)生的.s文件匯編產(chǎn)生二進(jìn)制文件 as -v --64 -o /tmp/ccF4Bar0.o /tmp/ccfC6J5E.s # 對(duì)hello.c的預(yù)處理和編譯過(guò)程 /usr/lib/gcc/x86_64-linux-gnu/7/cc1 hello.c -o /tmp/ccfC6J5E.s # 對(duì)hello.c產(chǎn)生的.s文件匯編產(chǎn)生二進(jìn)制文件 as -v --64 -o /tmp/cc7UmhQl.o /tmp/ccfC6J5E.s # 鏈接過(guò)程 /usr/lib/gcc/x86_64-linux-gnu/7/collect2 -dynamic-linker ld-linux-x86-64.so.2 Scrt1.o crti.o crtbeginS.o /tmp/ccF4Bar0.o /tmp/cc7UmhQl.o crtendS.o crtn.o
三晤锹、鏈接過(guò)程解析
Q:
目標(biāo)文件的格式是怎樣的?
多個(gè)目標(biāo)是如何鏈接到一起的彤委?
3.1 目標(biāo)文件
3.1.1目標(biāo)文件類型
- Window下的PE(Portable Executable)
- Linux下的ELF(Executable Linkable Format)
注:
- PE和ELF格式都是COFF(Common file format)格式的變種
- 目標(biāo)文件與可執(zhí)行文件的內(nèi)容和結(jié)構(gòu)類似鞭铆,所以一般采用相同的格式存儲(chǔ)。廣義上來(lái)可以將目標(biāo)文件和可執(zhí)行文件看做是同一種類型的文件,在window下統(tǒng)稱它們?yōu)镻E-COFF文件格式车遂,在Linux下統(tǒng)稱它們?yōu)镋LF文件封断。
- 不止是可執(zhí)行文件按照可執(zhí)行文件格式存儲(chǔ),動(dòng)態(tài)鏈接庫(kù)(DLL舶担,Dynamic Linking Library)(Window的.dll和Linux的.so)以及靜態(tài)鏈接庫(kù)(Static Linking Library)(Window的.lib和Linux的.a)文件都按照可執(zhí)行文件的格式存儲(chǔ)坡疼。(靜態(tài)鏈接庫(kù)稍有不同,它是把很多的目標(biāo)文件捆綁在一起形成一個(gè)文件衣陶,再加上一些索引柄瑰。可以理解為一個(gè)包含很多目標(biāo)文件的文件包)
3.1.2 ELF文件類型
ELF文件類型 | 說(shuō)明 | 實(shí)例 |
---|---|---|
可重定位文件(Relocatable File) | 包含代碼和數(shù)據(jù)剪况,可以被用來(lái)鏈接成可執(zhí)行文件或者共享目標(biāo)文件教沾,靜態(tài)鏈接庫(kù)可以歸為這一類 | Linux的.o,Window下的.obj |
可執(zhí)行文件(Executable File) | 包含可以直接執(zhí)行的程序译断,一般沒(méi)有擴(kuò)展名 | Linux的/bin/bash文件详囤,Window的.exe |
共享目標(biāo)文件(Shared Object File) | 包含代碼和數(shù)據(jù),鏈接器可以上映這種文件與其他可重定位文件和共享目標(biāo)文件進(jìn)行鏈接產(chǎn)生新的目標(biāo)文件镐作;動(dòng)態(tài)鏈接器可以將幾個(gè)共享目標(biāo)文件與可執(zhí)行文件結(jié)合藏姐,作為進(jìn)程映像的一部分來(lái)運(yùn)行 | Linux的.so,Window的.dll |
核心轉(zhuǎn)儲(chǔ)文件(Core Dump File) | 進(jìn)程意外終止時(shí)该贾,系統(tǒng)將該進(jìn)程的地址空間的內(nèi)容以及終止時(shí)的其它信息轉(zhuǎn)儲(chǔ)到核心轉(zhuǎn)儲(chǔ)文件 | Linux下的core dump |
3.1.3目標(biāo)文件結(jié)構(gòu)
目標(biāo)文件中包含編譯后的指令代碼羔杨、數(shù)據(jù),還包括了鏈接時(shí)需要的一些信息(符號(hào)表杨蛋,調(diào)試信息和字符串等)兜材,一般目標(biāo)文件將這些信息按照不同的屬性,以節(jié)(Section)的形式存儲(chǔ)(有時(shí)也稱為段(Segment))逞力。如下圖所示
3.1.3.1常見的段
段名 | 說(shuō)明 |
---|---|
.text/.code | 代碼段曙寡,編譯后的機(jī)器指令 |
.data | 數(shù)據(jù)段,全局變量和局部靜態(tài)變量 |
.bss | 未初始化的全局變量和局部靜態(tài)變量(.bss段只是為未初始化的全局變量和局部靜態(tài)變量預(yù)留位置) |
.rodata | 只讀信息段 |
.rodata1 | 存放只讀數(shù)據(jù)寇荧,字符串常量举庶,全局const變量。與.rodata一樣 |
.comment | 編譯器版本信息 |
.debug | 調(diào)試信息 |
.dynamic | 動(dòng)態(tài)鏈接信息 |
.hash | 符號(hào)哈希表 |
.line | 調(diào)試時(shí)的行號(hào)表揩抡,即源代碼行號(hào)與編譯后的指令的對(duì)應(yīng)表 |
.note | 額外的編譯器信息户侥。程序的公司名,發(fā)布版本號(hào) |
.strtab | String Table峦嗤,字符串表蕊唐,用來(lái)存儲(chǔ)ELF文件中用到的各種字符串 |
.symtab | Symbol Table,符號(hào)表 |
.shstrtab | Section String Table烁设,段名表 |
.plt/.got | 動(dòng)態(tài)鏈接的跳轉(zhuǎn)表和全局入口表 |
.init/.fini | 程序初始化與終結(jié)代碼段 |
3.1.3.2目標(biāo)文件結(jié)構(gòu)分析
-
ELF文件頭:
-
使用
gcc -c hello.c -o hello.o
生成目標(biāo)文件hello.o替梨,并使用readelf -h hello.o
讀取目標(biāo)文件的ELF文件頭,可以看出ELF文件頭定義了ELF魔數(shù)、文件機(jī)器字節(jié)長(zhǎng)度副瀑、數(shù)據(jù)存儲(chǔ)方式弓熏、版本,運(yùn)行平臺(tái)俗扇、ABI版本硝烂、ELF重定位類型箕别、硬件平臺(tái)铜幽、硬件平臺(tái)版本、入口地址串稀、程序入口和長(zhǎng)度除抛、段表的位置和長(zhǎng)度及段的數(shù)量等,如下圖所示ELF Header: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 Class: ELF64 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: REL (Relocatable file) Machine: Advanced Micro Devices X86-64 Version: 0x1 Entry point address: 0x0 Start of program headers: 0 (bytes into file) Start of section headers: 1328 (bytes into file) Flags: 0x0 Size of this header: 64 (bytes) Size of program headers: 0 (bytes) Number of program headers: 0 Size of section headers: 64 (bytes) Number of section headers: 15 Section header string table index: 14
-
ELF文件頭結(jié)構(gòu)體定義在/usr/include/elf.h中母截,目標(biāo)文件hello.o的文件頭中機(jī)器字節(jié)長(zhǎng)度為ELF64到忽,找到64位版本文件頭結(jié)構(gòu)體Elf64_Ehdr定義,如下所示
typedef struct { unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */ Elf64_Half e_type; /* Object file type */ Elf64_Half e_machine; /* Architecture */ Elf64_Word e_version; /* Object file version */ Elf64_Addr e_entry; /* Entry point virtual address */ Elf64_Off e_phoff; /* Program header table file offset */ Elf64_Off e_shoff; /* Section header table file offset */ Elf64_Word e_flags; /* Processor-specific flags */ Elf64_Half e_ehsize; /* ELF header size in bytes */ Elf64_Half e_phentsize; /* Program header table entry size */ Elf64_Half e_phnum; /* Program header table entry count */ Elf64_Half e_shentsize; /* Section header table entry size */ Elf64_Half e_shnum; /* Section header table entry count */ Elf64_Half e_shstrndx; /* Section header string table index */ } Elf64_Ehdr;
除結(jié)構(gòu)體中的e_ident對(duì)應(yīng)到readelf輸出的從Magic到ABI Version部分清寇,其它都是一一對(duì)應(yīng)關(guān)系
e_shstrndx
變量表示.shstrtab
在段表中的下標(biāo)
-
-
段表
-
使用
gcc -c hello.c -o hello.o
生成目標(biāo)文件hello.o喘漏,并使用readelf -S hello.o
讀取目標(biāo)文件的段表部分Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .text PROGBITS 0000000000000000 00000040 0000000000000035 0000000000000000 AX 0 0 1 [ 2] .rela.text RELA 0000000000000000 00000440 0000000000000048 0000000000000018 I 12 1 8 [ 3] .data PROGBITS 0000000000000000 00000078 000000000000000c 0000000000000000 WA 0 0 4 [ 4] .bss NOBITS 0000000000000000 00000084 0000000000000014 0000000000000000 WA 0 0 4 [ 5] .rodata PROGBITS 0000000000000000 00000084 000000000000000d 0000000000000000 A 0 0 1 [ 6] .data.rel.local PROGBITS 0000000000000000 00000098 0000000000000008 0000000000000000 WA 0 0 8 [ 7] .rela.data.rel.lo RELA 0000000000000000 00000488 0000000000000018 0000000000000018 I 12 6 8 [ 8] .comment PROGBITS 0000000000000000 000000a0 000000000000002c 0000000000000001 MS 0 0 1 [ 9] .note.GNU-stack PROGBITS 0000000000000000 000000cc 0000000000000000 0000000000000000 0 0 1 [10] .eh_frame PROGBITS 0000000000000000 000000d0 0000000000000038 0000000000000000 A 0 0 8 [11] .rela.eh_frame RELA 0000000000000000 000004a0 0000000000000018 0000000000000018 I 12 10 8 [12] .symtab SYMTAB 0000000000000000 00000108 0000000000000240 0000000000000018 13 16 8 [13] .strtab STRTAB 0000000000000000 00000348 00000000000000f6 0000000000000000 0 0 1 [14] .shstrtab STRTAB 0000000000000000 000004b8 0000000000000076 0000000000000000 0 0 1 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings), I (info), L (link order), O (extra OS processing required), G (group), T (TLS), C (compressed), x (unknown), o (OS specific), E (exclude), l (large), p (processor specific)
-
段表結(jié)構(gòu)體定義在/usr/include/elf.h中,目標(biāo)文件hello.o的文件頭中機(jī)器字節(jié)長(zhǎng)度為ELF64华烟,找到64位版本段表結(jié)構(gòu)體定義Elf64_Shdr(每個(gè)Elf64_Shdr對(duì)應(yīng)一個(gè)段翩迈,Elf64_Shdr又稱為段描述符<Section Descriptor>),如下所示
typedef struct { Elf64_Word sh_name; /* Section name (string tbl index) */ Elf64_Word sh_type; /* Section type */ Elf64_Xword sh_flags; /* Section flags */ Elf64_Addr sh_addr; /* Section virtual addr at execution */ Elf64_Off sh_offset; /* Section file offset */ Elf64_Xword sh_size; /* Section size in bytes */ Elf64_Word sh_link; /* Link to another section */ Elf64_Word sh_info; /* Additional section information */ Elf64_Xword sh_addralign; /* Section alignment */ Elf64_Xword sh_entsize; /* Entry size if section holds table */ } Elf64_Shdr;
-
Elf64_Shdr部分成員解釋
變量名 說(shuō)明 sh_name 段名是一個(gè)字符串盔夜,位于一個(gè)叫.shstrtab的字符串表中负饲,sh_name是段名字符串在.shstrtab中的偏移 sh_addr 段虛擬地址,如果該段可以加載喂链,sh_addr為該段被加載后在進(jìn)程地址空間的虛擬地址返十,否則為0 sh_offset 段偏移,如果該段存在于文件中則表示該段在文件中的偏移椭微,否則無(wú)意義 sh_link洞坑、sh_info 段鏈接信息,如果該段的類型是與鏈接相關(guān)的蝇率,則該字段有意義 sh_addralign 段地址對(duì)齊检诗,sh_addralign表示是地址對(duì)齊數(shù)量的指數(shù),如果sh_addralign為0或者1則該段沒(méi)有字節(jié)對(duì)齊要求 sh_entsize 對(duì)于一些段包含了一些固定大小的項(xiàng)瓢剿,比如符號(hào)表逢慌,則sh_entsize表示每個(gè)項(xiàng)的大小
-
- 重定位表:hello.o中包含一個(gè)
.rela.text
的段,類型為RELA间狂,它是一個(gè)重定位表攻泼。鏈接器在處理目標(biāo)文件時(shí)必須對(duì)文件中的某些部位進(jìn)行重定位,這些重定位信息都記錄在重定位表中。對(duì)于每個(gè)需要重定位的代碼段或者數(shù)據(jù)段忙菠,都會(huì)有一個(gè)相應(yīng)的重定位表何鸡。
-
字符串表
.strtab:字符串表,保存普通的字符串牛欢,比如符號(hào)的名字
.shstrtab:段表字符串表骡男,保存段表中用到的字符串,比如段名
結(jié)論:ELF文件頭中的e_shstrndx
變量表示.shstrtab
在段表中的下標(biāo)傍睹,e_shoff
表示段表在文件中的偏移隔盛,只有解析ELF文件頭,就可以得到段表和段表字符串表的位置拾稳,從而解析整個(gè)ELF文件
3.1.4 鏈接的接口——符號(hào)
3.1.4.1 符號(hào)定義
定義:在鏈接中吮炕,目標(biāo)文件之間相互拼合實(shí)際上是目標(biāo)文件之間對(duì)地址的引用,即對(duì)函數(shù)和變量地址的引用访得。在鏈接中龙亲,將函數(shù)和變量統(tǒng)稱為符號(hào)(Symbol),函數(shù)名或變量名稱為符號(hào)名(Symbol Name)悍抑。
-
每個(gè)目標(biāo)文件都有一個(gè)符號(hào)表記錄了目標(biāo)文件中用到的所有符號(hào)(每個(gè)定義的符號(hào)都有一個(gè)符號(hào)值鳄炉,對(duì)于函數(shù)和變量來(lái)說(shuō),符號(hào)值就是它們的地址)搜骡,常見分類如下
符號(hào)類型 說(shuō)明 定義在本目標(biāo)文件中的全局符號(hào) 可以被其它目標(biāo)文件引用的符號(hào) 在本目標(biāo)文件中引用的符號(hào)拂盯,卻沒(méi)有定義在本目標(biāo)文件中 外部符號(hào)(External Symbol) 段名,由編譯器產(chǎn)生 它的值就是該段的起始地址 局部符號(hào) 只在編譯單元內(nèi)部可見浆兰,鏈接器往往忽略它們 行號(hào)信息 目標(biāo)文件指令與代碼行的對(duì)應(yīng)關(guān)系磕仅,可選
3.1.4.2 符號(hào)結(jié)構(gòu)分析
-
符號(hào)表結(jié)構(gòu):符號(hào)表結(jié)構(gòu)體定義在/usr/include/elf.h中,如下所示
typedef struct { Elf64_Word st_name; /* Symbol name (string tbl index) */ unsigned char st_info; /* Symbol type and binding */ unsigned char st_other; /* Symbol visibility */ Elf64_Section st_shndx; /* Section index */ Elf64_Addr st_value; /* Symbol value */ Elf64_Xword st_size; /* Symbol size */ } Elf64_Sym;
Elf64_Sym成員解釋
變量名 說(shuō)明 st_name 符號(hào)名在字符串表中的下標(biāo) st_info 符號(hào)類型和綁定信息 st_other 符號(hào)可見性 st_shndx 符號(hào)所在的段 st_value 符號(hào)對(duì)應(yīng)的值 st_size 符號(hào)大小 -
使用
gcc -c hello.c -o hello.o
生成目標(biāo)文件hello.o簸呈,并使用readelf -s hello.o
讀取目標(biāo)文件的符號(hào)表部分Symbol table '.symtab' contains 24 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000000 0 FILE LOCAL DEFAULT ABS hello.c 2: 0000000000000000 0 SECTION LOCAL DEFAULT 1 3: 0000000000000000 0 SECTION LOCAL DEFAULT 3 4: 0000000000000000 0 SECTION LOCAL DEFAULT 4 5: 0000000000000000 0 SECTION LOCAL DEFAULT 5 6: 0000000000000000 0 SECTION LOCAL DEFAULT 6 7: 0000000000000004 4 OBJECT LOCAL DEFAULT 4 static_global_uninit_var 8: 0000000000000008 4 OBJECT LOCAL DEFAULT 4 static_global_init_var_0 9: 0000000000000004 4 OBJECT LOCAL DEFAULT 3 static_global_init_var_1 10: 0000000000000008 4 OBJECT LOCAL DEFAULT 3 static_local_init_var_1.1 11: 000000000000000c 4 OBJECT LOCAL DEFAULT 4 static_local_init_var_0.1 12: 0000000000000010 4 OBJECT LOCAL DEFAULT 4 static_local_uninit_var.1 13: 0000000000000000 0 SECTION LOCAL DEFAULT 9 14: 0000000000000000 0 SECTION LOCAL DEFAULT 10 15: 0000000000000000 0 SECTION LOCAL DEFAULT 8 16: 0000000000000004 4 OBJECT GLOBAL DEFAULT COM global_uninit_var 17: 0000000000000000 4 OBJECT GLOBAL DEFAULT 4 global_init_var_0 18: 0000000000000000 4 OBJECT GLOBAL DEFAULT 3 global_init_var_1 19: 0000000000000000 8 OBJECT GLOBAL DEFAULT 6 const_string_var 20: 0000000000000000 53 FUNC GLOBAL DEFAULT 1 main 21: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_ 22: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND func_call_test 23: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND export_func_var
注: 1. static_global_uninit_var榕订、static_local_init_var_0和static_local_uninit_var、static_global_init_var_0和global_init_var_0在bss段(因?yàn)槌跏蓟癁?和不初始化是一樣的) 2. static_global_init_var_1蜕便、static_local_init_var_1和global_init_var_1在data段(初始化的全局變量) 3. static變量的類型均為L(zhǎng)OCAL劫恒,表明該符號(hào)只為該目標(biāo)文件內(nèi)部可見;非Static全局變量的類型為GLOBAL轿腺,表明該符號(hào)外部可見 4. 在hello.c中引用了func_call_test和export_func_var符號(hào)两嘴,但是沒(méi)有定義,所以它的Ndx是UND(注:export一個(gè)變量但是并未使用則符號(hào)表中不會(huì)出現(xiàn)這個(gè)邊浪符號(hào)信息;export一個(gè)不存在的變量但是并未使用編譯不會(huì)報(bào)錯(cuò)蛉威;export一個(gè)不存在的變量并使用會(huì)報(bào)錯(cuò) <**注意系統(tǒng)環(huán)境**> ) 5. 未初始化的全局非靜態(tài)變量global_uninit_var在COM塊中 6. const_string_var在.data.rel.local段中
特殊符號(hào):當(dāng)使用鏈接器生成可執(zhí)行文件時(shí),會(huì)定義很多特殊的符號(hào)贰您,這些符號(hào)并未在程序中定義坏平,但是可以直接聲明并引用它們
3.1.4.3 符號(hào)修飾與函數(shù)簽名
? 符號(hào)修飾與函數(shù)簽名:在符號(hào)名前或者后面加上_
修飾符號(hào),防止與庫(kù)文件和其它目標(biāo)文件沖突〗跻啵現(xiàn)在的linux下的GCC編譯器中舶替,默認(rèn)情況下去掉了加上_
這種方式,可以通過(guò)參數(shù)選項(xiàng)打開
C++符號(hào)修飾:C++擁有類杠园,繼承顾瞪,重載和命名空間等這些特性,導(dǎo)致符號(hào)管理更為復(fù)雜抛蚁。例如重載的情況:函數(shù)名相同但是參數(shù)不一樣陈醒。然后就有了符號(hào)修飾和符號(hào)改編的機(jī)制,使用函數(shù)簽名(包括函數(shù)名篮绿,參數(shù)類型孵延,所在的類和命名空間等信息)來(lái)識(shí)別不同的函數(shù)
-
C++符號(hào)修飾栗子
class C { public: int func(int); class C2 { public: int func(int); }; }; namespace N { int func(int); class C { public: int func(int); }; } int func(int num){ return num; } float func(float num){ return num; } int C::func(int num){ return num; } int C::C2::func(int num){ return num; } int N::func(int num){ return num; } int N::C::func(int num){ return num; } int main(){ int int_res = func(1); float float_var = 1.1; float float_res = func(float_var); C class_C; int_res = class_C.func(1); return 0; }
使用
g++ -c hello.cpp -o hello_cpp.o
編譯產(chǎn)生目標(biāo)文件hello_cpp.o吕漂,使用readelf -a hello_cpp.o
查看目標(biāo)文件中的符號(hào)表亲配,如下Symbol table '.symtab' contains 18 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000000 0 FILE LOCAL DEFAULT ABS hello.cpp 2: 0000000000000000 0 SECTION LOCAL DEFAULT 1 3: 0000000000000000 0 SECTION LOCAL DEFAULT 3 4: 0000000000000000 0 SECTION LOCAL DEFAULT 4 5: 0000000000000000 0 SECTION LOCAL DEFAULT 5 6: 0000000000000000 0 SECTION LOCAL DEFAULT 7 7: 0000000000000000 0 SECTION LOCAL DEFAULT 8 8: 0000000000000000 0 SECTION LOCAL DEFAULT 6 9: 0000000000000000 12 FUNC GLOBAL DEFAULT 1 _Z4funci 10: 000000000000000c 16 FUNC GLOBAL DEFAULT 1 _Z4funcf 11: 000000000000001c 16 FUNC GLOBAL DEFAULT 1 _ZN1C4funcEi 12: 000000000000002c 16 FUNC GLOBAL DEFAULT 1 _ZN1C2C24funcEi 13: 000000000000003c 12 FUNC GLOBAL DEFAULT 1 _ZN1N4funcEi 14: 0000000000000048 16 FUNC GLOBAL DEFAULT 1 _ZN1N1C4funcEi 15: 0000000000000058 119 FUNC GLOBAL DEFAULT 1 main 16: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_ 17: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND __stack_chk_fail
可以看出函數(shù)簽名與修飾后的名稱的對(duì)應(yīng)關(guān)系
函數(shù)簽名 修飾后名稱(符號(hào)名) int func(int) _Z4funci float func(float) _Z4funcf int C::func(int) _ZN1C4funcEi int C::C2::func(int) _ZN1C2C24funcEi int N::func(int) _ZN1N4funcEi int N::C::func(int) _ZN1N1C4funcEi -
extern “C”:C++編譯器會(huì)將在extern C大括號(hào)內(nèi)的內(nèi)部代碼當(dāng)做C語(yǔ)言代碼處理,也就是名稱修飾機(jī)制將不會(huì)起作用惶凝。當(dāng)需要兼容C和C++吼虎,例如在C++代碼中調(diào)用C中的memset函數(shù),可以使用C++的宏
__cplusplus
苍鲜,C++在編譯程序時(shí)會(huì)默認(rèn)定義這個(gè)宏#ifdef __cplusplus extern “C” { #endif void *memset(void *, int, size_t); #ifdef __cplusplus } #endif
由于不同的編譯器采用不同的名字修飾方法思灰,必然會(huì)導(dǎo)致不同編譯器產(chǎn)生的目標(biāo)文件無(wú)法正常互相鏈接混滔,這是導(dǎo)致不同編譯器之間不能互操作的原因
3.1.4.4 弱符號(hào)與強(qiáng)符號(hào)
? 在編程中經(jīng)常遇到符號(hào)重定義的問(wèn)題洒疚,例如hello.c和func.c都定義了一個(gè)_global并將它們都初始化,在編譯時(shí)就會(huì)報(bào)錯(cuò)坯屿。對(duì)于C/C++來(lái)說(shuō)油湖,編譯器默認(rèn)函數(shù)和初始化的全局變量為強(qiáng)符號(hào),未初始化的全局變量為弱符號(hào)领跛。
-
編譯器處理符號(hào)規(guī)則
- 不允許強(qiáng)符號(hào)被多次定義
- 如果一個(gè)符號(hào)在一個(gè)文件中是強(qiáng)符號(hào)乏德,在其它文件中是弱符號(hào),則選擇強(qiáng)符號(hào)
- 如果一個(gè)符號(hào)在所有的文件中都是弱符號(hào)吠昭,則選擇其中占用空間最大的一個(gè)(int型和double型會(huì)選擇double型)
弱引用與強(qiáng)引用:對(duì)外部目標(biāo)文件中的符號(hào)引用在目標(biāo)文件最終被鏈接成可執(zhí)行文件時(shí)都喲啊被正確決議喊括,如果沒(méi)有找到該符號(hào)的定義,則會(huì)報(bào)未定義錯(cuò)誤矢棚,這種被稱為強(qiáng)引用郑什;與之對(duì)應(yīng)的弱引用,在處理弱引用時(shí)蒲肋,如果該符號(hào)有定義劫拢,則鏈接器將該符號(hào)的引用決議玄窝;如果該符號(hào)未被定義黎休,則鏈接器也不會(huì)報(bào)錯(cuò)。
-
弱符號(hào)與弱引用的作用(對(duì)庫(kù)來(lái)說(shuō)很有用)
- 庫(kù)中定義的弱符號(hào)可以被用戶定義的強(qiáng)符號(hào)所覆蓋建芙,從而使程序可以使用自定義版本的函數(shù)
- 程序可以對(duì)某些擴(kuò)展功能模塊的引用定義為弱引用炕倘,當(dāng)擴(kuò)展模塊與程序鏈接到一起時(shí),功能模塊可以正常使用趣苏;如果去掉了某些功能模塊氧敢,則程序也可以正常鏈接询张,只是缺少了相應(yīng)的功能,這使得程序的功能更容易裁剪和組合
3.2 靜態(tài)鏈接
3.2.1 空間和地址分配
鏈接器在合并多個(gè)目標(biāo)文件的段時(shí)浙炼,采用相似段合并的方式份氧,并分配地址和空間(虛擬地址空間的分配)
兩步鏈接法:
- 空間和地址分配:掃描所有的目標(biāo)文件,獲得它們的各個(gè)段的長(zhǎng)度弯屈、屬性和位置蜗帜,并且將輸入目標(biāo)文件中的符號(hào)表中所有的符號(hào)定義和符號(hào)引用收集起來(lái),統(tǒng)一放到一個(gè)全局符號(hào)表资厉,這一步中厅缺,鏈接器將能夠獲得所有輸入目標(biāo)文件的段長(zhǎng)度,并將它們合并宴偿,計(jì)算輸出文件中各個(gè)合并之后的段的長(zhǎng)度湘捎,建立映射關(guān)系。
- 符號(hào)解析與重定位:使用空間和地址分配中收集到的所有信息窄刘,讀取輸入文件中段的數(shù)據(jù)窥妇、重定位信息,并且進(jìn)行符號(hào)解析與重定位娩践、調(diào)整代碼中的地址等活翩。
當(dāng)進(jìn)行了空間和地址分配之后烹骨,各個(gè)段的虛擬地址也就確定了,由于各個(gè)符號(hào)在段內(nèi)的位置是相對(duì)的材泄,所以各個(gè)符號(hào)的地址也就確定了沮焕。
3.2.2 符號(hào)解析與重定位
-
使用
gcc -c hello.c -o hello.o
生成目標(biāo)文件hello.o,并使用objdump -d hello.o
讀取目標(biāo)文件的.text
的反匯編結(jié)果拉宗,如下所示(簡(jiǎn)略部分內(nèi)容)遇汞;同理使用gcc -c func.c -o func.o
生成目標(biāo)文件func.o。[delta@rabbit: c_code ]$ objdump -d hello.o Disassembly of section .text: 0000000000000000 <main>: 0: 55 push %rbp 1: 48 89 e5 mov %rsp,%rbp 4: 48 83 ec 10 sub $0x10,%rsp 8: c7 45 f8 00 00 00 00 movl $0x0,-0x8(%rbp) f: c7 45 fc 01 00 00 00 movl $0x1,-0x4(%rbp) 16: bf 08 00 00 00 mov $0x8,%edi 1b: e8 00 00 00 00 callq 20 <main+0x20> 20: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # 26 <main+0x26> 26: 01 c0 add %eax,%eax 28: 89 05 00 00 00 00 mov %eax,0x0(%rip) # 2e <main+0x2e> 2e: b8 00 00 00 00 mov $0x0,%eax 33: c9 leaveq 34: c3 retq
分析:由以上結(jié)果可以看出簿废,在鏈接之前空入,main函數(shù)在調(diào)用func_call_test函數(shù)時(shí),使用的地址是0x00000000族檬,根據(jù)反匯編結(jié)果就是下一條指令(
e8 00 00 00 00
之中e8
是callq的指令碼歪赢,00 00 00 00
是目的地址相對(duì)于下一條指令的偏移量);在使用export_func_var變量時(shí)单料,編譯器就將0x0看做是export_func_var的地址 -
使用
ld hello.o func.o -e main
鏈接兩個(gè)目標(biāo)文件埋凯,生成可執(zhí)行文件a.out(并不能執(zhí)行,因?yàn)槿鄙俨糠帜繕?biāo)文件扫尖,但是符號(hào)已經(jīng)被重新定位白对;-e main
表示將main函數(shù)作為程序入口),使用objdump -d a.out
查看a.out的.text
段反匯編結(jié)果换怖,如下圖所示(簡(jiǎn)略部分內(nèi)容)[delta@rabbit: c_code ]$ objdump -d a.out Disassembly of section .text: 00000000004000e8 <main>: 4000e8: 55 push %rbp 4000e9: 48 89 e5 mov %rsp,%rbp 4000ec: 48 83 ec 10 sub $0x10,%rsp 4000f0: c7 45 f8 00 00 00 00 movl $0x0,-0x8(%rbp) 4000f7: c7 45 fc 01 00 00 00 movl $0x1,-0x4(%rbp) 4000fe: bf 08 00 00 00 mov $0x8,%edi 400103: e8 15 00 00 00 callq 40011d <func_call_test> 400108: 8b 05 0a 0f 20 00 mov 0x200f0a(%rip),%eax # 601018 <export_func_var> 40010e: 01 c0 add %eax,%eax 400110: 89 05 02 0f 20 00 mov %eax,0x200f02(%rip) # 601018 <export_func_var> 400116: b8 00 00 00 00 mov $0x0,%eax 40011b: c9 leaveq 40011c: c3 retq 000000000040011d <func_call_test>: 40011d: 55 push %rbp 40011e: 48 89 e5 mov %rsp,%rbp 400121: 89 7d ec mov %edi,-0x14(%rbp) 400124: 8b 45 ec mov -0x14(%rbp),%eax 400127: 01 c0 add %eax,%eax 400129: 89 45 fc mov %eax,-0x4(%rbp) 40012c: 90 nop 40012d: 5d pop %rbp 40012e: c3 retq
使用
nm a.out
查看a.out中的符號(hào)信息(簡(jiǎn)略)甩恼,可以看到export_func_var的地址為0000000000601018[delta@rabbit: c_code ]$ nm a.out 0000000000601018 D export_func_var
分析:在鏈接之后,可以從反匯編中看出main函數(shù)的調(diào)用func_call_test函數(shù)的地方地址已經(jīng)被修正為func_call_test真正的地址000000000040011d沉颂,使用export_func_var變量的地方的地址也修正為export_func_var真正的地址0000000000601018(在nm a.out輸出的符號(hào)表中)条摸。所以鏈接器在完成地址空間分配之后就可以確定所有符號(hào)的虛擬地址了,鏈接器就可以根據(jù)符號(hào)的地址對(duì)每個(gè)需要重定位的地方進(jìn)行地址修正铸屉。
-
鏈接器如何知道哪些地址需要修正呢钉蒲?有一個(gè)重定位表的結(jié)構(gòu)專門保存與重定位相關(guān)的信息(比如
.text
如果有需要重定位的地方,那么就會(huì)有一個(gè)叫.rela.text
的段保存了代碼段的重定位信息)彻坛,使用objdump -r hello.o
查看重定位信息如下(簡(jiǎn)略)顷啼,可以看到所有需要重定位的地方[delta@rabbit: c_code ]$ objdump -r hello.o RELOCATION RECORDS FOR [.text]: OFFSET TYPE VALUE 000000000000001c R_X86_64_PLT32 func_call_test-0x0000000000000004 0000000000000022 R_X86_64_PC32 export_func_var-0x0000000000000004 000000000000002a R_X86_64_PC32 export_func_var-0x0000000000000004
-
符號(hào)解析:使用
nm hello.o
可以查看hello.o 中所有的符號(hào)信息,如下所示昌屉,可以看到export_func_var和func_call_test符號(hào)都是未定義狀態(tài)(U)钙蒙。所以檔鏈接器掃描完所有的輸入目標(biāo)文件之后,所有的這些未定義的符號(hào)都能夠在全局符號(hào)表中找到怠益,否則就會(huì)報(bào)符號(hào)未定義(undefined reference to)錯(cuò)誤仪搔。# 輸出hello.o 中所有的符號(hào)信息 [delta@rabbit: c_code ]$ nm hello.o 0000000000000000 D const_string_var U export_func_var U func_call_test 0000000000000000 B global_init_var_0 0000000000000000 D global_init_var_1 U _GLOBAL_OFFSET_TABLE_ 0000000000000004 C global_uninit_var 0000000000000000 T main 0000000000000008 b static_global_init_var_0 0000000000000004 d static_global_init_var_1 0000000000000004 b static_global_uninit_var 000000000000000c b static_local_init_var_0.1809 0000000000000008 d static_local_init_var_1.1810 0000000000000010 b static_local_uninit_var.1808
# 符號(hào)未定義錯(cuò)誤 [delta@rabbit: c_code ]$ ld hello.o ld: warning: cannot find entry symbol _start; defaulting to 00000000004000e8 hello.o: In function `main': hello.c:(.text+0x1c): undefined reference to `func_call_test' hello.c:(.text+0x22): undefined reference to `export_func_var' hello.c:(.text+0x2a): undefined reference to `export_func_var'
-
指令修正方式:(A:保存正在修正位置的值;P:被修正的位置<相對(duì)于段開始的偏移量或者虛擬地址>蜻牢;S:符號(hào)的實(shí)際地址烤咧;L:表示其索引位于重定位條目中的符號(hào)的值)以下計(jì)算參考
# hello.o中的重定位信息(簡(jiǎn)略) [delta@rabbit: c_code ]$ objdump -r hello.o RELOCATION RECORDS FOR [.text]: OFFSET TYPE VALUE 000000000000001c R_X86_64_PLT32 func_call_test-0x0000000000000004 0000000000000022 R_X86_64_PC32 export_func_var-0x0000000000000004 000000000000002a R_X86_64_PC32 export_func_var-0x0000000000000004 # 解析: # 根據(jù)輸出符號(hào)的重定位類型有R_X86_64_PLT32和R_X86_64_PC32 # R_X86_64_PLT32 : L + A - P(絕對(duì)地址修正) # R_X86_64_PC32 : S + A - P(相對(duì)尋址修正) # 其它方式參考:http://www.ucw.cz/~hubicka/papers/abi/node19.html
- 絕對(duì)地址修正:絕對(duì)地址修正后的地址為該符號(hào)的實(shí)際地址偏陪,例如調(diào)用func_call_test符號(hào)的地址被修正成為了絕對(duì)地址40011d
- 相對(duì)地址修正:相對(duì)地址修正后的地址為符號(hào)距離被修正位置的地址差,例如使用export_func_var符號(hào)的地址被修正成為了相對(duì)地址0x200f0a煮嫌,mov指令(第一個(gè)mov指令)的下一條地址40010e加上這個(gè)偏移量0x200f0a就是export_func_var的絕對(duì)地址0x601018
-
COMMON塊:根據(jù)
nm hello.o
的輸出笛谦,如下所示(簡(jiǎn)略),可以看到global_uninit_var符號(hào)的類型為COMMON類型昌阿,編譯器將未初始化的全局變量作為弱符號(hào)處理[delta@rabbit: c_code ]$ nm hello.o 0000000000000004 C global_uninit_var
多個(gè)符號(hào)定義類型情況分析
- 兩個(gè)或以上強(qiáng)符號(hào)類型不一致:報(bào)重定義錯(cuò)誤
- 有一個(gè)強(qiáng)符號(hào)和多個(gè)弱符號(hào):取強(qiáng)符號(hào)饥脑,若是有弱符號(hào)比強(qiáng)符號(hào)空間大的情況則編譯時(shí)會(huì)出現(xiàn)warning
- 兩個(gè)或者以上弱符號(hào)類型不一致:取占用空間最大的弱符號(hào)
注:當(dāng)編譯器將一個(gè)編譯單元編譯成目標(biāo)文件時(shí),如果該編譯單元包含弱符號(hào)(未初始化或者初始化為0的全局變量是典型)懦冰,那么該符號(hào)所占用的最終空間就是不確定的灶轰,所以編譯器無(wú)法在該階段為該符號(hào)在BSS段分配空間。但是經(jīng)過(guò)鏈接之后刷钢,任何一個(gè)符號(hào)的大小都確定了笋颤,所以它可以在最終輸出文件的BSS段為其分配空間∧诘兀總體來(lái)看伴澄,未初始化的全局變量是放在BSS段的
3.2.3 靜態(tài)庫(kù)鏈接
定義:靜態(tài)庫(kù)可以簡(jiǎn)單地看做是一組目標(biāo)文件的集合,即很多目標(biāo)文件經(jīng)過(guò)壓縮打包后形成的一個(gè)文件(Linux上常用的C語(yǔ)言靜態(tài)庫(kù)libc位于/usr/lib/x86_64-linux-gnu/libc.a)
-
靜態(tài)鏈接過(guò)程:在鏈接過(guò)程中l(wèi)d鏈接器會(huì)自動(dòng)尋找所有需要的符號(hào)以及它們所在的目標(biāo)文件阱缓,將這些目標(biāo)文件從libc.a中“解壓”出來(lái)非凌,最終將它們鏈接到一起形成一個(gè)可執(zhí)行文件。使用
gcc -v hello.c func.c
編譯生成可執(zhí)行文件a.out荆针,可以看到詳細(xì)的鏈接過(guò)程敞嗡,產(chǎn)生如下輸出(簡(jiǎn)化版本)[delta@delta: code ]$ gcc -v func.c hello.c # 對(duì)func.c的預(yù)處理和編譯過(guò)程 /usr/lib/gcc/x86_64-linux-gnu/7/cc1 func.c -o /tmp/ccfC6J5E.s # 對(duì)func.c產(chǎn)生的.s文件匯編產(chǎn)生二進(jìn)制文件 as -v --64 -o /tmp/ccF4Bar0.o /tmp/ccfC6J5E.s # 對(duì)hello.c的預(yù)處理和編譯過(guò)程 /usr/lib/gcc/x86_64-linux-gnu/7/cc1 hello.c -o /tmp/ccfC6J5E.s # 對(duì)hello.c產(chǎn)生的.s文件匯編產(chǎn)生二進(jìn)制文件 as -v --64 -o /tmp/cc7UmhQl.o /tmp/ccfC6J5E.s # 鏈接過(guò)程 /usr/lib/gcc/x86_64-linux-gnu/7/collect2 -dynamic-linker ld-linux-x86-64.so.2 Scrt1.o crti.o crtbeginS.o /tmp/ccF4Bar0.o /tmp/cc7UmhQl.o crtendS.o crtn.o ############################################ # 實(shí)際各個(gè)目標(biāo)文件的位置 /usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/Scrt1.o /usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/7/crtbeginS.o /tmp/ccF4Bar0.o /tmp/cc7UmhQl.o /usr/lib/gcc/x86_64-linux-gnu/7/crtendS.o /usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/crtn.o
可以看到Scrt1.o crti.o crtbeginS.o /tmp/ccF4Bar0.o /tmp/cc7UmhQl.o crtendS.o crtn.o被鏈接入了最終可執(zhí)行文件
各個(gè)文件的解釋(來(lái)源)
目標(biāo)文件 說(shuō)明 crt0.o Older style of the initial runtime code ? Usually not generated anymore with Linux toolchains, but often found in bare metal toolchains. Serves same purpose as crt1.o (see below). crt1.o Newer style of the initial runtime code. Contains the _start symbol which sets up the env with argc/argv/libc _init/libc _fini before jumping to the libc main. glibc calls this file 'start.S'. crti.o Defines the function prolog; _init in the .init section and _fini in the .fini section. glibc calls this 'initfini.c'. crtn.o Defines the function epilog. glibc calls this 'initfini.c'. scrt1.o Used in place of crt1.o when generating PIEs. gcrt1.o Used in place of crt1.o when generating code with profiling information. Compile with -pg. Produces output suitable for the gprof util. Mcrt1.o Like gcrt1.o, but is used with the prof utility. glibc installs this as a dummy file as it's useless on linux systems. crtbegin.o GCC uses this to find the start of the constructors. crtbeginS.o Used in place of crtbegin.o when generating shared objects/PIEs. crtbeginT.o Used in place of crtbegin.o when generating static executables. crtend.o GCC uses this to find the start of the destructors. crtendS.o Used in place of crtend.o when generating shared objects/PIEs. 通常鏈接順序:
crt1.o crti.o crtbegin.o [-L paths] [user objects] [gcc libs] [C libs] [gcc libs] crtend.o crtn.o
-
鏈接過(guò)程控制:鏈接過(guò)程需要考慮很多內(nèi)容:使用哪些目標(biāo)文件?使用哪些庫(kù)文件祭犯?是否保留調(diào)試信息秸妥、輸出文件格式等等。
鏈接器控制鏈接過(guò)程方法:
- 使用命令行來(lái)給鏈接器指定參數(shù)
- 將鏈接器指令存放在目標(biāo)文件里面沃粗,編譯器通常會(huì)使用這種方式向鏈接器傳遞指令。
- 使用鏈接控制腳本
3.2.4 BFD庫(kù)簡(jiǎn)介
- 定義:由于現(xiàn)代的硬件和軟件平臺(tái)種類繁多键畴,每個(gè)平臺(tái)都有不同的目標(biāo)文件格式最盅,導(dǎo)致編譯器和鏈接器很難處理不同平臺(tái)的目標(biāo)文件。BFD庫(kù)(Binary File Descriptor library)希望通過(guò)統(tǒng)一的接口來(lái)處理不同的目標(biāo)文件格式起惕。
現(xiàn)代GCC(具體來(lái)講是GNU 匯編器GAS)涡贱、鏈接器、調(diào)試器和GDB及binutils的其他工具都是通過(guò)BFD庫(kù)來(lái)處理目標(biāo)文件惹想,而不是直接操作目標(biāo)文件问词。
3.3 裝載與動(dòng)態(tài)鏈接
3.3.1可執(zhí)行文件的裝載
-
進(jìn)程的虛擬地址空間:每個(gè)程序運(yùn)行起來(lái)之后,它將擁有自己獨(dú)立的虛擬地址空間嘀粱,這個(gè)虛擬地址空間的大小由計(jì)算機(jī)的硬件平臺(tái)決定激挪,具體來(lái)說(shuō)是CPU的位數(shù)決定(32位平臺(tái)下的虛擬空間為4G<2^32>辰狡,通過(guò)
cat /proc/cpuinfo
可以看到虛擬地址的位數(shù),如本機(jī)為address sizes : 39 bits physical, 48 bits virtual
垄分,虛擬地址位數(shù)為48位宛篇,則虛擬空間為2^48)。- 進(jìn)程只能使用操作系統(tǒng)分配給進(jìn)程的地址薄湿,否則系統(tǒng)會(huì)捕獲到這些訪問(wèn)并將其關(guān)閉(Window:進(jìn)程因非法操作需要關(guān)閉叫倍;Linux:Segment Fault段錯(cuò)誤)
-
裝載的方式:程序運(yùn)行時(shí)是有局部性原理的,所以可以將程序最常用的部分駐留在內(nèi)存中豺瘤,將不常用的數(shù)據(jù)存放在磁盤里(動(dòng)態(tài)裝入的基本原理)
- 覆蓋裝入(幾乎被淘汰):覆蓋裝入的方法吧挖掘內(nèi)存潛力的任務(wù)交給了程序員吆倦,程序員在編寫程序時(shí)將程序分為若干塊,然后編寫一個(gè)輔助代碼來(lái)管理這些這些模塊何時(shí)應(yīng)該駐留內(nèi)存坐求,何時(shí)應(yīng)該被替換掉(在多個(gè)模塊的情況下逼庞,程序員需要手工將它們之間的依賴關(guān)系組織成樹狀結(jié)構(gòu))
- 頁(yè)映射:頁(yè)映射不是一下子將指令和數(shù)據(jù)一下子裝入內(nèi)存,而是將內(nèi)存和磁盤中的所有數(shù)據(jù)和指令按照頁(yè)(Page)為單位劃分瞻赶,之后所有的裝載和操作的單位就是頁(yè)赛糟。
-
操作系統(tǒng)角度來(lái)看可執(zhí)行文件的加載:
創(chuàng)建一個(gè)獨(dú)立的虛擬地址空間:創(chuàng)建映射函數(shù)所需要的對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu)
讀取可執(zhí)行文件頭,建立虛擬空間和可執(zhí)行文件的映射關(guān)系:程序在發(fā)生頁(yè)錯(cuò)誤時(shí)砸逊,操作系統(tǒng)從物理空間分配出來(lái)一個(gè)物理頁(yè)璧南,然后將“缺頁(yè)”從磁盤讀取到內(nèi)存中,并設(shè)置缺頁(yè)的虛擬頁(yè)與物理頁(yè)的映射關(guān)系师逸,很明顯司倚,操作系統(tǒng)捕獲到缺頁(yè)錯(cuò)誤時(shí),它應(yīng)該知道當(dāng)前所需要的頁(yè)在可執(zhí)行文件的哪一個(gè)位置篓像。這就是虛擬空間與可執(zhí)行文件之間的映射關(guān)系(這種映射關(guān)系只是保存在操作系統(tǒng)內(nèi)部的一個(gè)數(shù)據(jù)結(jié)構(gòu)动知,Linux中將進(jìn)程虛擬空間中的一個(gè)段叫做虛擬內(nèi)存區(qū)域(VMA))。
-
將CPU的指令寄存器設(shè)置成可執(zhí)行文件的入口地址员辩,啟動(dòng)運(yùn)行
注:頁(yè)錯(cuò)誤處理:
- CPU將控制權(quán)交給操作系統(tǒng)
- 操作系統(tǒng)查詢裝載過(guò)程 第二部建立起來(lái)的數(shù)據(jù)結(jié)構(gòu)盒粮,找到空白頁(yè)所在的VMA,計(jì)算出相應(yīng)頁(yè)面在可執(zhí)行文件中的便宜奠滑,然后在物理內(nèi)存中分配一個(gè)物理頁(yè)面丹皱,將進(jìn)程中該虛擬地頁(yè)與分配的物理頁(yè)之間建立映射關(guān)系
- 把控制權(quán)還給進(jìn)程
3.3.2 動(dòng)態(tài)鏈接
- 為什么需要?jiǎng)討B(tài)鏈接:1、靜態(tài)鏈接方式對(duì)于計(jì)算機(jī)內(nèi)存和磁盤空間的浪費(fèi)非常嚴(yán)重宋税;2摊崭、靜態(tài)鏈接庫(kù)對(duì)程序的更新部署會(huì)帶來(lái)很多麻煩(如果其中一個(gè)依賴進(jìn)行了更新,那么該程序就要重新鏈接發(fā)布)
-
動(dòng)態(tài)鏈接:將鏈接的過(guò)程推遲到了運(yùn)行的時(shí)候再進(jìn)行杰赛,通過(guò)動(dòng)態(tài)鏈接器(第二部分GCC編譯過(guò)程中最后的鏈接設(shè)置了動(dòng)態(tài)鏈接器參數(shù)
-dynamic-linker ld-linux-x86-64.so.2
)完成鏈接工作呢簸,通過(guò)延遲綁定等來(lái)將動(dòng)態(tài)鏈接損失的性能盡可能的小。- 動(dòng)態(tài)地選擇加載各種程序模塊
- 加強(qiáng)程序的兼容性:一個(gè)程序在不同的平臺(tái)運(yùn)行時(shí)可以動(dòng)態(tài)地鏈接到由操作系統(tǒng)提供的動(dòng)態(tài)鏈接庫(kù),這些動(dòng)態(tài)鏈接庫(kù)相當(dāng)于在程序和操作系統(tǒng)之間添加了一個(gè)中間層根时,從而消除程序?qū)Σ煌脚_(tái)之間依賴的差異性
-
地址無(wú)關(guān)代碼:共享對(duì)象在編譯時(shí)不能假設(shè)自己在進(jìn)程虛擬空間中的位置瘦赫。把指令中那些需要修改的部分分離出來(lái)與數(shù)據(jù)部分放在一起,這樣指令部分就可以保持不變啸箫,而數(shù)據(jù)部分可以在每個(gè)進(jìn)程中有一個(gè)副本耸彪,這種方案就是地址無(wú)關(guān)代碼(PIC,Position-Independent Code)
- 裝載時(shí)重定位:一旦模塊裝載地址確定忘苛,即目標(biāo)地址確定蝉娜,那么系統(tǒng)就對(duì)程序中所有的絕對(duì)地址進(jìn)行重定位(靜態(tài)鏈接時(shí)的重定位叫做鏈接時(shí)重定位;動(dòng)態(tài)鏈接的重定位叫做裝載時(shí)重定位 )
- 模塊中國(guó)各種類型地址類型引用方式:
- 模塊內(nèi)部的函數(shù)調(diào)用扎唾、跳轉(zhuǎn):采用相對(duì)地址調(diào)用召川,不需要重定位
- 模塊內(nèi)部的數(shù)據(jù)訪問(wèn),比如模塊中定義的全局變量胸遇,靜態(tài)變量:采用相對(duì)地址訪問(wèn)荧呐,獲取當(dāng)前的PC值,加上偏移量就能訪問(wèn)變量了
- 模塊外部的數(shù)據(jù)訪問(wèn)纸镊,比如其它模塊定義的全局變量:ELF的做法是子啊數(shù)據(jù)段里面建立一個(gè)指向這些變量的指針數(shù)組倍阐,稱為全局偏移表(GOT,Global Offset Table)逗威。GOT是放在數(shù)據(jù)段的峰搪,可以在模塊裝載時(shí)被修改,并且每個(gè)進(jìn)程都可以有獨(dú)立的副本凯旭,互相不影響概耻。
- 模塊外部的函數(shù)調(diào)用、跳轉(zhuǎn)等:通過(guò)GOT中的項(xiàng)進(jìn)行間接跳轉(zhuǎn)
- 延遲綁定:當(dāng)函數(shù)第一次被用到才進(jìn)行綁定(符號(hào)查找罐呼、重定位等)鞠柄,如果沒(méi)有用到則不綁定。ELF使用PLT(Procedure Linkage Table)的方式來(lái)事先延遲綁定(PLT使解析只會(huì)在符號(hào)未解析時(shí)進(jìn)行一次)嫉柴。
- 動(dòng)態(tài)鏈接的步驟
- 動(dòng)態(tài)鏈接器自舉:動(dòng)態(tài)鏈接器不依賴其它任何共享對(duì)象厌杜;動(dòng)態(tài)鏈接器本身所需的全局和靜態(tài)變量的重定位工作由它本身完成
- 裝載共享對(duì)象:將可執(zhí)行文件和鏈接器本身的符號(hào)都合并到一個(gè)全局符號(hào)表中(圖的遍歷過(guò)程),當(dāng)一個(gè)符號(hào)需要加入到全局符號(hào)表時(shí)差凹,如果相同的符號(hào)已經(jīng)存在期奔,則忽略后加入的符號(hào)
- 重定位與初始化:重新遍歷可執(zhí)行文件和每個(gè)共享對(duì)象的重定位表,將它們的GOT/PLT中的每個(gè)需要重定位的地方進(jìn)行修正危尿。
四、參考文獻(xiàn)
[0] 程序員的自我修養(yǎng) :鏈接馁痴、裝載與庫(kù) / 俞甲子谊娇,石凡,潘愛民著.—北京:電子工業(yè)出版社
[1] GNU ONLINE DOC - collect2 https://gcc.gnu.org/onlinedocs/gccint/Collect2.html