文章目录
一、ELF文件的种类
Linux下符合ELF格式的文件主要有四种:可重定位文件(目标文件)、可执行文件、共享目标文件、核心转储文件。
- 可重定位文件,也常叫目标文件。包含二进制代码和数据,其可以在编译时与其他可重定位目标文件合并起来,创建一个可执行文件
- 可执行文件。包含二进制代码和数据,其形式可以被直接拷贝到存储器并执行
- 共享目标文件。一种特殊类型的可重定位目标文件,可以在加载或者运行时被动态地加载到存储器并链接
- 核心转储文件。当进程意外终止时,系统可以将该进程的地址空间的内容及终止时的一些其他信息转储到核心转储文件。
目标文件就是源代码编译后但未进行链接的那些中间文件(Linux下的.o文件),它跟可执行文件的内容与结构很相似,所以一般跟可执行文件格式一起采用一种格式存储。从广义上看,目标文件与可执行文件的格式其实几乎是一样的,所以我们可以广义地将目标文件与可执行文件看成是一种类型的文件。
在Linux 下使用file file-name命令来查看相应的文件格式:
$ ls
objdump_h.log objdump_s_d.log SimpleSection.c SimpleSection.o
$ file objdump_h.log
objdump_h.log: UTF-8 Unicode text
$ file SimpleSection.o
SimpleSection.o: ELF 64-bit LSB relocatable, ARM aarch64, version 1 (SYSV), not stripped
$ file SimpleSection.c
SimpleSection.c: C source, ASCII text
二、ELF文件的格式
ELF 格式被用于描述目标文件、可执行文件、核心转储文件以及共享库的所有信息。无论在什么场合,使用 ELF 格式的目的只有一个,那就是把机器代码及其对应的元数据以方便链接器和加载器处理的形式保存起来。代码的元数据指的是如下的信息:
- 代码文件的大小以及转换前的源代码文件名
- 符号信息(存放在符号表)
- 重定位信息
- 调试信息
如果以节头(section header,也就是段表)信息来处理,则可以解释成节集合;如果以程序头(program header)信息来处理,则 ELF 文件可以解释成段集合。
- 节(section)是汇编器、链接器等处理 ELF 文件内容的单位。ELF 文件把不同目的的代码、数据等分割成节保存。譬如机器码统一保存到
.text节中,全局变量的初始化数据则保存在.data节中。 - 段(segment)则是把程序加载到内存的加载器处理 ELF 文件时的单位。段由 1 个以上的节构成。内存上不同范围有着只读、可写、可执行等不同属性,因而需要根据属性进行分段。譬如机器码如果不可执行就毫无意义,因此要统一到具有可执行属性的段中。
节和段本质上都表示一个一定长度的区域。如果是可重定位文件(目标文件),内容叫做节的集合;如果是可执行文件、共享目标文件、核心转储文件,内容叫做段的集合。后面内容都将其叫做段,注意区分。

上图就是ELF文件的结构,可以看到,ELF文件的开头是一个文件头(readelf -h elf-file命令可查看文件头内容),它描述了整个文件的文件属性,包括文件是否可执行、是静态链接还是动态链接及入口地址(如果是可执行文件)、目标硬件、目标操作系统等信息。
文件头还包括一个段表(Section Table,也就是section header)(readelf -S elf-file命令可查看段表结构),段表其实是一个描述文件中各个段的数组。段表描述了文件中各个段在文件中的偏移位置及段的属性等,从段表里面可以得到每个段的所有信息。
文件头后面就是各个段的内容。程序源代码编译后的机器指令经常被放在代码段(Code Section),代码段常见的名字有.code或.text;全局变量和局部静态变量数据经常放在数据段(Data Section),数据段的一般名字都叫.data。
一般C语言的编译后执行语句都编译成机器代码,保存在.text段;已初始化的全局变量和局部静态变量都保存在.data段;未初始化的全局变量和局部静态变量一般放在一个叫.bss的段里。未初始化的全局变量和局部静态变量默认值都为0,本来它们也可以被放在.data段,但是因为它们都是0,所以为它们在.data段分配空间并且存放数据0是没有必要的。程序运行的时候它们的确是要占内存空间的,并且可执行文件必须记录所有未初始化的全局变量和局部静态变量的大小总和,记为.bss段。所以.bss段只是为未初始化的全局变量和局部静态变量预留位置而已,它并没有内容,所以它在文件中也不占据空间。
总体来说,程序源代码被编译以后主要分成两种段:程序指令和程序数据。代码段属于程序指令,而数据段和.bss段属于程序数据。 让数据和指令分段主要有以下好处:
- 一方面是当程序被装载后,数据和指令分别被映射到两个虚存区域。由于数据区域对于程序来说是可读写的,而指令区域对于进程来说是只读的,所以这两个虚存区域的权限可以被分别设置成可读写和只读。这样可以防止程序的指令被有意或无意地改写。
- 另外一方面是对于现代的CPU来说,它们有着极为强大的缓存(Cache)体系。由于缓存在现代的计算机中地位非常重要,所以程序必须尽量提高缓存的命中率。指令区和数据区的分离有利于提高程序的局部性。现代CPU的缓存一般都被设计成数据缓存和指令缓存分离,所以程序的指令和数据被分开存放对CPU的缓存命中率提高有好处。
- 第三个原因,其实也是最重要的原因,就是当系统中运行着多个程序的副本时,它们的指令都是一样的,所以内存中只须要保存一份该程序的指令部分。对于指令这种只读的区域来说是这样,对于其它的只读数据也一样,比如很多程序里面带有的图标、图片、文本等资源也是属于可以共享的。当然每个副本进程的数据区域是不一样的,它们是进程私有的。不要小看这个共享指令的概念,它在现代的操作系统里面占据了极为重要的地位,特别是在有动态链接的系统中,可以节省大量的内存。
三、挖掘SimpleSection.o
SimpleSection.c文件内容:
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;
}
执行$ gcc -c SimpleSection.c会生成目标文件SimpleSection.o,执行objdump -h SimpleSection.o,打印SimpleSection.o文件的各个段信息:
$ objdump -h SimpleSection.o
SimpleSection.o: 文件格式 elf64-littleaarch64
节:
Idx Name Size VMA LMA File off Algn
0 .text 00000074 0000000000000000 0000000000000000 00000040 2**2
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000008 0000000000000000 0000000000000000 000000b4 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000004 0000000000000000 0000000000000000 000000bc 2**2
ALLOC
3 .rodata 00000004 0000000000000000 0000000000000000 000000c0 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .comment 00000012 0000000000000000 0000000000000000 000000c4 2**0
CONTENTS, READONLY
5 .note.GNU-stack 00000000 0000000000000000 0000000000000000 000000d6 2**0
CONTENTS, READONLY
6 .eh_frame 00000060 0000000000000000 0000000000000000 000000d8 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
打印的段中除了最基本的代码段(.text)、数据段(.data)和BSS段(.bss)以外,还有只读数据段(.rodata)、注释信息段(.comment)、堆栈提示段(.note.GNU-stack)、异常处理段(.eh_frame)。
Size为段的长度,File off为段所在的位置;CONTENTS、ALLOC等表示段的各种属性;CONTENTS表示该段在文件中存在。BSS段没有CONTENTS,表示它实际上在ELF文件中不存在内容。.note.GNU-stack段虽然有CONTENTS,但它的长度为0,是个很古怪的段,暂且认为它在ELF文件中也不存在。那么ELF文件中实际存在的是.text、.data、.rodata、.comment、.note.gnu.property、.eh_frame段。
下图是SimpleSection.o文件各个段的基本布局:

