在學習鏈接的具體過程前,有必要好好了解一下ELF目標文件。
ELF的目標文件分為三類:
- 可重定位目標文件(.o)
- 其代碼和數(shù)據(jù)可和其他可重定位文件合并為可執(zhí)行文件
- 每個 .o 文件由對應(yīng)的 .c 文件生成
- 每個 .o 文件的代碼和數(shù)據(jù)地址都是從0開始的偏移
- 可執(zhí)行目標文件(默認為a.out)
- 包含的代碼和數(shù)據(jù)可以被直接復制到內(nèi)存并執(zhí)行
- 代碼和數(shù)據(jù)的地址是虛擬地址空間中的地址
- 共享的目標文件(.so 共享庫)
- 特殊的可重定位目標文件,能在裝載到內(nèi)存或運行時自動被鏈接,稱為共享庫文件
可通過objdump命令對比可重定位目標文件和可執(zhí)行目標文件的不同:

可以看到,確實可重定位目標文件中的地址是從0開始的,而可執(zhí)行目標文件中的地址是虛擬地址空間中的地址。
接下來介紹ELF文件的兩種視圖:
- 鏈接視圖:可重定位文件(Relocatable object files)
-
執(zhí)行視圖:可執(zhí)行目標文件(Executable object files)
image.png
鏈接視圖 —— 可重定位目標文件
來看一個簡單的C代碼及其所生成的可重定位目標文件的關(guān)系圖:

如上圖,編譯后的代碼部分放到 .text節(jié),已初始化的全局變量和已初始化的靜態(tài)變量會放到 .data節(jié),未初始化的全局變量和未初始化的靜態(tài)變量會放到 .bss節(jié)。
實際上,為了進行鏈接,可重定位目標文件還需要許多其他信息,如符號表、重定位信息等。這些后面會陸續(xù)介紹。
在這里要特別說明一下 .bss節(jié)。該節(jié)在可重定位目標文件中并不占用空間,只是在節(jié)頭表相應(yīng)的表項中說明要為 .bss節(jié)預留多大的空間。
可重定位目標文件中包含有很多的節(jié),格式如下圖:

其中:
- ELF頭
包括16字節(jié)的標識信息、文件類型(.o,exec,.so)、機器類型(如Intel 80386)、節(jié)頭表的偏移、節(jié)頭表的表項大小及表項個數(shù)。
- .text節(jié)
編譯后的代碼部分。
- .rodata節(jié)
只讀數(shù)據(jù),如 printf用到的格式串、switch跳轉(zhuǎn)表等。
- .data節(jié)
已初始化的全局變量和靜態(tài)變量。
- .bss節(jié)
未初始化全局變量和靜態(tài)變量,僅是占位符,不占據(jù)任何磁盤空間。區(qū)分初始化和非初始化是為了空間效率。
- .symtab節(jié)
存放函數(shù)和全局變量(符號表)的信息,它不包括局部變量。
- .rel.text節(jié)
.text節(jié)的重定位信息,用于重新修改代碼段的指令中的地址信息。
- .rel.data節(jié)
.data節(jié)的重定位信息,用于對被模塊使用或定義的全局變量進行重定位的信息。
- .debug節(jié)
調(diào)試用的符號表(gcc -g)
- .strtab節(jié)
包含 .symtab節(jié)和 .debug節(jié)中的符號及節(jié)名
- 節(jié)頭表(Section header table)
包含每個節(jié)的節(jié)名在.strtab節(jié)中的偏移、節(jié)的偏移和節(jié)的大小.
下邊分別舉例講一下ELF頭和節(jié)頭表:
-------------------------------------- ELF頭 (ELF Header)------------------------------
ELF頭位于ELF文件的開始,其包含了文件結(jié)構(gòu)的說明信息。其結(jié)構(gòu)體定義如下:
#define EI_NIDENT 16
typedef struct {
unsigned char e_ident[EI_NIDENT];
uint16_t e_type;
uint16_t e_machine;
uint32_t e_version;
ElfN_Addr e_entry;
ElfN_Off e_phoff;
ElfN_Off e_shoff;
uint32_t e_flags;
uint16_t e_ehsize;
uint16_t e_phentsize;
uint16_t e_phnum;
uint16_t e_shentsize;
uint16_t e_shnum;
uint16_t e_shstrndx;
} ElfN_Ehdr;
用readelf -h 查看ELF頭。下表是ELF頭中各個成員的含義與readelf輸出結(jié)果的對照表:
| 成員 | readelf輸出結(jié)果及含義 |
|---|---|
| e_ident | Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 Class: ELF32 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 |
| e_type | Type: REL(Relocatable file) ELF文件類型 |
| e_machine | Machine: Intel 80386 ELF文件的CPU平臺屬性,相關(guān)常量以EM_開頭 |
| e_version | Version: 0x1 ELF版本號。一般為常數(shù)1 |
| e_entry | Entry point address: 0x0 入口地址,規(guī)定ELF程序的入口虛擬地址,操作系統(tǒng)在加載完該程序后從這個地址開始執(zhí)行進程的指令??芍囟ㄎ荒繕宋募话銢]有入口地址,則這個值為0 |
| e_phoff | Start of program header: 0 (bytes into file) 程序頭表在文件中的偏移,可重定位目標文件不存在程序頭表,故該值為0 |
| e_shoff | Start of section header: 280(bytes into file) 節(jié)頭表在文件中的偏移 |
| e_flags | Flags: 0x0 ELF標志位,用來標識一些ELF文件平臺相關(guān)的屬性,相關(guān)常量的格式一般為EF_machine_flag,machine為平臺,flag為標志 |
| e_ehsize | Size of this header: 52(bytes) 即ELF文件頭本身的大小 |
| e_phentsize | Size of program headers: 0(bytes) 程序頭表表項的大小 |
| e_phnum | Number of program headers: 0 程序頭表表項的數(shù)目 |
| e_shentsize | Size of section headers: 40(bytes) 節(jié)表表項的大小 |
| e_shnum | Number of section headers: 11 節(jié)表表項的數(shù)目 |
| e_shstrndx | Section header string table index: 8 節(jié)表字符串表在節(jié)頭表中的下標 |
-------------------------------------- 節(jié)頭表(Section Header Table) ----------------------
除了ELF頭之外,節(jié)頭表是ELF可重定位目標文件中最重要的部分內(nèi)容。
它描述了每個節(jié)的節(jié)名、在文件中的偏移、大小、訪問屬性、對齊方式等。其32位結(jié)構(gòu)定義如下(每項占40字節(jié)):
typedef struct {
Elf32_Word sh_name; //節(jié)名字符串在.strtab中的偏移
Elf32_Word sh_type; //節(jié)類型:無效/代碼或數(shù)據(jù)/符號/字符串/…
Elf32_Word sh_flags; //節(jié)標志:該節(jié)在虛擬空間中的訪問屬性
Elf32_Addr sh_addr; //虛擬地址:若可被加載,則對應(yīng)虛擬地址
Elf32_Off sh_offset; //在文件中的偏移地址,對.bss節(jié)而言則無意義
Elf32_Word sh_size; //節(jié)在文件中所占的長度
Elf32_Word sh_link; //sh_link和sh_info用于與鏈接相關(guān)的節(jié)(如
Elf32_Word sh_info; // .rel.text節(jié)、.rel.data節(jié)、.symtab節(jié)等)
Elf32_Word sh_addralign; //節(jié)的對齊要求
Elf32_Word sh_entsize; //節(jié)中每個表項的長度,0表示無固定長度表項
} Elf32_Shdr;
使用 readelf -S命令查看節(jié)頭表,示例如下:


