成就更好的自己
本篇是基础技术系列中ELF相关技术的第二篇,将会详细介绍一下ELF文件的结构。
没有看过之前的文章的朋友请重新开始,博主观点比较清奇,否则可能会有一些不太明白的地方:
基础技术-ELF系列(1)-ELF文件基础-CSDN博客
目录
Libelf的简要说明
ELF格式说明
ELF头结构体- GElf_Ehdr
节头结构体- GElf_Shdr
程序头结构体- GElf_Phdr
符号描述结构体- GElf_Sym
Libelf的简要说明
近期工作上有一个小任务,需要在不进行编译的条件下,对比几个库之间是否存在符号重复;原本可以使用nm或者readelf可以通过人肉方式去筛查,但为了减少重复工作,就打算通过程序进行对比;一般来讲使用C/C++的话都会使用libelf进行ELF文件的解析,但是一搜才发现讲ELF的都不是很细致更别提libelf了,这也是撰写本系列文章的根本原因。
Libelf的包名大多是情况下叫做libelf-dev或者libelf-devel,一般的系统不一定会带有这个包,需要通过apt或者yum进行安装,或通过源码的方式进行编译安装。
Apt安装命令:apt install libelf-dev
Yum安装命令:yum install elfutils-libelf-devel
源码地址:GitHub - WolfgangSt/libelf: libelf
Libelf是一个对ELF文件进行解析的的一个库,按照既定的规则调用病解析这个库你可以查看一个ELF文件中的所有想要知道的结构和信息。
目前网上关于ELF文件的解析都会莫名奇妙的拿出来一些结构体进行讲解,实际上,这些结构体都是出自这个库中的libelf.h和gelf.h。
ELF格式说明
主要与ELF格式相关的结构体有下面这几种:
- GElf_Ehdr:存放ELF文件头的结构,对应于ELF文件的ELF头;
- GElf_Shdr:存放单个节头的结构,对应于节头表中的单个节头;
- GElf_Phdr:存放单个程序头的结构,对应于程序头表中的单个程序头;
- GElf_Sym:存放一个节中一个符号的结构,对应于节中的一个符号描述,注意指的不是符号的内容,而是这个符号的一个描述结构,包括这个符号的名称,实际数据对应地址等信息;
还有几种中间类型结构体暂时没有用到,下一篇博客会进行说明。了解了这些基本结构,就可以解析ELF文件了。这张图我这里不做解释,大家可以参考上篇文章内容和这里所说的进行对比理解一下(理解了才能往下看,对吧)。
接下来是对上述结构体中的内容进行简单说明。
ELF头结构体- GElf_Ehdr
typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
Elf64_Half e_type; /* Object file type */
Elf64_Half e_machine; /* Architecture */
Elf64_Word e_version; /* Object file version */
Elf64_Addr e_entry; /* Entry point virtual address */
Elf64_Off e_phoff; /* Program header table file offset */
Elf64_Off e_shoff; /* Section header table file offset */
Elf64_Word e_flags; /* Processor-specific flags */
Elf64_Half e_ehsize; /* ELF header size in bytes */
Elf64_Half e_phentsize; /* Program header table entry size */
Elf64_Half e_phnum; /* Program header table entry count */
Elf64_Half e_shentsize; /* Section header table entry size */
Elf64_Half e_shnum; /* Section header table entry count */
Elf64_Half e_shstrndx; /* Section header string table index */
} Elf64_Ehdr;
成员 | 意义 |
e_ident | 16 字节数组,用于标识 ELF 对象,始终以“\x7fELF”开头 |
e_type | 指定 该ELF文件类型,是可执行文件还是重定位还是其他的 |
e_machine | 该ELF文件使用或运行的目标架构 |
e_version | 使用的ELF版本 |
e_entry | 若为可执行文件,则为程序的虚拟地址入口。有时候可以为0 |
e_phoff | 表示程序头表中首个程序头对于该文件的偏移量,单位字节 |
e_shoff | 表示节头表中首个节头对于该文件的偏移量,单位字节 |
e_flags | 与处理器相关的一些标志 |
e_ehsizeEhdr | ELF文件头大小,单位字节。(通常在 64 位 ELF 中为 64 字节,在 32 位中为 52 字节) |
e_phentsize | ELF文件中单个程序头的大小,单位字节 |
e_phnum | ELF文件中程序头的数量 |
e_shentsize | ELF文件中单个节头的大小,单位字节 |
e_shnum | ELF文件中节头的数量 |
e_shstrndx | 存放每个节的名称字符串的字符串表 |
节头结构体- GElf_Shdr
typedef struct
{
Elf64_Word sh_name; /* Section name (string tbl index) */
Elf64_Word sh_type; /* Section type */
Elf64_Xword sh_flags; /* Section flags */
Elf64_Addr sh_addr; /* Section virtual addr at execution */
Elf64_Off sh_offset; /* Section file offset */
Elf64_Xword sh_size; /* Section size in bytes */
Elf64_Word sh_link; /* Link to 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;
成员 | 意义 |
sh_name | 节名称,实际上是个偏移值,是一个基于在ELF头中的e_shstrndx指定的节中进行偏移取字符串的偏移值 |
sh_type | 该节的类型 |
sh_flags | 该节的内容是什么操作属性的,可读可写可执行等 |
sh_addr | 若该节处于某个要加载到进程空间的段中,该项说明该节数据内容第一个字节在进程空间所处的虚拟地址 |
sh_offset | 该节数据内容第一个字节基于该ELF文件的偏移量 |
sh_size | 该节数据内容长度,单位字节 |
sh_link | 与该节类型相关的另一个节的节头索引,比如对于类型为SHT_SYMTAB, SHT_DYNSYM的节,该项表示该节相关联的字符串表节的节头索引 |
sh_info | ~ |
sh_addralign | 若该节的内容为代码,则包含与对齐相关的信息 |
sh_entsize | 某些节中包含固定大小的项目,如符号表。对于这类节,该成员给出每个表项的长度字节数 |
特殊节
- .init:执行初始化任务的可执行代码,需要在执行二进制文件中的任何其他代码之前运行(SHF_EXECINSTR类型),系统在将控制权转移到二进制文件的主入口点之前执行 .init 部分中的代码。
- .fini:与.init相反,它具有在主程序完成后必须运行的可执行代码。
- .text:是程序的main所在位置(SHT_PROGBITS类型),包含用户定义的代码。
- .bss:包含未初始化的数据(SHT_NOBITS类型)。它不占用磁盘空间而占用内存,因此所有数据通常在运行时初始化为零,flag是可写的。
- .data:程序初始化数据(SHT_PROGBITS类型),flag是可写的。
- .rodata:只读数据,例如代码使用的字符串和常量。
- .plt:过程链接表,用于动态链接的代码,有助于在 got(全局偏移表)的帮助下从动态库调用外部函数。
- .got.plt:存储外部函数解析地址的表。默认情况下可写。
- .rel.*:包含有关在链接或运行时如何修复或修改 ELF 对象或进程映像的各个部分的信息(SHT_REL类型)。
- .rela.*:包含有关如何在链接或运行时修复或修改 ELF 对象或进程映像的各个部分的信息(带加数)(类型SHT_RELA)。
- .dynamic:动态链接结构。包含结构表ElfN_Dyn,还包含指向动态链接器所需的其他重要信息的指针。
- .init_array:包含指向用作构造函数的函数的指针数组(初始化二进制文件时依次调用这些函数)。在gcc中,您可以通过__attribute__((constructor)将其标记为构造函数。默认情况下,中有一个.init_array用于执行frame_dummy的条目。
- .fini_array:包含指向用作析构函数的函数指针数组。
- .shstrtab:保存节名称的节,通过节头中的name的值进行偏移获取名称字符串。
- .symtab:包含静态符号的符号表。
- .strtab:保存相关符号名称的节,通过sym结构中的name进行偏移获取名称字符串。
- .dynsym:与.symtab相同,但包含动态链接而非静态链接所需的符号。
- .dynstr:与.strtab相同,但包含动态链接而不是静态链接所需的字符串。
- .rel.dyn:全局变量重定位表。
- .rel.plt:函数重定位表。
程序头结构体- GElf_Phdr
typedef struct
{
Elf64_Word p_type; /* Segment type */
Elf64_Word p_flags; /* Segment 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 */
} Elf64_Phdr;
成员 | 意义 |
p_type | 该段的类型 |
p_flags | 该段的内容是什么操作属性的,可读可写可执行等 |
p_offset | 该段数据内容第一个字节基于该ELF文件的偏移量 |
p_vaddr | 该段数据内容第一个字节在进程空间所处的虚拟地址 |
p_paddr | 该段数据内容第一个字节在进程空间所处的物理地址,一般用不到 |
p_filesz | 该段在磁盘中占用的大小,单位字节 |
p_memsz | 该段在内存中占用的大小,单位字节,与上一点的区别在于有些段需要在内存中占用空间而不在磁盘中占用,比如.bss |
符号描述结构体- GElf_Sym
typedef struct
{
Elf64_Word st_name; /* Symbol name (string tbl index) */
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* Symbol visibility */
Elf64_Section st_shndx; /* Section index */
Elf64_Addr st_value; /* Symbol value */
Elf64_Xword st_size; /* Symbol size */
} Elf64_Sym;
成员 | 意义 |
st_name | 符号名称对应的偏移量,可通过与符号所属节st_shndx的节头中的sh_link成员指定的字符串节进行偏移得到名称字符串 |
st_info | 指定符号的类型和绑定属性,分为BIND和TYPE两种信息组合得到 |
st_other | ~ |
st_shndx | 存放该符号描述所属的节的节头索引 |
st_value | 与该符号相关联的值,存放的都是该符号数据内容的偏移或地址等信息,根据节类型不同情况不同 |
st_size | 该符号数据的尺寸数据,根据符号类型不同情况不同 |