3.1 目标文件格式
目标文件 经过编译器编译后产生的文件叫做目标文件。
可执行文件 链接后的目标文件。
现在pc平台流行的可执行文件格式主要为windows 的 PD 和 Linux 下的ELF文件。
3.2 ELF文件格式
文件头(File Header) 描述文件属性(可执行, 静态链接,动态链接,入口地址), 目标硬件和目标操作系统信息,以及一个段表(描述文件中各个段在文件中偏移位置以及段属性)。
代码段(.text section) 编译完成的机器码。
.data section
已经初始完成的全局变量和局部静态变量。.bss section 未初始化的全局变量和局部静态变量。
未初始完成的全局变量和局部静态变量的值为0, 放在.data数据段中要占用内存,并且可执行文件要记录未初始化的全局变量和局部静态变量的大小总和, 放入.bss段中。 .bss段中只是预留了位置,并没有内容,在文件中不占用空间。
所以,为什么要分为代码段和数据段?
可以设置成为代码段只读, 数据段读写, 防止程序指令被有意或者无意篡改。
现代cpu一般被设计成为数据缓存和指令缓存分离。
最重要的原因, 当系统中运行了很多该程序的副本时,内存中只用保存一份程序的指令部分,其他只读数据也一样,但是副本进程的数据区是不一样的。
3.3 细节挖掘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// SimpleSection.c 文件示例
int printf(const char* format, ...);
int global_init_var = 84;
int global_uint_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;
}
编译
gcc -c SimpleSection.c //-c表示只编译不连接
使用工具查看目标文件内部结构
objdump -h SimpleSection.o // -h 为现实每节的主要信息 可通过objdump查看参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
SimpleSection.o: file format elf64-x86-64 Sections: Idx Name Size VMA LMA File off Algn 0 .text 00000057 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 CONTENTS, ALLOC, LOAD, READONLY, DATA 4 .comment 0000002a 0000000000000000 0000000000000000 000000a4 2**0 CONTENTS, READONLY 5 .note.GNU-stack 00000000 0000000000000000 0000000000000000 000000ce 2**0 CONTENTS, READONLY 6 .eh_frame 00000058 0000000000000000 0000000000000000 000000d0 2**3 CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
- Size 段大小
- File off 文件偏移
- CONTENTS 该段是都存在, 即段内是否有内容
- .comment 为注释信息段
- .note.GNU-stack为堆栈提示段
` size SimpleSection.o // 查看各个段长度`
1 2
text data bss dec hex filename 179 8 4 191 bf SimpleSection.o
可能会出现的其他段
3.3.1 代码段(.text)
略
3.3.2 数据段(.data)和只读数据段(.rodata)
数据段(.data) 保存的是已经初始化了的全局变量和局部静态变量。global_init_var和static_var变量,所以数据段位8字节。
只读数据段(.rodata) 存放只读变量,如SimpleSection.c中的
"%d\n"
,是一个字符串常量,为4字节。 设置只读数据段的好处:1.语义上支持C++const关键字。2. 操作系统将.rodata数据段设为只读,保证程序安全性。另外,极个别编译器会将字符串常量放在.data数据段,这里不做展开。
那么,普通的局部变量在哪个数据段? 栈中。
局部静态变量的声明周期是多久? 贯穿于整个程序生命周期。
3.3.3 .BSS段
.BSS段存放的是为初始化的全局变量和局部静态变量。 上述代码中的global_uint_var和static_var2。
C 中,全局变量和局部静态变量详解
- static变量被放在程序的全局存储区中,这样在下一次调用的时候还可以保持原来的赋值。这一点是它与堆栈变量和堆变量的区别。
- 变量用static告知编译器,自己仅在变量的作用范围内可见。这一点是它与全局变量的区别。
- 若全局变量仅在单个C文件中访问,则可以将这个变量修改为静态全局变量,以降低模块间的耦合度;
- 若全局变量仅由单个函数访问,则可以将这个变量改为该函数的静态局部变量,以降低模块间的耦合度;
- 设计和使用访问动态全局变量、静态全局变量、静态局部变量的函数时,需要考虑重入问题;
.BSS段有4字节,而global_uint_var和 static_var2 为8字节,实际上只有static_var2被放入了.BSS段中(那为什么.BSS段又没有COMMENT属性?), 这与编译器有关,有些编译器会将全局的未初始化的全局变量和局部静态变量放入.bss段中,有些则不存放,只是预留一个为定义的全局变量符号,后续将会详细讨论。
3.4 ELF 文件结构
ELF结构
前文中已介绍过的东西不再重复, 这里主要介绍:
Sectiong header table (段表) 描述ELF文件中所有段的信息,段名、段长度、文件中的偏移、读写权限等其他属性。
String Table (字符串表) 略
Symbol Table (符号表) 略
3.4.1 头文件具体结构 (ELF Header)
查看命令
readelf -h SimpleSection.o
结构详情
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
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: 1104 (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: 13 Section header string table index: 12
Magic(魔法数详解) 魔法数解析ELF文件版本的信息,这里不做展开。
3.4.2 段表 (Section head table)
查看命令
1 2
objdump -h SimpleSection.o // 只能查看主要段 readelf -S SimpleSection.o // 查看所有段。
结构详情
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
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 0000000000000057 0000000000000000 AX 0 0 1 [ 2] .rela.text RELA 0000000000000000 00000340 0000000000000078 0000000000000018 I 10 1 8 [ 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 000000000000002a 0000000000000001 MS 0 0 1 [ 7] .note.GNU-stack PROGBITS 0000000000000000 000000ce 0000000000000000 0000000000000000 0 0 1 [ 8] .eh_frame PROGBITS 0000000000000000 000000d0 0000000000000058 0000000000000000 A 0 0 8 [ 9] .rela.eh_frame RELA 0000000000000000 000003b8 0000000000000030 0000000000000018 I 10 8 8 [10] .symtab SYMTAB 0000000000000000 00000128 0000000000000198 0000000000000018 11 11 8 [11] .strtab STRTAB 0000000000000000 000002c0 000000000000007a 0000000000000000 0 0 1 [12] .shstrtab STRTAB 0000000000000000 000003e8 0000000000000061 0000000000000000 0 0 1
3.4.3 重定位表(.rela.text)
SimpleSection.o 中有一个叫做”rel.text”段, 他是一个重定位表(它是段,叫重定位段其实更合适?), 对于每个重要的数据段或者代码段,都会有一个相应的重定位表。至于原因,“text”段中至少有一个绝对地址引用,那就是”printf“函数调用。
3.4.4 字符串表
ELF文件中用到了很多字符串(段名,变量名等),长度不一,不好统一的用数据结构表达,所以,一般把字符串集中起来放在一个表,用偏移表示。
.symtab(字符串表) 保存普通字符串,符号名等。
.shstrtab(段表字符串表) 保存段表名字符串.
3.5 符号表(Symbol Table, 亦或者符号段?)
我们将函数和变量统称为符号,函数名和变量名称为符号名, 对应的值叫做符号值。 对于变量和函数来说,符号值就是他们的地址。
分类为:
- 全局符号,可悲其他目标文件引用。(func1、main、golbal_init_var)
- 在本目标文件中引用的全局符号,缺没有定义在本目标文件中。(printf)
- 段名,
- 局部符号,编译单元内部可见。
- 行号信息。
1
2
3
4
5
6
7
8
9
10
nm SimpleSection.o // 查看符号结果
0000000000000000 T func1
0000000000000000 D global_init_var
U _GLOBAL_OFFSET_TABLE_
0000000000000004 C global_uint_var
0000000000000024 T main
U printf
0000000000000004 d static_var.1802
0000000000000000 b static_var2.1803
3.5.1 ELF符号表结构
ELF符号表中往往是文件中的一个段,段名一般叫做”.symtab”.它是一个Elf32_Sym结构的数组,具体如下:
1
2
3
4
5
6
7
8
typedef strcut {
Elfd32_Word st_name;
Elfd32_Addr st_value;
Elf32_Word st_size;
unsigned char st_info;
unsigned char st_other;
Elf32_Half st_shndx;
} Elf32_Symn;
st_name 符号名,该符号在字符串表中的下标。
st_value 如果是函数或者变量名,则为地址。特殊情况参考后期(深入静态链接)。
st_size 大小。
st_info (符号类型和绑定信息) 32位, 前4位表示符号类型(数组,变量,或者为可执行函数),后28位表示符号绑定信息(全局符号,或局部符号,或弱引用)。
st_other 暂时没用这个字段。
st_shndx(符号所在的段) 符号定义在本目标文件? 或者符号定义在引用文件中? 暂略。
符号表可视化命令,readelf -s SimpleSection.o
,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
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.1802
7: 0000000000000000 4 OBJECT LOCAL DEFAULT 4 static_var2.1803
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
12: 0000000000000004 4 OBJECT GLOBAL DEFAULT COM global_uint_var
13: 0000000000000000 36 FUNC GLOBAL DEFAULT 1 func1
14: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_
15: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf
16: 0000000000000024 51 FUNC GLOBAL DEFAULT 1 main
- func1 和 main 都在段1,.text代码段中。
- printf被引用,但是没有定义,所以被记为UND。
- global_init_var 是已经初始化的全局变量,被定义在.bss段。
- global_uint_var 参考COMMON块。
- static_var.1802 和 static_var2.1803,仅单元内部可见。 至于为什么会改名, 下节符号修饰中描述。
3.5.2 特殊符号
使用ld链接器来链接生产可执行文件时,它会自定义很多特殊符号。
3.5.3 c++符号修饰与函数签名
为什么要引入符号修饰和函数签名? 在70年代以前,在编译器产生目标文件时,符号名与相应的变量名是一样的。 A 语言定义了 foo函数名, B语言的foo函数就会相应冲突。
符号修饰和函数签名。 符号修饰: 将变量编译前后增加规则,例如
int func(int) 编译后 _Z$funci
. 函数签名: 与上述类似, 具体命名转换规则这里不展开描述。几个重点。
- 变量类型没有符号修饰,double或者int都没有。
- 名称修饰可用于静态变量。
- 不同的编译器采用不同的名字修饰方法,所以不同的编译器产生的目标文件无法相互链接。
4. 推荐几篇博客
有关强弱符号 强、弱符号,强、弱引用;
.h 和 .c 文件解析 文件解析
为什么需要extern c 为什么需要extern c