本文以目标文件的结构为引子,通过探索在Linux环境下,一个具体的目标文件的结构来窥探ELF文件的结构。了解ELF文件的结构,对于加深对链接的理解、认识操作系统背后机理都有很大好处。
编译和链接
在正式讨论目标文件的结构之前,需要先对一个C/C++程序从源代码到可执行程序的构建过程有所了解。由于这部分不是本文重点,所以只做简要介绍。
平常使用IDE进行程序设计时,这些工具往往会将编译和链接的过程一步完成,这个过程叫做构建(build)。在Linux环境下,一句gcc
或者g++
指令就包含复杂的编译和链接过程。事实上,一个C/C++程序从源代码到可执行程序,需要经过预编译(preprocess)、编译(compile)、汇编(assembly)和链接(link)的过程。这些过程中各自依赖不同的工具,以hello.cpp为例,预处理器对该源文件进行各种文本替换操作,此时生成hello.i文件;编译器对hello.i文件进行编译,这个过程包含词法分析、语法分析、语义分析直至汇编代码的生成,此时hello.i文件被处理为hello.s文件;汇编器负责将汇编语言翻译为二进制机器指令,生成hello.o文件,这种文件即目标文件,是本文即是所深入探讨的对象。当各个源文件被分别编译成不同的目标文件后,链接器将这些不同的目标文件与动态库或静态库一齐进行链接,生成一个单独的可执行程序。
目标文件的格式
讨论目标文件的格式之前,需要先了解可执行文件(excutable)的结构。目前PC流行的可执行文件格式主要是Windows平台下的PE和Linux平台下的ELF。目标文件本质是经过编译而未经链接的中间文件,这些中间文件的结构与可执行文件的结构相似,在Windows下,这些文件的格式统称为PE-COFF格式,在Linux下,统称为ELF格式。本文的剖析主要准对ELF格式。
除此之外,动态链接库(Windows的.dll和Linux的.so)和静态链接库(Windows的.lib和Linux的.a)也以对应的格式存储。
ELF文件结构
目标文件中除了有编译后的机器指令代码、数据,还包括链接时所需要的一些信息,例如符号表、字符串等,这些信息按照不同的属性,以节(Section)或段(Segment)的形式被存储在目标文件中。节或段都表示一个一定长度的区域,基本不加以区别。
大体上,程序源代码被编译以后主要分成两种段:程序指令和程序数据。将指令和数据分开存放,主要有以下几点好处:
- 数据区域对于进程来说是可读可写的,而程序区域对进行来说是只读的,装载时,将数据和指令分别映射到两个虚拟内存区域,分别设置区域权限为可读写和可读,可以防止程序指令被有意或无意改写。
- 程序区和数据区分离有利于提高程序的局部性,以提高CPU的缓存命中率。
- 当系统中运行多个该程序的副本时,只需要在内存中保存一份程序的指令部分,可以节省大量的内存。
现给出一个 example.c 示例程序,下文会逐步探索将其编译为 example.o 目标文件后的结构和各个段的信息。
/*
Name:example.c
gcc -c example -o example.o
*/
#include <stdio.h>
int global_init_var = 84;
int global_uninit_var;
void func1(int i)
{
printf("%d\n", i);
}
int main()
{
static int static_var = 85;
static int static_var2;
int a = 1;
int b;
func1(static_var + static_var2 + a + b);
return a;
}
文件头(ELF Header)
ELF目标文件格式最前部是ELF文件头(ELF Header),文件头描述了整个文件的基本属性,包括ELF文件版本、目标机器信息和程序入口等。在Linux中,使用 readelf
工具的 -h
选项详细查看 ELF文件头:
各个信息的意义标注如下:
这里对ELF魔数做一个简要说明,在Magic一栏中,这16个字节被规定用来标识ELF文件的平台属性:最开始的前4个字节是所有ELF文件都必须相同的标识码,被称为ELF文件的魔数,用来确认文件的类型;第 5 个字节被用来标识ELF文件类型;第 6 个字节被用来标识文件的存储字节序;第 7 个字节被用来标识ELF文件的主版本号,因为ELF标准自 1.2 版本以后再未更新,所以该数一般是 1;最后的 9 个字节ELF标准没有定义,一般填 0。
在Linux平台下,ELF文件头结构和相关常数被定义在 /usr/include/elf.h
中。ELF文件在各种平台下都通用,有32位版本(Elf32_Ehdr)和64位版本(Elf64_Ehdr),两种版本的文件头内容相同,只是有些成员的大小不同。Elf64_Ehdr 的定义如下:
段表(Section Header Table)
上文说道,ELF文件是分段的,其中有很多各种各样的段,段表(Section Header Table)是保存这些段的基本属性的结构,它描述了各个段的信息,可以说,ELF文件的段结构就是由段表决定的。在Linux中,使用 readelf
工具的 -S
选项来查看 example.o 的段表内容:
可见,段表描述了各个段的段名(Name)、段的类型(Type),段虚拟地址、段偏移(Offset)、段的大小(Size)、段中的项的长度、段的标志位(Flags)、链接信息和对齐数(Align)。段表的存储结构是一个结构体(Elf32_Shdr或Elf64_Shdr)数组,每个结构体元素又被称为段描述符(Section Descriptor),除第一个元素表示无效段描述符外,其他每个元素都是一个有效段。
可以注意到,下标为 4 的 .bss 段和下标为 5 的 .rodata 段的段偏移和大小都相同,且在空间上是一个段,所以这里将其看做一个段,如此,加上 ELF Header 和 Section Header Table 本身,该ELF文件共 13 个段,与ELF Header中的描述一致。结合 ELF Header 和 段表 中的信息,并加以计算,我们可以得到该ELF文件的整体结构如下:
下面我们择几个较重要的段进行详细说明,分析这些段的内容和意义。
代码段、数据段、只读数据段和.bss段
代码段记录了程序的所有指令,通过 objdump
工具的 -s
和 -d
选项可以查看代码段及其将其反汇编的内容:
数据段(.data)保存的是已经被初始化的全局变量和静态变量(局部和全局),在 example.c 中一共有2个这样的变量,即 global_init_varabal 和 static_var,这两个变量共8个字节,所以 .data 的大小为 8 字节。
只读数据段(.rodata)保存的是只读数据,一般是程序中的只读变脸,例如const 修饰的变量,有时字符串常量也被放在 .rodata 中。单独设立 .rodata段,不仅在语义上支持了C/C++的const关键字,而且操作系统在加载时可以将 .rodata 段的属性映射成只读,保证了程序的安全性。
.bss段 为未被初始化的全局变量和静态变量预留空间。需要注意的是,有些语言和编译器会为未初始化的全局变量预留一个未定义的全局变量符号,而不将其放在任何段,等到链接时再为其在 .bss 段分配空间,例如 example.c 中的global_uninit_var。在 example.c 中只有 static_var2 在 .bss 被预留了空间,所以这里 .bss 段的大小为 4 字节。
字符串表(String Table)
字符串表存放了ELF文件中用到的字符串,例如段名、变量名等。因为字符串的长度是不确定的,所以字符串表中存放的是所有字符串的集合,使用时根据字符串在表中的偏移来引用字符串,不用考虑字符串长度的问题。字符串表(.strtab)用来保存普通的字符串,比如符号名;段表字符串表(.shstrtab)用来保存段表中用到的字符串,比如段名。
符号表(Symbol Table)
符号表汇总了链接时ELF文件中所需的符号(Symbol),包括函数和变量。在64位机器下,符号表是一个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;
使用 readelf
工具的 -s
选项查看ELF文件的符号表:
可见,符号表保存了各个符号的在符号表中的索引(Num)、符号值(Value)、符号大小(Size)、符号类型(Type)、符号绑定信息(Bind)、符号所在段(Ndx)和符号名(Name)。
每个符号都有一个对应的值,如果一个符号是一个函数或者变量的定义,那么该符号值就是这个函数或变量的地址;符号类型包括五种,分别是未知类型符号(NOTYPE)、数据对象(OBJECT)、函数或可执行代码(FUNC)、段(SECTION)和文件名(FILE)。对于SECTION类型的符号,它们的符号名未显示,因为它们的符号名即是它们的段名。符号绑定信息有3种:局部符号(LOCAL)、全局符号(GLOBAL)和弱引用(WEAK)。符号所在段表示该符号所在的段在段表中的下标,如果该符号未被定义在本目标文件中,或者对于一些特殊符号,符号所在段有些特殊:ABS表示该符号包含一个绝对的值,例如文件名的符号;COMMON表示该符号是一个COMMON块类型的符号;UNDEF表示该符号在该文件中未被定义。
将符号表中的符号进行分类,它们大致是下面类型中的一种:
- 定义(define)在本文件中的全局符号;
- 在本文件中引用(reference)的全局符号;
- 段名;
- 局部符号;
- 行号信息,即目标文件指令与源代码中代码行的对应关系。
链接的接口
链接的本质即是将多个不同的目标文件互相结合在一起,形成一个大的模块,这个相互拼合的过程实际上是目标文件之间对函数和变量的地址的引用,因此每个函数或变量都需要有自己独特的名字,才能避免链接过程中的混淆。函数和变量统称为符号(symbol),函数名和变量名为符号名(symbol name)。
符号是链接中的接口,链接过程正是基于符号正确完成的。
标签:文件,探索,符号,ELF,Linux,字符串,链接 From: https://blog.51cto.com/158SHI/6457665