編譯
一、系統(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來完成鏈接工作
- collect2通過第一次鏈接程序查看鏈接器輸出文件來查找具有特定名稱表明是構(gòu)造函數(shù)的符號,如果找得到則會(huì)創(chuàng)建一個(gè)新的臨時(shí)‘.c’文件包含這些符號,然后編譯這個(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編譯過程
2.1 GCC編譯過程

-
預(yù)處理
- 刪除所有的#define,展開所有的宏定義
- 處理所有的條件預(yù)編譯指令<#if,#endif,#ifdef,#ifndef,#elif,#else>
- 處理#include預(yù)編譯指令,將包含的文件插入到include的位置(遞歸進(jìn)行)
- 刪除所有的注釋
- 添加行號和文件名標(biāo)識(調(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)行一系列的詞法分析、語法分析、語義分析、中間代碼生成、目標(biāo)代碼生成與優(yōu)化之后產(chǎn)生相應(yīng)的匯編代碼文件
- 詞法分析:掃描器運(yùn)行類似于有限狀態(tài)機(jī)的算法將代碼的字符序列分割成一系列的記號
- 語法分析:語法分析器對掃描器產(chǎn)生的記號進(jìn)行語法分析,從而產(chǎn)生語法樹(以表達(dá)式為節(jié)點(diǎn)的樹)
- 語義分析:語義分析器確定語句的意義(比如兩個(gè)指針做乘法是沒有意義的),編譯器只能分析靜態(tài)語義(在編譯時(shí)能夠確定的語義,通常包括聲明和類型的匹配,類型的轉(zhuǎn)換;與之相對的動(dòng)態(tài)語義是在運(yùn)行時(shí)才能確定的語義,例如將0作為除數(shù)是一個(gè)運(yùn)行期語義錯(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的程序來完成這個(gè)過程 $ /usr/lib/gcc/x86_64-linux-gnu/7/cc1 hello.c -o hello.s -
匯編:將匯編代碼轉(zhuǎn)變成機(jī)器可以執(zhí)行的指令(根據(jù)匯編指令和機(jī)器指令的對照表一一翻譯)
# 使用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í)行文件,主要包括地址和空間分配,符號決議,和重定位等步驟
符號決議:也叫做符號綁定、名稱綁定、名稱決議等等。從細(xì)節(jié)上來講,決議更傾向于靜態(tài)鏈接,綁定更傾向與動(dòng)態(tài)鏈接
重定位:編譯一個(gè)文件時(shí)不知道一個(gè)要調(diào)用的函數(shù)或者需要操作的一個(gè)變量的地址,就會(huì)把這些調(diào)用函數(shù)或者操作變量的指令目標(biāo)地址擱置,等到最后鏈接的時(shí)候由鏈接器去將這些指令的目標(biāo)地址修正,這個(gè)地址修正的過程也被叫做重定位,每一個(gè)需要修正的地方叫做重定位入口。
2.2 實(shí)際編譯過程
-
使用如下樣例,包含hello.c和func.c兩個(gè)源文件(之后也是用這兩個(gè)文件進(jìn)行分析)
/* hello.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è)簡單的被調(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)生如下輸出(簡化版本)[delta@delta: code ]$ gcc -v func.c hello.c # 對func.c的預(yù)處理和編譯過程 /usr/lib/gcc/x86_64-linux-gnu/7/cc1 func.c -o /tmp/ccfC6J5E.s # 對func.c產(chǎn)生的.s文件匯編產(chǎn)生二進(jìn)制文件 as -v --64 -o /tmp/ccF4Bar0.o /tmp/ccfC6J5E.s # 對hello.c的預(yù)處理和編譯過程 /usr/lib/gcc/x86_64-linux-gnu/7/cc1 hello.c -o /tmp/ccfC6J5E.s # 對hello.c產(chǎn)生的.s文件匯編產(chǎn)生二進(jìn)制文件 as -v --64 -o /tmp/cc7UmhQl.o /tmp/ccfC6J5E.s # 鏈接過程 /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
三、鏈接過程解析
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ǔ)。廣義上來可以將目標(biāo)文件和可執(zhí)行文件看做是同一種類型的文件,在window下統(tǒng)稱它們?yōu)镻E-COFF文件格式,在Linux下統(tǒng)稱它們?yōu)镋LF文件。
- 不止是可執(zhí)行文件按照可執(zhí)行文件格式存儲(chǔ),動(dòng)態(tài)鏈接庫(DLL,Dynamic Linking Library)(Window的.dll和Linux的.so)以及靜態(tài)鏈接庫(Static Linking Library)(Window的.lib和Linux的.a)文件都按照可執(zhí)行文件的格式存儲(chǔ)。(靜態(tài)鏈接庫稍有不同,它是把很多的目標(biāo)文件捆綁在一起形成一個(gè)文件,再加上一些索引??梢岳斫鉃橐粋€(gè)包含很多目標(biāo)文件的文件包)
3.1.2 ELF文件類型
| ELF文件類型 | 說明 | 實(shí)例 |
|---|---|---|
| 可重定位文件(Relocatable File) | 包含代碼和數(shù)據(jù),可以被用來鏈接成可執(zhí)行文件或者共享目標(biāo)文件,靜態(tài)鏈接庫可以歸為這一類 | Linux的.o,Window下的.obj |
| 可執(zhí)行文件(Executable File) | 包含可以直接執(zhí)行的程序,一般沒有擴(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)程映像的一部分來運(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í)需要的一些信息(符號表,調(diào)試信息和字符串等),一般目標(biāo)文件將這些信息按照不同的屬性,以節(jié)(Section)的形式存儲(chǔ)(有時(shí)也稱為段(Segment))。如下圖所示

3.1.3.1常見的段
| 段名 | 說明 |
|---|---|
| .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 | 符號哈希表 |
| .line | 調(diào)試時(shí)的行號表,即源代碼行號與編譯后的指令的對應(yīng)表 |
| .note | 額外的編譯器信息。程序的公司名,發(fā)布版本號 |
| .strtab | String Table,字符串表,用來存儲(chǔ)ELF文件中用到的各種字符串 |
| .symtab | Symbol Table,符號表 |
| .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é)長度、數(shù)據(jù)存儲(chǔ)方式、版本,運(yùn)行平臺(tái)、ABI版本、ELF重定位類型、硬件平臺(tái)、硬件平臺(tái)版本、入口地址、程序入口和長度、段表的位置和長度及段的數(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é)長度為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對應(yīng)到readelf輸出的從Magic到ABI Version部分,其它都是一一對應(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é)長度為ELF64,找到64位版本段表結(jié)構(gòu)體定義Elf64_Shdr(每個(gè)Elf64_Shdr對應(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部分成員解釋
變量名 說明 sh_name 段名是一個(gè)字符串,位于一個(gè)叫.shstrtab的字符串表中,sh_name是段名字符串在.shstrtab中的偏移 sh_addr 段虛擬地址,如果該段可以加載,sh_addr為該段被加載后在進(jìn)程地址空間的虛擬地址,否則為0 sh_offset 段偏移,如果該段存在于文件中則表示該段在文件中的偏移,否則無意義 sh_link、sh_info 段鏈接信息,如果該段的類型是與鏈接相關(guān)的,則該字段有意義 sh_addralign 段地址對齊,sh_addralign表示是地址對齊數(shù)量的指數(shù),如果sh_addralign為0或者1則該段沒有字節(jié)對齊要求 sh_entsize 對于一些段包含了一些固定大小的項(xiàng),比如符號表,則sh_entsize表示每個(gè)項(xiàng)的大小
-
- 重定位表:hello.o中包含一個(gè)
.rela.text的段,類型為RELA,它是一個(gè)重定位表。鏈接器在處理目標(biāo)文件時(shí)必須對文件中的某些部位進(jìn)行重定位,這些重定位信息都記錄在重定位表中。對于每個(gè)需要重定位的代碼段或者數(shù)據(jù)段,都會(huì)有一個(gè)相應(yīng)的重定位表。
-
字符串表
.strtab:字符串表,保存普通的字符串,比如符號的名字
.shstrtab:段表字符串表,保存段表中用到的字符串,比如段名
結(jié)論:ELF文件頭中的e_shstrndx變量表示.shstrtab在段表中的下標(biāo),e_shoff表示段表在文件中的偏移,只有解析ELF文件頭,就可以得到段表和段表字符串表的位置,從而解析整個(gè)ELF文件
3.1.4 鏈接的接口——符號
3.1.4.1 符號定義
定義:在鏈接中,目標(biāo)文件之間相互拼合實(shí)際上是目標(biāo)文件之間對地址的引用,即對函數(shù)和變量地址的引用。在鏈接中,將函數(shù)和變量統(tǒng)稱為符號(Symbol),函數(shù)名或變量名稱為符號名(Symbol Name)。
-
每個(gè)目標(biāo)文件都有一個(gè)符號表記錄了目標(biāo)文件中用到的所有符號(每個(gè)定義的符號都有一個(gè)符號值,對于函數(shù)和變量來說,符號值就是它們的地址),常見分類如下
符號類型 說明 定義在本目標(biāo)文件中的全局符號 可以被其它目標(biāo)文件引用的符號 在本目標(biāo)文件中引用的符號,卻沒有定義在本目標(biāo)文件中 外部符號(External Symbol) 段名,由編譯器產(chǎn)生 它的值就是該段的起始地址 局部符號 只在編譯單元內(nèi)部可見,鏈接器往往忽略它們 行號信息 目標(biāo)文件指令與代碼行的對應(yīng)關(guān)系,可選
3.1.4.2 符號結(jié)構(gòu)分析
-
符號表結(jié)構(gòu):符號表結(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成員解釋
變量名 說明 st_name 符號名在字符串表中的下標(biāo) st_info 符號類型和綁定信息 st_other 符號可見性 st_shndx 符號所在的段 st_value 符號對應(yīng)的值 st_size 符號大小 -
使用
gcc -c hello.c -o hello.o生成目標(biāo)文件hello.o,并使用readelf -s hello.o讀取目標(biā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變量的類型均為LOCAL,表明該符號只為該目標(biāo)文件內(nèi)部可見;非Static全局變量的類型為GLOBAL,表明該符號外部可見 4. 在hello.c中引用了func_call_test和export_func_var符號,但是沒有定義,所以它的Ndx是UND(注:export一個(gè)變量但是并未使用則符號表中不會(huì)出現(xiàn)這個(gè)邊浪符號信息;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段中 特殊符號:當(dāng)使用鏈接器生成可執(zhí)行文件時(shí),會(huì)定義很多特殊的符號,這些符號并未在程序中定義,但是可以直接聲明并引用它們
3.1.4.3 符號修飾與函數(shù)簽名
? 符號修飾與函數(shù)簽名:在符號名前或者后面加上_修飾符號,防止與庫文件和其它目標(biāo)文件沖突。現(xiàn)在的linux下的GCC編譯器中,默認(rèn)情況下去掉了加上_這種方式,可以通過參數(shù)選項(xiàng)打開
C++符號修飾:C++擁有類,繼承,重載和命名空間等這些特性,導(dǎo)致符號管理更為復(fù)雜。例如重載的情況:函數(shù)名相同但是參數(shù)不一樣。然后就有了符號修飾和符號改編的機(jī)制,使用函數(shù)簽名(包括函數(shù)名,參數(shù)類型,所在的類和命名空間等信息)來識別不同的函數(shù)
-
C++符號修飾栗子
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)文件中的符號表,如下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ù)簽名與修飾后的名稱的對應(yīng)關(guān)系
函數(shù)簽名 修飾后名稱(符號名) 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大括號內(nèi)的內(nèi)部代碼當(dāng)做C語言代碼處理,也就是名稱修飾機(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)文件無法正?;ハ噫溄?,這是導(dǎo)致不同編譯器之間不能互操作的原因
3.1.4.4 弱符號與強(qiáng)符號
? 在編程中經(jīng)常遇到符號重定義的問題,例如hello.c和func.c都定義了一個(gè)_global并將它們都初始化,在編譯時(shí)就會(huì)報(bào)錯(cuò)。對于C/C++來說,編譯器默認(rèn)函數(shù)和初始化的全局變量為強(qiáng)符號,未初始化的全局變量為弱符號。
-
編譯器處理符號規(guī)則
- 不允許強(qiáng)符號被多次定義
- 如果一個(gè)符號在一個(gè)文件中是強(qiáng)符號,在其它文件中是弱符號,則選擇強(qiáng)符號
- 如果一個(gè)符號在所有的文件中都是弱符號,則選擇其中占用空間最大的一個(gè)(int型和double型會(huì)選擇double型)
弱引用與強(qiáng)引用:對外部目標(biāo)文件中的符號引用在目標(biāo)文件最終被鏈接成可執(zhí)行文件時(shí)都喲啊被正確決議,如果沒有找到該符號的定義,則會(huì)報(bào)未定義錯(cuò)誤,這種被稱為強(qiáng)引用;與之對應(yīng)的弱引用,在處理弱引用時(shí),如果該符號有定義,則鏈接器將該符號的引用決議;如果該符號未被定義,則鏈接器也不會(huì)報(bào)錯(cuò)。
-
弱符號與弱引用的作用(對庫來說很有用)
- 庫中定義的弱符號可以被用戶定義的強(qiáng)符號所覆蓋,從而使程序可以使用自定義版本的函數(shù)
- 程序可以對某些擴(kuò)展功能模塊的引用定義為弱引用,當(dāng)擴(kuò)展模塊與程序鏈接到一起時(shí),功能模塊可以正常使用;如果去掉了某些功能模塊,則程序也可以正常鏈接,只是缺少了相應(yīng)的功能,這使得程序的功能更容易裁剪和組合
3.2 靜態(tài)鏈接
3.2.1 空間和地址分配
鏈接器在合并多個(gè)目標(biāo)文件的段時(shí),采用相似段合并的方式,并分配地址和空間(虛擬地址空間的分配)
兩步鏈接法:
- 空間和地址分配:掃描所有的目標(biāo)文件,獲得它們的各個(gè)段的長度、屬性和位置,并且將輸入目標(biāo)文件中的符號表中所有的符號定義和符號引用收集起來,統(tǒng)一放到一個(gè)全局符號表,這一步中,鏈接器將能夠獲得所有輸入目標(biāo)文件的段長度,并將它們合并,計(jì)算輸出文件中各個(gè)合并之后的段的長度,建立映射關(guān)系。
- 符號解析與重定位:使用空間和地址分配中收集到的所有信息,讀取輸入文件中段的數(shù)據(jù)、重定位信息,并且進(jìn)行符號解析與重定位、調(diào)整代碼中的地址等。
當(dāng)進(jìn)行了空間和地址分配之后,各個(gè)段的虛擬地址也就確定了,由于各個(gè)符號在段內(nèi)的位置是相對的,所以各個(gè)符號的地址也就確定了。
3.2.2 符號解析與重定位
-
使用
gcc -c hello.c -o hello.o生成目標(biāo)文件hello.o,并使用objdump -d hello.o讀取目標(biāo)文件的.text的反匯編結(jié)果,如下所示(簡略部分內(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是目的地址相對于下一條指令的偏移量);在使用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)文件,但是符號已經(jīng)被重新定位;-e main表示將main函數(shù)作為程序入口),使用objdump -d a.out查看a.out的.text段反匯編結(jié)果,如下圖所示(簡略部分內(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中的符號信息(簡略),可以看到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輸出的符號表中)。所以鏈接器在完成地址空間分配之后就可以確定所有符號的虛擬地址了,鏈接器就可以根據(jù)符號的地址對每個(gè)需要重定位的地方進(jìn)行地址修正。
-
鏈接器如何知道哪些地址需要修正呢?有一個(gè)重定位表的結(jié)構(gòu)專門保存與重定位相關(guān)的信息(比如
.text如果有需要重定位的地方,那么就會(huì)有一個(gè)叫.rela.text的段保存了代碼段的重定位信息),使用objdump -r hello.o查看重定位信息如下(簡略),可以看到所有需要重定位的地方[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 -
符號解析:使用
nm hello.o可以查看hello.o 中所有的符號信息,如下所示,可以看到export_func_var和func_call_test符號都是未定義狀態(tài)(U)。所以檔鏈接器掃描完所有的輸入目標(biāo)文件之后,所有的這些未定義的符號都能夠在全局符號表中找到,否則就會(huì)報(bào)符號未定義(undefined reference to)錯(cuò)誤。# 輸出hello.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# 符號未定義錯(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:被修正的位置<相對于段開始的偏移量或者虛擬地址>;S:符號的實(shí)際地址;L:表示其索引位于重定位條目中的符號的值)以下計(jì)算參考
# hello.o中的重定位信息(簡略) [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ù)輸出符號的重定位類型有R_X86_64_PLT32和R_X86_64_PC32 # R_X86_64_PLT32 : L + A - P(絕對地址修正) # R_X86_64_PC32 : S + A - P(相對尋址修正) # 其它方式參考:http://www.ucw.cz/~hubicka/papers/abi/node19.html- 絕對地址修正:絕對地址修正后的地址為該符號的實(shí)際地址,例如調(diào)用func_call_test符號的地址被修正成為了絕對地址40011d
- 相對地址修正:相對地址修正后的地址為符號距離被修正位置的地址差,例如使用export_func_var符號的地址被修正成為了相對地址0x200f0a,mov指令(第一個(gè)mov指令)的下一條地址40010e加上這個(gè)偏移量0x200f0a就是export_func_var的絕對地址0x601018
-
COMMON塊:根據(jù)
nm hello.o的輸出,如下所示(簡略),可以看到global_uninit_var符號的類型為COMMON類型,編譯器將未初始化的全局變量作為弱符號處理[delta@rabbit: c_code ]$ nm hello.o 0000000000000004 C global_uninit_var多個(gè)符號定義類型情況分析
- 兩個(gè)或以上強(qiáng)符號類型不一致:報(bào)重定義錯(cuò)誤
- 有一個(gè)強(qiáng)符號和多個(gè)弱符號:取強(qiáng)符號,若是有弱符號比強(qiáng)符號空間大的情況則編譯時(shí)會(huì)出現(xiàn)warning
- 兩個(gè)或者以上弱符號類型不一致:取占用空間最大的弱符號
注:當(dāng)編譯器將一個(gè)編譯單元編譯成目標(biāo)文件時(shí),如果該編譯單元包含弱符號(未初始化或者初始化為0的全局變量是典型),那么該符號所占用的最終空間就是不確定的,所以編譯器無法在該階段為該符號在BSS段分配空間。但是經(jīng)過鏈接之后,任何一個(gè)符號的大小都確定了,所以它可以在最終輸出文件的BSS段為其分配空間??傮w來看,未初始化的全局變量是放在BSS段的
3.2.3 靜態(tài)庫鏈接
定義:靜態(tài)庫可以簡單地看做是一組目標(biāo)文件的集合,即很多目標(biāo)文件經(jīng)過壓縮打包后形成的一個(gè)文件(Linux上常用的C語言靜態(tài)庫libc位于/usr/lib/x86_64-linux-gnu/libc.a)
-
靜態(tài)鏈接過程:在鏈接過程中l(wèi)d鏈接器會(huì)自動(dòng)尋找所有需要的符號以及它們所在的目標(biāo)文件,將這些目標(biāo)文件從libc.a中“解壓”出來,最終將它們鏈接到一起形成一個(gè)可執(zhí)行文件。使用
gcc -v hello.c func.c編譯生成可執(zhí)行文件a.out,可以看到詳細(xì)的鏈接過程,產(chǎn)生如下輸出(簡化版本)[delta@delta: code ]$ gcc -v func.c hello.c # 對func.c的預(yù)處理和編譯過程 /usr/lib/gcc/x86_64-linux-gnu/7/cc1 func.c -o /tmp/ccfC6J5E.s # 對func.c產(chǎn)生的.s文件匯編產(chǎn)生二進(jìn)制文件 as -v --64 -o /tmp/ccF4Bar0.o /tmp/ccfC6J5E.s # 對hello.c的預(yù)處理和編譯過程 /usr/lib/gcc/x86_64-linux-gnu/7/cc1 hello.c -o /tmp/ccfC6J5E.s # 對hello.c產(chǎn)生的.s文件匯編產(chǎn)生二進(jìn)制文件 as -v --64 -o /tmp/cc7UmhQl.o /tmp/ccfC6J5E.s # 鏈接過程 /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è)文件的解釋(來源)
目標(biāo)文件 說明 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
-
鏈接過程控制:鏈接過程需要考慮很多內(nèi)容:使用哪些目標(biāo)文件?使用哪些庫文件?是否保留調(diào)試信息、輸出文件格式等等。
鏈接器控制鏈接過程方法:
- 使用命令行來給鏈接器指定參數(shù)
- 將鏈接器指令存放在目標(biāo)文件里面,編譯器通常會(huì)使用這種方式向鏈接器傳遞指令。
- 使用鏈接控制腳本
3.2.4 BFD庫簡介
- 定義:由于現(xiàn)代的硬件和軟件平臺(tái)種類繁多,每個(gè)平臺(tái)都有不同的目標(biāo)文件格式,導(dǎo)致編譯器和鏈接器很難處理不同平臺(tái)的目標(biāo)文件。BFD庫(Binary File Descriptor library)希望通過統(tǒng)一的接口來處理不同的目標(biāo)文件格式。
現(xiàn)代GCC(具體來講是GNU 匯編器GAS)、鏈接器、調(diào)試器和GDB及binutils的其他工具都是通過BFD庫來處理目標(biāo)文件,而不是直接操作目標(biāo)文件。
3.3 裝載與動(dòng)態(tài)鏈接
3.3.1可執(zhí)行文件的裝載
-
進(jìn)程的虛擬地址空間:每個(gè)程序運(yùn)行起來之后,它將擁有自己獨(dú)立的虛擬地址空間,這個(gè)虛擬地址空間的大小由計(jì)算機(jī)的硬件平臺(tái)決定,具體來說是CPU的位數(shù)決定(32位平臺(tái)下的虛擬空間為4G<2^32>,通過
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ì)捕獲到這些訪問并將其關(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è)輔助代碼來管理這些這些模塊何時(shí)應(yīng)該駐留內(nèi)存,何時(shí)應(yīng)該被替換掉(在多個(gè)模塊的情況下,程序員需要手工將它們之間的依賴關(guān)系組織成樹狀結(jié)構(gòu))
- 頁映射:頁映射不是一下子將指令和數(shù)據(jù)一下子裝入內(nèi)存,而是將內(nèi)存和磁盤中的所有數(shù)據(jù)和指令按照頁(Page)為單位劃分,之后所有的裝載和操作的單位就是頁。
-
操作系統(tǒng)角度來看可執(zhí)行文件的加載:
創(chuàng)建一個(gè)獨(dú)立的虛擬地址空間:創(chuàng)建映射函數(shù)所需要的對應(yīng)的數(shù)據(jù)結(jié)構(gòu)
讀取可執(zhí)行文件頭,建立虛擬空間和可執(zhí)行文件的映射關(guān)系:程序在發(fā)生頁錯(cuò)誤時(shí),操作系統(tǒng)從物理空間分配出來一個(gè)物理頁,然后將“缺頁”從磁盤讀取到內(nèi)存中,并設(shè)置缺頁的虛擬頁與物理頁的映射關(guān)系,很明顯,操作系統(tǒng)捕獲到缺頁錯(cuò)誤時(shí),它應(yīng)該知道當(dāng)前所需要的頁在可執(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)行
注:頁錯(cuò)誤處理:
- CPU將控制權(quán)交給操作系統(tǒng)
- 操作系統(tǒng)查詢裝載過程 第二部建立起來的數(shù)據(jù)結(jié)構(gòu),找到空白頁所在的VMA,計(jì)算出相應(yīng)頁面在可執(zhí)行文件中的便宜,然后在物理內(nèi)存中分配一個(gè)物理頁面,將進(jìn)程中該虛擬地頁與分配的物理頁之間建立映射關(guān)系
- 把控制權(quán)還給進(jìn)程
3.3.2 動(dòng)態(tài)鏈接
- 為什么需要?jiǎng)討B(tài)鏈接:1、靜態(tài)鏈接方式對于計(jì)算機(jī)內(nèi)存和磁盤空間的浪費(fèi)非常嚴(yán)重;2、靜態(tài)鏈接庫對程序的更新部署會(huì)帶來很多麻煩(如果其中一個(gè)依賴進(jìn)行了更新,那么該程序就要重新鏈接發(fā)布)
-
動(dòng)態(tài)鏈接:將鏈接的過程推遲到了運(yùn)行的時(shí)候再進(jìn)行,通過動(dòng)態(tài)鏈接器(第二部分GCC編譯過程中最后的鏈接設(shè)置了動(dòng)態(tài)鏈接器參數(shù)
-dynamic-linker ld-linux-x86-64.so.2)完成鏈接工作,通過延遲綁定等來將動(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)鏈接庫,這些動(dòng)態(tài)鏈接庫相當(dāng)于在程序和操作系統(tǒng)之間添加了一個(gè)中間層,從而消除程序?qū)Σ煌脚_(tái)之間依賴的差異性
-
地址無關(guān)代碼:共享對象在編譯時(shí)不能假設(shè)自己在進(jìn)程虛擬空間中的位置。把指令中那些需要修改的部分分離出來與數(shù)據(jù)部分放在一起,這樣指令部分就可以保持不變,而數(shù)據(jù)部分可以在每個(gè)進(jìn)程中有一個(gè)副本,這種方案就是地址無關(guān)代碼(PIC,Position-Independent Code)
- 裝載時(shí)重定位:一旦模塊裝載地址確定,即目標(biāo)地址確定,那么系統(tǒng)就對程序中所有的絕對地址進(jìn)行重定位(靜態(tài)鏈接時(shí)的重定位叫做鏈接時(shí)重定位;動(dòng)態(tài)鏈接的重定位叫做裝載時(shí)重定位 )
- 模塊中國各種類型地址類型引用方式:
- 模塊內(nèi)部的函數(shù)調(diào)用、跳轉(zhuǎn):采用相對地址調(diào)用,不需要重定位
- 模塊內(nèi)部的數(shù)據(jù)訪問,比如模塊中定義的全局變量,靜態(tài)變量:采用相對地址訪問,獲取當(dāng)前的PC值,加上偏移量就能訪問變量了
- 模塊外部的數(shù)據(jù)訪問,比如其它模塊定義的全局變量:ELF的做法是子啊數(shù)據(jù)段里面建立一個(gè)指向這些變量的指針數(shù)組,稱為全局偏移表(GOT,Global Offset Table)。GOT是放在數(shù)據(jù)段的,可以在模塊裝載時(shí)被修改,并且每個(gè)進(jìn)程都可以有獨(dú)立的副本,互相不影響。
- 模塊外部的函數(shù)調(diào)用、跳轉(zhuǎn)等:通過GOT中的項(xiàng)進(jìn)行間接跳轉(zhuǎn)
- 延遲綁定:當(dāng)函數(shù)第一次被用到才進(jìn)行綁定(符號查找、重定位等),如果沒有用到則不綁定。ELF使用PLT(Procedure Linkage Table)的方式來事先延遲綁定(PLT使解析只會(huì)在符號未解析時(shí)進(jìn)行一次)。
- 動(dòng)態(tài)鏈接的步驟
- 動(dòng)態(tài)鏈接器自舉:動(dòng)態(tài)鏈接器不依賴其它任何共享對象;動(dòng)態(tài)鏈接器本身所需的全局和靜態(tài)變量的重定位工作由它本身完成
- 裝載共享對象:將可執(zhí)行文件和鏈接器本身的符號都合并到一個(gè)全局符號表中(圖的遍歷過程),當(dāng)一個(gè)符號需要加入到全局符號表時(shí),如果相同的符號已經(jīng)存在,則忽略后加入的符號
- 重定位與初始化:重新遍歷可執(zhí)行文件和每個(gè)共享對象的重定位表,將它們的GOT/PLT中的每個(gè)需要重定位的地方進(jìn)行修正。
四、參考文獻(xiàn)
[0] 程序員的自我修養(yǎng) :鏈接、裝載與庫 / 俞甲子,石凡,潘愛民著.—北京:電子工業(yè)出版社
[1] GNU ONLINE DOC - collect2 https://gcc.gnu.org/onlinedocs/gccint/Collect2.html