Background

🚧 本文于近日进行更新,换用更 “友好” 的王爽《汇编语言》,因为 Intel 的教程过于古早并且较难阅读。如你对后者仍有兴趣,请从 An Introdution to ASM86 处获取。

GHCTF 2025 新生赛 的 ASM?Signin! 时提示需要 8086 汇编基础,然而我没有基础…… 所以来学一学了!

我们现在学习汇编语言不是为了实际编写,仅是为了学会阅读它。本笔记介绍的是 16 位汇编,虽然过时但却是 x86 架构的基石,有很多东西被后继的 32/64 为汇编所继承。

请注意,有些教程所编写的示例程序是用于构建 .COM 文件的。根据 NASM 汇编器手册,DOS 系统下的大型程序和 Windows 程序应构建为 .EXE 文件。在选择网络教程时,请注意这一点。

0. ASM86 概述

ASM86 是 Intel 8086/8080 汇编语言的名字。使用本语言写成的语句将用于在 8086/8080 CPU 上生成特定的机器指令或分配程序内存空间。

将汇编语句转化为机器语言的程序称为 ASM86 汇编器 (assembler)。输入汇编器的文件称为源文件 (source file)。汇编器将生成两个文件:目标文件 (object file) 和列表文件 (listing file)。目标文件保存了转化得到的机器语言,列表文件则以十六进制表示这些机器语言,并附上对应的汇编代码。

生成的 .obj 目标文件需要经过链接器 (linker) 的处理才能生成 .EXE 文件,因为较大规模程序往往会分割为多个模块,而每个模块源文件都会生成对应的目标文件。

编译与构建

我们初学 C/C++ 时,会使用 “编译” 来描述将源代码转换成可执行文件的过程,这实际上是不准确的。编译实际只负责整个过程中的语法检查和目标文件生成,而目标文件到可执行文件还需要经过链接。这整个过程实际上称为构建 (build)。

1. ASM86 语言元素

ASM86 源码由语句 (statement) 构成。一条语句的组成元素有:关键字 (keyword)、标识符 (identifier)、数值 (number)、字符串 (string)、特殊字符 (special character) 和注释 (comment)。

  • 关键字:在汇编时有特殊意义的符号,例如指令助记符或者其他保留字。
  • 标识符:用户定义的符号,常用于表示变量、标签和数值常量。标识符可以由字母、数字、下划线、@? 组成,但不能以数字开头。一些标识符示例:COUNT@1A_BYTE
  • 数值:可以是十进制、十六进制、八进制或二进制数字。数值必须以一个十进制数字开头,对于除十进制以外的数制,需要在结尾加上表示当前数制的字母。例如:0ABChh 代表十六进制);1776qq 代表八进制,现代汇编器也支持 o);10100110bb 表示二进制)。
  • 字符串:被单引号 ‘’ 包围的一串字符。
  • 特殊字符‘’$&?;:=,[].+-()*/<>,包括作为分隔符的空格字符和制表符。
  • 注释:仅用于撰写程序文档的一串字符,会被编译器忽略。注释以 ; 开头。

在 ASM86 汇编中,一个语句应仅占一行(即不允许跨行语句)。

Warning

1
2
mov ax,
bx ; incorrect

大部分情况下,ASM86 汇编不要求语句的纵向排列。这有两个例外:一个是上述的 & 符号,另一个是声明控制行的 $ 符号。


以语句为单位,一份 ASM86 源码可以分为三个主要部分:指令语句、数据分配语句和汇编器指令。

  • 指令语句 (instruction statements) 由助记符(和操作数)组成,生成特定的机器码。Intel 标准规定助记符应为全大写。
  • 数据分配语句 (data allocation statements) 为程序数据保留并选择性初始化内存空间。
  • 汇编器指令 (assembler directive) 向汇编器传递特定指令。这些指令的汇编结果可能会在目标文件(object file,源码汇编后得到的产物)中出现,但不会影响程序原本的内存数据。

2. 寄存器

寄存器 (register) 是 CPU 内部用来存储指令、数据和地址的存储器,在计算机的存储器体系中处于最高层次。寄存器的存储容量有限,但读写速度非常快。一般认为,访问寄存器中的数据不需要时间,是存储计算临时数据的绝佳位置。大多数寄存器都有特殊用途。

