一、目标文件基本阐述
-
目标文件:编译器编译源代码后但未进行链接的中间文件(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文件头可以知道基本信息和重要的段的具体位置,比如字符串表和段表位置
-
段表通常是一个段,记录了目标文件所有段的具体位置和大小,可以清楚知道整个段结构布局