一、目標文件基本闡述
-
目標文件:編譯器編譯源代碼后但未進行鏈接的中間文件(Linux下為.o文件)
-
結構特點:分段(主要為代碼段和數據段)
-
分段的好處
-
可以分別設置不同屬性,數據虛存區域設置為可讀寫,指令虛存區域設置為只讀
-
符合現代CPU的緩存體系(數據緩存和指令緩存分離)
-
節省內存,系統中運行多個該程序副本時,只需保留一份該程序的指令部分或只讀數據(圖標、圖片、文本資源等)
- 學習的目的:認識底層具體工作細節,提高自己的修養境界
二、深入細節的示例分析
源碼與基本段信息
- 源碼
/*
* SimpleSection.c
* Linux: gcc -c SimpleSection.c
* Windows: cl SimpleSection.c /c /Za
*/
int printf( const char* format, ...);
int global_init_var = 84;
int global_uninit_var;
void func1( int i )
{
printf("%d\n", i);
}
int main(void)
{
static int static_var = 85;
static int static_var2;
int a = 1;
int b;
func1(static_var + static_var2 + a + b);
return a;
}
- 基本段信息
# 平台: Ubuntu16.04
# 參數: -h 打印基本的段信息
$ objdump -h SimpleSection.o
SimpleSection.o: file format elf64-x86-64
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000055 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000008 0000000000000000 0000000000000000 00000098 2**2 # 存放的是已經初始化的全局靜態變量和局部靜態變量
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000004 0000000000000000 0000000000000000 000000a0 2**2 # 存放的是未初始化的全局變量和局部靜態變量
ALLOC
3 .rodata 00000004 0000000000000000 0000000000000000 000000a0 2**0 # 存放只讀數據,一般是程序里邊的只讀變量(const修飾的變量)和字符串常量
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .comment 00000036 0000000000000000 0000000000000000 000000a4 2**0 # 存放的是編譯器的版本信息
CONTENTS, READONLY
5 .note.GNU-stack 00000000 0000000000000000 0000000000000000 000000da 2**0
CONTENTS, READONLY
6 .eh_frame 00000058 0000000000000000 0000000000000000 000000e0 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
# 簡單說明:
# Size為段的長度,File Off為段所在的位置,也就是偏移
# 屬性: CONTENTS表示該段在文件中存在(.bss段里沒有)
$ size SimpleSection.o
text data bss dec hex filename
177 8 4 189 bd SimpleSection.o
-
當前框架圖
-
代碼段解析
# 平台: Ubuntu16.04
# 參數: -s: 以十六進制打印所有的段的內容
# -d: 將所有包含指令的段反匯編
$ objdump -s -d SimpleSection.o
SimpleSection.o: file format elf64-x86-64
Contents of section .text: # .text的數據內容。總共0x55字節,字節的內容是指令機器碼
0000 554889e5 4883ec10 897dfc8b 45fc89c6 UH..H....}..E...
0010 bf000000 00b80000 0000e800 00000090 ................
0020 c9c35548 89e54883 ec10c745 f8010000 ..UH..H....E....
0030 008b1500 0000008b 05000000 0001c28b ................
0040 45f801c2 8b45fc01 d089c7e8 00000000 E....E..........
0050 8b45f8c9 c3 .E...
Contents of section .data: # 對應程序中 global_init_var的值為84, static_var為85
0000 54000000 55000000 T...U...
Contents of section .rodata: # printf的字符串常量“%d\n”對應ASCII的十六進制
0000 25640a00 %d..
Contents of section .comment:
0000 00474343 3a202855 62756e74 7520352e .GCC: (Ubuntu 5.
0010 342e302d 36756275 6e747531 7e31362e 4.0-6ubuntu1~16.
0020 30342e31 32292035 2e342e30 20323031 04.12) 5.4.0 201
0030 36303630 3900 60609.
Contents of section .eh_frame:
0000 14000000 00000000 017a5200 01781001 .........zR..x..
0010 1b0c0708 90010000 1c000000 1c000000 ................
0020 00000000 22000000 00410e10 8602430d ...."....A....C.
0030 065d0c07 08000000 1c000000 3c000000 .]..........<...
0040 00000000 33000000 00410e10 8602430d ....3....A....C.
0050 066e0c07 08000000 .n......
Disassembly of section .text:
0000000000000000 <func1>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 10 sub $0x10,%rsp
8: 89 7d fc mov %edi,-0x4(%rbp)
b: 8b 45 fc mov -0x4(%rbp),%eax
e: 89 c6 mov %eax,%esi
10: bf 00 00 00 00 mov $0x0,%edi
15: b8 00 00 00 00 mov $0x0,%eax
1a: e8 00 00 00 00 callq 1f <func1+0x1f>
1f: 90 nop
20: c9 leaveq
21: c3 retq
0000000000000022 <main>:
22: 55 push %rbp
23: 48 89 e5 mov %rsp,%rbp
26: 48 83 ec 10 sub $0x10,%rsp
2a: c7 45 f8 01 00 00 00 movl $0x1,-0x8(%rbp)
31: 8b 15 00 00 00 00 mov 0x0(%rip),%edx # 37 <main+0x15>
37: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # 3d <main+0x1b>
3d: 01 c2 add %eax,%edx
3f: 8b 45 f8 mov -0x8(%rbp),%eax
42: 01 c2 add %eax,%edx
44: 8b 45 fc mov -0x4(%rbp),%eax
47: 01 d0 add %edx,%eax
49: 89 c7 mov %eax,%edi
4b: e8 00 00 00 00 callq 50 <main+0x2e>
50: 8b 45 f8 mov -0x8(%rbp),%eax
53: c9 leaveq
54: c3 retq
- VMA與LMA的關系(轉載)
https://www.crifan.com/detailed_lma_load_memory_address_and_vma_virtual_memory_address/
ELF文件結構描述
ELF文件頭
- 目標:查看ELF文件頭,可以找到段表所在的位置,以此得知整個目標文件的段結構
$ readelf -h SimpleSection.o
# ELF標記0x7F, 'E'(0x45)、'L'(0x4c)、'F'(0x46)
# 文件類型: 0x0:無效文件 0x1:32為ELF文件 0x2:64位ELF文件
# 字節序: 0x0: 無效格式 0x1:小端格式 0x2:大端格式
# ELF主版本號: 0x1,ELF標准自1.2版本以后再也沒有更新
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 # 對應e_ident成員
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: 1072 (bytes into file) # 段表的位置0x430
Flags: 0x0
Size of this header: 64 (bytes) # ELF文件頭的大小為64字節
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 64 (bytes) # 段表描述符的大小,也就是段表結構體Elf64_Shdr的大小
Number of section headers: 13 # 段表描述符的數量
Section header string table index: 10 #表示.shstrtab所在的段在段表中的下標為10
- 相關的數據結構
#數據結構與上邊的每一項是完全相符合的
#32位的elf結構體為Elf32_Ehdr,64位的elf結構體為Elf64_Ehdr,成員完全一樣,只是有些成員大小不一樣
$ cat /usr/include/elf.h
...
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;
...
段表(Section Table)
- 段表是目標文件中最重要的部分,記錄每個段的地址、長度和屬性等信息
# objdump -h只是將關鍵段顯示出來,省略其他輔助性的段,用readelf查看elf文件的段才是真正的段表結構
# 段表結構由ELF文件頭的e_shoff決定
$ readelf -S SimpleSection.o
There are 13 section headers, starting at offset 0x430: # 表明段表的位置位0x430
Section Headers: # 在這個程序中,段表就是有13個元素的數組,但第一個是無效的,也就是有12個有效的段,每個段是Elf64_Shdr結構體
[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
0000000000000055 0000000000000000 AX 0 0 1
[ 2] .rela.text RELA 0000000000000000 00000320
0000000000000078 0000000000000018 I 11 1 8 # .rela.text的info值為1代表重定向text的下標,11代表所使用的符號表在段表的下標
[ 3] .data PROGBITS 0000000000000000 00000098
0000000000000008 0000000000000000 WA 0 0 4
[ 4] .bss NOBITS 0000000000000000 000000a0
0000000000000004 0000000000000000 WA 0 0 4
[ 5] .rodata PROGBITS 0000000000000000 000000a0
0000000000000004 0000000000000000 A 0 0 1
[ 6] .comment PROGBITS 0000000000000000 000000a4
0000000000000036 0000000000000001 MS 0 0 1
[ 7] .note.GNU-stack PROGBITS 0000000000000000 000000da
0000000000000000 0000000000000000 0 0 1
[ 8] .eh_frame PROGBITS 0000000000000000 000000e0
0000000000000058 0000000000000000 A 0 0 8
[ 9] .rela.eh_frame RELA 0000000000000000 00000398
0000000000000030 0000000000000018 I 11 8 8
[10] .shstrtab STRTAB 0000000000000000 000003c8
0000000000000061 0000000000000000 0 0 1
[11] .symtab SYMTAB 0000000000000000 00000138
0000000000000180 0000000000000018 12 11 8
[12] .strtab STRTAB 0000000000000000 000002b8
0000000000000066 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)
# 說明:
1. Type: PROGBITS(程序段,代碼段和數據段都是這種類型)、RELA(重定位表,針對那些對絕對地址引用的位置的記錄)、NOBITS(該段在文件中沒內容,如.bss)、STRTAB(字符串表)等
2. Flags: ALLOC(表示在進程空間中需要分配空間)
3. Link: 段的類型與鏈接相關的(如重定位表、符號表等)才有意義,當類型為REL時,Link表示所使用的相應符號表在段表的下標,info表示所作用的段在段表的下標
- 相關數據結構
#32位的段表結構為Elf32_Shdr,64位的elf結構體為Elf64_Shdr,成員完全一樣,只是類型大小不一樣
$ cat /usr/include/elf.h
...
typedef struct # 與上邊的一一對應上
{
Elf64_Word sh_name; /* Section name (string tbl index) */ # 段名,位於.shstrtab字符串表
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;
...
- 根據段表可以清楚知道整體的結構
字符串表(.strtab、.shstrtab)
-
由於ELF中的字符串的長度往往是不定的,所以采用偏移的方式來引用字符串(字符串helloworld的偏移為1,world的偏移為6,以\0來衡量)
-
常見的段名
-
字符串表:.strtab,保存普通字符串,比如符號的名字
-
段表字符串表(Section Header String Table):.shstrtab,保存段表中用到的字符串,比如段名
- ELF文件頭中有表明段表字符串表在段表中的位置(成員e_shstrndx),因此只要分析ELF文件頭,就可以得到段表和段表字符串表的位置
ELF符號表(.symtab)
- 符號的概念:
-
函數和變量統稱為符號(Symbol),函數名或變量名就是符號名
-
每個目標文件都有一個相應的符號表(Stmbol Table),這個表里邊記錄了目標文件中所要用到的所有符號,每一個定義的符號有個對應的值,也就是符號值,對於變量或者函數來說,符號值就是它們的地址
- 查看符號表
# nm命令可以查看目標文件的符號信息
$ nm SimpleSection.o
0000000000000000 T func1
0000000000000000 D global_init_var
0000000000000004 C global_uninit_var
0000000000000022 T main
U printf
0000000000000004 d static_var.1840
0000000000000000 b static_var2.1841
#說明:
1. t,T 該符號位於代碼段(text section)
2. d,D 該符號位於初始化數據段(data section)
3. C 該符號為common。common symbol是未初始化的數據。該符號沒有包含在一個普通section中,只有在鏈接過程中才進行分配。符號的值表示該符號需要的字節數
4. U 該符號在當前文件中是未定義的,即該符號定義在別的文件中
5. b,B 該符號的值出現在非初始化數據段(BSS)中
# -s打印sym,即符號表
$ readelf -s SimpleSection.o
Symbol table '.symtab' contains 16 entries: # 16個元素的符號表數組
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS SimpleSection.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1 # 這些沒有名字的代表下標為Ndx的段的段名,比如這里為1,即.text段的段名,可以通過objdump -t來查看
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000000 0 SECTION LOCAL DEFAULT 5
6: 0000000000000004 4 OBJECT LOCAL DEFAULT 3 static_var.1840 # 變量名稱變了,由於符號修飾
7: 0000000000000000 4 OBJECT LOCAL DEFAULT 4 static_var2.1841
8: 0000000000000000 0 SECTION LOCAL DEFAULT 7
9: 0000000000000000 0 SECTION LOCAL DEFAULT 8
10: 0000000000000000 0 SECTION LOCAL DEFAULT 6
11: 0000000000000000 4 OBJECT GLOBAL DEFAULT 3 global_init_var # 已初始化,.data的下標為3 (不能看objdump -h的輸出信息,只是列出主要的段,索引不對)
12: 0000000000000004 4 OBJECT GLOBAL DEFAULT COM global_uninit_var # 未初始化,.bss的下標為4
13: 0000000000000000 34 FUNC GLOBAL DEFAULT 1 func1 # func1函數定義在SimpleSection.c中,是代碼段,.text段的下標為1,所以Ndx為1(readelf -a 或 objdump -x)
14: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf # 在SimpleSection中被引用,但沒有定義
15: 0000000000000022 51 FUNC GLOBAL DEFAULT 1 main # 與func1函數一樣
# 說明
1. Bind: LOCAL: 局部符號(外部不可見),GLOBAL: 全局符號(外部可見),WEAK: 弱引用
2. Type: NOTYPE: 未知類型 SECTION: 該符號表示一個段(必須是LOCAL) OBJECT: 數據對象(變量、數組等) FUNC: 函數或其他可執行代碼 FILE: 源文件名(必須是LOCAL,並且Ndx為ABS)
3. Ndx: 符號所在的段, ABS: 符號包含一個絕對的值(文件名) COMMON: “COMMON塊”類型,一般來說是未初始化的全局符號定義 UNDEF: 未定義
- 相關數據結構
#32位的段表結構為Elf32_Sym,64位的elf結構體為Elf64_Sym,成員完全一樣,只是類型大小不一樣
$ cat /usr/include/elf.h
...
typedef struct
{
Elf32_Word st_name; /* Symbol name (string tbl index) */
Elf32_Addr st_value; /* Symbol value */
Elf32_Word st_size; /* Symbol size */
unsigned char st_info; /* Symbol type and binding */ # 低4位為符號類型,高4位為符號綁定信息
unsigned char st_other; /* Symbol visibility */
Elf32_Section st_shndx; /* Section index */
} Elf32_Sym;
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;
三、結論
-
目標文件是以段的結構組織起來的,通過ELF文件頭可以知道基本信息和重要的段的具體位置,比如字符串表和段表位置
-
段表通常是一個段,記錄了目標文件所有段的具體位置和大小,可以清楚知道整個段結構布局