本篇介绍
本篇详细介绍下ELF 64的文件格式。
ELF文件分类 可重定位文件(.o),包含代码和数据,但是代码和数据都没有指定绝对地址,需要链接其他目标文件来生成可执行文件或共享目标文件 共享目标文件(.so),包含代码和数据,以供链接器使用 可执行文件, 包含代码和数据,是可以执行运行的程序,代码和数据都有固定的地址 ELF文件内容 一个ELF文件需要包含以下部分:
elf文件头,必须出现在elf文件的开头 节头表(Section header table), 重定位的文件(可重定位文件)必须包含,可加载的文件可选(共享目标文件,可执行文件) 段头表(Program header table),可加载的文件必选,重定位文件可选 段和节的实际内容,包括可加载的数据,符号表等 节头表和段头表其实分别是链接和加载的视图,结构大致如下:
image.png
ELF 64的数据类型定义如下:
image.png
ELF文件头格式 文件头格式如下:
image.png
可以找一个so,用 readelf -h 看看输出,结果可以完全对上:
image.png
对于MacBook M1 的设备可能没有readelf,objdump等命令,一个简单的方法是可以直接使用ndk中的命令,llvm-readelf, llvm-objdump等
接下来分别看下各个字段的含义:
e_ident elf文件的标识,一共16个字节,各个字段的含义如下:
image.png
e_ident[EI_MAG0] ~ e_ident[EI_MAG3] 是用来标识ELF文件的魔数,0x7f, 'E','L','F' e_ident[EI_CLASS] 用来标识对应的ELF文件类别,可取的值如下:
image.png
e_ident[EI_DATA] 用来区分字节序,可取的值如下
image.png
e_ident[EI_VERSION] 目标文件格式的版本,目前就是EV_CURRENT,也就是1 e_ident[EI_OSABI] 该文件的目标操作系统和ABI,可取的值如下
image.png
e_ident[EI_ABIVERSION] 该文件的目标ABI版本,如果兼容System V ABI 第三版,该字段应该是0 e_type 该文件的类型,可取的值如下
image.png
e_machine 标识目标架构 e_version 文件格式的版本 e_entry 程序入口的虚拟地址 e_phoff 程序段头表在该文件内的偏移,单位是字节 e_shoff 节头表在该文件内的偏移,单位是字节 e_flags 包含处理器特定的标记 e_ehsize ELF头的大小,单位是字节 e_phentsize 程序段头表项的大小,单位是字节 e_phnum 程序段头表项的数量 e_shentsize 节头表项的大小,单位是字节 e_shnum 节头表项的数量 e_shstrndx 节头表中包含节名字的字符串表索引。 ELF节 节包含了ELF文件中除了文件头,程序段头表,节头表之外的所有内容。
节的索引中有几项是特殊的,比如如下几个:
image.png
可以实际看一下节的内容,通过readelf -S 命令就可以看到:
image.png
再看下节头表中项结构的定义,可以和输出的格式对上:
image.png
sh_name 节头名字在字符串表中的偏移,单位是字节。举一个例子,上图中第一项的名字是".note.android.ident",看看是如何计算的。
节头表的位置是1393984, 转成16进制 0xD4EBC0, 每项64字节,加上第0个保留项,那偏移就是0xD4EC00,具体偏移是0x0b
image.png
字符串表的索引是24,那偏移就是0xD4F1C0,内容如下:
image.png
按照上述的结构定义,可以看到sh_offset 的偏移是0xD4EADC, 再加上字符串偏移 0x0B,位置就是0xD4EAE7
image.png
这样就字符串位置计算出来了。
image.png
image.png
sh_addr 该节在内存中的虚拟地址,如果不加载到内存中,地址是0 sh_offset 该节在文件中的偏移,单位是字节 sh_size 当前节在文件中占用的空间,唯一的例外是SHT_NOBITS,不占用文件空间 sh_link 当前节关联的节索引,用途如下所示
image.png
image.png
sh_addralign 对齐参考,需要是2的幂 sh_entsize 如果节中包含表,该字段表中每项的大小,单位是字节 用于程序代码和数据的节如下:
image.png
用于文件信息的节如下:
image.png
字符串表 字符串节表包含用于节名字和符号名字的字符串,内部的字符串表是包含C格式的字符串,对外的索引就是对应字符串的起始位置偏移,单位是字节。
符号表 符号表结构如下:
image.png
st_name 符号名字在符号字符串表中的偏移 st_info 符号的绑定属性和类型,高4比特是绑定属性,低4比特是符号类型,
绑定属性定义如下:
image.png
符号类型定义如下:
image.png
image.png
st_other 保留字段,保持是0就行 st_shndx 定义当前符号的节索引,如果是未定义的,字段值是SHN_UNDEF,对于绝对符号的,值是SHN_ABS,common符号的,值是SHN_COMMON st_value 符号的地址,可能是绝对或相对的地址,对于可重定位的文件,值是定义该符号的节的相对偏移,对于可执行或可供行的文件,值是定义该符号的虚拟地址 st_size 该符号对应的值的存储空间大小,如果未知,字段值是0 可重定位表 ELF 文件有2种重定位格式,"Rel"和"Rela", 前者较短,记录相对于符号原始值的偏移,后者是记录相对于特定字段的偏移。结构定义如下:
image.png
r_offset 标识需要重定位的位置,对于可重定位文件,是从节开头到需要被重定位的存储位置的偏移量,对于可执行或共享库,是需要被重定位的存储位置的虚拟地址,单位都是字节 r_info 包含符号表索引和重定向类型,符号表索引用于标识当前项在对应符号表中的符号,重定向类型是处理机指定的。 #define ELF64_R_SYM(i)((i) >> 32)
#define ELF64_R_TYPE(i)((i) & 0xf f f f f f f f L)
#define ELF64_R_INFO(s, t)(((s) << 32) + ((t) & 0xf f f f f f f f L))
r_addend 计算重定向位置时候需要额外加的常数项 程序段头表 对于可执行和共享库,为了加载方便,用的视图是段,也就是内容一样,只是分类方式变化了。
可以先实际看下共享库的段表信息,readelf -l libtxffmpeg.so
image.png
可以看到,段是由节组成的,这是对于加载器,权限一样的,就可以合并到一块,方便内存的管理。
再看下段结构,可以和上图对得上:
image.png
image.png
image.png
p_flags 段属性,高8比特是处理器专用,接下来8比特是环境变量专用
image.png
p_offset 当前段相对于文件的偏移,单位是字节 p_vaddr 当前段在内存中的虚拟地址 p_paddr 保留项,用于物理寻址的系统 p_filesz 当前段在文件中的大小,单位是字节 p_memsz 当前段在内存中的大小,单位是字节 p_align 段的对齐约束,需要是2的幂 Note 节 SHT_NOTE的节或PT_NOTE的段,被编译器或其他用于用于存放一些特殊的信息,而这些信息可以被特定的工具所用,格式如下:
image.png
namesz and name 记录用于该项所有者的名字长度和名字,name 包含一个C格式的字符串,并且按8字节对齐 descsz and desc 该项的描述符长度和描述符信息,描述符信息需要 8字节对齐 type 和该项所有者,信息解析者相关的一个值 动态段表 动态段表的实际内容如下:
image.png
结构定义如下:
image.png
d_tag 该项的类型,也会决定d_un的解析,具体取值如下:
image.png
image.png
image.png
d_val 按整数值解析 d_ptr 按虚拟地址解析 哈希表 使用哈希表可以加快动态符号表的查找速度。哈希表就是DT_HASH的节,看下实际例子的输出,命令是readelf --gnu-hash-table
:
image.png
内容比较多,忽略了一部分,接下来看下结构:
image.png
对应的哈希函数如下:
image.png
一个哈希表需要解决如何快速查找,如何解决冲突的问题。
看看hash 表如何快速查找,这儿用到了一个Bloom Filter, 本质上就是在查找前先用Bloom Filter判断下,如果结果是不在,那么就没必要查找了,如果是在,实际上也不一定在,就需要实际去查一下。
解决冲突时利用了chain数组,在查找符号时,如果Bloom Filter判断出在,然后就在bueket中对应索引位置看看,如果等于期望的字符串,那么直接返回,然后在chain数组同样索引位置拿到下一个需要查找的位置,然后递归查找下去,直到 chain中的值是STN_UNDEF。