ELF32-i386 文件格式简要分析
通过简单的一些 Case,通过实际操作对 ELF32-i386 文件做概要分析。
使用 Docker 来统一整体的开发、测试环境,适用于 Linux、Windows、MacOS(包括 Intel、M1 芯片)。
ELF
The Executable and Linking Format (中文名为:可执行与可链接格式)是一种在计算机领域,通用的标准化的可执行文件(同时,也用于目标代码 obj、共享库 so 等文件)。
ELF 文件是 Linux 下最常用的可执行文件的格式。在 gcc hello.cc 后生成的 a.out 可执行文件就是 ELF 格式的。
ELF 文件格式
ELF 文件的总体格式如下:
</img>
因为 ELF 格式的目标文件,是参与了程序构建、程序执行两个流程,所以 ELF 文件也分别对这两个流程,有两种视图:一种是链接视图(Linking View),一种是执行视图(Executing View)。[5]
同时也对应了上图的不同方向的箭头指向。
</img>
ELF 格式结构
根据 Tool Interface Standard (TIS) Executable and Linking Format (ELF) Specification 的规范描述。
约定的基础类型有:
| Name | Size | Alignment | Purpose | 
|---|---|---|---|
| Elf32_Addr | 4 | 4 | Unsigned program address | 
| Elf32_Half | 2 | 2 | Unsigned medium integer | 
| Elf32_Off | 4 | 4 | Unsigned file offset | 
| Elf32_Sword | 4 | 4 | Signed large integer | 
| Elf32_Word | 4 | 4 | Unsigned large integer | 
| unsigned char | 1 | 1 | Unsigned small integer | 
ELF Header
#define EI_NIDENT 16
typedef struct {
  unsigned char e_ident[EI_NIDENT];
    Elf32_Half e_type;
    Elf32_Half e_machine;
    Elf32_Word e_version;
  Elf32_Addr e_entry;
  Elf32_Off e_phoff;
  Elf32_Off e_shoff;
  Elf32_Word e_flags;
  Elf32_Half e_ehsize;
  Elf32_Half e_phentsize;
  Elf32_Half e_phnum;
  Elf32_Half e_shentsize;
  Elf32_Half e_shnum;
  Elf32_Half e_shstrndx;
} Elf32_Ehdr;
相关 readelf 实现可参考 bminor/binutils-gdb
Program Header
typedef struct {
  Elf32_Word p_type;
  Elf32_Off p_offset;
  Elf32_Addr p_vaddr;
  Elf32_Addr p_paddr;
  Elf32_Word p_filesz;
  Elf32_Word p_memsz;
  Elf32_Word p_flags;
  Elf32_Word p_align;
} Elf32_Phdr;
Selection Header
typedef struct {
  Elf32_Word sh_name;
  Elf32_Word sh_type;
  Elf32_Word sh_flags;
  Elf32_Addr sh_addr;
  Elf32_Off sh_offset;
  Elf32_Word sh_size;
  Elf32_Word sh_link;
  Elf32_Word sh_info;
  Elf32_Word sh_addralign;
  Elf32_Word sh_entsize;
} Elf32_Shdr;
构建环境
构建镜像
本文采用 docker 来进行环境的统一,避免各种操作系统、各种版本、环境、CPU 所带来的差异。
新建并编辑 Dockerfile 如下:
# 指定 AMD64
# 按需替换为自己的加速站点,比如 dockerpull.com/ubuntu:22.04
FROM --platform=linux/amd64 mirror.gcr.io/ubuntu:22.04
# 使用清华源
COPY <<EOF /etc/apt/sources.list
deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ jammy main restricted universe multiverse
deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ jammy-updates main restricted universe multiverse
deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ jammy-backports main restricted universe multiverse
deb http://security.ubuntu.com/ubuntu/ jammy-security main restricted universe multiverse
EOF
RUN apt-get update
# 安装必要的基础工具
RUN apt-get install gcc nasm make xxd -y
WORKDIR /home
执行构建命令
$ docker build -t gcc-amd64 .
别忘了最后的
.,用于指定是当前目录的Dockerfile文件。
运行镜像
为了方便文件的操作,一般会新建一个文件夹和 docker 环境进行共享,这样复制、添加、删除比较多的文件时,就可以直接在非 docker 命令行下操作。
$ docker run -it --rm -v .:/home gcc-amd64
- -itinteractive & terminal,进入交互式命令行终端
- --rm运行后删除,避免退出容器后,保留过多不必要的运行容器
- -v挂载当面目录至 docker /home 目录下
最简单的汇编程序
为避免传统 Hello World 较为复杂的字符串数据存储、中断、标准输入输出等概念,本文使用更为简单的加法程序,并使用 return 进行输出返回。
加法汇编源码
新建 plus.s 文件,参考 Assembly Programming Tutorial,编辑代码如下:
注意此处为 nasm 汇编,非 GAS 汇编,相关内容参考:Linux assemblers: A comparison of GAS and NASM
section .text     ; .text 段落,用于存放具体执行代码
global _start     ; 声明入口函数
_start:           ;
   mov   ax, 1    ; ax 寄存器放入 1
   mov   bx, 2    ; bx 寄存器放入 2
   add   bx, ax   ; 执行加法,并将结果放入 bx 寄存器
   ; 执行程序退出
   mov	ax,1     ; 中断号 1
   int	0x80     ; 执行内核中断,中断号为 ax 的 1
