Skip to content

最简汇编

Computing 101. - Your First Program | pwn.college

其实汇编也没有想象中的那么复杂. 一个只有两条数据搬运指令和一条系统调用指令的程序, 也可以完整演示从汇编文本到可执行文件的经典过程.

示例程序

下面的程序适用于Linux x86-64:

.intel_syntax noprefix

.global _start

_start:
    mov rdi, 42
    mov rax, 60
    syscall

其中既有 CPU 指令, 也有只供汇编器和链接器使用的声明:

内容 作用
.intel_syntax noprefix 告诉 GNU as 按 Intel 语法解析后续指令, 且寄存器名前不需要 % 前缀
.global _start 将 _start 符号导出, 让链接器可以用它作为程序入口
_start: 定义一个标签, 指向程序入口处的第一条指令
mov rdi, 42 将退出状态 42 放入 rdi, 即 Linux x86-64 系统调用的第一个参数寄存器
mov rax, 60 将系统调用号 60 放入 rax; 在 Linux x86-64 中, 60 表示 exit
syscall 进入内核执行系统调用. 内核读取 rax 中的调用号和相应参数, 结束当前进程. 本例是单线程程序

这里使用的是 Linux x86-64 的系统调用约定, 不是普通函数调用约定.

系统调用号和参数寄存器会随操作系统及处理器架构而变化.

汇编、链接和运行

汇编器将 .s 汇编源文件转换成目标文件 .o; 链接器再根据目标文件中的符号等信息生成可执行文件:

as -o simplest.o simplest.s
ld -o simplest simplest.o
./simplest
echo $?

本地运行时, 汇编和链接成功后没有额外输出. 程序结束后 shell 显示的状态为:

42

echo $? 用于读取上一条命令的退出状态并输出到终端. 这里的 42 来自 rdi 中的参数.

另外因为源文件定义并导出了 `_start', 链接器会将它作为入口, 因此不会出现找不到入口符号的警告. 如果没有这个标签, 在链接时链接器会发出找不到入口符号的警告:

> ld -o simplest simplest.o
/usr/bin/ld: 警告: 无法找到项目符号 _start; 缺省为 0000000000401000

同时链接后的可执行文件会从链接器设置的缺省地址开始执行. 这里的缺省地址一般是汇编码的第一条CPU指令所在的地址, 结合反汇编也可以看出:

> ld simplest.o -o simplest
/usr/bin/ld: 警告: 无法找到项目符号 _start; 缺省为 0000000000401000
> objdump -d -Mintel simplest

simplest:     文件格式 elf64-x86-64


Disassembly of section .text:

0000000000401000 <.text>:
  401000:       48 c7 c7 2a 00 00 00    mov    rdi,0x2a
  401007:       48 c7 c0 3c 00 00 00    mov    rax,0x3c
  40100e:       0f 05                   syscall

这个最小程序没有 C 运行时, 也没有 main 函数. Linux 加载 ELF 可执行文件后, 从链接器设置的入口 _start 开始执行; 与之相对, 常见的 C 程序通常由 C 运行时启动, 再调用 main.

            汇编                链接
simplest.s --as--> simplest.o --ld--> simplest --运行--> 退出状态 42

反汇编

objdump -d -M intel simplest

本地反汇编输出如下:

simplest:     file format elf64-x86-64

Disassembly of section .text:

0000000000401000 <_start>:
  401000:  48 c7 c7 2a 00 00 00  mov    rdi,0x2a
  401007:  48 c7 c0 3c 00 00 00  mov    rax,0x3c
  40100e:  0f 05                 syscall

每行先列出指令地址和机器码字节, 再列出 objdump 解码出的汇编指令.

  • 42 的十六进制是 0x2a, 所以第一条指令把 0x2a 写入 rdi

  • 60 的十六进制是 0x3c, 对应第二条指令写入 rax

  • 最后一条 syscall 的机器码是 0f 05.

0x401000 等地址是本次链接生成的地址, 可能因链接选项或工具链而变化; 指令含义才是要关注的重点. GNU as 负责生成目标文件中的机器码, ld 负责生成 ELF 并设置入口, 而 objdump 则反过来把机器码解码为可读的指令.