第三章:目标文件里有什么
3.2 目标文件是什么样
总体来说,目标文件编译后主要分成两段:程序指令和程序数据. 代码(.text)属于指令段,而.data和.bss段属于程序数据
为什么要分开?
- 安全 程序映射到内存区域, 程序指令区域内存映射为只读,程序数据区域内存映射为可读写
- cpu缓存 现代CPU设计成数据缓存和指令缓存,对CPU命中缓存率提高有好处
- 最重要原因 系统运行多个该程序副本,他的指令都是一样,所以内存只需保存一份该程序指令 (尤其动态链接中)
3.3.2 数据段和只读数据段
.data段保存的是初始化了的全局静态变量和局部静态变量
.rodata段存放的是只读数据(const修饰的变量)
.bss段存放的是未初始化的全局变量和局部静态变量
3.3.4 其他段
.
作为前缀,表示这些表的名字是系统保留的
自定义段,GCC提供扩展机制,使得程序可以指定变量所处段(自定义段名不能使用.命名)
__attribute__ ((section("FOO"))) int global = 42;
__attribute__ ((section("BAR"))) void foo(){};
3.4.3 ELF文件头
数据结构如下
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;
readelf -h
查看elf文件头信息, 其中比较重要的信息(我这是elf64,书中为32)
REL(Relocatable file),代表这是一个重定位文件,对应e_type
Start of section headers: 1104(bytes into file) 是段表在文件的偏移,对应e_shoff
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
3.4.2 段表
段表数据结构
{
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;
3.4.3 重定位表
链接器在处理目标文件时,需要对目标文件中的某些部分进行重定位,即代码段和数据段中那些绝对地址的引用位置,这些重定位信息记录在ELF文件的重定位表中, 对于每个需要重定位的代码段和数据段,都有个对应的重定位表
比如SimpleSection.o中的.rela.text
就是针对.text
的重定位表, 因为.text中至少有一个绝对地址的引用,那就是printf
函数的调用,而.data则没有,它只包含几个常量
$ readelf -S SimpleSection.o
There are 13 section headers, starting at offset 0x450:
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 // link表示符号表下标为10 info表示作用与下标为1的段(这里也就是.text段)
[ 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
000000000000007c 0000000000000000 0 0 1
[12] .shstrtab STRTAB 0000000000000000 000003e8
0000000000000061 0000000000000000 0 0 1
.rela.text
段的sh_type
为SHT_REL
类型,常量为1. 表示为重定位表
sh_link
表示符号表的下标, 上面的值为10 表示.symtab
段
sh_info
表示作用于哪个段,上面值为1,表示作用与.text
段
3.4.4 字符串表
ELF中用到许多字符串,比如变量名global_init_var
,函数名main
,文件名SimpleSection.c
,因为字符串长度往往不固定,固定结构表示比较苦难,常见是将字符串全部放到一个表中.
因此在ELF中引用字符串只需给出一个数字下标即可,在ELF文件中也是以段的形式保存,常见段名.strtab
字符串表和.shstrtab
段表字符串表
ps: 还记得段表嘛? 段表的名字就是在.shstrtab
中
实战 读取字符表和段表字符串表
段表开始处在文件的偏移为1104,总共13个段,描述段表的结构体大小为64,因此我们可以用hexdump直接读取.strtab
和 .shstrtab
在文件的偏移量
.strtab
段在文件 12 * 64 + 1104 = 1808
处开始
.shstrtab
段在文件 12 * 64 + 1104 = 1872
处开始
$ hexdump -x -s 1808 -n 64 SimpleSection.o
0000710 0009 0000 0003 0000 0000 0000 0000 0000
0000720 0000 0000 0000 0000 02c0 0000 0000 0000
0000730 007c 0000 0000 0000 0000 0000 0000 0000
0000740 0001 0000 0000 0000 0000 0000 0000 0000
0000750
$ hexdump -x -s 1872 -n 64 SimpleSection.o
0000750 0011 0000 0003 0000 0000 0000 0000 0000
0000760 0000 0000 0000 0000 03e8 0000 0000 0000
0000770 0061 0000 0000 0000 0000 0000 0000 0000
0000780 0001 0000 0000 0000 0000 0000 0000 0000
0000790
sh_offset
在Elf64_Shdr
结构体中的偏移为24字节,大小为8h
, 表示字符串内容在文件的偏移处,因此字符串表.strtab
的内容在文件的02c0
处
段表字符串表在文件的.shstrtab
在文件的03e8
处,继续hexdump这两处,我们已经看到字符串表中的内容和段表字符串表的内容
3.5 链接的接口-符号
我们将函数和变量名统称为符号,函数名和变量名通称为符号名,符号看作是链接中的粘合剂,链接整个过程基于符号才能正确完成, ELF文件中符号表是一个段.symtab
每个目标文件中都有一个相应符号表,这个表中记录所有用到的符号(readelf -s
可查看),每个符号有一个对应的值,叫做符号值,对于变量和函数来说,符号值就是他们的地址
符号分类:
3.5.1 符号表数据结构
符号表的数据结构
typedef struct
{
Elf64_Word st_name; /* Symbol name (string tbl index) */ //对应字符串表的下标
unsigned char st_info; /* Symbol type and binding */ //高28位 绑定 低4 类型
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;
3.5.3 符号修饰和函数签名
这节主要讲C++实现不同函数的重载,通过修饰符号, 例如void foo(int a)
和 void foo(double a)
,编译器会生成不同的符号和函数签名已区分
3.5.4 extern "C"
以memset(void *, int, size_t)
为例, 如果没有extern关键字,编译器会当作C++函数,因此查找符号的时候可能会查找不到
正确的写法
#ifdef __cplusplus
extern "C" {
#endif
void *memset(void *, int, size_t);
#ifdef __cplusplus
}
#endif
3.5.5 弱符号 强符号
gcc中使用__attribure__((wake))
定义一个强符号为弱符号,例子:
extern int ext; //既非弱符号也非强符号
int wake; // 弱符号 未初始化
int stronge; //强符号
__attribure__((wake)) wake2 = 2; // 虽然初始化了,但是使用attr定义为弱符号
int main(){} //强符号
- 不允许强符号被多次定义
- 如果一个符号在不同文件中,分别为强符号和弱符号,那么选中强符号
- 如果一个符号在不同文件中都是弱符号,选择占用空间最大的
比如int global
四字节double global
8字节 链接后,符号global占8字节
gcc中使用__attribure__((wakeref))
定义一个强引用为弱引用,例子:
//此时编译不会报错,因为foo是一个弱引用,foo地址为0
//但会出现运行时错误
__attribure__((wakeref)) void foo();
int main(){
foo();
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix