来自《深入理解计算机系统》
写在前面
ELF可重定位目标文件的格式
ELF(executable and linkable format)
常见的文件类型包括:可执行文件、共享库.so、目标文件.o和core文件
| 格式 | 全称 | 所属平台 | 常见系统 |
|---|---|---|---|
| ELF | Executable and Linkable Format | 类Unix系统 | Linux、FreeBSD、Android等 |
| PE | Portable Executable | Windows | Windows 32/64位系统 |
ELF链接视图
ELF Linking View
链接阶段,可以忽略program header table
ELF执行视图
ELF Execution View
运行阶段,可以忽略section header table
- ELF header
- ELF32_Ehdr 结构体
- 包括
ELF标识信息,类型,目标文件的体系架构,程序入口虚拟地址,程序头表偏移地址,节头部表偏移地址,ELF头大小,每个程序头表的大小,程序头表的数量,每个节头部表的大小,节头部表的数量,节字符串表在节头表中的索引等。 - 程序头表的
offset==ELF头大小 节字符串表在节头表中的索引一般== 节头部表的数量- 1
- Section Header Table
- 为ELF文件的所有section都设置一个entry,
- section的entry中包括
名字,类型,标志,在内存映像中的起始位置(如果该section将出现在进程内存中的话),section的第一个字节相对于文件头的offset,section长度,等名字,其实是section在节头部表字符串表section的索引。- 某些section还有其他内容
- Sections
- 常见的section有:
.bss .text .data .rodata.comment版本控制info.debug.dynamic动态链接info.dynstr动态链接的字符串.dynsym动态链接符号表.fini进程终止代码.got全局偏移表.hash符号哈希表.init进程初始代码.interp程序解释器的路径.line符号调试的行号info.note注释info.plt过程链接表.relname重定位info.shstrtab节名称.strtab与符号表entry相关的info.symtab符号表 - 其中,.dynamic节的属性设置
SHF_ALLOC位为1(SHF_ALLOC表示执行期间是否占用内存);若程序包含一个可加载的段,段中的.interp、.relname、strtab、.symtab等section的属性都要设置SHF_ALLOC位为1
- 常见的section有:
什么叫“可加载的段”
不要被这个奇怪的名字唬住
loadable segment,指的是那些用于执行程序的部分,要在执行前或者运行时加载到内存
通常有代码段,数据段,可加载的库等
那什么叫“可加载的库”
程序中的一种模块化组件,用于动态加载,包括函数、数据或资源
静态库和动态库都属于可加载的库,不过静态库在编译时就复制到程序中了,不需要动态加载,但会浪费内存空间;动态库支持内存共享
静态库和动态库都提供了可重用的代码和资源,有利于提高程序的模块化
动态库包括.so(类Unix)和.dll(Windows)
GOT表的存在合理性
由于存在ASLR机制,共享库的地址不能使用常基址(constant base address),每次的程序运行都需要重新解析库函数地址,故而引入GOT和PLT
特别的,二进制利用中,可以借助PLT和GOT绕过ASLR
-----------------------
| ELF header | // 文件基本信息:文件类型(可执行文件、共享库、目标文件)、机器架构(arm、x86)、入口点地址
-----------------------
| .text | // 已编译的程序机器代码
-----------------------
| .rodata | // 只读数据
-----------------------
| .data | // 已初始化的全局和静态变量
-----------------------
| .bss | // 未初始化的全局和静态变量
-----------------------
| .symtab | // 程序定义引用的func和global信息
-----------------------
| | // .text节中位置的列表?linking时需要修改
| .rel.text | // 调用外部func或引用global的指令需要修改
| | // 调用本地func不需要修改
-----------------------
| | // 被引用或定义的global的重定位信息
| .rel.data | // 已初始化的全局变量若初始值是全局变量地址或外部定义函数的地址,则需要被修改
| |
-----------------------
| | // 调试符号表,条目包括:
| .debug | // 局部变量和类型定义、定义和引用的全局变量,以及原始的C源文件
| | // 以 - g 选项调用编译器驱动程序时,才 会得到这张表
-----------------------
| .line | // 原始程序的行号与.text节中机器指令的映射
| | // 以 -g 选项调用编译器驱动程序时,才 会得到这张表
-----------------------
| .strtab | // 包括.symtab和.debug节中的符号表,以及节头部中的节名字
-----------------------
| 节头部表 |
-----------------------
symtab节
// code/link/elfstructs.c
typedef struct {
int name; /* String table offset */
char type:4, /* Function or data (4 bits) */
binding:4; /* Local or global (4 bits) */
char reserved; /* Unused */
short section; /* Section header index */
long value; /* Section offset or absolute address */
long size; /* Object size in bytes */
} Elf64_Symbol;
GOT节
Global Offset Table
用于地址解析
- 当
hello程序要访问共享库中的全局变量时,编译器和链接器会执行以下步骤
mov 0x200271(%rip),%rax # 200828 <_DYNAMIC+0x1a0>
mov (%rax),%eax- 第一条指令从
.got表中加载变量的实际地址,动态链接器将其保存至%rax; - 第二条指令通过
%rax里的值间接访问变量的实际地址
- 第一条指令从
PLT节
Produre Linkage Table
调用外部function或procedure (这些方法和过程在linking阶段地址是未知的,在运行时由动态链接器负责解析)
当hello程序要call hello@plt调用共享库中的函数或过程时,链接器会转到PLT节,并执行jmp *(hello@got):
- 当第一次调用该函数时,
hello@got中的值不是hello的实际地址,而是PLT中prepare resolver指令的地址,- PLT跳转到
prepare resolver,利用.plt.got表中存储的偏移信息计算函数的实际地址; - PLT存储该地址,填充到
hello@got; - 跳转到
hello相应地址;
- PLT跳转到
- 非首次调用某函数时,PLT直接跳转到已计算好的函数地址(如
<puts@plt>)
啥是延迟绑定
延迟绑定
动态链接中,函数在第一次被调用时,才进行符号查找、重定位等,如果函数不被调用就不进行绑定,以加快程序启动速度
- GOT是 数据段中确定全局变量和外部函数地址的表
- 有两种GOT表:
.got表用于全局变量的引用地址 .got.plt表用于保存函数引用地址
- 有两种GOT表:
- PLT是用于延迟绑定的的表
可执行目标文件的结构
linker把多个目标文件合并成一个可执行目标文件
-----------------------
| ELF header |
-----------------------
| 段头部表 | // 将连续的文件节映射到运行时内存段
-----------------------
| .init |
-----------------------
| .text |
-----------------------
| .rodata |
----------------------- // 以上为代码段/只读内存段
| .data |
-----------------------
| .bss |
----------------------- // 以上为数据段/读写内存段
| .symtab |
-----------------------
| .debug |
-----------------------
| .line |
-----------------------
| .strtab |
-----------------------
| 节头部表 | // 描述目标文件的节
----------------------- // 以上为不加载到内存的符号表和调试信息
加载目标文件的过程
加载器loader将可执行目标文件中的代码和数据从磁盘复制到内存,然后通过跳转到程序的第一条指令或入口点来运行该程序。