手撕ELF文件——从字节的角度


motivation:

网络上已有许多关于elf文件详解的优质文章,但对于强迫症患者或许还希望了解文件每一个字节的含义,因此本文将从elf文件字节码角度去简单观察其结构。阅读本文也许能帮助你理解编程语言中的各类数据如char, int以及结构体类型数据在内存中的布局方式。阅读本文前建议先阅读参考文章。

Reference:

  1. 简单了解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段可以用类似的方法,分割文件得到。