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
-it
interactive & 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
标记class
ELF320x01
, 2 为 64 位data
data 2’s complement, little endian0x01
,小端编码version
1 (current)0x01
padding
, 补齐到 16 位0x00 0000 0000 0000
0x10
e_type
EXEC (Executable file)0x02
e_machine
Intel 803860x03
e_version
version0x01
e_entry
entry0x08049000
e_phoff
Start of program headers 52 (bytes into file)0x34
0x20
e_shoff
Start of section headers 4132 (bytes into file),0x1024
e_flags
flags0x00
e_ehsize
Size of this header): 52 (bytes)0x34
e_phentsize
Size of program headers):32 (bytes)0x20
e_phnum
Number of program headers): 20x02
e_shentsize
Size of section headers): 40 (bytes)0x28
0x30
e_shnum
Number of section headers): 3 0x03e_shstrndx
Section 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 文件由上述组成