做 GHCTF 2025 新生赛 的 ASM?Signin! 时提示需要 8086 汇编基础,然而我没有基础…… 所以来学一学了!
正如 WikiBooks 里面写的 “用汇编语言开发程序可能是一个非常耗时的过程。虽然用汇编语言编写新项目可能不是个好主意……” 我们现在学习汇编语言不是为了实际编写,更多是为了学会阅读它。MC 笑传之常常崩!神秘崩溃牵扯出了计算机底层原理呈现了从汇编层面找到导致 JVM 出现 SIGNSEGV 的原因的过程。
请注意,有些 8086 汇编教程所编写的示例程序是用于构建 .COM 文件的。根据 NASM 汇编器手册,DOS 系统下的大型程序和 Windows 程序应构建为 .EXE 文件。在选择网络教程时,请注意这一点。
0. x86 汇编概述
x86 架构以 1978 年的 Intel 8086 处理器为开端,覆盖 Intel 从 16 位到 64 位的处理器。由于汇编语言非常依赖硬件,因此目前将 x86 汇编语言分为 IA-32 (x86-32) 和 x86-64 两种 [1]
将汇编语句转化为机器语言的程序称为汇编器 (assembler)。输入汇编器的文件称为源文件 (source file)。汇编器将生成两个文件:目标文件 (object file) 和列表文件 (listing file)。目标文件保存了转化得到的机器语言和对应的汇编代码,列表文件提供的信息会更加详细,一般用于辅助分析调试。
生成的 .obj 目标文件还需要经过链接器 (linker) 的处理才能生成 .EXE 文件,这是因为较大规模程序往往会分割为多个模块,而每个模块的源文件都会生成对应的目标文件,所以需要将它们链接起来。
graph LR A(源文件) --> B{汇编器} B -->|汇编| C(目标文件) B -->|汇编| D(列表文件) C --> E{链接器} E -->|链接| F(可执行文件)
我们初学 C / C++ 时,会使用 “编译” 来描述将源代码转换成可执行文件的过程,这实际上是不准确的。编译实际只负责整个过程中的语法检查和目标文件生成,而目标文件到可执行文件还需要经过链接。这整个过程实际上称为构建 (build)。
简化的语言变化过程:
> graph LR A[高级语言] --> B[汇编语言] B --> C[机器指令]
然而,现代编译器还会对源码做优化工作,再加上一些编译器需要同时支持多语言、多平台,于是现在的语言变化流程是这样的:
> graph LR A[高级语言] --> B[抽象语法树 AST] B --> C[中间表示 IR] C --> D[汇编指令] D --> E[机器指令]
编译器前端 (frontend) 负责高级语言到 IR 的转换,后端 (backend) 负责 IR 到机器指令的转换。
现在我们暂时打住,不再继续深究。
2. 从 C 语言认知汇编
本节假设读者已经有 C 语言基础
前文说过,源文件经由汇编器得到目标文件,里面就存放着我们想看到的汇编代码。
如果你的网络条件较好,那么有一个很好的在线编译器平台 Compiler Explorer。该平台提供了丰富的编译器选择,有助于学习和比较编译器对源码的编译方式。
如果你需要本地查看汇编,就要让构建工具生成目标文件。对于 GCC,输入:
1 | gcc –c example.c # -c 表示仅编译不链接 |
在当前目录下生成 example.o
。此时的 example.o
不能直接查看,需要使用 objdump
:
1 | objdump -h example.o # 查看文件段信息 |
对于一个简单的 Hello World:
1 |
|
Compiler Explorer 在 x86_64 GCC 15.2 下的输出:
1 | .LC0: |
通过 objdump
输出的汇编:
1 |
|
Compiler Explorer 输出的是 Intel 语法,而 objdump
默认输出 AT & T 语法。
我们看的更多的是 Intel 语法。使用 -M
选项可以控制 objdump
的输出格式:
1 | objdump -s -d -M intel hello.o > hello.s # 以 intel 语法显示汇编指令 |
3. 汇编语言本地开发环境搭建
如果不追求完全的汇编环境的话,可以直接使用 C / C++ 的内联汇编。
Assembly Environment Setup 记载了这位作者的开发环境搭建方案。
也有一个类似于 Dev-C++ 的轻量 IDE——SASM。它具有语法突出显示和调试器,且开箱即用,非常适合初学者学习汇编语言。
SASM 附带一组汇编示例。例如,这是 NASM 汇编器的 Hello World:
1 | .data |
4. x86 汇编语言元素
x86 汇编语言由语句 (statement) 构成。一条语句的组成元素有:关键字 (keyword)、标识符 (identifier)、数值 (number)、字符串 (string)、特殊字符 (special character) 和注释 (comment)。
-
关键字:在汇编时有特殊意义的符号,例如指令助记符或者其他保留字。
-
标识符:用户定义的符号,常用于表示变量、标签和数值常量。标识符可以由字母、数字、下划线、
@
和?
组成,但不能以数字开头。一些标识符示例:COUNT
、@1
和A_BYTE
。 -
数值:可以是十进制、十六进制、八进制或二进制数字。数值必须以一个十进制数字开头,对于除十进制以外的数制,需要在结尾加上表示当前数制的字母。例如:
0ABCh
(h
代表十六进制);1776q
(q
代表八进制,现代汇编器也支持o
);10100110b
(b
表示二进制)。 -
字符串:被单引号
‘’
包围的一串字符。 -
特殊字符:
‘’$&?;:=,[].+-()*/<>
,包括作为分隔符的空格字符和制表符。 -
注释:仅用于撰写程序文档的一串字符,会被编译器忽略。注释以
;
开头。
在 x86 汇编中,一个语句应仅占一行(即不允许跨行语句)。
1 | mov ax, |
以语句为单位,一份 x86 源码可以分为三个主要部分:指令语句、数据分配语句和汇编器指令。
- 指令语句 (instruction statements) 由助记符(和操作数)组成,生成特定的机器码。
- 数据分配语句 (data allocation statements) 为程序数据保留并选择性初始化内存空间。
- 汇编器指令 (assembler directive) 向汇编器传递特定指令。这些指令的汇编结果可能会在目标文件中出现,但不会影响程序原本的内存数据。
5. 寄存器
寄存器 (register) 是 CPU 内部用来存储指令、数据和地址的存储器,在计算机的存储器体系中处于最高层次。寄存器的存储容量有限,但读写速度非常快。一般认为,访问寄存器中的数据不需要时间,是存储计算临时数据的绝佳位置。大多数寄存器都有特殊用途。
现代 CPU 有很多寄存器,我们主要了解这些种类:通用寄存器 (general purpose register)、指令指针寄存器 (instruction pointer)、段寄存器 (segement register) 和标志寄存器 (FLAGS)。前三种寄存器长度为当前架构的字长,即 64 bits、32 bits 及 16 bits。标志寄存器的长度固定为 16 bits。
在汇编语言中,这些寄存器的名字是大小写无关的,既可以用 EAX,也可以写 eax。
5.1 通用寄存器
现代 CPU 有 16 个通用寄存器,其中 8 个拥有特定用途:
64-bit | 32-bit | 16-bit | 8 high bits of lower 16 bits | 8-bit | 描述 |
---|---|---|---|---|---|
RAX | EAX | AX | AH | AL | 主寄存器 / 累加器 |
RBX | EBX | BX | BH | BL | 基址寄存器 |
RCX | ECX | CX | CH | CL | 计数寄存器 |
RDX | EDX | DX | DH | DL | 数据寄存器 (通常作为主寄存器的扩充) |
RSI | ESI | SI | - | SIL | 源索引寄存器(用于字符串操作) |
RDI | EDI | DI | - | DIL | 目标索引寄存器(用于字符串操作) |
RSP | ESP | SP | - | SPL | 堆栈指针寄存器 |
RBP | EBP | BP | - | BPL | 基址指针寄存器(用于堆栈帧) |
R8 | R8D | R8W | - | R8B | 通用 |
R9 | R9D | R9W | - | R9B | 通用 |
R10 | R10D | R10W | - | R10B | 通用 |
R11 | R11D | R11W | - | R11B | 通用 |
R12 | R12D | R12W | - | R12B | 通用 |
R13 | R13D | R13W | - | R13B | 通用 |
R14 | R14D | R14W | - | R14B | 通用 |
R15 | R15D | R15W | - | R15B | 通用 |
AX、BX、CX、DX 虽然是 16 位寄存器,但也可以拆成两个 8 位寄存器使用,因为这四个通用寄存器本来就是两个独立的 8 位寄存器拼起来得到的。两个 8 位存储器中的 “H” 或 “L” 分别表示这个寄存器处于内存中的高位或低位。
如果 AX=0011000000111001b
,则 AH=00110000b
,AL=00111001b
。因此,当修改任一 8 位寄存器时,16 位寄存器也会更新,反之亦然。
因此,像 AX
这样的寄存器也称为通用字寄存器 (general purpose word register),像 AH/AL
这样的寄存器也称为通用字节寄存器 (general purpose byte register)。
5.2 指令指针寄存器
IP 寄存器指向当前正在执行的指令(内存位置),用于 RIP 相对寻址模式。
64-bit | 32-bit | 16-bit |
---|---|---|
RIP | EIP | IP |
5.3 段寄存器
名称 | 描述 |
---|---|
CS | 代码段寄存器,指向包含当前程序的段地址。 |
DS | 数据段寄存器,通常指向包含变量定义的段地址。 |
SS | 堆栈段寄存器,指向包含堆栈的段地址。 |
ES | 附加段寄存器(用于字符串操作) |
FS | 通用段寄存器 |
GS | 通用段寄存器 |
总结起来,段寄存器都有一个目的 —— 指向可访问的内存块。
不建议在段寄存器中存储数据。
段寄存器与通用寄存器协同工作以访问任意内存值。2 个寄存器形成的地址称为有效地址 (effective address)。
例如,如果我们想访问物理地址 12345h
(十六进制),则应设置 DS = 1230h
和 SI = 0045h
。
CPU 通过将段寄存器存储的值乘以 10h
(偏移值,十进制下为 16) 并加上通用寄存器存储的值来计算物理地址,即:1230h * 10h + 45h = 12345h
。
这样做的话,我们可以访问比只使用单个 16 位寄存器多得多的内存。
默认情况下,BX、SI 和 DI 寄存器与 DS 段寄存器一起工作;BP 和 SP 与 SS 段寄存器协同工作。其他通用寄存器则无法形成有效地址。此外,尽管 BX 可以用于形成有效地址,但 BH 和 BL 不能,因为 x86 CPU 必须使用 16 位寄存器进行内存寻址。
5.4 标志寄存器
标志寄存器通常反映算术运算的结果以及有关当前时间对 CPU 作施加的限制的信息。
标志寄存器是按位起作用的,也就是说,它的每一位都有专门的含义,记录特定的信息。
CPU 标志寄存器的结构:
1 | ... 15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00 |
有些位不是没有对应的标志,只是在设计上数值固定,对于一般开发者没有用处。参考 FLAGS register - Wikipedia
CF
标志
进位、借位标志位 (Carry Flag)。在进行无符号数运算的时候,它记录了运算结果的最高有效位向更高位的进位值,或从更高位的借位值。PF
标志
奇偶标志位 (Parity Flag)。记录相关指令执行后,其结果的所有 bit 位中 1 的个数是否为偶数。如果为偶数,PF
= 1;如果为奇数,那么PF
= 0。AF
标志
辅助进位标志位 (Auxiliary Carry Flag)。记录在四个最低有效位或低四位中是否产生了进位或借位。它主要用于二进制编码的十进制 (BCD) 算术。ZF
标志
零标志位 (Zero Flag)。记录相关指令执行后,其结果是否为 0。如果结果为 0,那么ZF
= 1;如果结果不为 0,那么ZF
= 0。SF
标志
符号标志位 (Sign Flag)。在进行有符号数运算的时候,它记录相关指令执行后,其结果是否为负。如果结果为负,SF
= 1;如果非负,SF
= 0。TF
标志
自陷标志位 (Trap Flag)。如果此标志可用,调试器可以使用它来逐步执行计算机程序。CPU 在执行完一条指令后,如果检测到标志寄存器的TF
位为 1,则会产生一个 type-1 中断,然后再将TF
置为 0,后进行 type-1 中断后继续执行。
8086 CPU 没有直接设置或重置自陷标志位的指令。这些操作可以通过将标志寄存器的内容压入堆栈,改变内存内容以设置或重置自陷标志,并将内存内容弹出堆栈以保存回标志寄存器来完成。用到的指令是pushf
和popf
。IF
标志
中断标志位 (Interrupt Flag)。用于确定 CPU 是否将立即响应可屏蔽硬件中断。如果标志设置为 1 ,则中断被启用。如果清除(设置为 0 ),则中断被禁用。DF
标志
方向标志位 (Direction Flag)。此标志用于确定将数据从内存的一个位置复制到另一个位置的方向(正向或反向)。
如果设置为 0(使用清除方向标志指令CLD
),则表示从最低地址到最高地址处理字符串。这种指令模式称为自动递增模式。
如果设置为 1(使用设置方向标志指令STD
),则表示从最高地址到最低地址处理字符串。这称为自动递减模式。OF
标志
溢出标志位 (Overflow Flag)。对于有符号数运算,运算结果超出补码表示范围(8 位:-128~+127,16 位:-32768~+32767)就是溢出。若溢出,OF
置 1,否则OF
清 0。
6. CTF 中常见汇编知识
了解了一些基本的汇编指令后,下面我们来看看 CTF 中常见的一些指令:
loop
循环指令:
1 | mov rax,0 |
上面的指令执行的是对 123 累加 236 次,从以上代码中我们可以看到,rcx
寄存器保存的是循环次数,loop
指令每执行一次,rcx 的数值就会减少 1,当其数值减少到 0 时,loop
指令就会停止,继续执行下面的指令。
- 无条件跳转指令:
1 | lable: |
无条件跳转指令为 jmp
,其意思从字面上就可以看出来,只要是执行到 jmp
这里,无论什么情况,都会直接跳转到 lable
的代码块中继续往下执行指令。
- 条件跳转:
1 | lable: |
以上代码将 ebx
的值与 0 对比,如果相等,则会跳转到 lable
处,条件跳转指令 je
代表相等则跳转,还有其他与之条件不一样的条件跳转指令,一般条件跳转指令是与 cmp
、test
指令混在一起用的。以上代码也可以用 test
来写:
1 | lable: |
test
是逻辑与指令,以上代表的即为 ebx&0
,jnz
代表标志位为 0 就跳转,其实现效果与 cmp
相同。
下面是一些常见的条件跳转指令:
指令 | 英文全称 | 含义 | 判断标志位 |
---|---|---|---|
JE , JZ |
jump equal,jump zero | 结果为零则跳转 (相等时跳转) | ZF=1 |
JNE , JNZ |
jump not equal,jump not zero | 结果不为零则跳转 (不相等时跳转) | ZF=0 |
JS |
jump sign | 结果为负则跳转 | SF=1 |
JNS |
jump not sign | 结果为非负则跳转 | SF=0 |
JP , JPE |
jump parity,jump parity even | 结果中 1 的个数为偶数则跳转 | PF=1 |
JNP , JPO |
jump not parity,jump parity odd | 结果中 1 的个数为偶数则跳转 | PF=0 |
JO |
jump overflow | 结果溢出了则跳转 | OF=1 |
JNO |
jump not overflow | 结果没有溢出则跳转 | OF=0 |
JB , JNAE |
jump below,jump not above equal | 小于则跳转 (无符号数) | CF=1 |
JNB , JAE |
jump not below,jump above equal | 大于等于则跳转 (无符号数) | CF=0 |
JBE , JNA |
jump below equal,jump not above | 小于等于则跳转 (无符号数) | CF=1 or ZF=1 |
JNBE , JA |
jump not below equal,jump above | 大于则跳转 (无符号数) | CF=0 and ZF=0 |
JL , JNGE |
jump less,jump not greater equal | 小于则跳转 (有符号数) | SF≠ OF |
JNL , JGE |
jump not less,jump greater equal | 大于等于则跳转 (有符号数) | SF=OF |
JLE , JNG |
jump less equal,jump not greater | 小于等于则跳转 (有符号数) | ZF=1 or SF≠ OF |
JNLE , JG |
jump not less equal,jump greater | 大于则跳转 (有符号数) | ZF=0 and SF=OF |
下面是一些常见的位运算指令:
AND
:与运算,AND AX,BX
将AX
与BX
进行逻辑与运算,并将结果保存到AX
寄存器中OR
:或运算,OR AX,BX
将AX
与BX
进行逻辑或运算,并将结果保存到AX
寄存器中XOR
:异或运算,XOR AX,BX
将AX
与BX
进行异或运算,并将结果保存到AX
寄存器中NOT
:取反操作,NOT CX
将CX
进行取反,并将结果保存到CX
寄存器中TEST
:逻辑与运算,TEST AX,BX
将AX
与BX
进行与运算,并设置标志位,结果不保存
- 函数传参:
在汇编中,我们有时候需要知道参数被保存在那个寄存器里,这里我列举一般情况:
通常函数的返回值保存在 ax
寄存器里,比如 eax
,rax
等; 函数的参数一般按顺序保存在 cx
、dx
、si
中,在 64 位汇编中,一般按 rcx
, rdx
, r8
, r9
, r10
, r11
, … 的顺序保存参数。
有时候根据函数调用约定以及编译器和平台的不同,这个储存规律也会发生改变,我们需要根据当时情况动态调整。
lea
地址加载:
其使用格式为 lea rdx,[my_var]
,将 my_var
的地址而不是内容赋值给 rdx
寄存器。
xchg
数值交换指令:
其使用格式为 xchg ax,bx
将 ax
和 bx
的数值交换。
Ad
今天在查找资料的过程中看到了一个交互式教程 Ketman Assembly Language Tutorial 。根据作者的介绍,该教程原先是为了聚焦于学习语言本身而开发的,之后发展成为一个完整的开发环境。
它的一大特点就是汇编代码的即时运行与调试,因此可以快速明确每条指令对 CPU 的影响。

左侧是编辑器,右侧是调试器,可以看出还是很完善的。
ACTF 提供的 x86 汇编入门文件,包含文件内注释指导和课后作业 asm_tour_1
你也许还听说有 AMD64、Intel 64 和 IA-64 。首先,AMD64 就是 x86-64,Intel 64 是对 AMD64 的兼容,也可以视作属于 x86-64;其次,IA-64 是 Intel 推出的仅 64 位指令集,但没有流行开来。 ↩︎