通过简单的一些 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 个字节。

image

0x00
  • e_ident[EI_NIDENT]
    • .ELF 标记
    • class ELF32 0x01, 2 为 64 位
    • data data 2’s complement, little endian 0x01,小端编码
    • version 1 (current) 0x01
    • padding, 补齐到 16 位 0x00 0000 0000 0000
0x10
  • e_type EXEC (Executable file) 0x02
  • e_machine Intel 80386 0x03
  • e_version version 0x01
  • e_entry entry 0x08049000
  • 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 flags 0x00
  • e_ehsize Size of this header): 52 (bytes) 0x34
  • e_phentsize Size of program headers):32 (bytes) 0x20
  • e_phnum Number of program headers): 2 0x02
  • e_shentsize Size of section headers): 40 (bytes) 0x28
0x30
  • e_shnum Number of section headers): 3 0x03
  • e_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

image

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

image

根据后面的 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

image

# 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 文件由上述组成

References

  1. Executable_and_Linkable_Format
  2. 可执行和可链接格式
  3. wiki.osdev.org/ELF_Tutorial
  4. github.com/bminor/binutils-gdb
  5. Tool Interface Standard (TIS) Executable and Linking Format (ELF) Specification
  6. Assembly Programming Tutorial
  7. Linux assemblers: A comparison of GAS and NASM