编译执行
此处都在 docker 环境中执行。
# 编译汇编代码为 obj 代码
$ nasm -f elf plus.s
# 链接生成可执行文件
$ ld -m elf_i386 -s -o ./plus ./plus.o
# 执行代码
$ ./plus
# 打印程序 exit code,此处打印出 3,说明正确执行了 1 + 2
$ echo $?
查看程序执行完成后的 exit code,可以使用
echo $?命令。
ELF 格式简析
使用 xxd ./plus 查看可执行加法程序的二进制文件格式。
# 解析 plus 可执行程序为二进制,并保持到 ./plus.txt
$ xxd ./plus > ./plus.txt
后续就可以基于这个文件,分字节来做解析了。
另外,可以通过 readelf 命令,来快速查看相关二进制的说明。
$ readelf -a ./plus
ELF Header
ELF 文件头解析,一般为 52 个字节。
0x00
- e_ident[EI_NIDENT]
    - .ELF标记
- classELF32- 0x01, 2 为 64 位
- datadata 2’s complement, little endian- 0x01,小端编码
- version1 (current)- 0x01
- padding, 补齐到 16 位- 0x00 0000 0000 0000
 
0x10
- e_typeEXEC (Executable file)- 0x02
- e_machineIntel 80386- 0x03
- e_versionversion- 0x01
- e_entryentry- 0x08049000
- e_phoffStart of program headers 52 (bytes into file)- 0x34
0x20
- e_shoffStart of section headers 4132 (bytes into file),- 0x1024
- e_flagsflags- 0x00
- e_ehsizeSize of this header): 52 (bytes)- 0x34
- e_phentsizeSize of program headers):32 (bytes)- 0x20
- e_phnumNumber of program headers): 2- 0x02
- e_shentsizeSize of section headers): 40 (bytes)- 0x28
0x30
- e_shnumNumber of section headers): 3 0x03
- e_shstrndxSection header string table index):2 0x02
readelf
$ readelf -h ./plus
ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Intel 80386
  Version:                           0x1
  Entry point address:               0x8049000
  Start of program headers:          52 (bytes into file)
  Start of section headers:          4132 (bytes into file)
  Flags:                             0x0
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         2
  Size of section headers:           40 (bytes)
  Number of section headers:         3
  Section header string table index: 2
Program Header
Program Header 为以下两者相乘:
- Size of program headers: 32 (bytes)
- 
    Number of program headers: 2 即 64 个字节,并有两个 Program Headers 
0100 0000   : p_type: TYPE LOAD
0000 0000   : p_offset 0x00000000
0080 0408   : p_vaddr
0080 0408   : p_paddr
7400 0000   : p_filesz 0x74
7400 0000   : p_memsz
0400 0000   : p_flags
0010 0000   : p_align
0100 0000   : p_type: TYPE LOAD
0010 0000   : p_offset 0x00001000
0090 0408   : p_vaddr
0090 0408   : p_paddr
1100 0000   : p_filesz 0x11 17字节
1100 0000   : p_memsz
0500 0000   : p_flags
0010 0000   : p_align
readelf
$ readelf -l ./plus
Elf file type is EXEC (Executable file)
Entry point 0x8049000
There are 2 program headers, starting at offset 52
Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  LOAD           0x000000 0x08048000 0x08048000 0x00074 0x00074 R   0x1000
  LOAD           0x001000 0x08049000 0x08049000 0x00011 0x00011 R E 0x1000
 Section to Segment mapping:
  Segment Sections...
   00
   01     .text