8086 CPU 拥有三类寄存器:通用寄存器 (general purpose register)、段寄存器 (segement register) 和两个特殊用途的寄存器。这些寄存器均为 16 位宽度

2.1 通用寄存器

8086 CPU 有 8 个主要的通用寄存器:

  • AX - primary register /accumulator(主寄存器,累加器),分为 AH/AL
  • BX - base register(基址寄存器),分为 BH/BL
  • CX - count register(计数寄存器),分为 CH/CL
  • DX - data register(数据寄存器),分为 DH/DL
  • SI - source index register(源变址寄存器)
  • DI - destination index register(目的变址寄存器)
  • BP - base pointer(基址指针寄存器)
  • SP - stack pointer(堆栈指针寄存器)
Tip

其中 AX、BX、CX、DX 虽然是 16 位寄存器,但也可以各自拆成两个 8 位寄存器使用 [1],因为这四个通用寄存器本来就是两个独立的 8 位寄存器拼起来得到的。两个 8 位存储器中的 “H” 或 “L” 分别表示这个寄存器处于内存中的高位或低位。

Example

如果 AX=0011000000111001b,则 AH=00110000bAL=00111001b。因此,当修改任一 8 位寄存器时,16 位寄存器也会更新,反之亦然。

因此,像 AX 这样的寄存器也称为通用字寄存器 (general purpose word register),像 AH/AL 这样的寄存器也称为通用字节寄存器 (general purpose byte register)。

2.2 段寄存器

8086 CPU 有四个段寄存器:

  • CS - 代码段寄存器,指向包含当前程序的段地址。
  • DS - 数据段寄存器,通常指向包含变量定义的段地址。
  • ES - 附加段寄存器,其使用由程序员定义。
  • SS - 堆栈段寄存器,指向包含堆栈的段地址。

总结起来,段寄存器都有一个目的 —— 指向可访问的内存块

Warning

尽管可以在段寄存器中存储数据,但这从来不是一个好主意。

段寄存器与通用寄存器协同工作以访问任意内存值。2 个寄存器形成的地址称为有效地址 (effective address)

Example

例如,如果我们想访问物理地址 12345h(十六进制),则应设置 DS = 1230hSI = 0045h

CPU 通过将段寄存器存储的值乘以 10h(偏移值,十进制下为 16) 并加上通用寄存器存储的值来计算物理地址,即:1230h * 10h + 45h = 12345h

这样做的话,我们可以访问比只使用单个 16 位寄存器多得多的内存。

默认情况下,BX、SI 和 DI 寄存器与 DS 段寄存器一起工作;BP 和 SP 与 SS 段寄存器协同工作。其他通用寄存器则无法形成有效地址。此外,尽管 BX 可以用于形成有效地址,但 BH 和 BL 不能,因为 x86 CPU 必须使用 16 位寄存器进行内存寻址。

2.3 特殊用途寄存器

  • IP - 指令指针 (the Instruction Pointer)
  • FLAGS - 标志寄存器,用于确定微处理器的当前状态。分为 FLAGSH/FLAGSL

IP 寄存器始终与 CS 段寄存器协同工作,并指向当前正在执行的指令(内存位置)。

标志寄存器在数学运算后由 CPU 自动修改,用于确定结果类型,并确定将控制权转移到程序其他部分的条件。

标志寄存器是按位起作用的,也就是说,它的每一位都有专门的含义,记录特定的信息。

8086 CPU 标志寄存器的结构:

1
2
3
15  14  13  12  11  10  09  08  07  06  05  04  03  02  01  00

OF DF IF TF SF ZF AF PF CF
Note

12 - 15 位不是没有对应的标志,只是在 8086 上数值固定,对于一般开发者没有用处。参考 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 没有直接设置或重置自陷标志位的指令。这些操作可以通过将标志寄存器的内容压入堆栈,改变内存内容以设置或重置自陷标志,并将内存内容弹出堆栈以保存回标志寄存器来完成。用到的指令是 pushfpopf
  • 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 中常见汇编知识

