一、定义
ELF(Executable and Linkable Format,可执行与可链接格式) 是一种用于静态存储在磁盘上的二进制文件列如可执行文件、目标代码、共享库和核心转储(core dump 程序崩溃时生成的调试信息文件)的标准文件格式。它被广泛应用于类 Unix 系统(如 Linux、FreeBSD)、嵌入式系统以及部分其他操作系统中,是程序编译、链接和运行过程中的关键格式。
二、结构
-
ELF 头部(ELF Header):位于文件开头,包含文件的基本信息,如 ELF 格式版本、目标架构(32 位 / 64 位)、文件类型(可执行文件 / 共享库等)、入口点地址(程序开始执行的位置)、各个段(Section)和节区(Segment)的偏移量等,是系统识别和解析 ELF 文件的入口。
-
节区表(Section Header Table):描述文件中的各个 “节区”(Section),根据不同的文件内容分区,如代码段(
.text,存放机器指令)、数据段(.data,存放已初始化的全局变量)、未初始化数据段(.bss)、符号表(.symtab)、字符串表(.strtab)等。 -
程序头表(Program Header Table):描述文件中的各个 “段”(Segment)根据不同的文件属性分区,如代码段(包含
.text等可执行节区)、数据段(包含.data等可读写数据节区)、动态链接信息段等。
数据区:实际存放代码、数据、符号等内容的区域,由节区表和程序头表指向。

三、编译,链接,运行
简单的代码初略的观察在编译链接运行下elf格式的文件是如何变化的
代码如下
$ cat code1.h
#pragma oncevoid TestCode1();
$ cat code1.cpp
#include<iostream>
#include "code1.h"void TestCode1(){
std::cout<<"Linked with the static library successfually"<<std::endl;
}$ cat code2.h
#pragma oncevoid TestCode2();
$ cat code2.cpp
#include<iostream>
#include "code2.h"void TestCode2(){
std::cout<<"Linked with the dynamic library successfully"<<std::endl;
}$ cat test.cpp
#include"code1.h"
#include"code2.h"
#include<iostream>int main(){
TestCode1();
TestCode2();
return 0;
}
$ cat makefile
libcode1.a:code1.o
@ar -rc $@ $^ #archive/replace/create
@echo "build $^ to $@ ... done"
libcode2.so:code2.o
@g++ -shared -fPIC -o $@ $^ #GUN c++ compiler/Position-Independent Code
@echo "build $^ to $@ ... done"
code1.o:code1.cpp
@g++ -c -g -o $@ $^ #compile/debug information
@echo "compiling $^ to $@ ... done"
code2.o:code2.cpp
@g++ -fPIC -c -g -o $@ $^
@echo "compiling $^ to $@ ... done"
test.o:test.cpp
@g++ -c -g -o $@ $^
@echo "compiling $^ to $@ ... done"
test.exe: test.o libcode1.a libcode2.so
@g++ -o $@ $< -L. -lcode1 -lcode2 #这里-L. 作用于-lcode1,
-lcode2默认到系统标准库路径寻找
@echo "link $^ to produce $@ ...done"
.PHONY: all
all: libcode1.a libcode2.so test.exe
#
.PHONY: run #PHONY伪目标,make run时创建子进程
run: test.exe
LD_LIBRARY_PATH=./:$LD_LIBRARY_PATH ./$^ # 仅在运行时临时设置环境变量
优先搜索当前目录动态库
@echo "Running $<..."
.PHONY:clean
clean:
@rm -rf *o *.a *.so stdc* *.exe
@echo "clean ... done"
.PHONY:output
output:
@mkdir -p stdc/include
@mkdir -p stdc/lib
@cp -f *.h stdc/include
@cp -f *.a *.so stdc/lib
@tar -czf stdc.tgz stdc
@echo "output stdc ... done"
3.1编译
$make
code1.cpp、code2.cpp test.cpp编译形成.o文件,.o文件以elf格式存在磁盘上
$objdump -d code1.o
//object dump(转储,即提取并显示文件内容)disassemble(反汇编)

code1.o调用动态库函数std::cout和std::endl时还没有链接,不知道库函数的实际位置,偏移量用00 00 00 00占位,链接阶段才会更改为真实地址。
$readelf -s code1.o

UND表示undefine,未链接调用的库函数是未定义的
code2.o的情况同上
未来把code1.o文件归档成.a静态库文件,code2.o文件动态链接成.so动态库文件,test.o调用这两个库里面的函数时
$objdump -d -S test.o