- 第一个程序头起始为 0x00,大小为0x74= 52 (ELF Header) + 32 * 2 (Program Headers),也就是 ELF Header + Program Headers 的长度
- 第二个程序头起始为 0x001000, 长度为0x11,也就是真正的加法程序
Program
真正的程序代码在文件位置的 0x00001000,虚拟地址的 0x08049000,也就是读取 .text 段落的代码:
读取文件地址的二进制(指定长度为 0x11,即为所有程序代码):
$ xxd -s 0x1000 -l 0x11 ./plus
00001000: 66b8 0100 66bb 0200 6601 c366 b801 00cd  f...f...f..f....
00001010: 80
使用 readelf 读取并转为虚拟地址:
$ readelf --hex-dump=.text ./plus
Hex dump of section '.text':
  0x08049000 66b80100 66bb0200 6601c366 b80100cd f...f...f..f....
  0x08049010 80                                  .
另外,可以通过反编译,将二进制转换为汇编代码:
$ objdump -S ./plus
./plus:     file format elf32-i386
Disassembly of section .text:
08049000 <.text>:
 8049000:       66 b8 01 00             mov    $0x1,%ax
 8049004:       66 bb 02 00             mov    $0x2,%bx
 8049008:       66 01 c3                add    %ax,%bx
 804900b:       66 b8 01 00             mov    $0x1,%ax
 804900f:       cd 80                   int    $0x80
String Table
根据后面的 section headers 中 STRTAB 的描述,offset 为 0x1011, length 为 0x11:
$ xxd -s 0x1011 -l 0x11 ./plus
00001011: 002e 7368 7374 7274 6162 002e 7465 7874  ..shstrtab..text
00001021: 00
Section Headers
# section 0
0000 0000 sh_name
0000 0000 sh_type
0000 0000 sh_flags
0000 0000 sh_addr
0000 0000 sh_offset
0000 0000 sh_size
0000 0000 sh_link
0000 0000 sh_info
0000 0000 sh_addralign
0000 0000 sh_entsize
# section 1
0b00 0000  sh_name 0x1011 + 0x0b 开始读取的字符串 => .text
0100 0000  sh_type 0x01 PROGBITS
0600 0000  sh_flags
0090 0408  sh_addr: 0x08049000
0010 0000  sh_offset: 0x00001000
1100 0000  sh_size: 0x000011
0000 0000  sh_link
0000 0000  sh_info
1000 0000  sh_addralign: 0x10
0000 0000  sh_entsize
# section 2
0100 0000   sh_name 0x1011 + 0x0b 开始读取的字符串 => .shstrtab
0300 0000   sh_type 0x03 STRTAB
0000 0000   sh_flags
0000 0000   sh_addr
1110 0000   sh_offset 0x00001011
1100 0000   sh_size 0x11
0000 0000   sh_link
0000 0000   sh_info
0100 0000   sh_addralign 0x01
0000 0000   sh_entsize
readelf
$ readelf -S ./plus
There are 3 section headers, starting at offset 0x1024:
Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .text             PROGBITS        08049000 001000 000011 00  AX  0   0 16
  [ 2] .shstrtab         STRTAB          00000000 001011 000011 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  D (mbind), p (processor specific)
小结
至此,通过一个最简单的加法,解析了一个最简单的 ELF 文件,从前到后分别是:
- ELF Header(52字节)文件起始位置:- 0x00
- Program Header(32*2=64字节)文件起始位置:- 0x34
- Program(11字节)文件起始位置:- 0x1000(由 Program Header 指定)
- String Table(11字节)文件起始位置:- 0x1011(由 Section Table 指定)
- 
    Section Table(40*3=120字节)文件起始位置:0x1024(由 ELF Header 指定)最小的一个 ELF 文件由上述组成