Tip

Source: Quick Start:汇编语言 - Hello CTF[调试逆向] 逆向基础笔记六 汇编跳转和比较指令 - 52pojie
这些知识不仅限于 ASM86,有些可能是后继架构的东西。

了解了一些基本的汇编指令后,下面我们来看看 CTF 中常见的一些指令:

  1. loop 循环指令:
1
2
3
4
5
6
7
mov rax,0
mov rcx,236
s:
add rax,123
loop s
leave
ret

上面的指令执行的是对 123 累加 236 次,从以上代码中我们可以看到,rcx 寄存器保存的是循环次数,loop 指令每执行一次,rcx 的数值就会减少 1,当其数值减少到 0 时,loop 指令就会停止,继续执行下面的指令。

  1. 无条件跳转指令:
1
2
3
4
lable:
mov edx,0

jmp lable

无条件跳转指令为 jmp,其意思从字面上就可以看出来,只要是执行到 jmp 这里,无论什么情况,都会直接跳转到 lable 的代码块中继续往下执行指令。

  1. 条件跳转:
1
2
3
4
5
lable:
mov ebx,1

cmp ebx,0
je lable

以上代码将 ebx 的值与 0 对比,如果相等,则会跳转到 lable 处,条件跳转指令 je 代表相等则跳转,还有其他与之条件不一样的条件跳转指令,一般条件跳转指令是与 cmptest 指令混在一起用的。以上代码也可以用 test 来写:

1
2
3
4
5
lable:
mov ebx,1

test ebx,0
jnz lable

test 是逻辑与指令,以上代表的即为 ebx&0jnz 代表标志位为 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,BXAXBX 进行逻辑与运算,并将结果保存到 AX 寄存器中
  • OR:或运算,OR AX,BXAXBX 进行逻辑或运算,并将结果保存到 AX 寄存器中
  • XOR:异或运算,XOR AX,BXAXBX 进行异或运算,并将结果保存到 AX 寄存器中
  • NOT:取反操作,NOT CXCX 进行取反,并将结果保存到 CX 寄存器中
  • TEST:逻辑与运算,TEST AX,BXAXBX 进行与运算,并设置标志位,结果不保存
  1. 函数传参:

在汇编中,我们有时候需要知道参数被保存在那个寄存器里,这里我列举一般情况:

通常函数的返回值保存在 ax 寄存器里,比如 eaxrax 等; 函数的参数一般按顺序保存在 cxdxsi 中,在 64 位汇编中,一般按 rcx, rdx, r8, r9, r10, r11, … 的顺序保存参数。

有时候根据函数调用约定以及编译器和平台的不同,这个储存规律也会发生改变,我们需要根据当时情况动态调整。

  1. lea 地址加载:

其使用格式为 lea rdx,[my_var],将 my_var 的地址而不是内容赋值给 rdx 寄存器。

  1. xchg 数值交换指令:

其使用格式为 xchg ax,bxaxbx 的数值交换。


今天在查找资料的过程中看到了一个交互式教程 Ketman Assembly Language Tutorial 。根据作者的介绍,该教程原先是为了聚焦于学习语言本身而开发的,之后发展成为一个完整的开发环境。

它的一大特点就是汇编代码的即时运行与调试,因此可以快速明确每条指令对 CPU 的影响。

左侧是编辑器,右侧是调试器,可以看出还是很完善的。


在学习 Late binding 时发现一个很好的在线编译器平台 Compiler Explorer

该平台提供了丰富的编译器选择,有助于学习和比较编译器对源码的编译方式。


  1. Intel 8086 - 维基百科 ↩︎


©2025-Present Watermelonabc | 萌ICP备20251229号

Powered by Hexo & Stellar latest & Vercel & 𝙌𝙞𝙪𝙙𝙪𝙣 𝘾𝘿𝙉 & HUAWEI Cloud
您的访问数据将由 Vercel 和自托管的 Umami 进行隐私优先分析,以优化未来的访问体验

本博客总访问量:capoo-2

| 开往-友链接力 | 异次元之旅 | 中文独立博客列表

猫猫🐱 发表了 41 篇文章 · 总计 209.8k 字