芜湖✈️
ELF头部的格式在include/linux/elf.h
中有定义 (实际上在本人测试的5.11内核中 头部定义在include/uapi/linux/elf.h
)
ctypedef struct elf64_hdr {
unsigned char e_ident[EI_NIDENT]; /* ELF "magic number" */
Elf64_Half e_type; /* 对象文件类型 */
Elf64_Half e_machine; /* 架构 */
Elf64_Word e_version; /* 对象文件版本 */
Elf64_Addr e_entry; /* 程序入口的虚拟地址 */
Elf64_Off e_phoff; /* Program header table file offset 程序头表的偏移 */
Elf64_Off e_shoff; /* Section header table file offset 节头表的偏移 */
Elf64_Word e_flags; /* 保存与文件相关的 特定于处理器的标志 */
Elf64_Half e_ehsize; /* ELF头部大小 */
Elf64_Half e_phentsize; /* 程序头表的条目大小 */
Elf64_Half e_phnum; /* 程序头表的条目数 */
Elf64_Half e_shentsize; /* 节头表的条目大小 */
Elf64_Half e_shnum; /* 节头表的条目数 */
Elf64_Half e_shstrndx; /* 节头表中与节名称字符串表相关的条目索引 */
} Elf64_Ehdr;
ELF文件以ELF头部为起始 而ELF头部以e_ident数组
作为起始
通常来说e_ident数组
开头的4字节是固定的 第一个字节为0xf
后面三个字节分别为ELF
的ASCII码 这样处理的好处是方便file
或者二进制加载器能够快速识别正在处理的是ELF二进制程序
e_ident
数组其余键值的意义同样存储在include/uapi/linux/elf.h
中 (5.11内核)
c#define EI_MAG0 0 /* e_ident[] indexes */
#define EI_MAG1 1
#define EI_MAG2 2
#define EI_MAG3 3
#define EI_CLASS 4
#define EI_DATA 5
#define EI_VERSION 6
#define EI_OSABI 7
#define EI_PAD 8
EI_CLASS
主要表示的是该二进制程序架构的位宽 (是64位架构的还是32位架构的)
如果为64位则该键值为2 (常量ELFCLASS64)
如果为32位则为1 (常量ELFCLASS32)
EI_DATA
主要表示的是架构的字节序 其实就是在内存中是大端序还是小端序
如果为小端序则该键值为1 (常量ELFDATA2LSB)
如果为大端序则为2 (常量ELFDATA2MSB)
EI_VERSION
主要表示的是创建二进制文件时使用的ELF规范版本 有效值只有1 (常量EV_CURRENT)
EI_OSABI
主要表示的是关于二进制程序的二进制接口(Application Binary Interface)的相关信息
如果EI_OSABI
的值为非0 则二进制文件中会使用一些ABI-
或者OS-
的具体扩展名 这可能会改变二进制文件中某些字段的含义
如果EI_OSABI
的值为0 则表示该二进制文件以UNIX System V ABI
为目标
什么是ABI?
简单来说 就是一系列约定的集合 比如FreeBSD约定函数调用的头六个参数放到RDI RSI RDX RCX R8 R9上 XMM0-XMM7用来放置浮点变元一样 可以说调用惯例就是ABI 一般来说ABI和CPU具体架构和OS相关
具体而言ABI包含以下内容:
- 特定的处理器指令集
- 函数调用惯例
- 系统调用方式
- 可执行文件的格式
统一ABI的目的是 只要OS遵守统一的ABI那么不同的应用就自动可以实现向前兼容 不用担心版本兼容性问题
EI_PAD
跟计算机网络中头部的拓展字段一样 为了以后的发展空闲出来的空间 保留以供将来使用 当前设置值为0
使用readelf
命令可以查看二进制程序的头部的e_ident数组及其含义
bashreadelf -h 2
该字段指明了二进制文件的类型 常见的值有ET_REL(可重定位二进制程序)
ET_EXEC(可执行二进制程序)
ET_DYN(动态库/共享对象文件)
表示了二进制文件计划在某个体系结构上运行 常见的值有EX_X86_64(64位x86架构二进制文件)
EM_386(32位x86架构二进制文件)
EM_ARM(ARM架构二进制文件)
与EI_VERSION
作用类似 表示的是创建二进制文件时使用的ELF版本规范 唯一有效值为1 (常量EV_CURRENT)
该字段表明了二进制文件的入口点 对于上面使用readelf
来测试的样例来说 程序入口点为0x400530
此处也是解释器(在第一章解释过 为ld-linux.so
)将二进制程序加载到虚拟内存后转移控制权的地方
该字段表明了对于程序头表(program header table)相对于与文件起始的偏移值 单位为字节 以上面的测试样例来说 程序头表相对于文件开始为64B 此处值可以为0 表示程序不包含文件头表
该字段的作用和上面的e_phoff
差不多 只不过该字段表明的是节头表(section header table)相对于文件起始的偏移值 单位为字节
该字段表明了二进制程序在特定CPU处理的标志 例如在计划在ARM上运行的二进制程序可以在e_flags
上设置ARM的特定标志 但是针对一般的x86程序来说 此处一般设置为0
该字段表明了二进制程序的ELF头部大小 对于64位的二进制程序 头部大小始终为64B 对于32位的二进制程序 头部大小始终为52B
根据e_phoff
字段可以知道程序头表的起始位置 但是链接器和加载器要遍历这些表的时候还需要知道各个程序头的大小 以及每个表中程序头的数量 而这些信息就是由e_phentsize
和e_phnum
字段来表示的
跟上面的差不多 只不过此处提供的是节头表的相关信息
该字段包含一个名为.shstrtab
的头索引(在节头表中) 这是一个专用节 专门用来保存所有节的名称 使用readelf
可以通过这个专用节来正确显示所有节的名称
二进制文件中的代码和数据在逻辑上被分为连续的非重叠块 称为节 每个节都是由节头描述 节头定义了节的属性 并且允许找到节中字节的位置 二进制文件中所有节的节头都包含在节头表中 但是实际上并不是所有节的内容在执行时都是需要的 比如符号信息和重定位信息在执行时其实是没有什么用的
节主要是为链接器提供服务 所以节头表其实是ELF文件的可选部分 不需要链接的文件不需要节头表
节头的定义同样存在与include/uapi/linux/elf.h
中
ctypedef struct elf64_shdr {
Elf64_Word sh_name; /* Section name, index in string tbl */
Elf64_Word sh_type; /* Type of section */
Elf64_Xword sh_flags; /* Miscellaneous section attributes */
Elf64_Addr sh_addr; /* Section virtual addr at execution */
Elf64_Off sh_offset; /* Section file offset */
Elf64_Xword sh_size; /* Size of section in bytes */
Elf64_Word sh_link; /* Index of 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;
如果该字段被设置 则表示在字符串表中存在索引 也即在之前说过的.shstrtab
的特殊节中存在该段的名称的字符串 sh_name
字段将对本段的名称在.shstrtab
中的位置进行索引 以找到本节名称的字符串
表明了本节的类型 此信息主要提供给链接器在链接时使用
类型SHT_PROGBITS
的节包含了程序数据 如机器指令或者常量 这些节没有特定的结构供链接器解析
类型SHT_SYMTAB(静态符号表)
SHT_DYNSYM(动态符号表)
SHT_STRTAB(字符串表)
为特殊节类型
符号表的内容是格式明确的符号 该表会描述特定文件的偏移 地址 符号名称和类型等信息
类型为 SHT_REL
或者 SHT_RELA
的节往往包含着格式明确的重定位项 链接器可以解析该重定位项然后以便在其他节中实现重定位 (SHT_REL
和SHT_RELA
用于静态链接)
每个重定位项都会告诉链接器二进制文件中需要重定位的特定位置 以及重定位需要解析的符号
类型SHT_DYNAMIC
的节包含动态链接中所需要的信息
描述了节的一些其他信息
SHF_WRITE
表示该节运行时可写 SHF_ALLOC
表示在执行二进制程序时将该节加载到虚拟内存中 SHF_EXECINSTR
表示该节包含可执行指令
描述该节的虚拟地址 当该值为0时 表示该节不会加载到虚拟内存中
描述该节的相对于文件起始位置的文件偏移
描述该节的大小
该字段表示了节与节之间的关系 比如重定位节(SHT_REL
或者SHT_RELA
)与描述重定位所涉及到的符号的符号表相关联 sh_link
字段通过表示相关节之间在节头表中的索引来表明这些节之间的关系
该字段存储与节相关的额外信息 比如对于SHT_REL
或者SHT_RELA
的节来说 此处存放的就是用于重定位节的节头索引
指明了该节在内存中的对齐方式 如果为16则表示该节的基址必须为16的倍数 如果为0或者1表示没有特殊的对齐要求
某些节(符号表或者重定位表)包含着固定大小的条目 对于这些节使用sh_entsize
字段来规定每个条目的长度 单位为字节
如果该节不包含固定长度条目的表格 那么本字段为0
对于一个ELF文件我们可以使用readelf
来查看该文件的节
bashreadelf --sections --wide 2
该节的``sh_flags字段为
SHF_EXECINSTR 即包含可执行代码 在控制权转移到二进制程序main入口点之前会先执行
.init节`的代码 作用类似于构造函数
作用类似于.init节
效果类似于析构函数
包含程序的主要代码 其类型为SHF_EXECINSTR
一般来说是可执行不可写的
其中除了主要的用户代码 .text节
内同样包含了许多执行初始化和终止任务的标准函数 例如_start
register_tm_clones
等
对于一个程序的运行 通常认为是从main函数
开始的 但是检查入口点会发现 最初执行的函数是_start
asmDisassembly of section .text: 0000000000400530 <_start>: 400530: 31 ed xor ebp,ebp 400532: 49 89 d1 mov r9,rdx 400535: 5e pop rsi 400536: 48 89 e2 mov rdx,rsp 400539: 48 83 e4 f0 and rsp,0xfffffffffffffff0 40053d: 50 push rax 40053e: 54 push rsp 40053f: 49 c7 c0 60 07 40 00 mov r8,0x400760 400546: 48 c7 c1 f0 06 40 00 mov rcx,0x4006f0 40054d: 48 c7 c7 71 06 40 00 mov rdi,0x400671 400554: e8 a7 ff ff ff call 400500 <__libc_start_main@plt> 400559: f4 hlt 40055a: 66 0f 1f 44 00 00 nop WORD PTR [rax+rax*1+0x0] ... 0000000000400671 <main>: 400671: 55 push rbp 400672: 48 89 e5 mov rbp,rsp 400675: 48 83 ec 20 sub rsp,0x20 ...
通过观察_start
的汇编可以发现 在0x400554处调用函数_libc_start_main
其参数为0x400671
也即为main函数的地址
那么整个调用流程即为_start->_libc_start_main->main
该节类型为SHT_NOBITS
即这一节的内容是空的 节并不占用实际的空间
这是因为.bss节的作用是当二进制程序建立执行环境时为未初始化变量分配适当大小内存块用的
一般来说该节中的变量初始化为0 并且该节可写
该节通常保存初始化变量的默认值 因为在运行时可能变量的值需要修改 所以该节可写
该节通常存储只读数据 即常量 所以该节不可写
这里再看一眼之前使用readelf
获取到二进制程序中的节信息
可以发现在所有节中存在两个.rela.*的节
其类型为SHT_RELA
表名其包含链接器用于重定位的信息
每个SHT_RELA
类型的节都是一个重定位条目 每个条目都说明了需要应用重定位的特定地址 以及在此地址需要插入的值
bashreadelf --relocs 2
可以发现针对重定位条目也存在两种不同的类型 分别为R_X86_64_GLOB_DAT
和R_X86_64_JUMP_SLO
其中R_X86_64_GLOB_DAT
主要用于将全局变量的地址存储到GOT表 当程序调用全局变量时 GOT会将函数的地址动态解析出来 以实现动态链接。
而``R_X86_64_JUMP_SLOT`主要用于将函数的地址存储到PLT中 当程序调用该函数时 PLT会将函数的地址动态解析出来 以实现动态链接
bashreadelf --dynamic 2
在.dynamic节
中每个条目都称为标签
每个标签都有一个关联值和其类型
类型NEEDED
主要是通知动态链接器解决关于可执行文件的依赖问题
类型DT_VERNEED
和DT_VERNEEDNUM
指定了版本依赖表的旗帜位置和条目数 而版本依赖表指定了可执行文件各种依赖的版本
.init_array节
包含一个指向构造函数的指针数组 在程序被初始化之后 main函数调用之前会一次调用这些构造函数
bashobjdump -d --section .init_array 2
可以发现在之前测试的二进制程序中只包含一个函数__frame_dummy_init_array_entry
其地址为0x400500
基本同.init_array节类似
程序头表提供了二进制文件的段视角 而节头表提供了二进制程序的节视角 在二进制文件加载到进程中并执行的时候 定位相关代码和数据并确定加载到虚拟内存中时 操作系统和链接器就会使用到段视角
一般来说每个段包含多个节 程序头表使用Elf64_Phdr
结构体的类型来对段视图进行描述 该结构体定义于include/uapi/linux/elf.h
ctypedef struct elf64_phdr {
Elf64_Word p_type;
Elf64_Word p_flags;
Elf64_Off p_offset; /* Segment file offset */
Elf64_Addr p_vaddr; /* Segment virtual address */
Elf64_Addr p_paddr; /* Segment physical address */
Elf64_Xword p_filesz; /* Segment size in file */
Elf64_Xword p_memsz; /* Segment size in memory */
Elf64_Xword p_align; /* Segment alignment, file & memory */
} Elf64_Phdr;
bashreadelf --wide -segments 2
![image-20230427152036937](/Users/du4t/Library/Application Support/typora-user-images/image-20230427152036937.png)
表明了段的类型 该字段比较重要的类型有PT_LOAD
PT_DYNAMIC
PT_INTERP
PT_LOAD
类型的段会在创造进程时直接加载到内存中 程序头的剩余部分表明可加载块的大小和将其加载到的地址
PT_INTERP
类型的段包含了.interp节
该节通常提供加载二进制程序的解释器的名称
PT_DYNAMIC
类型的段相对应的包含了.dynamic节
该节告诉解释器如何解析二进制程序以执行
PT_PHDR
类型的段表示其包含程序头表
表明该段在执行时的权限
PF_X
该段可执行
PF_W
该段可写
PF_R
该段可读
效果同节头中的sh_offset
sh_addr
sh_size
分别指定了该段的起始文件偏移 加载的虚拟地址 以及段大小
对于可加载段p_vaddr
与p_offset
相等
同sh_addralign
表明了内存对齐方式
本文作者:Du4t
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!