A(alloc)標志表示該節(jié)將進程空間中必須要分配空間。可以看到,.text、.data、.bss、.rodata節(jié)都有這個標志。
執(zhí)行視圖 —— 可執(zhí)行目標文件
再來看ELF的執(zhí)行視圖,也就是可執(zhí)行目標文件的格式:

它與可重定位目標文件稍有不同:
- ELF文件頭中的字段e_entry給出了執(zhí)行程序時第一條指令的地址,而在可重定位目標文件中,此字段為0;程序頭表的偏移e_poff和大小e_phentsiz和程序頭表項的個數(shù)e_phnum不為0;
- 多了一個程序頭表,也稱為段頭表(segment header table),是一個結(jié)構(gòu)體數(shù)組;
- 多了一個 .init節(jié),用于定義 _init函數(shù),該函數(shù)用于在可執(zhí)行目標文件開始執(zhí)行時的初始化工作。
- 少了兩個 .rel節(jié)(.rel.text和.rel.data),因為可執(zhí)行目標文件已經(jīng)在鏈接的過程中完成了重定位,已無須重定位。
使用readelf -h 來看可執(zhí)行目標文件的ELF頭,示例如下

程序頭表描述的可執(zhí)行文件中的節(jié)(section)與虛擬地址空間中的存儲段(segment)之間的映射關(guān)系。
程序頭表的結(jié)構(gòu)定義如下:
typedef struct {
Elf32_Word p_type; //段類型
Elf32_Off p_offset; //該段在文件中的起始偏移
Elf32_Addr p_vaddr; //該段的虛擬地址
Elf32_Addr p_paddr; //該段的物理地址
Elf32_Word p_filesz; //該段在文件中的大小
Elf32_Word p_memsz; //該段在內(nèi)存中的大小
Elf32_Word p_flags; //段的標志位,表示訪問權(quán)限(Read|Write|Exec)
Elf32_Word p_align; //段在內(nèi)存中的對齊要求
} Elf32_Phdr;
下圖是用readelf -l 查看到的某可執(zhí)行目標文件的程序頭表:

Type為Load表示可裝入內(nèi)存的段。
第一個可裝入段:第0x00000~0x004d3字節(jié)(包括ELF頭、程序頭表、.init節(jié)、.text節(jié)和.rodata節(jié)),映射到虛擬地址0x8048000開始長度為0x4d4字節(jié)的區(qū)域,按0x1000=4KB對齊,具有只讀/執(zhí)行權(quán)限(Flg=RE),是只讀代碼段。
第二個可裝入段:第0x000f0c~開始長度為0x108字節(jié)的 .data節(jié),映射到虛擬地址0x8049f0c開始長度為0x110字節(jié)的存儲區(qū)域,在0x110=272B存儲區(qū)中,前0x108=264B用 .data節(jié)內(nèi)容初始化,后面272-264=8B對應(yīng).bss節(jié),初始化為0,按0x1000=64K對齊,具有可讀可寫權(quán)限(Flg=RW),是可讀寫數(shù)據(jù)段。
映射到虛擬地址空間中如圖:

