手撕ELF文件——从字节的角度
motivation:
网络上已有许多关于elf文件详解的优质文章,但对于强迫症患者或许还希望了解文件每一个字节的含义,因此本文将从elf文件字节码角度去简单观察其结构。阅读本文也许能帮助你理解编程语言中的各类数据如char, int以及结构体类型数据在内存中的布局方式。阅读本文前建议先阅读参考文章。
Reference:
- 简单了解ELF文件类型
-
一、简介
ELF(Executable and Linkable Format)是在Unix系统上编译执行文件时采用的文件格式,用于使汇编器,链接器认识文件内容,以及在加载执行的时候能够适当地执行。
通俗地讲,是给你编写的代码加上一些控制首部,类似于计算机网络中分层体系结构给下层传递数据时,总是加上首部,使对方能够合适地解码这个包。
Elf文件可分为四部分:elf header、program header table、section header table以及section(包含每个具体的section)。需要首先了解的是,elf文件不仅用于编译过程,也用于执行过程,每个过程使用到的信息
不尽相同。在编译过程中,编译器需要使用到elf header、section header table和每个section的具体信息。
编译器首先通过elf header,得到文件整体情况以及program header table和section header table在文件中的起始地址及其长度,找到section header table之后,可得到每个section的位置长度信息。我们可以按照这个思路先分析header得到section header table的信息,进而确定整个文件每个section的位置长度信息,对文件按section进行分割,完成对文件的字节解码。
elf header以及section header table每个元素的结构体在/usr/include/elf.h中定义。
二、实验环境
环境:ubuntu20.04
工具:readelf、objdump、sublime-text
源文件example.c:
1 int printf(const char* format, ...);
2
3 int global_var1 = 1;
4 int global_var2;
5
6 void fun();
7
8 int main() {
9 int local_var1 = 3;
10 int local_var2;
11 fun();
12 return 0;
13 }
14
15 void fun() {
16 printf("Hello! elf.\n");
17 }
使用gcc编译得到objExample.o文件,以下的实验过程都将基于objExample.o文件进行:
gcc -o objExample.o -c example.c
三、分析elf header和section header table
使用sublime-text打开objExample.o可以直接看到其二进制代码,我们只取elf header结构体所代表的64个字节。
1 7f45 4c46 0201 0100 0000 0000 0000 0000
2 0100 3e00 0100 0000 0000 0000 0000 0000
3 0000 0000 0000 0000 e803 0000 0000 0000
4 0000 0000 4000 0000 0000 4000 0e00 0d00
将这64个字节与elf.h中的Elf64_Ehdr结构体定义相比较即可得到每一个字节代表的信息:
1 typedef struct
2 {
3 unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
4 Elf64_Half e_type; /* Object file type */
5 Elf64_Half e_machine; /* Architecture */
6 Elf64_Word e_version; /* Object file version */
7 Elf64_Addr e_entry; /* Entry point virtual address */
8 Elf64_Off e_phoff; /* Program header table file offset */
9 Elf64_Off e_shoff; /* Section header table file offset */
10 Elf64_Word e_flags; /* Processor-specific flags */
11 Elf64_Half e_ehsize; /* ELF header size in bytes */
12 Elf64_Half e_phentsize; /* Program header table entry size */
13 Elf64_Half e_phnum; /* Program header table entry count */
14 Elf64_Half e_shentsize; /* Section header table entry size */
15 Elf64_Half e_shnum; /* Section header table entry count */
16 Elf64_Half e_shstrndx; /* Section header string table index */
17 } Elf64_Ehdr;
对比之后可以发现,section header table的起始地址在0x03e8,每个header大小为0x40字节,共有0x0e个header。sublime-text默认打开以每行16个字节显示,我们定位到第62行,往后8个字节,依次分割长度为64字节的部分,即可得到每个section header的字节信息。
将每个header的信息与elf.h中的结构体定义相比较即可得到每个字节所代表的信息:
1 typedef struct
2 {
3 Elf64_Word sh_name; /* Section name (string tbl index) */
4 Elf64_Word sh_type; /* Section type */
5 Elf64_Xword sh_flags; /* Section flags */
6 Elf64_Addr sh_addr; /* Section virtual addr at execution */
7 Elf64_Off sh_offset; /* Section file offset */
8 Elf64_Xword sh_size; /* Section size in bytes */
9 Elf64_Word sh_link; /* Link to another section */
10 Elf64_Word sh_info; /* Additional section information */
11 Elf64_Xword sh_addralign; /* Section alignment */
12 Elf64_Xword sh_entsize; /* Entry size if section holds table */
13 } Elf64_Shdr;
我们取其中关键的位置信息sh_offset和sh_size(也就是每个header中第24到40个字节中的信息),可以得到每个section的起始地址以及长度。上图中第一个header内容为全0,没有具体信息;第二个header起始地址为0x40,长度为0x3b;第三个header起始地址为0x02f8,长度为0x48;依次类推。
为了方便,我们可以直接使用readelf命令得到每个section的位置及大小:
readelf -SW objExample
依据readelf的输出,我们就可以开始分割objExample.o文件了:
为节约篇幅,此处仅分析elf文件中的前两个section,.text和.data。
.text段中存储的是源文件中可执行代码编译成的二进制代码,我们可以通过objdump进行反汇编得到其对应的汇编代码:
1 objdump -d objExample
.data段存储的是源文件中的已赋初值的全局变量的值,此处对应的是4字节的int型变量:int global_var1 = 1;源文件中仅定义了这一个已赋值的全局变量,因此.data段的长度仅为4字节,存储的值为0x00000001。
.text段与.data段之间的一个字节00,个人猜测是为补齐而填充的字节。
其他的section段可以用类似的方法,分割文件得到。