最简汇编¶
其实汇编也没有想象中的那么复杂. 一个只有两条数据搬运指令和一条系统调用指令的程序, 也可以完整演示从汇编文本到可执行文件的经典过程.
示例程序¶
下面的程序适用于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 则反过来把机器码解码为可读的指令.