3.1 代码段
objdump的-s参数可以将所有段的内容以十六机制的方式打印出来,-d参数可以将所有包含指令的段反汇编。以下是AArch64架构下objdump -s -d SimpleSection.o指令的部分输出:
Idx Name Size VMA LMA File off Algn
0 .text 00000074 0000000000000000 0000000000000000 00000040 2**2
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
Contents of section .text:
0000 fd7bbea9 fd030091 a01f00b9 00000090 .{..............
0010 00000091 a11f40b9 00000094 1f2003d5 ......@...... ..
0020 fd7bc2a8 c0035fd6 fd7bbea9 fd030091 .{...._..{......
0030 20008052 a01f00b9 00000090 00000091 ..R............
0040 010040b9 00000090 00000091 000040b9 ..@...........@.
0050 2100000b a01f40b9 2100000b a01b40b9 !.....@.!.....@.
0060 2000000b 00000094 a01f40b9 fd7bc2a8 .........@..{..
0070 c0035fd6 .._.
...
0000000000000000 <func1>:
0: a9be7bfd stp x29, x30, [sp, #-32]!
4: 910003fd mov x29, sp
8: b9001fa0 str w0, [x29, #28]
...
0000000000000028 <main>:
28: a9be7bfd stp x29, x30, [sp, #-32]!
2c: 910003fd mov x29, sp
30: 52800020 mov w0, #0x1 // #1
34: b9001fa0 str w0, [x29, #28]
...
Contents of section .text就是.text的数据以十六进制方式打印出来的内容,总共0x74字节,跟前面执行-h中.text段长度相符合,最左面一列是偏移量,中间4列是十六进制内容,最右面一列是.text段的ASCII码形式。对照下面的反汇编结果,可以明显地看到,.text段里面所包含的正是SimpleSection.c里两个函数func1()和main()的指令。.text段的前四个字节0xfd7bbea9就是func1()函数的第一条stp x29, x30, [sp, #-32]!指令,而最后四个字节0xc0035fd6正是main()函数的最后一条指令ret(注意这里是小端排列)。
3.2 数据段和只读数据段
.data段保存的是那些已经初始化了的全局静态变量和局部静态变量。SimpleSection.c代码里面一共有两个这样的变量,分别是global_init_var和static_var,这两个变量每个4个字节,一共刚好8个字节,所以.data这个段的大小为8个字节。
.data段里的前4个字节,从低到高分别为0x54,0x00,0x00,0x00。这个值刚好是global_init_var,即十进制的84。global_init_var是个4字节长度的int类型,为什么存放的次序是0x54,0x00,0x00,0x00而不是0x00,0x00,0x00,0x54?这涉及CPU的字节序(Byte Order)的问题,也就是所谓的大端(Big-endian)和小端(Little-endian)的问题。而最后4个字节刚好是static_var的值,即85.
Idx Name Size VMA LMA File off Algn
1 .data 00000008 0000000000000000 0000000000000000 000000b4 2**2
CONTENTS, ALLOC, LOAD, DATA
Contents of section .data:
0000 54000000 55000000 T...U...
SimpleSection.c里面在调用printf的时候,用到了一个字符串常量%d\n,它是一种只读数据,所以它被放到了.rodata段,我们可以从输出结果看到.rodata这个段的4个字节刚好是这个字符串常量的ASCII字节序,最后以"\0"结尾。
.rodata段存放的是只读数据,一般是程序里面的只读变量(如const修饰的变量)和字符串常量。单独设立.rodata段有很多好处,不光在语义上支持了C++的const关键字,而且操作系统在加载的时候可以将.rodata段的属性映射成只读,这样对于这个段的任何修改操作都会作为非法操作处理,保证了程序的安全性。另外在某些嵌入式平台下,有些存储区域是采用只读存储器的,如ROM,这样将.rodata段放在该存储区域中就可以保证程序访问存储器的正确性。
Idx Name Size VMA LMA File off Algn
3 .rodata 00000004 0000000000000000 0000000000000000 000000c0 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
Contents of section .rodata:
0000 25640a00 %d..
另外值得一提的是,有时候编译器会把字符串常量放到.data段,而不会单独放在.rodata段。
3.3 BSS段
.bss段存放的是未初始化的静态变量,以及初始化为 0 的全局或静态变量。
global_uninit_var和static_var2就是存放在.bss段,其实更准确的说法是.bss段为它们预留了空间。但是我们可以看到该段的大小只有4个字节,这与global_uninit_var和static_var2的大小的8个字节不符。
其实我们可以通过符号表(Symbol Table)看到,只有static_var2被存放了.bss段,而global_uninit_var却没有被存放在任何段,只是一个未定义的COMMON符号。这其实是跟不同的语言与不同的编译器实现有关,有些编译器会将全局的未初始化变量存放在目标文件.bss段,有些则不存放,只是预留一个COMMON的全局变量符号,等到最终链接成可执行文件的时候再在.bss段分配空间。
Idx Name Size VMA LMA File off Algn
2 .bss 00000004 0000000000000000 0000000000000000 000000bc 2**2
ALLOC
COMMON 和 .bss 的区别很细微。现代的 GCC 版本根据以下规则来将可重定位目标文件中的符号分配到 COMMON 和 .bss 中:
| 名称 | 规则 |
|---|---|
| COMMON | 未初始化的全局变量 |
| .bss | 未初始化的静态变量,以及初始化为 0 的全局或静态变量 |
3.4 其他段
除了.text、.data、.bss这3个最常用的段之外,ELF文件也有可能包含其它的段,用来保存与程序相关的其它信息,参见本章六、常见节总结。
这些段的名字都是由.作为前缀,表示这些表的名字是系统保留的,应用程序也可以使用一些非系统保留的名字作为段名。比如我们可以在ELF文件中插入一个music的段,里面存放了一首MP3音乐,当ELF文件运行起来以后可以读取这个段播放这首MP3。但是应用程序自定义的段名不能使用.作为前缀,否则容易跟系统保留段名冲突。一个ELF文件也可以拥有几个相同段名的段,比如一个ELF文件中可能有两个或两个以上叫做.text的段。还有一些保留的段名是因为ELF文件历史遗留问题造成的,以前用过的一些名字如.sdata、.tdesc、.sbss、.lit4、.lit8、.reginfo、.gptab、.liblist、.conflict。可以不用理会这些段,它们已经被遗弃了。
使用objcopy工具给ELF文件添加段:
- 添加一个自定义的段到ELF文件,从而产生一个新的ELF文件,段的内容由一个文件指定:
objcopy --add-section section_name=file_name elf_file new_elf_file - 将ELF文件中指定的段拷贝出来,存放到一个文件中:
objcopy --only-section=section_name elf_file copy_file - 在ELF文件中删除一个指定名称的段:
objcopy -R section_name elf_file new_elf_file
3.5 自定义段
正常情况下,GCC编译出来的目标文件中,代码会被放到.text段,全局变量和静态变量会被放到.data和.bss段。但是有时候你可能希望变量或某些部分代码能够放到你所指定的段中去,以实现某些特定的功能。比如为了满足某些硬件的内存和I/O的地址布局,或者是像Linux操作系统内核中用来完成一些初始化和用户空间复制时出现页错误异常等。GCC提供了一个扩展机制,使得程序员可以指定变量所处的段:
__attribute__((section(FOO))) int global = 42;
__attribute__((section(BAR))) void foo() {}
我们在全局变量或函数之前加上__attribute__((section(name)))属性就可以把相应的变量或函数放到以name作为段名的段中。
四、ELF文件结构描述
ELF目标文件格式的最前部是ELF文件头(ELF Header),它包含了描述整个文件的基本属性,比如ELF文件版本、目标机器型号、程序入口地址等。紧接着是ELF文件各个段。其中ELF文件中与段有关的重要结构就是段表(Section Header Table),该表描述了ELF文件包含的所有段的信息,比如每个段的段名、段的长度、在文件中的偏移、读写权限及段的其它属性。
4.1 文件头
可以使用readelf -h elf_file查看ELF文件头,如下图所示(这里是Arm64,同下文中的Intel 80386部分内容对不上,但不影响理解):

从输出结果可以看到,ELF的文件头中定义了ELF魔数、文件机器字节长度、数据存储方式、版本、运行平台、ABI版本、ELF重定位类型、硬件平台、硬件平台版本、入口地址、程序头入口和长度、段表的位置和长度及段的数量等。
4.1.1 数据表示形式
为了对每个成员的大小做出明确的规定以便于在不同的编译环境下都拥有相同的字段长度,elf.h使用typedef定义了一套自己的变量体系,32位和64为数据类型如下表所示:
| 名称 | 大小 | 对齐 | 目的 |
|---|---|---|---|
| Elf32_Addr | 4 | 4 | 无符号程序地址 |
| Elf32_Half | 2 | 2 | 无符号中整数 |
| Elf32_Off | 4 | 4 | 无符号文件偏移 |
| Elf32_Sword | 4 | 4 | 带符号整数 |
| Elf32_Word | 4 | 4 | 无符号整数 |
| unsigned char | 1 | 1 | 无符号小整数 |
| ELf64_Addr | 8 | 8 | 无符号程序地址 |
| Elf64_Half | 2 | 2 | 无符号中整数 |
| Elf64_Off | 8 | 8 | 无符号文件偏移 |
| Elf64_Sword | 4 | 4 | 带符号整数 |
| Elf64_Word | 4 | 4 | 无符号整数 |
| Elf64_Xword | 8 | 8 | 无符号长整数 |
| Elf64_Sxword | 8 | 8 | 带符号长整数 |
| unsigned char | 1 | 1 | 无符号小整数 |
目标文件格式定义的所有数据结构都遵循相关类的自然大小和对齐规则。数据结构可以包含显式填充,以确保 4 字节目标的 4 字节对齐,从而强制结构大小为 4 的倍数,依此类推。数据在文件的开头也会适当对齐。例如,包含 Elf32_Addr 成员的结构在文件中与 4 字节边界对齐。同样,包含 Elf64_Addr 成员的结构与 8 字节边界对齐。
4.1.2 ELF文件头结构
ELF文件头结构及相关常数被定义在/usr/include/elf.h里,因为ELF文件在各种平台下通用,ELF文件有32位版本和64位版本。它的文件头结构也有这两种版本,分别叫做Elf32_Ehdr和Elf64_Ehdr。32位版本与64位版本的ELF文件的文件头内容是一样的,只不过有些成员的大小不一样。
64位版本的文件头结构Elf64_Ehdr定义如下:
#define EI_NIDENT (16)
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;
ELF文件头结构跟前面readelf -h SimpleSection.o输出的ELF文件头信息相比照,可以看到输出的信息与ELF文件头中的结构很多都一一对应。有点例外的是Elf64_Ehdr中的e_ident这个成员对应了readelf输出结果中的类别(Class)、数据(Data)、Version、OS/ABI和ABI Version这5个参数。剩下的参数与Elf64_Ehdr中的成员基本一一对应。
4.1.3 ELF标识(e_ident)
可以从前面readelf的输出看到,最前面的Magic的16个字节刚好对应Elf64_Ehdr的e_ident这个成员。这16个字节被ELF标准规定用来标识ELF文件的平台属性,比如这个ELF字长(32位/64位)、字节序、ELF文件版本。
最开始的4个字节是所有ELF文件都必须相同的标识码,分别为0x7F、0x45、0x4c、0x46,第一个字节对应ASCII字符里面的DEL控制符,后面3个字节刚好是ELF这3个字母的ASCII码。这4个字节又被称为ELF文件的魔数,几乎所有的可执行文件格式的最开始的几个字节都是魔数。这种魔数用来确认文件的类型,操作系统在加载可执行文件的时候会确认魔数是否正确,如果不正确会拒绝加载。
第5个字节是用来标识ELF的文件类的,0x01表示是32位的,0x02表示是64位的;
名称 值 含义
ELFCLASSNONE 0 无效类
ELFCLASS32 1 32位目标文件
ELFCLASS64 2 64目标文件
第6个字节是字节序,规定该ELF文件是大端的还是小端的。
名称 值 含义
ELFDATANONE 0 无效数据编码
ELFDATA2LSB 1 小端
ELFDATA2MSB 2 大端
第7个字节规定ELF文件的主版本号,一般是1,因为ELF标准自1.2版以后就再也没有更新了,该值必须为 EV_CURRENT。
后面的9个字节ELF标准没有定义,一般填0,有些平台会使用这9个字节作为扩展标志。
4.1.4 文件类型(e_type)
e_type成员表示ELF文件类型,有以下几种ELF文件类型,每个文件类型对应一个常量。系统通过这个常量来判断ELF的真正文件类型,而不是通过文件的扩展名。相关常量以ET_开头,如下图所示:
常量 值 含义
ET_NONE 0 无文件类型
ET_REL 1 可重定位文件
ET_EXEC 2 可执行文件
ET_DYN 3 共享目标文件
ET_CORE 4 核心文件
ET_LOPROC 0xff00 特定于处理器
ET_HIPROC 0xffff 特定于处理器
虽然未指定核心文件内容,但类型 ET_CORE 保留用于标记文件。从 ET_LOPROC 到ET_HIPROC 之间的值(包括这两个值)保留用于特定于处理器的语义。其他值保留供将来使用。
4.1.5 机器类型(e_machine)
ELF文件格式被设计成可以在多个平台下使用。这并不表示同一个ELF文件可以在不同的平台下使用,而是表示不同平台下的ELF文件都遵循同一套ELF标准。e_machine成员就表示该ELF文件的平台属性,相关常量以EM_开头,如下图所示:
常量 值 含义
EM_NONE 0 无计算机
EM_SPARC 2 SPARC
EM_386 3 Intel 80386
EM_SPARC32PLUS 18 Sun SPARC 32+
EM_SPARCV9 43 SPARC V9
EM_AMD64 62 AMD 64
4.1.6 文件版本(e_version)
标识目标文件版本,如下表中所列。
常量 值 含义
EV_NONE 0 无效版本
EV_CURRENT >=1 当前版本
值 1 表示原始文件格式。EV_CURRENT 的值可根据需要进行更改,以反映当前版本号。
4.1.7 入口地址(e_entry)
规定ELF程序执行时的入口虚拟地址,操作系统加载完该程序后,从这个位置开始执行进程的指令。系统首先将控制权转移到该地址,进而启动进程。可重定位文件一般没有入口地址,则这个值为0。
4.1.8 程序头表和节头表偏移(e_phoff、e_shoff)
程序头表在目标文件中的偏移(以字节为单位)。如果文件没有程序头表,则e_phoff的值为零。
节头表在目标文件中的偏移(以字节为单位)。如果文件没有节头表,则e_shoff的值为零。
4.1.9 程序/节/文件头大小(e_xxsize)
e_ehsize:ELF 文件头本身的大小(以字节为单位)。
e_phentsize:程序头大小。程序头是程序头表中的一项,也就是sizeof(Elf64_Phdr),所有项的大小都相同。
e_phnum:程序头表中的项数。e_phentsize 和 e_phnum 的积指定了程序头表的大小,如果文件没有程序头表,则 e_phnum 值为零。
e_shentsize:节头大小。节头是节头表中的一项,也就是sizeof(Elf64_Shdr),所有项的大小都相同。
e_shnum:节头表中的项数。e_shentsize 和 e_shnum 的积指定了程序头表的大小,如果文件没有程序头表,则 e_shnum 值为零。
4.1.10 机器类型(e_flags)
与文件关联的特定于处理器的标志。标志名称采用 EF_machine_flag 形式。对于x86,此成员目前为零。下表中列出了 SPARC 标志。
常量 值 含义
EF_SPARC_EXT_MASK 0xffff00 供应商扩展掩码
EF_SPARC_32PLUS 0x000100 通用 V8+ 功能
EF_SPARC_SUN_US1 0x000200 Sun UltraSPARC 1 扩展
EF_SPARC_HAL_R1 0x000400 HAL R1 扩展
EF_SPARC_SUN_US3 0x000800 Sun UltraSPARC 3 扩展
EF_SPARCV9_MM 0x3 内存型号掩码
EF_SPARCV9_TSO 0x0 总体存储排序
EF_SPARCV9_PSO 0x1 部分存储排序
EF_SPARCV9_RMO 0x2 非严格内存排序
4.1.11 段表字符串表下标(e_shstrndx)
段表字符串表所在的段在段表中的下标,或者说与节名称字符串表关联的项在节头表的索引。
如果文件没有节名称字符串表,则此成员值为 SHN_UNDEF。
如果节名称字符串表的节索引大于或等于 SHN_LORESERVE (0xff00),则此成员值为SHN_XINDEX (0xffff),节名称字符串表的实际节索引包含在节头中索引为 0 的sh_link 字段中。否则,初始节头项的 sh_link 成员值为零。
4.2 节头表(段表)
节头表也就是段表,readelf -S 、objdump -h elf-file命令可查看段表结构,即一个ELF文件有那些段。
使用目标文件的节头表,可以定位文件的所有节。节头表是 Elf32_Shdr 或 Elf64_Shdr结构的数组。节头表索引是此数组的下标。ELF 头的 e_shoff 成员表示从文件的起始位置到节头表的字节偏移,e_shnum 成员表示节头表包含的项数,e_shentsize 成员表示每一项的大小(以字节为单位)。
如果节数大于或等于 SHN_LORESERVE (0xff00),则 e_shnum 值为 SHN_UNDEF (0)。节头表的实际项数包含在段中索引为 0 的 sh_size 字段中。否则,初始项的 sh_size 成员值为零。
如果上下文中限制了索引大小,则会保留部分节头表索引。例如,符号表项的st_shndx 成员以及 ELF 头的 e_shnum 和 e_shstrndx 成员。在这类上下文中,保留的值不表示目标文件中的实际各节。同样在这类上下文中,转义值表示会在其他位置(较大字段中)找到实际节索引。
ELF文件里面很多地方采用了这种与节头表类似的数组方式保存。一般定义一个固定长度的结构,然后依次存放。这样我们就可以使用下标来引用某个结构。
ELF节头表的这个数组的第一个元素是无效的段描述符,它的类型为NULL,除此之外每个段描述符都对应一个段。在某些情况下,节头表的第一个元素也有特殊用途,如 4.1.10 及上文提到的“段中索引为 0”中的字段用途。
4.2.1 特殊节索引
对于每个索引值的详细的介绍,请参考:殊节索引。
SHN_UNDEF 0
SHN_LORESERVE 0xff00
SHN_LOPROC 0xff00
SHN_BEFORE 0xff00
SHN_AFTER 0xff01
SHN_AMD64_LCOMMON 0xff02
SHN_HIPROC 0xff1f
SHN_LOOS 0xff20
SHN_LOSUNW 0xff3f
SHN_SUNW_IGNORE 0xff3f
SHN_HISUNW 0xff3f
SHN_HIOS 0xff3f
SHN_ABS 0xfff1
SHN_COMMON 0xfff2
SHN_XINDEX 0xffff
SHN_HIRESERVE 0xffff
虽然索引 0 保留为未定义的值,但节头表包含对应于索引 0 的项。即,如果 ELF 头 的 e_shnum 成员表示文件在节头表中具有 6 项,则这些节的索引为 0 到 5。初始项的内容会在本节的后面指定。
4.2.2 节头结构
节包含目标文件中的所有信息,但 ELF 头、程序头表和节头表除外。此外,目标文件中的各节还满足多个条件:
- 目标文件中的每一节仅有一个说明该节的节头。可能会有节头存在但节不存在的情况。
- 每一节在文件中占用可能为空的一系列相邻字节。
- 文件中的各节不能重叠。文件中的字节不能位于多个节中。
- 目标文件可以包含非活动空间。各种头和节可能不会包括目标文件中的每个字节。非活动数据的内容未指定。
64位的节头结构Elf64_Shdr被定义在/usr/include/elf.h,代码清单如下:
typedef struct
{
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;
结构体Elf64_Shdr的各个成员的含义如下图所示:
sh_name:节的名称。此成员值是一个名叫.shstrtab的节头字符串表节的索引,用于指定以空字符结尾的字符串的位置。节头字符串表节的索引,用于指定以空字符结尾的字符串的位置。事实上节的名称对于编译器、链接器来说是有意义的,但是对于操作系统来说并没有实质的意义,对于操作系统来说,一个节该如何处理取决于它的属性和权限,即由节的类型和节的标志位这两个成员决定。sh_addr:如果节显示在进程的内存映像中,也就是该节被加载到进程,则此成员会指定节的第一个字节所在的地址。否则,此成员值为零。sh_offset:从文件的起始位置到节中第一个字节的字节偏移。对于SHT_NOBITS节,此成员表示文件中的概念性偏移,因为该节在文件中不占用任何空间。sh_size:节的大小(以字节为单位)。除非节类型为SHT_NOBITS,否则该节将在文件中占用sh_size个字节。SHT_NOBITS类型的节大小可以不为零,但该节在文件中不占用任何空间。sh_addralign:一些节具有地址对齐约束。例如,如果某节包含双字,则系统必须确保整个节双字对齐。由于地址对齐都是2的整数倍,sh_addralign表示对其数量中的指数,即sh_addralign = 3表示2^3 = 8字节对齐。在此情况下,sh_addr的值在以sh_addralign的值为模数进行取模时,同余数必须等于0(sh_addr % (2 ^ sh_addralign ) == 0)。当前,仅允许使用0和2的正整数幂。值0和1表示节没有对齐约束。sh_entsize:一些节包含固定大小的项的表,如符号表。对于这样的节,此成员会指定每一项的大小(以字节为单位)。如果节不包含固定大小的项的表,则此成员值为零。sh_type、sh_flag、sh_link、sh_info,见下文介绍。
4.2.3 节的类型(sh_type)
节的名字只是在链接和编译过程中有意义,但它不能真正地表示段的属性。我们也可以将一个数据节命名为.text,对于编译器和链接器来说,主要决定节的属性的是节的类型(sh_type)和节的标志位(sh_flags)。
节的类型相关常量以SHT_开头,列举如下图所示(更多节的类型介绍,请参考:节的类型):
SHT_NULL
// 将节头标识为无效。此节头没有关联的节。节头的其他成员具有未定义的值。
SHT_PROGBITS
// 标识由程序定义的信息,这些信息的格式和含义完全由程序确定。程序段、数据段、代码段都是这种类型。
SHT_SYMTAB、SHT_DYNSYM、SHT_SUNW_LDYNSYM
// 标识符号表。通常,SHT_SYMTAB 节会提供用于链接编辑的符号。作为完整的符号表,该表可能包含许多对于动态链接不必要的符号。
// 因此,目标文件还可以包含一个 SHT_DYNSYM 节,其中包含一组尽可能少的动态链接符号,从而节省空间。
// SHT_DYNSYM 还可以使用 SHT_SUNW_LDYNSYM 节进行扩充。此附加节为运行时环境提供局部函数符号,但对于动态链接来说不是必需的。
// 当不可分配的 SHT_SYMTAB 不可用或已从文件中剥离时,调试器通过使用此节可在运行时上下文中产生精确的栈跟踪。
// 此节还可以为运行时环境提供其他符号信息,以便与 dladdr(3C) 一起使用。
// 如果 SHT_SUNW_LDYNSYM 节和 SHT_DYNSYM 节同时存在,链接编辑器会将这两者的数据区域紧邻彼此放置。
// SHT_SUNW_LDYNSYM 节位于 SHT_DYNSYM 节的前面。这种放置方式可以使两个表看起来像是一个更大的连续符号表,其中包含 SHT_SYMTAB 中的缩减符号集合。
// 有关详细信息,请参见符号表节。
SHT_STRTAB、SHT_DYNSTR
// 标识字符串表。目标文件可以有多个字符串表节。有关详细信息,请参见字符串表节。
SHT_RELA
// 标识包含显式加数的重定位项,如 32 位目标文件类的 Elf32_Rela 类型。目标文件可以有多个重定位节。有关详细信息,请参见重定位节。
SHT_HASH
// 标识符号散列表。动态链接的目标文件必须包含符号散列表。当前,目标文件只能有一个散列表,但此限制在将来可能会放宽。有关详细信息,请参见散列表节。
SHT_DYNAMIC
// 标识动态链接的信息。当前,目标文件只能有一个动态节。有关详细信息,请参见动态节。
SHT_NOTE
// 标识以某种方法标记文件的信息。有关详细信息,请参见注释节。
SHT_NOBITS
// 标识在文件中不占用任何空间(如.bss),但在其他方面与 SHT_PROGBITS 类似的节。虽然此节不包含任何字节,但 sh_offset 成员包含概念性文件偏移。
SHT_REL
// 标识不包含显式加数的重定位项,如 32 位目标文件类的 Elf32_Rel 类型。目标文件可以有多个重定位节。有关详细信息,请参见重定位节。
SHT_SHLIB
// 标识具有未指定的语义的保留节。包含此类型的节的程序不符合 ABI。
4.2.4 节的标志位(sh_flag)
节头的 sh_flags 成员包含用于说明节属性的 1 位标志。如果在 sh_flags 中设置了标志位,则该节的此属性处于启用状态。否则,此属性处于禁用状态,或者不适用。未定义的属性会保留并设置为零。
相关常量以SHF_开头,如下图所示(更多节的标志位介绍,请参考:节的标志位):
SHF_WRITE //表示节包含可写数据。如果这个标志被设置,链接器将允许对该节进行写操作。
SHF_ALLOC //表示节在内存中有对应的空间。这意味着该节会被加载到内存中,并在运行时被程序访问。
SHF_EXECINSTR //表示节包含指令。通常,这种类型的节用于存储可执行代码。
SHF_MERGE //表示节中的数据可以进行合并。通常用于存储重复的数据,以节省空间。
SHF_STRINGS //表示节包含字符串。这种类型的节经常用于存储程序中的字符串常量。
SHF_INFO_LINK //表示 sh_info 字段包含了与该节相关联的其他节的索引信息。
SHF_LINK_ORDER //表示该节有特殊的排序要求。链接器需要按照特定的顺序处理这些节。
SHF_OS_NONCONFORMING //表示该节需要特定于操作系统的处理。这种标志通常用于标识与标准操作系统不兼容的节。
SHF_GROUP //表示该节是某个节组的成员。节组用于将相关联的节组织在一起。
SHF_TLS //表示节包含线程本地存储(TLS)数据。这种类型的节用于存储与线程相关的数据。
SHF_COMPRESSED //表示节是压缩的。这意味着链接器在处理这种类型的节时需要进行解压缩操作。
SHF_MASKOS //表示操作系统特定的语义标志。
SHF_MASKPROC //表示处理器特定的语义标志。
对于系统保留段节的各个属性:
4.2.5 节的链接信息(sh_link、sh_info)
sh_link表示节头表索引链接,其解释依赖于节类型。
sh_info表示额外信息,其解释依赖于节类型;如果此节头的 sh_flags 字段包含属性 SHF_INFO_LINK,则此成员表示节头表索引。
如果节的类型是与链接相关的(不论是动态链接或静态链接),比如重定位表、符号表等,那么sh_link和sh_info这两个成员所包含的意义如下所示。对于其它类型的节,这两个成员没有意义。
| sh_type | sh_link | sh_info |
|---|---|---|
| SHT_DYNAMIC | 关联的字符串表的节头索引 | 0 |
| SHT_HASH | 关联的符号表的节头索引 | 0 |
| SHT_REL、SHT_RELA | 关联的符号表的节头索引 | 如果 sh_flags 成员包含 SHF_INFO_LINK 标志,则为应用重定位的节的节头索引,否则为 0。另请参见表 12-10 和重定位节 |
| SHT_SYMTAB、SHT_DYNSYM | 关联的字符串表的节头索引 | 比上一个局部符号 STB_LOCAL 的符号表索引大一。也就是第一个全局符号。 |
| SHT_GROUP | 关联的符号表的节头索引 | 关联的符号表中项的符号表索引。指定的符号表项的名称用于提供节组的签名 |
| SHT_SYMTAB_SHNDX | 关联的符号表的节头索引 | 0 |
| SHT_SUNW_cap | 如果符号功能存在,则为关联的 SHT_SUNW_capinfo 表的节头索引,否则为 0 | 如果任何功能引用指定的字符串,则为关联的字符串表的节头索引,否则为 0 |
| SHT_SUNW_capinfo | 关联的符号表的节头索引 | 对于动态目标文件,为关联的 SHT_SUNW_capchain 表的节头索引,否则为 0 |
| SHT_SUNW_symsort、SHT_SUNW_tlssort、SHT_SUNW_move、SHT_SUNW_versym | 关联的符号表的节头索引 | 0 |
| SHT_SUNW_LDYNSYM | 关联的字符串表的节头索引。此索引是与 SHT_DYNSYM 节所使用的字符串表相同的字符串表 | 比上一个局部符号 STB_LOCAL 的符号表索引大一。由于 SHT_SUNW_LDYNSYM 仅包含局部符号,因此 sh_info 等于表中的符号数 |
| SHT_SUNW_COMDAT | 0 | |
| SHT_SUNW_syminfo | 关联的符号表的节头索引 | 关联的 .dynamic 节的节头索引 |
| SHT_SUNW_verdef | 关联的字符串表的节头索引 | 节中版本定义的数量 |
| SHT_SUNW_verneed | 关联的字符串表的节头索引 | 节中版本依赖性的数量 |
4.3 重定位表
重定位是连接符号引用与符号定义的过程。例如,程序调用函数时,关联的调用指令必须在执行时将控制权转移到正确的目标地址。可重定位文件必须包含说明如何修改其节内容的信息。通过此信息,可执行文件和共享目标文件可包含进程的程序映像的正确信息。重定位项即是这些数据。
4.3.1 重定位表项结构
重定位项可具有以下结构。请参见 sys/elf.h。
typedef struct {
Elf32_Addr r_offset;
Elf32_Word r_info;
} Elf32_Rel;
typedef struct {
Elf32_Addr r_offset;
Elf32_Word r_info;
Elf32_Sword r_addend;
} Elf32_Rela;
typedef struct {
Elf64_Addr r_offset;
Elf64_Xword r_info;
} Elf64_Rel;
typedef struct {
Elf64_Addr r_offset;
Elf64_Xword r_info;
Elf64_Sxword r_addend;
} Elf64_Rela;
r_offset 字段
重定位修改位置的偏移。
对于可重定位文件来说,这个值是该重定位所要修正位置的第一个字节相对于段起始的偏移;
对于可执行文件或共享对象文件来说,这个值是该重定位入口索要修正位置的第一个字节的虚拟地址。此信息使重定位项对于运行时链接程序更为有用。
虽然为了使相关程序可以更有效地访问,不同目标文件的成员的解释会发生变化,但重定位类型的含义保持相同。
r_info 字段
重定位修改位置的类型和符号。这个成员的低8位表示重定位修改位置的类型,高24位表示重定位入口的符号在符号表中的下标。例如,调用指令的重定位项包含所调用的函数的符号表索引。如果索引是未定义的符号索引 STN_UNDEF,则重定位将使用零作为符号值。
重定位类型特定于处理器。重定位项的重定位类型或符号表索引是将 ELF32_R_TYPE 或 ELF32_R_SYM 分别应用于项的 r_info 成员所得的结果。
#define ELF32_R_SYM(info) ((info)>>8)
#define ELF32_R_TYPE(info) ((unsigned char)(info))
#define ELF32_R_INFO(sym, type) (((sym)<<8)+(unsigned char)(type))
#define ELF64_R_SYM(info) ((info)>>32)
#define ELF64_R_TYPE(info) ((Elf64_Word)(info))
#define ELF64_R_INFO(sym, type) (((Elf64_Xword)(sym)<<32)+ \
(Elf64_Xword)(type))
r_addend 字段:此成员指定常量加数,用于计算将存储在可重定位字段中的值。该字段所存储的值就是在进行重定位时需要额外加到可重定位字段值上的一个修正值。
Rela 项包含显式加数。Rel 类型的项会在要修改的位置中存储一个隐式加数。32 位 SPARC 仅使用 Elf32_Rela 重定位项。64 位 SPARC 和 64 位 x86 仅使用 Elf64_Rela 重定位项。因此,r_addend 成员用作重定位加数。x86 仅使用 Elf32_Rel 重定位项。要重定位的字段包含该加数。在所有情况下,加数和计算所得的结果使用相同的字节顺序。
重定位节可以引用其他两个节:符号表(由 sh_link 节头项标识)和要修改的节(由 sh_info 节头项标识)。节中指定了这些关系。如果可重定位目标文件中存在重定位节,则需要 sh_info 项,但对于可执行文件和共享目标文件,该项是可选的。重定位偏移满足执行重定位的要求。
在所有情况下,r_offset 值都会指定受影响存储单元的第一个字节的偏移或虚拟地址。重定位类型可指定要更改的位以及计算这些位的值的方法。
4.3.2 重定位计算
这些表示法用于说明在重定位计算过程中涉及到的各种值和位置,下面对它们进行简要解释:
- A: 用于计算可重定位字段的值的加数。通常,它表示了一个偏移量或者一个常数值。
- B: 执行过程中将共享目标文件装入内存的基本地址。在大多数情况下,共享目标文件的基本虚拟地址为 0,但是在执行过程中,它可能会被装载到不同的地址。
- G: 在执行过程中,重定位项的符号地址所在的全局偏移表中的偏移。这个值表示了符号在全局偏移表中的位置。
- GOT: 全局偏移表的地址。全局偏移表(GOT)存储了动态链接库中全局变量的地址,它是一种数据结构,用于解决全局变量的动态访问。
- L: 符号的过程链接表项的节偏移或地址。过程链接表(PLT)用于进行函数调用的动态链接,L 表示了符号在过程链接表中的位置。
- P: 使用 r_offset 计算出的重定位的存储单元的节偏移或地址。r_offset 是一个偏移值,用于计算重定位的目标位置。
- S: 索引位于重定位项中的符号的值。这个值通常是符号的地址。
- Z: 索引位于重定位项中的符号的大小。它表示了符号的大小。
4.4 字符串表
字符串表节包含以空字符结尾的字符序列,通常称为字符串。目标文件使用这些字符串表示符号和节的名称。可以将字符串作为字符串表节的索引进行引用。
第一个字节(索引零)包含空字符。同样,字符串表的最后一个字节也包含空字符,从而确保所有字符串都以空字符结尾。根据上下文,索引为零的字符串不会指定任何名称或指定空名称。
允许使用空字符串表节。节头的 sh_size 成员值为零。对于空字符串表,非零索引无效。
节头的 sh_name 成员包含节头字符串表的节索引。节头字符串表由 ELF 头的 e_shstrndx成员指定。
ELF文件中用到了很多字符串,比如节名、变量名等。因为字符串的长度往往是不定的,所以用固定的结构来表示它比较困难。一种很常见的做法是把字符串集中起来存放到一个表,然后使用字符串在表中的偏移来引用字符串。
下图显示了具有 25 个字节的字符串表,并且其字符串与各种索引关联。

下表显示了上图所示的字符串表中的字符串。

如示例所示,字符串表索引可以指向节中的任何字节。一个字符串可以出现多次。可以存在对子字符串的引用。一个字符串可以多次引用。另外,还允许使用未引用的字符串。
通过这种方法,在ELF文件中引用字符串只需给出一个数字下标即可,不用考虑字符串长度的问题。一般字符串表在ELF文件中也以Section的形式保存,常见的Section名为.strtab或.shstrtab。这两个字符串表分别为字符串表(String Table)和节头字符串表(Section Header String Table)。顾名思义,字符串表用来保存普通的字符串,比如符号的名字;节头字符串表用来保存Section表中用到的字符串,最常见的就是段名(sh_name)。
4.5 节合并
SHF_MERGE 节标志(sh_flag)可用于标记可重定位目标文件中的 SHT_PROGBITS(sh_type) 节。此标志表示该节可以与其他目标文件中的兼容节合并。这类合并有可能减小通过这些可重定位目标文件生成的任何可执行文件或共享目标文件的大小。减小文件大小还有助于改善生成的目标文件的运行时性能。
带有 SHF_MERGE 标志的节表示该节遵循以下特征:
- 该节为只读。包含该节的程序在运行时绝对不可能修改该节的数据。
- 从单独的重定位记录可以访问该节中的每一项。生成访问这些项的代码时,程序代码无法针对该节中的项的相对位置做出任何假设。
- 如果该节还设置了
SHF_STRINGS标志,那么该节只能包含以空字符结尾的字符串。空字符只能作为字符串结束符,而不能出现在任何字符串的中间位置。
SHF_MERGE 是一个可选标志,用于表示进行优化的可能性。允许链接编辑器执行优化,或忽略优化。任一情况下,链接编辑器都会创建一个有效的输出目标文件。当前,链接编辑器仅对包含使用 SHF_STRINGS 标志进行标记的字符串数据的节执行节合并。
同时设置了 SHF_STRINGS 节标志和 SHF_MERGE 标志时,该节中的字符串就可以与其他兼容节中的字符串合并。链接编辑器使用与用于压缩 SHT_STRTAB 字符串表(.strtab 和 .dynstr)的字符串压缩算法相同的字符串压缩算法来合并此类节。
- 重复字符串会缩减为一个。
- 会消除尾部字符串。例如,如果输入节包含字符串
"bigdog"和"dog",那么将消除较小的"dog",并使用较大的字符串的尾部表示较小的字符串。
目前,链接编辑器仅对由没有特殊对齐限制的单字节大小字符组成的字符串执行字符串合并。具体来说,必须具备以下节特征。
sh_entsize必须为0或1。不支持包含宽字符的节。- 仅合并字节对齐的节,其中
sh_addralign为0或1。
五、链接的接口——符号
链接过程的本质就是要把多个不同的目标文件之间相互粘到一起,在链接中,目标文件之间相互粘合实际上是目标文件之间对地址的引用,即对函数和变量的地址的引用。
比如目标文件B要用到了目标文件A中的函数foo,那么我们就称目标文件A定义(Define)了函数foo,称目标文件B引用(Reference)了目标文件A中的函数foo。这两个概念也同样适用于变量。每个函数或变量都有自己独特的名字,才能避免链接过程中不同变量和函数之间的混淆。
在链接中,我们将函数和变量统称为符号(Symbol),函数名或变量名就是符号名(Symbol Name)。
每一个目标文件都会有一个相应的符号表(Symbol Table),这个表里面记录了目标文件中所用到的(定义和引用)所有符号。每个定义的符号有一个对应的值,叫做符号值(Symbol Value),对应变量和函数来说,符号值就是它们的地址。除了函数和变量之外,还存在其它几种不常用到的符号。我们将符号表中所有的符号进行分类,它们有可能是下面这些类型中的一种:
- 定义在本目标文件的全局符号,可以被其它目标文件引用。比如SimpleSection.o里面的
func1、main和global_init_var。 - 在本目标文件中引用的全局符号,却没有定义在本目标文件,这一般叫做外部符号(External Symbol),也就是我们前面所讲的符号引用。比如SimpleSection.o里面的
printf。 - 段名,这种符号往往由编译器产生,它的值就是该段的起始地址。比如SimpleSection.o里面的
.text、.data等。 - 局部符号,这类符号只在编译单元内部可见。比如SimpleSection.o里面的
static_var和static_var2。调试器可以使用这些符号来分析程序或崩溃时的核心转储文件。这些局部符号对于链接过程没有作用,链接器往往也忽略它们。 - 行号信息,即目标文件指令与源代码中代码行的对应关系,它也是可选的。
对于我们来说,最值得关注的就是全局符号,即上面分类中的第一类和第二类。因为链接过程只关心全局符号的相互粘合,局部符号、段名、行号等都是次要的,它们对于其它目标文件来说是不可见的,在链接过程中也是无关紧要的。我们可以使用很多工具来查看ELF文件的符号表,比如readelf -s SimpleSection.o、objdump -t SimpleSection.o、nm SimpleSection.o等。
5.1 符号表项结构
ELF文件中的符号表往往是文件中的一个段,段名一般叫.symtab。符号表的结构很简单,它是一个Elf64_Sym结构(64位ELF文件)的数组,每个Elf64_Sym结构对应一个符号。Elf64_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;
这几个成员的定义如下所示:
st_name:目标文件的符号字符串表的索引,其中包含符号名称的字符表示形式。如果该值为非零,则表示指定符号名称的字符串表索引。否则,符号表项没有名称。st_value:关联符号的值。根据上下文,该值可以是绝对值或地址,见下文详解。st_size:许多符号具有关联大小。例如,数据目标文件的大小是目标文件中包含的字节数,一个double型的符号大小是8字节。如果符号没有大小或大小未知,则此成员值为零。st_info:符号的类型和绑定属性,见下文详解。st_other:符号的可见性,见下文详解。st_shndx:所定义的每一个符号表项都与某节有关。此成员包含相关节头表索引。部分节索引会表示特殊含义,参照4.2.1 特殊节索引。如果此成员值为SHN_XINDEX,则实际节头索引会过大而无法放入此字段中。实际值包含在SHT_SYMTAB_SHNDX类型的关联节中。
5.1.1 符号值(st_value)
每个符号都有一个对应的值,如果这个符号是一个函数或变量的定义,那么符号的值就是这个函数或变量的地址。更准确地讲应该按下面这几种情况区别对待:
- 在目标文件中,如果是符号的定义并且该符号不是
COMMON块类型的(即st_shndx不为SHN_COMMON),则st_value表示该符号在符号所在段中的偏移。即符号所对应的函数或变量位于由st_shndx指定的段,偏移st_value的位置。这也是目标文件中定义全局变量的符号的最常见情况,比如SimpleSection.o中的func1、main、global_init_var。 - 在目标文件中,如果符号是
COMMON块类型的(即st_shndx为SHN_COMMON),则st_value表示该符号的对齐属性。比如SimpleSection.o中的global_uninit_var。 - 在可执行文件中,
st_value表示符号的虚拟地址。这个虚拟地址对于动态链接器来说十分有用。
5.1.2 符号类型和绑定信息(st_info)
根据符号的 st_info 字段确定的符号绑定可确定链接可见性和行为。该成员低4位表示符号的类型(Symbol Type),高28位表示符号绑定信息(Symbol Binding)。以下代码说明了如何处理这些值,请参见 sys/elf.h。
#define ELF32_ST_BIND(info) ((info) >> 4)
#define ELF32_ST_TYPE(info) ((info) & 0xf)
#define ELF32_ST_INFO(bind, type) (((bind)<<4)+((type)&0xf))
#define ELF64_ST_BIND(info) ((info) >> 4)
#define ELF64_ST_TYPE(info) ((info) & 0xf)
#define ELF64_ST_INFO(bind, type) (((bind)<<4)+((type)&0xf))
符号绑定的值和含义:
STB_LOCAL:局部符号。这些符号在包含其定义的目标文件的外部不可见。名称相同的局部符号可存在于多个文件中而不会相互干扰。STB_GLOBAL:全局符号。这些符号对于合并的所有目标文件都可见。一个文件的全局符号定义满足另一个文件对相同全局符号的未定义引用。STB_WEAK:弱符号。这些符号与全局符号类似,但其定义具有较低的优先级。STB_LOOS - STB_HIOS:此范围内包含的值(包括这两个值)保留用于特定于操作系统的语义。STB_LOPROC - STB_HIPROC:此范围内包含的值(包括这两个值)保留用于特定于处理器的语义。
符号类型的值和含义:
STT_NOTYPE:未指定符号类型。STT_OBJECT:此符号与变量、数组等数据目标文件关联。STT_FUNC:此符号与函数或其他可执行代码关联。STT_SECTION:此符号与节关联。此类型的符号表各项主要用于重定位,并且通常具有STB_LOCAL绑定。STT_FILE:通常,符号的名称是与目标文件关联的源文件名。文件符号具有STB_LOCAL绑定和节索引SHN_ABS。此符号(如果存在)位于文件的其他STB_LOCAL符号前面。
符号索引为1的SHT_SYMTAB是表示目标文件的STT_FILE符号。通常,此符号后跟文件的STT_SECTION符号。这些节符号又后跟已降为局部符号的任何全局符号。STT_COMMON:此符号标记未初始化的通用块。此符号的处理与STT_OBJECT的处理完全相同。STT_TLS:此符号指定线程局部存储实体。定义后,此符号可为符号指明指定的偏移,而不是实际地址。
线程局部存储重定位只能引用STT_TLS类型的符号。从可分配节中引用STT_TLS类型的符号只能通过使用特殊线程局部存储重定位来实现。有关详细信息,请参见线程局部存储。从非可分配节中引用STT_TLS类型的符号没有此限制。
5.1.3 符号可见性(st_other)
以下代码说明了如何处理 32 位目标文件和 64 位目标文件的值。其他位设置为零,并且未定义任何含义。
#define ELF32_ST_VISIBILITY(o) ((o)&0x3)
#define ELF64_ST_VISIBILITY(o) ((o)&0x3)
以下是符号可见性的名字和值,具体值的含义,参考:ELF 符号可见性。
名称 值
STV_DEFAULT 0
STV_INTERNAL 1
STV_HIDDEN 2
STV_PROTECTED 3
STV_EXPORTED 4
STV_SINGLETON 5
STV_ELIMINATE 6
5.2 符号表布局和约定
按照以下顺序将符号写入符号表。
- 任何符号表中的索引 0 用于表示未定义的符号。符号表中的第一项始终完全为零。因此符号类型为
STT_NOTYPE。 - 如果符号表包含任何局部符号,符号表中的第二项是提供文件名的
STT_FILE符号。 STT_SECTION类型的节符号。STT_REGISTER类型的寄存器符号。- 缩减到局部作用域的全局符号。
- 对于提供局部符号的每个输入文件,提供输入文件名称的
STT_FILE符号,后跟相关符号。 - 在符号表中,全局符号紧跟局部符号。第一个全局符号由符号表
sh_info值标识。局部符号和全局符号始终以这种方式保持彼此独立,不能混合。
5.3 符号表实例
SimpleSection.o中的符号如下图所示:
[ARM64-01 cpp-file]$ readelf -s SimpleSection.o
Symbol table '.symtab' contains 21 entries:
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 NOTYPE LOCAL DEFAULT 3 $d
6: 0000000000000000 0 SECTION LOCAL DEFAULT 5
7: 0000000000000000 0 NOTYPE LOCAL DEFAULT 5 $d
8: 0000000000000000 0 NOTYPE LOCAL DEFAULT 1 $x
9: 0000000000000004 4 OBJECT LOCAL DEFAULT 3 static_var.3113
10: 0000000000000000 4 OBJECT LOCAL DEFAULT 4 static_var2.3114
11: 0000000000000000 0 NOTYPE LOCAL DEFAULT 4 $d
12: 0000000000000000 0 SECTION LOCAL DEFAULT 7
13: 0000000000000014 0 NOTYPE LOCAL DEFAULT 8 $d
14: 0000000000000000 0 SECTION LOCAL DEFAULT 8
15: 0000000000000000 0 SECTION LOCAL DEFAULT 6
16: 0000000000000000 4 OBJECT GLOBAL DEFAULT 3 global_init_var
17: 0000000000000004 4 OBJECT GLOBAL DEFAULT COM global_uninit_var
18: 0000000000000000 40 FUNC GLOBAL DEFAULT 1 func1
19: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf
20: 0000000000000028 76 FUNC GLOBAL DEFAULT 1 main
readelf的输出格式与上面描述的Elf64_Sym的各个成员几乎一一对应,第一列Num表示符号表数组的下标,从0开始,共20个符号;第二列Value就是符号值,即st_value;第三列Size为符号大小,即st_size;第四列和第五列分别为符号类型和绑定信息,即对应st_info的地4位和高28位;第六列Vis目前在C/C++语言中未使用,我们可以暂时忽略它;第七列Ndx即st_shndx,表示该符号所属的段;最后一列即符号名称。从上面的输出可以看到,第一个符号,即下标为0的符号,永远是一个未定义的符号。对于另外几个符号解释如下:
func1和main函数都是定义在SimpleSection.c里面的,它们所在的位置都为代码段,所以Ndx为1,即SimpleSection.o里面.text段的下标为1。这一点可以通过readelf -a或objdump -x得到验证。它们是函数,所以类型是STT_FUNC;它们是全局可见的,所以是STB_GLOBAL;Size表示函数指令所占的字节数;Value表示函数相对于代码段起始位置的偏移量。printf这个符号,该符号在SimpleSection.o里面被引用,但是没有被定义,所以它的Ndx是SHN_UNDEF。global_init_var是已初始化的全局变量,它被定义在.data段,即下标为3.global_uninit_var是未初始化的全局变量,它是一个SHN_COMMON类型的符号,它本身并没有存在于BSS段。static_var.1752和static_var2.1753是两个静态变量,它们的绑定属性是STB_LOCAL,即只是编译单元内部可见。- 对于那些STT_SECTION类型的符号,它们表示下标为Ndx的段的段名。它们的符号名没有显示,其实它们的符号名即它们的段名。比如2号符号的Ndx为1,那么它即表示
.text段的段名,该符号的符号名应该就是.text。如果我们使用objdump -t就可以清楚地看到这些段名符号。 SimpleSection.o这个符号表示编译单元的源文件名。
5.4 特殊符号
当我们使用ld作为链接器来链接生产可执行文件时,它会为我们定义很多特殊的符号,这些符号并没有在你的程序中定义,但是你可以直接声明并且引用它,我们称之为特殊符号。其实这些符号是被定义在ld链接器的链接脚本中的。链接器会在将程序最终链接成可执行文件的时候将其解析成正确的值,注意,只有使用ld链接产生最终可执行文件的时候这些符号才会存在。几个很具有代表性的特殊符号如下:
__executable_start:该符号为程序起始地址,注意,不是入口地址,是程序的最开始的地址。__etext或_etext或etext:该符号为代码段结束地址,即代码段最末尾的地址。_edata或edata:该符号为数据段结束地址,即数据段最末尾的地址。_end或end:该符号为程序结束地址。- 以上地址都为程序被装载时的虚拟地址。
我们可以在程序中直接使用这些符号,测试代码如下:
#include <stdio.h>
extern char __executable_start[];
extern char etext[], _etext[], __etext[];
extern char edata[], _edata[];
extern char end[], _end[];
int main()
{
printf("Executable Start %X\n", __executable_start);
printf("Text End %X %X %X\n", etext, _etext, __etext);
printf("Data End %X %X\n", edata, _edata);
printf("Executable End %X %X\n", end, _end);
return 0;
}
执行结果如下:
[@ARM64-01 cpp-file]$ gcc SpecialSymbol.c -o SpecialSymbol
[@ARM64-01 cpp-file]$ ./SpecialSymbol
Executable Start 400000
Text End 40073C 40073C 40073C
Data End 420030 420030
Executable End 420038 420038
5.5 符号修饰与函数名
约在20世纪70年代以前,编译器编译源代码产生目标文件时,符号名与相应的变量和函数的名字是一样的。比如一个汇编源代码里面包含了一个函数foo,那么汇编器将它编译成目标文件以后,foo在目标文件中的相对应的符号名也是foo。当后来UNIX平台和C语言发明时,已经存在了相当多的使用汇编编写的库和目标文件。这样就产生了一个问题,那就是如果一个C程序要使用这些库的话,C语言中不可以使用这些库中定义的函数和变量的名字作为符号名,否则将会跟现有的目标文件冲突。为了防止类似的符号名冲突,UNIX下的C语言就规定,C语言源代码文件中的所有全局的变量和函数经过编译以后,相对应的符号名前加上下划线_。而Fortran语言的源代码经过编译以后,所有的符号名前加上_,后面也加上_。比如一个C语言函数foo,那么它编译后的符号名就是_foo;如果是Fortran语言,就是_foo_。
这种简单而原始的方法的确能够暂时减少多种语言目标文件之间的符号冲突的概率,但还是没有从根本上解决符号冲突的问题。比如同一种语言编写的目标文件还有可能会产生符号冲突,当程序很大时,不同的模块由多个部门(个人)开发,它们之间的命名规范如果不严格,则有可能导致冲突。于是像C++这样的后来设计的语言增加了名称空间(Namespace)的方法解决多模块的符号冲突问题。
在现在的Linux下的GCC编译器中,默认情况下已经去掉了在C语言符号前加_的这种方式;但是Windows平台下的编译器还保持的这样的传统,比如Visucal C++编译器就会在C语言符号前加_,GCC在Windows平台下的版本(Cygwin, mingw)也会加_。GCC编译器也可以通过参数选项-fleading-underscore或-fno-leading-underscrore来打开和关闭是否在C语言符号前加上下划线。
C++符号修饰
函数签名(Function Signature):包含了一个函数的信息,包括函数名、它的参数类型、它所在的类和名称空间及其它信息。函数签名用于识别不同的函数,函数的名字只是函数签名的一部分。在编译器及链接器处理符号时,它们使用某种名称修饰的方法,使得每个函数签名 对应一个修饰后名称(Decorated Name)。编译器在将C++源代码编译成目标文件时,会将函数和变量的名字进行修饰,形成符号名,也就是说,C++的源代码编译后的目标文件中所使用的符号名是相应的函数和变量的修饰后名称。C++编译器和链接器都使用符号来识别和处理函数和变量,所以对于不同函数签名的函数,即使函数名相同,编译器和链接器都认为它们是不同的函数。
GCC的基本C++名称修饰方法如下:所有的符号都以_Z开头,对于嵌套的名字(在名称空间或在类里面的),后面紧跟N,然后是各个名称空间和类的名字,每个名字前是名字字符串长度,再以E结尾。对于一个函数来说,它的参数列表紧跟在E后面,对于int类型来说,就是字母i。binutils里面提供了一个叫c++filt的工具可以用来解析被修饰过的名称。
5.6 extern “C”
C++为了与C兼容(因为有些函数是用C的风格编译的),在符号的管理上,C++有一个用来声明或定义一个C的符号的extern C关键字用法。C++编译器会将在extern C的大括号内部的代码当作C语言代码处理。
#ifdef __cplusplus
extern "C" {
#endif
void cfuncall();
#ifdef __cplusplus
}
#endif
5.7 弱符号与强符号
链接器如何解析多重定义的全局符号中也有对这块内容做解释。
对于C/C++语言来说,编译器默认函数和初始化了的全局变量为强符号(Strong Symbol),未初始化的全局变量为弱符号(Weak Symbol)。我们也可以通过GCC的__attribute__((weak))来定义任何一个强符号为弱符号。注意:强符号和弱符号都是针对定义来说的,不是针对符号的引用。
针对强弱符号的概念,链接器就会按如下规则处理与选择被多次定义的全局符号:
- 规则1:不允许强符号被多次定义(即不同的目标文件中不能有同名的强符号);如果有多个强符号定义,则链接器报符号重复定义错误。
- 规则2:如果一个符号在某个目标文件中是强符号,在其它文件中都是弱符号,那么选择强符号。
- 规则3:如果一个符号在所有目标文件中都是弱符号,那么选择其中占用空间最大的一个。比如目标文件A定义全局变量
global为int型,占4个字节;目标文件B定义global为double型,占8个字节,那么目标文件A和B链接后,符号global占8个字节(尽量不用使用多个不同类型的弱符号,否则容易导致很难发现的程序错误)。
弱引用(Weak Reference)和强引用(Strong Reference)
对外部目标文件的符号引用在目标文件被最终链接成可执行文件时,它们须要被正确决议,如果没有找到该符号的定义,链接器就会报符号未定义错误,这种被称为强引用。与之相对应还有一种弱引用,在处理弱引用时,如果该符号有定义,则链接器将该符号的引用决议;如果该符号未被定义,则链接器对于该引用不报错。链接器处理强引用和弱引用的过程几乎一样,只是对于未定义的弱引用,链接器不认为它是一个错误。一般对于未定义的弱引用,链接器默认其为0,或者是一个特殊的值,以便于程序代码能够识别。弱引用和弱符号主要用于库的链接过程。在GCC中,我们可以通过使用__attribute__((weakref))这个扩展关键字来声明对一个外部函数的应用为弱引用。
这种弱符号和弱引用对于库来说十分有用,比如库中定义的弱符号可以被用户定义的强符号所覆盖,从而使得程序可以使用自定义版本的库函数;或者程序可以对某些扩展功能模块的引用定义为弱引用,当我们将扩展模块与程序链接在一起时,功能模块就可以正常使用;如果我们去掉了某些功能模块,那么程序也可以正常链接,只是缺少了相应的功能,这使得程序的功能更加容易裁剪和组合。
六、ELF常见节总结
6.1 节类型和属性
包含程序和控制信息的各种节。下表中的各节由系统使用,并且具有指明的类型和属性。
|
6.2 节含义
-
.bss
-
构成程序的内存映像的未初始化数据。根据定义,系统在程序开始运行时会将数据初始化为零。如节类型 SHT_NOBITS 所指明的那样,此节不会占用任何文件空间。
.comment
-
注释信息,通常由编译系统的组件提供。此节可以由 mcs(1) 进行处理。
.data、
.data1
-
构成程序的内存映像的已初始化数据。
.dynamic
-
动态链接信息。有关详细信息,请参见动态节。
.dynstr
-
进行动态链接所需的字符串,通常是表示与符号表各项关联的名称的字符串。
.dynsym
-
动态链接符号表。有关详细信息,请参见符号表节。
.eh_frame_hdr、
.eh_frame
-
用于展开栈的调用帧信息。
.fini
-
可执行指令,用于构成包含此节的可执行文件或共享目标文件的单个终止函数。有关详细信息,请参见初始化和终止例程。
.fini_array
-
函数指针数组,用于构成包含此节的可执行文件或共享目标文件的单个终止数组。有关详细信息,请参见初始化和终止例程。
.got
-
全局偏移表。有关详细信息,请参见全局偏移表(特定于处理器)。
.hash
-
符号散列表。有关详细信息,请参见散列表节。
.init
-
可执行指令,用于构成包含此节的可执行文件或共享目标文件的单个初始化函数。有关详细信息,请参见初始化和终止例程。
.init_array
-
函数指针数组,用于构成包含此节的可执行文件或共享目标文件的单个初始化数组。有关详细信息,请参见初始化和终止例程。
.interp
-
程序的解释程序的路径名。有关详细信息,请参见程序的解释程序。
.lbss
-
特定于 x64 的未初始化的数据。此数据与 .bss 类似,但用于大小超过 2 GB 的节。
.ldata、
.ldata1
-
特定于 x64 的已初始化数据。此数据与 .data 类似,但用于大小超过 2 GB 的节。
.lrodata、
.lrodata1
-
特定于 x64 的只读数据。此数据与 .rodata 类似,但用于大小超过 2 GB 的节。
.note
-
注释节中说明了该格式的信息。
.plt
-
过程链接表。有关详细信息,请参见过程链接表(特定于处理器)。
.preinit_array
-
函数指针数组,用于构成包含此节的可执行文件或共享目标文件的单个预初始化数组。有关详细信息,请参见初始化和终止例程。
.rela
-
不适用于特定节的重定位。此节的用途之一是用于寄存器重定位。有关详细信息,请参见寄存器符号。
.rel
name
、
.rela
name
-
重定位信息,如重定位节中所述。如果文件具有包括重定位的可装入段,则此节的属性将包括 SHF_ALLOC 位。否则,该位会处于禁用状态。通常,name 由应用重定位的节提供。因此,.text 的重定位节的名称通常为 .rel.text 或 .rela.text。
.rodata、
.rodata1
-
通常构成进程映像中的非可写段的只读数据。有关详细信息,请参见程序头。
.shstrtab
-
节名称。
.strtab
-
字符串,通常是表示与符号表各项关联的名称的字符串。如果文件具有包括符号字符串表的可装入段,则此节的属性将包括 SHF_ALLOC 位。否则,该位会处于禁用状态。
.symtab
-
符号表,如符号表节中所述。如果文件具有包括符号表的可装入段,则此节的属性将包括 SHF_ALLOC 位。否则,该位会处于禁用状态。
.symtab_shndx
-
此节包含特殊符号表的节索引数组,如 .symtab 所述。如果关联的符号表节包括 SHF_ALLOC 位,则此节的属性也将包括该位。否则,该位会处于禁用状态。
.tbss
-
此节包含构成程序的内存映像的未初始化线程局部数据。根据定义,为每个新执行流实例化数据时,系统都会将数据初始化为零。如节类型 SHT_NOBITS 所指明的那样,此节不会占用任何文件空间。有关详细信息,请参见第 14 章。
.tdata、
.tdata1
-
这些节包含已初始化的线程局部数据,这些数据构成程序的内存映像。对于每个新执行流,系统会对其内容的副本进行实例化。有关详细信息,请参见第 14 章。
.text
-
程序的文本或可执行指令。
.SUNW_bss
-
共享目标文件的部分初始化数据,这些数据构成程序的内存映像。数据会在运行时进行初始化。如节类型 SHT_NOBITS 所指明的那样,此节不会占用任何文件空间。
.SUNW_cap
-
功能要求。有关详细信息,请参见功能节。
.SUNW_capchain
-
功能链表。有关详细信息,请参见功能节。
.SUNW_capinfo
-
功能符号信息。有关详细信息,请参见功能节。
.SUNW_heap
-
从 dldump(3C) 中创建的动态可执行文件的堆。
.SUNW_dynsymsort
-
.SUNW_ldynsym – .dynsym 组合符号表中符号的索引数组。该索引进行排序,以按照地址递增的顺序引用符号。不表示变量或函数的符号不包括在内。对于冗余全局符号和弱符号,仅保留弱符号。有关详细信息,请参见符号排序节。
.SUNW_dyntlssort
-
.SUNW_ldynsym – .dynsym 组合符号表中线程局部存储符号的索引数组。该索引进行排序,以按照偏移递增的顺序引用符号。不表示 TLS 变量的符号不包括在内。对于冗余全局符号和弱符号,仅保留弱符号。有关详细信息,请参见符号排序节。
.SUNW_ldynsym
-
扩充 .dynsym 节。此节包含局部函数符号,以在完整的 .symtab 节不可用时在上下文中使用。链接编辑器始终将 .SUNW_ldynsym 节的数据放置在紧邻 .dynsym 节之前。这两个节始终使用相同的 .dynstr 字符串表节。这种放置和组织方式使两个符号表可以被视为一个更大的符号表。请参见符号表节。
.SUNW_move
-
部分初始化数据的附加信息。有关详细信息,请参见移动节。
.SUNW_reloc
-
重定位信息,如重定位节中所述。此节是多个重定位节的串联,用于为引用各个重定位记录提供更好的临近性。由于仅有重定位记录的偏移有意义,因此节的 sh_info 值为零。
.SUNW_syminfo
-
其他符号表信息。有关详细信息,请参见Syminfo 表节。
.SUNW_version
-
版本控制信息。有关详细信息,请参见版本控制节。
具有点 (.) 前缀的节名为系统而保留,但如果这些节的现有含义符合要求,则应用程序也可以使用这些节。应用程序可以使用不带前缀的名称,以避免与系统节产生冲突。使用目标文件格式,可以定义非保留的节。一个目标文件可以包含多个同名的节。
保留用于处理器体系结构的节名称通过在节名称前加上体系结构名称的缩写而构成。该名称应来自用于 e_machine 的体系结构名称。例如,.Foo.psect 是根据 FOO 体系结构定义的 psect 节。
现有扩展使用其历史名称。
本文详细探讨了ELF文件的种类、格式及其内部结构,包括代码段、数据段、BSS段以及其他特殊段如.rodata、.comment、.note.GNU-stack和.eh_frame。通过实例SimpleSection.o,展示了如何使用objdump分析目标文件,揭示了段的布局、属性及内容。还介绍了ELF文件头的组成,如e_ident、e_type等,以及如何利用ELF头信息理解目标文件的属性。
8103

被折叠的 条评论
为什么被折叠?



