目录
一、目标文件
二、ELF文件
三、可执行文件形成
四、可执行文件加载
五、结语
一、目标文件
编译和链接这两个步骤,在Windows下被IDE封装的很完美,一般通过一键构建即可完成编译链接,但一旦遇到错误,尤其是链接相关的错误,大多数情况下就束手无策了。在Linux中,可以通过gcc编译器来完成编译链接这一系列操作,如下图所示:
编译的过程其实就是将程序的源代码翻译成CPU能够直接运行的机器代码。
hello.c
#include<stdio.h> void run(); int main() { printf("hello world!\n"); run(); return 0; }code.c
#include<stdio.h> void run() { printf("running...\n"); }例如这里就可以通过调用gcc -c来分别编译hello.c,code.c这两个源文件。
gcc -c hello.c gcc -c code.c可以看到,在编译之后会生成两个扩展名为.o的文件,它们被称为目标文件。可通过file命令查看文件类型。
file hello.o通过file命令查看hello.o文件,结果如下图所示:
目标文件是一个二进制文件,文件的格式为ELF,ELF是对二进制代码的一种封装。
二、ELF文件
要理解编译链接的细节,就不得不了解ELF文件,ELF文件有以下4种类型:
1、可重定位文件:即xxx.o文件,包含适合于与其他目标文件链接来创建可执行文件或者共享目标文件的代码和数据。
2、可执行文件:即可执行程序。
3、共享目标文件:即xxx.so文件。
4、内核转储,存放当前进程的执行上下文,用于dump信号触发。
一个ELF文件通常由以下四部分组成:
ELF头:描述文件的主要特性,位于文件的开始位置,主要目的是定位文件的其他部分。
程序头表:列举了所有有效的段和它们的属性。表里记着每个段开始的位置、位移和长度。这些段,都紧密放在二进制文件中,需要段表的描述信息,才能把每个段分割开。
节头表:包含对节的描述。
节:ELF文件的基本组成单位,包含了特定类型的数据。ELF文件的各种信息和数据都存储在不同的节中,如代码节存储了可执行代码,数据节存储了全局变量和静态数据等。
最常见的节:
1、代码节(.text):用于保存机器指令,是程序的主要执行部分。
2、数据节(.data):保存已初始化的全局变量和局部静态变量。
size code.o执行size命令,可以查看code.o文件的内存布局大小,结果如下所示:
在ELF文件中,内存通常被划分为以下几个不同的段:
1、text:代码段,用于存放程序的可执行机器指令,即编译后的代码,以及只读常量,这部分通常是只读的。
2、data:已初始化数据段,用于存放程序中已经初始化的全局变量和静态变量。
3、bss:未初始化数据段,用于存放程序中未初始化或初始化为0的全局变量和静态变量。
4、dec:text+data+bss字节数总和的十进制表示。
5、hex:dec数值的十六进制表示。
三、可执行文件形成
ELF形成可执行文件的步骤如下:
1、将多份C/C++源代码,翻译成为目标.o文件+动静态库(ELF)
2、将多份.o文件section进行合并
四、可执行文件加载
一个ELF会有多种不同的Section,在加载到内存的时候,也会进行Section合并,形成segment
合并原则:相同属性,比如:可读,可写,可执行,需要加载时申请空间等
这样,即便是不同的Section,在加载到内存中,可能会以segment的形式,加载到一起
具体合并原则被记录在了ELF的程序头表中
readelf -S hello # 查看可执行程序的section如上图所示,可以看到,ELF文件被划分为30个节(Section),节头表本身从文件偏移0x5d40处开始存放,节头表就是一张目录,它记录了文件中每一个节的名字、类型、在文件中的偏移量、大小以及权限等属性。
readelf -l hello # 查看section合并的segment如上图所示,readelf -l hello命令可查看ELF可执行文件的程序头表以及节到段的映射。
section合并的主要原因是为了减少页面碎片,提高内存使用效率。如果不进行合并,假设页面大小为4096字节(内存块基本大小、加载、管理的基本单位),如果.text部分为4097字节,.init部分为512字节,那么它们将占用3个页面,而合并后,它们只需2个页面。此外,操作系统在加载程序时,会将具有相同属性的section合并成一个大的segment,这样就可以实现不同的访问权限,从而优化内存管理和权限访问控制。
程序头表和节头表其实是ELF文件提供2个不同的视图来理解这两个部分:
链接视图:对应节头表,文件结构的粒度更细,将文件按功能模块的差异进行划分,静态链接分析的时候一般关注的是链接视图,能够理解ELF文件中包含的各个部分的信息。
为了空间布局上的效率,将来在链接目标文件时,链接器会把很多节(section)合并,规整成可执行的段(segment)、可读写的段、只读段等。合并后,空间利用率就高了。
执行视图:对应程序头表,用于告诉操作系统,如何加载可执行文件,完成进程内存的初始化。一个可执行程序的格式中,一定会有program header table。
链接视图和执行视图一个在链接时作用,一个在运行加载时作用。
如上图所示,从链接视图来看:
命令readelf -S hello.o可以帮助查看ELF文件的节头表
.text节:保存了程序代码指令的代码节
.data节:保存了初始化的全局变量和局部静态变量等数据
.rodata节:保存了只读的数据,如一行C语言代码中的字符串。由于.rodata节是只读的,所以只能存在于一个可执行文件的只读段中。因此,只能是在text段中找到.rodata节
.BSS节:为未初始化的全局变量和局部静态变量预留位置
.symtab节:Symbol Table符号表,就是源码里面那些函数名、变量名和代码的对应关系
.got.pit节:为全局偏移表-过程链接表,.got节保存了全局偏移表,.got节和.pit节一起提供了对导入的共享库函数的访问入口,由动态链接器在运行时进行修改。
使用readelf命令查看.so文件可以看到该节。
从执行视图来看:
告诉操作系统哪些模块可以被加载进内存
加载进内存之后哪些分段是可读可写,哪些分段是只读,哪些分段是可执行的
可以在ELF头中找到文件的基本信息,以及可以看到ELF头是如何定位程序头表和节头表的。
可通过以下命令查看code.o这个可重定位文件的主要信息:
readelf -h code.o # 查看目标文件结果如下图所示:
对于ELF HEADER,它的主要作用是用于定位文件的其他部分。
五、结语
本文主要围绕编译链接ELF文件从形成到可执行加载展开介绍,系统梳理了从编译、链接到加载执行的全链路机制。以目标文件为切入点,分析了编译器生成的中间产物及其可重定位属性,深入剖析了ELF文件的内部结构,如节头表、程序头表及关键节的组成与语义。通过这一完整链条的梳理,不仅是建立对ELF文件格式的体系化认知,也是深入理解进程地址空间、动态链接机制的必要条件!