编译时调用动静态库偏移量都是00 00 00 00占位。
3.2链接
链接前准备:
code1.o归档形成.a文件,不再是 ELF 格式,而是采用 ar 归档格式(由 ar 工具创建)
所有 .o 中的代码段(.text)、数据段(.data)、重定位表(.rel.text)等 ELF 段完全保留在.a文件,不发生合并或修改。
链接:
|
code2.o动态链接形成.so文件,.so文件以elf格式存在磁盘上 动态链接生成 段(Section)的合并与优化: 多个 新增动态链接专用段:
符号与重定位的处理 内部符号(仅在 外部符号(如依赖的 重定位表从 ELF 头的修改 ELF 头中的 新增 |
|
将test.o链接.a 、.so文件生成.exe文件 静态库链接时,代码段与数据段合并:可执行文件的 $readelf -s *exe
静态库链接时,符号表整理:
这里动态库函数的UND说明还不知道动态库函数的地址 $ objdump -d -S *exe 由于静态库代码已经被合并到可执行文件中,链接时链接器会完成所有地址重定位(将 但是这里链接时调用的动态库函数拥有偏移量并不指向动态库函数的实际地址,而是plt(中间桩Procedure Linkage Table)的偏移量。PLT 是可执行文件自身 |
3.3运行

分配虚拟地址:
启动一个程序时,操作系统的进程创建机制会先创建一个新的进程。在创建进程的过程中,操作系统会为这个新进程分配一个独立的虚拟地址空间。虚拟地址空间是一个连续的、逻辑上的地址范围,它被划分成不同的区域,包括代码段(.text)、数据段(.data、.bss)、堆、栈以及共享库映射区域等。
加载内存:
运行⼀个程序,操作系统会首先将程序的数据代码连同它用到的一系列动态库先加载到内存,其中每个动态库的加载地址都是不固定的,操作系统会根据当前地址空间的使用情况为它们动态分配⼀段内存。
形成页表:
此时通过进程分配的虚拟空间知道了库的起始虚拟地址,通过库的ELF格式内容知道动态库函数方法偏移量,于是就可以通过库的起始虚拟地址+方法偏移量即可定位库中的方法,关联了库的虚拟空间与物理空间,形成了页表。整个调用过程,是从代码区跳转到共享区,调⽤完毕在返回到代码区,整个过程完全在进程地址空间中进行的。
加载地址重定位:
在 .data(可执行程序或者库自己)中专门预留一片区域用来存放函数的跳转地址,它也被叫做全局偏移表 GOT,表中每一项都是本运行模块要引用的一个全局变量或函数的地址。而data 区域是可读写的,所以可以支持动态进行修改,会对加载到内存中的程序的库函数调用进行地址修改,在内存中二次完成地址设置(这个叫做加载地址重定位)。由于同用ELF格式内容,库调用库也像俄罗斯套娃一样通过该方法进行地址重定位,如下图。

由于动态链接在程序加载的时候需要对大量函数进行重定位,这一步显然是非常耗时的。为了进一步降低开销,我们的操作系统还做了一些其他的优化,比如延迟绑定,或者也叫 PLT(过程连接表 Procedure Linkage Table)。与其在程序一开始就对所有函数进行重定位,不如将这个过程推迟到函数第一次被调用的时候,因为绝大多数动态库中的函数可能在程序运行期间一次都不会被使用到。其思路是:GOT 中的跳转地址默认会指向一段辅助代码,它也被叫做桩代码 /stup。在我们第一次调用函数的时候,这段代码会负责查询真正函数的跳转地址,并且去更新 GOT 表。于是我们再次调用函数的时候,就会直接跳转到动态库中真正的函数实现,如下图。

四、作用
作用由格式的名字ELF可执行与可链接两个方面思考
1.链接
链接在合并目标文件时,会根据节区表(Section Header Table)区分的文件内容相同的section合并。会进行符号去重数据整合,其目的是消除冗余、统一布局,为后续程序运行或进一步链接做准备。

2.执行
可执行文件加载到内存,需要将文件根据程序头表(Program Header Table)区分的不同属性(如:可读,可写,可执行)的section合并成segment加载到内存,减少了储存的页片碎片,相同的属性合并优化了内存管理和权限访问控制。
五、优势
- 跨平台兼容性:作为标准格式,ELF 被多种系统支持,便于软件在不同类 Unix 系统间移植。
- 灵活性:支持动态链接(共享库)、位置无关代码(PIC,使共享库可在内存任意位置加载)、调试信息嵌入等功能,满足复杂程序的开发和运行需求。
- 可扩展性:设计时预留了扩展空间,可通过新增节区或段支持新功能(如安全相关的标记、自定义元数据等)。
总之,ELF 是现代类 Unix 系统中程序生命周期(编译、链接、加载、运行)的核心格式,理解 ELF 有助于深入掌握程序的执行机制和系统底层原理。



1302

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



