资源
- 汇编语言(第4版) | 王爽 | download on Z-Library
- 将这本书送给 PaddleOCR - 文档解析与智能文字识别 | 支持API调用与MCP服务 - 飞桨星河社区 得到 Markdown 格式便于拿去跟 AI 对线!
正文
EMU8086 安装
见 emu8086 v4.08安装教程_emu8086安装教程-CSDN博客:

第 1 章 基础知识
1.2 汇编语言的产生

机器语言完全看不懂,汇编语言程序员还能看懂一点点。
1.3 汇编语言的组成
汇编语言发展至今,有以下 3 类指令组成。
- 汇编指令:机器码的助记符,有对应的机器码。
- 伪指令:没有对应的机器码,由编译器执行,计算机并不执行。
- 其他符号:如+、-、*、/等,由编译器识别,没有对应的机器码。
| 类型 | 谁处理 | CPU会执行吗 | 有机器码吗 |
|---|---|---|---|
| 汇编指令 | 汇编器→CPU | 是 | 是 |
| 伪指令 | 汇编器 | 否 | 否 |
| 其他符号 | 汇编器 | 否 | 否 |
data segment ; ←伪指令
num db 2+3 ; ←db是伪指令,+是运算符
data ends ; ←伪指令
code segment ; ←伪指令
start:
mov al,num ; ←真正的汇编指令
add al,1 ; ←真正的汇编指令
code ends ; ←伪指令
end start ; ←伪指令1.5 指令和数据
它们都是二进制信息,对于 1000100111011000,可以当作数据 89D8H 或是指令 mov ax,bx。
1.7 CPU 对存储器的读写
总线从物理上来讲,就是一根根导线的集合,从逻辑上分为 3 类:地址总线、控制总线和数据总线。

对于:
MOV AX, [3]- CPU 把数据
3放在地址线上告诉内存提供地址为3的数据; - CPU 通过控制线发送读信号告诉内存为读操作;
- 内存通过数据线将地址为
3的数据08H返回给 CPU; - CPU 收到数据后,
AX = 8。
1.8 地址总线

一个 CPU 有 N 根地址线,则可以说这个 CPU 的地址总线的宽度为 N。这样的 CPU 最多可以寻找 2 的 N 次方个内存单元。
1.9 数据总线
数据总线的宽度决定了 CPU 和外界的数据传送速度。8 根数据总线一次可传送一个 8 位二进制数据(即一个字节)。16 根数据总线一次可传送两个字节。


8086 有 16 根数据线,可一次传送 16 位数据,所以可一次传送数据 89D8H;而 8088 只有 8 根数据线,一次只能传 8 位数据,所以向内存写入数据 89D8H 时需要进行两次数据传送。
1.10 控制总线
有多少根控制总线,就意味着 CPU 提供了对外部器件的多少种控制。所以,控制总线的宽度决定了 CPU 对外部器件的控制能力。
重要
- 汇编指令是机器指令的助记符,同机器指令一一对应。
- 每一种 CPU 都有自己的汇编指令集。
- CPU 可以直接使用的信息在存储器中存放。
- 在存储器中指令和数据没有任何区别,都是二进制信息。
- 存储单元从零开始顺序编号。
- 一个存储单元可以存储 8 个 bit,即 8 位二进制数。
- 1Byte=8bit 1KB=1024B 1MB=1024KB 1GB=1024MB.
- 每一个 CPU 芯片都有许多管脚,这些管脚和总线相连。也可以说,这些管脚引出总线。一个 CPU 可以引出 3 种总线的宽度标志了这个 CPU 的不同方面的性能:
- 地址总线的宽度决定了 CPU 的寻址能力:
- 数据总线的宽度决定了 CPU 与其他器件进行数据传送时的一次数据传送量:
- 控制总线的宽度决定了 CPU 对系统中其他器件的控制能力。
检测点 1.1
1.14 各类存储器芯片
一台 PC 机中装有多个存储器芯片,CPU 通过总线与其相连。从读写属性上分为随机存储器(RAM,掉电易失)和只读存储器(ROM,非掉电易失)。
现代很多所谓的"ROM"实际上是可以写的,甚至可以反复擦写。只要它是非易失性存储器(掉电后数据不丢失),在很多场合仍然会被统称为 ROM。

1.15 内存地址空间

CPU 对于通过总线连接上的存储器,将它们都当作内存来看待。所有的物理存储器被看作一个由若干存储单元组成的逻辑存储器,每个物理存储器在这个逻辑存储器中占有一个地址段,即一段地址空间。CPU 在这段地址空间中读写数据,实际上就是在相对应的物理存储器中读写数据。

- 从地址
0~9FFFF的内存单元中读取数据,实际上就是在读取主随机存储器中的数据; - 向地址
A0000~BFFFF的内存单元中写数据,就是向显存中写入数据,这些数据会被显示卡输出到显示器上; - 向地址
C0000~FFFFF的内存单元中写入数据的操作是无效的,因为这等于改写只读存储器中的内容。
第 2 章 寄存器
一个典型的 CPU 由运算器、控制器、寄存器等器件构成。
-
运算器进行信息处理;
-
寄存器进行信息存储:
-
控制器控制各种器件进行工作;
内部总线连接各种器件,在它们之间进行数据的传送。
对于一个汇编程序员来说,CPU 中的主要部件是寄存器。不同的 CPU,寄存器的个数、结构是不相同的。8086 CPU 有 14 个寄存器,每个寄存器有一个名称。这些寄存器是:AX、BX、CX、DX、SI、DI、SP、BP、IP、CS、SS、DS、ES、PSW。
| 寄存器 | 全称 | 位数 | 主要作用 | 常见用途 |
|---|---|---|---|---|
| AX | Accumulator | 16 位 | 累加器 | 算术运算、乘除法、I/O 操作、函数返回值 |
| BX | Base Register | 16 位 | 基址寄存器 | 存放内存基地址,用于寻址 |
| CX | Count Register | 16 位 | 计数寄存器 | 循环计数(LOOP)、移位次数(SHL、SHR) |
| DX | Data Register | 16 位 | 数据寄存器 | 乘除法扩展、I/O 端口地址 |
| SI | Source Index | 16 位 | 源变址寄存器 | 字符串操作中的源地址(默认 DS:SI) |
| DI | Destination Index | 16 位 | 目的变址寄存器 | 字符串操作中的目标地址(默认 ES:DI) |
| SP | Stack Pointer | 16 位 | 栈顶指针 | 指向栈顶,配合 PUSH、POP |
| BP | Base Pointer | 16 位 | 基址指针 | 访问栈中的参数和局部变量(默认 SS:BP) |
| IP | Instruction Pointer | 16 位 | 指令指针 | 指向下一条待执行指令的偏移地址 |
| CS | Code Segment | 16 位 | 代码段寄存器 | 存放代码段地址,与 IP 组成指令地址 |
| DS | Data Segment | 16 位 | 数据段寄存器 | 存放数据段地址,大多数数据访问默认使用它 |
| SS | Stack Segment | 16 位 | 栈段寄存器 | 存放栈段地址,与 SP/BP 配合使用 |
| ES | Extra Segment | 16 位 | 附加段寄存器 | 字符串操作的目标段(默认 ES:DI) |
| PSW(FLAGS) | Program Status Word | 16 位 | 标志寄存器 | 保存运算结果状态和 CPU 控制标志 |

2.1 通用寄存器
8086 CPU的所有寄存器都是 16 位的,可以存放两个字节。AX、BX、CX、DX 这 4 个寄存器通常用来存放一般性的数据,被称为通用寄存器。
8086 CPU 的 AX、BX、CX、DX 这 4 个寄存器都可分为两个可独立使用的 8 位寄存器来用:
-
AX 可分为 AH 和 AL;
-
BX 可分为 BH 和 BL;
-
CX 可分为 CH 和 CL;
-
DX 可分为 DH 和 DL。

2.2 字在寄存器中的存储
出于对兼容性的考虑,8086 CPU可以一次性处理以下两种尺寸的数据。
- 字节:记为 byte,一个字节由 8 个 bit 组成,可以存在 8 位寄存器中。
- 字:记为 word,一个字由两个字节组成,这两个字节分别称为这个字的高位字节和低位字节。

2.3 几条汇编指令
| 汇编指令 | 控制 CPU 完成的操作 | 用高级语言的方式描述 |
|---|---|---|
mov ax,18 | 将 18 送入寄存器 AX | AX=18 |
mov ah,78 | 将 78 送入寄存器 AH | AH=78 |
add ax,8 | 将寄存器 AX 中的数值加上 8 | AX=AX+8 |
mov ax,bx | 将寄存器 BX 中的数据送入寄存器 AX | AX=BX |
add ax,bx | 将 AX 和 BX 中的数值相加,结果存在 AX 中 | AX=AX+BX |
检测点 2.1
只能使用目前学过的汇编指令,用 4 条指令编程计算 2 的 4 次方。
mov ax,2
add ax,ax
add ax,ax
add ax,ax
2.5 16 位结构的 CPU
8086 是 16 位结构的 CPU。具有以下方面的结构特性:
-
运算器一次最多可以处理 16 位的数据:
-
寄存器的最大宽度为 16 位;
-
寄存器和运算器之间的通路为 16 位。
2.6 8086 CPU 给出物理地址的方法
8086 CPU 由 20 位地址总线,拥有 1MB(220 字节)的寻址能力。但是它是 16 结构,因此采用用两个 16 位地址(段地址和偏移地址)合成的方法来形成一个 20 位的物理地址。

当 8086 CPU 要读写内存时:
-
CPU 中的相关部件提供两个 16 位的地址,一个称为段地址,另一个称为偏移地址;
-
段地址和偏移地址通过内部总线送入一个称为地址加法器的部件;
-
地址加法器将两个 16 位地址合成为一个 20 位的物理地址;
-
地址加法器通过内部总线将 20 位物理地址送入输入输出控制电路;
-
输入输出控制电路将 20 位物理地址送上地址总线;
-
20 位物理地址被地址总线传送到存储器。
地址加法器采用物理地址=段地址 × 16 + 偏移地址的方法(其实是段地址左移 4 位再与偏移地址相加,从 16 进制看后面多了个 0)用段地址和偏移地址合成物理地址。
段地址 x 16 又可以叫做基础地址。

2.8 段的概念
内存中并没有分段,但是 CPU 这种合成地址的方式让 CPU 用分段的方式来管理内存。

段地址 x 16 必然是 16 的倍数,偏移地址为 16 位,16 位地址的寻址能力为 64 KB,所以一个段的长度最大为 64 KB。
| 物理地址 | 段地址 | 偏移地址 |
| 21F60H | 2000H | 1F60H |
| 2100H | 0F60H | |
| 21F0H | 0060H | |
| 21F6H | 0000H | |
| 1F00H | 2F60H |
段的长度并不是固定的。CPU 可以用不同的段地址和偏移地址形成同一个物理地址。
检测点 2.2
2.9 段寄存器
8086 CPU 有 4 个段寄存器:CS、DS、SS、ES。
当 8086 CPU 要访问内存时由这 4 个段寄存器提供内存单元的段地址。
2.10 CS 和 IP
CS 为代码段寄存器,IP 为指令指针寄存器,从名称上我们可以看出它们和指令的关系。
在 8086PC 机中,任意时刻,设 CS 中的内容为 M,IP 中的内容为 N,8086 CPU 将从内存 M×16+N 单元开始,读取一条指令并执行。
任何时刻,CPU 将 CS:IP 指向的内容当作指令执行。

2.11 修改 CS、IP 的指令
之前可以用 mov 指令修改通用寄存器(AX 等)的值,但是这对 CS 和 IP 无效。
8086 CPU 为 CS、IP 提供了另外的指令来改变它们的值。能够改变 CS、IP 的内容的指令被统称为转移指令(我们以后会深入研究)。最简单的可以修改 CS、IP 的指令:jmp 指令。
若想同时修改 CS、IP 的内容,可用形如 jmp 段地址:偏移地址 的指令完成。如:
jmp 2AE3:3,执行后:CS=2AE3H,IP=0003H,CPU 将从2AE33H处读取指令。jmp 3:0B16,执行后:CS=0003H,IP=0B16H,CPU 将从00B46H处读取指令。
如果只想修改 IP 的内容,可用形如 jmp 某一合法寄存器 的指令完成,如:
jmp ax,指令执行前:ax=1000H,CS=2000H,IP=0003H;指令执行后:ax=1000H,CS=2000H,IP=1000H;jmp bx,指令执行前:bx=0B16H,CS=2000H,IP=0003H;指令执行后:bx=0B16H,CS=2000H,IP=0B16H。
jmp 某一合法寄存器指令的功能为:用寄存器中的值修改 IP。jmp ax,在含义上好似:mov IP,ax。
对于初始状态 CS=2000H,IP=0000H:

经分析后,可知指令执行序列为:
mov ax,6622Hjmp 1000:3mov ax,0000mov bx,axjmp bxmov ax,0123H- 转到第 3 步执行
2.12 代码段
"代码段不是 CPU 规定出来的,而是程序员人为定义出来的。"对于 8086PC 机,在编程时,可以根据需要,将一组内存单元定义为一个段。
mov ax,0000 ;(B8 00 00)
add ax,0123H ;(05 23 01)
mov bx,ax ;(8B D8)
jmp bx ;(FF E3)| 物理地址 | 内容 | 指令 |
|---|---|---|
| 123B0 | B8 | mov ax,0000 |
| 123B1 | 00 | |
| 123B2 | 00 | |
| 123B3 | 05 | add ax,0123H |
| 123B4 | 23 | |
| 123B5 | 01 | |
| 123B6 | 8B | mov bx,ax |
| 123B7 | D8 | |
| 123B8 | FF | jmp bx |
| 123B9 | E3 |
这段长度为 10 个字节的指令,存放在 123B0H~123B9H 的一组内存单元中,我们就可以认为,123B0H~123B9H 这段内存是用来存放代码的,是一个代码段,它的段地址为 123BH,长度为 10 个字节。
如何使得代码段中的指令被执行呢?将一段内存当作代码段,仅仅是我们在编程时的一种安排,CPU 并不会由于这种安排,就自动地将我们定义的代码段中的指令当作指令来执行。CPU 只认被 CS:IP 指向的内存单元中的内容为指令。所以,要让 CPU 执行我们放在代码段中的指令,必须要将 CS:IP 指向所定义的代码段中的第一条指令的首地址。
CPU 只看到一大片连续的字节,而代码段、数据段、栈段都是程序员赋予这段内存的角色。8086 之所以有 CS、DS、SS、ES 四个段寄存器,就是为了告诉 CPU:当前这块内存应该按什么用途来访问,而不是因为内存天生就分成了四种不同的区域。
重要
- 段地址在 8086CPU 的段寄存器中存放。当 8086CPU 要访问内存时,由段寄存器提供内存单元的段地址。8086CPU 有 4 个段寄存器,其中 CS 用来存放指令的段地址。
- CS 存放指令的段地址,IP 存放指令的偏移地址。
8086 机中,任意时刻,CPU 将 CS:IP 指向的内容当作指令执行。
- 8086CPU 的工作过程:
- 从 CS:IP 指向的内存单元读取指令,读取的指令进入指令缓冲器;
- IP 指向下一条指令;
- 执行指令。(转到步骤 1,重复这个过程。)
- 8086CPU 提供转移指令修改 CS、IP 的内容。
检测点 2.3
实验 1
Win11 已经不能自带 DOS 了,自行装一个吧:Releases · joncampbell123/dosbox-x
| 步骤 | 提示符 | 输入命令 | 作用 |
|---|---|---|---|
| ① | C:\> | debug | 进入 DEBUG |
| ② | - | e 2000:0000 | 将机器码写入内存 |
| ③ | - | d 2000:0000 | 检查机器码是否正确 |
| ④ | - | r | 查看寄存器 |
| ⑤ | - | r cs | 修改 CS |
| ⑥ | - | r ip | 修改 IP |
| ⑦ | - | t | 单步执行一条指令 |
| ⑧ | - | r | 查看寄存器变化 |
| ⑨ | - | u cs:ip | 查看下一条即将执行的指令 |
| ⑩ | - | q | 退出 DEBUG |

第 3 章 寄存器(内存访问)
3.1 内存中字的存储
CPU 中,用 16 位寄存器来存储一个字。高 8 位存放高位字节,低 8 位存放低位字节。在内存中存储时,由于内存单元是字节单元(一个单元存放一个字节),则一个字要用两个地址连续的内存单元来存放,这个字的低位字节存放在低地址单元中,高位字节存放在高地址单元中。

8086 采用小端(Little Endian)存储(事实上 x86 架构始终采用小端存储),对于数据 20000(4E20H),0 号单元是低地址单元,1 号单元是高地址单元,则字型数据 4E20H 的低位字节存放在 0 号单元中,高位字节存放在 1 号单元中。
字单元,即存放一个字型数据(16 位)的内存单元,由两个地址连续的内存单元组成。高地址内存单元中存放字型数据的高位字节,低地址内存单元中存放字型数据的低位字节。
3.2 DS 和 [address]
在 8086PC 中,内存地址由段地址和偏移地址组成。8086CPU 中有一个 DS 寄存器,通常用来存放要访问数据的段地址。比如我们要读取 10000H 单元的内容,可以用如下的程序段进行。
mov bx,1000H
mov ds,bx; 8086 不支持 mov ds,1000H
mov al,[0]; 实际指 mov al,DS:[0][…] 表示一个内存单元,[…]中的 0 表示内存单元的偏移地址,即地址 0 对应的内存。
将 AL 中的数据送入内存单元 10000H 中:
mov bx,1000H
mov ds,bx
mov [0],al3.3 字的传送
mov bx,1000H
mov ds,bx
mov ax,[0] ; 1000:0 处的字型数据送入 ax
mov [0],cx ; cx 中的 16 位数据送到 1000:0 处3.4 mov、add、sub 指令
| 汇编格式 | 示例 | 含义 | 高级语言类比(C/C++) |
|---|---|---|---|
mov 寄存器, 数据 | mov ax,8 | 将立即数送入寄存器 | ax = 8; |
mov 寄存器, 寄存器 | mov ax,bx | 一个寄存器的值复制到另一个寄存器 | ax = bx; |
mov 寄存器, 内存单元 | mov ax,[0] | 从内存读取数据到寄存器 | ax = memory[0];(实际为 ax = *(uint16_t*)(DS:0)) |
mov 内存单元, 寄存器 | mov [0],ax | 将寄存器中的值写入内存 | memory[0] = ax;(实际为 *(uint16_t*)(DS:0) = ax) |
mov 段寄存器, 寄存器 | mov ds,ax | 设置段寄存器 | 类似于修改一个"内存基址",高级语言通常没有直接对应概念 |
| 汇编格式 | 示例 | 含义 | 高级语言类比(C/C++) |
|---|---|---|---|
add 寄存器, 数据 | add ax,8 | 寄存器加立即数 | ax += 8; |
add 寄存器, 寄存器 | add ax,bx | 两个寄存器相加 | ax += bx; |
add 寄存器, 内存单元 | add ax,[0] | 寄存器加内存中的值 | ax += memory[0]; |
add 内存单元, 寄存器 | add [0],ax | 内存中的值加寄存器,再写回内存 | memory[0] += ax; |
| 汇编格式 | 示例 | 含义 | 高级语言类比(C/C++) |
|---|---|---|---|
sub 寄存器, 数据 | sub ax,9 | 寄存器减立即数 | ax -= 9; |
sub 寄存器, 寄存器 | sub ax,bx | 两个寄存器相减 | ax -= bx; |
sub 寄存器, 内存单元 | sub ax,[0] | 寄存器减去内存中的值 | ax -= memory[0]; |
sub 内存单元, 寄存器 | sub [0],ax | 内存中的值减去寄存器,再写回内存 | memory[0] -= ax; |
3.5 数据段
用 ds 存放数据段的段地址,再根据需要,用相关指令访问数据段中的具体单元。
如,将 123B0H~123BH 的内存单元定义为数据段:
mov ax,123BH
mov ds,ax ; 将 123BH 送入 ds 中,作为数据段的段地址
mov al,0 ; 用 al 存放累加结果
add al,[0] ; 将数据段第一个单元(偏移地址为 0)中的数值加到 al 中
add al,[1] ; 将数据段第二个单元(偏移地址为 1)中的数值加到 al 中
add al,[2] ; 将数据段第三个单元(偏移地址为 2)中的数值加到 al 中mov ax,123BH
mov ds,ax ;将 123BH 送入 ds 中,作为数据段的段地址
mov ax,0 ;用 ax 存放累加结果
add ax, [0] ;将数据段第一个字(偏移地址为 0)加到 ax 中
add ax, [2] ;将数据段第二个字(偏移地址为 2)加到 ax 中
add ax, [4] ;将数据段第三个字(偏移地址为 4)加到 ax 中一个字型数据占两个单元,所以偏移地址是 0、2、4。
重要
- 字在内存中存储时,要用两个地址连续的内存单元来存放,字的低位字节存放在低地址单元中,高位字节存放在高地址单元中。
- 用 mov 指令访问内存单元,可以在 mov 指令中只给出单元的偏移地址,此时,段地址默认在 DS 寄存器中。
- [address] 表示一个偏移地址为 address 的内存单元。
- 在内存和寄存器之间传送字型数据时,高地址单元和高 8 位寄存器、低地址单元和低 8 位寄存器相对应。
- mov、add、sub 是具有两个操作对象的指令。jmp 是具有一个操作对象的指令。
- 可以根据自己的推测,在 Debug 中实验指令的新格式。
3.6 栈
栈,LIFO(Last In First Out,后进先出)


3.7 CPU 提供的栈机制
8086CPU 提供入栈和出栈指令,最基本的两个是 PUSH(入栈)和 POP(出栈)。比如,push ax 表示将寄存器 ax 中的数据送入栈中,pop ax 表示从栈顶取出数据送入 ax。8086CPU 的入栈和出栈操作都是以字为单位进行的。
将 10000H~1000FH 这段内存当作栈来用:
mov ax,0123H
push ax
mov bx,2266H
push bx
mov cx,1122H
push cx
pop ax
pop bx
pop cx
CPU 如何知道栈顶的位置?8086CPU 中,有两个寄存器,段寄存器 SS 和寄存器 SP,栈顶的段地址存放在 SS 中,偏移地址存放在 SP 中。任意时刻,SS:SP 指向栈顶元素。push 指令和 pop 指令执行时,CPU 从 SS 和 SP 中得到栈顶的地址。
push ax 的执行由以下两部完成:
SP=SP-2,SS:SP指向当前栈顶前面的单元,以当前栈顶前面的单元为新的栈顶;- 将 ax 中的内容送入
SS:SP指向的内存单元处,SS:SP此时指向新栈顶。

入栈时,栈顶从高地址向低地址增长。

出栈的时候,只会移动 SP,数据依然存在,如果入栈写入新的数据,旧的数据将被覆盖。

3.8 栈顶超界的问题
8086 不保证对栈的操作不会出界。


3.9 push、pop 指令
| 汇编格式 | 示例 | 含义 | 高级语言类比(C/C++) |
|---|---|---|---|
push 寄存器 | push ax | 将寄存器中的一个字压入栈 | stack.push(ax); |
push 段寄存器 | push ds | 将段寄存器中的一个字压入栈 | stack.push(ds); |
push 内存单元 | push [0] | 将内存中的一个字压入栈 | stack.push(memory[0]);(实际为 stack.push(*(uint16_t*)(DS:0))) |
| 汇编格式 | 示例 | 含义 | 高级语言类比(C/C++) |
|---|---|---|---|
pop 寄存器 | pop ax | 从栈顶弹出一个字,送入寄存器 | ax = stack.pop(); |
pop 段寄存器 | pop ds | 从栈顶弹出一个字,送入段寄存器 | ds = stack.pop(); |
pop 内存单元 | pop [2] | 从栈顶弹出一个字,写入内存 | memory[2] = stack.pop();(实际为 *(uint16_t*)(DS:2) = stack.pop()) |
-
将
10000H~1000FH这段空间当作栈,初始状态栈是空的; -
设置
AX=001AH,BX=001BH; -
将 AX、BX 中的数据入栈;
-
然后将 AX、BX 清零;
-
从栈中恢复 AX、BX 原来的内容。
mov ax,001AH
mov bx,001BH
push ax
push bx
sub ax, ax ; 将 ax 清零,也可以用 mov ax, 0, sub ax, ax 的机器码为 2 个字节,mov ax, 0 的机器码为 3 个字节。
sub bx,bx
pop bx ;从栈中恢复 ax、bx 原来的数据,当前栈顶的内容是 bx pop ax ;中原来的内容:001BH,ax 中原来的内容 001AH 在栈顶的下面,所以要先 pop bx,然后再 pop ax。出栈的顺序要和入栈的顺序相反。
重要
- 8086CPU 提供了栈操作机制,方案如下。
在 SS、SP 中存放栈顶的段地址和偏移地址;
提供入栈和出栈指令,它们根据 SS:SP 指示的地址,按照栈的方式访问内存单元。
- push 指令的执行步骤:①SP=SP-2;②向 SS:SP 指向的字单元中送入数据。
- pop 指令的执行步骤:①从 SS:SP 指向的字单元中读取数据;②SP=SP+2。
- 任意时刻,SS:SP 指向栈顶元素。
- 8086CPU 只记录栈顶,栈空间的大小我们要自己管理。
- 用栈来暂存以后需要恢复的寄存器的内容时,寄存器出栈的顺序要和入栈的顺序相反。
- push、pop 实质上是一种内存传送指令,注意它们的灵活应用。
3.10 栈段
CPU只认寄存器:
CS:IP决定从哪里取指令;DS决定默认从哪里读写数据;SS:SP决定PUSH、POP等栈操作访问哪里。SS决定栈在哪个段SP决定栈顶在哪里
第 4 章 第一个程序
4.1 一个源程序从写出到执行的过程

编写:创建 .asm 源文件。
编译:用汇编器(如 MASM)把汇编翻译成机器码,生成 .obj。
连接:用连接器生成可以运行的 .exe(或 .com)文件(让操作系统知道程序入口,所需内存,数据段/代码段位置)。
加载:由操作系统把程序装入内存,初始化 CS:IP、SS:SP、DS 等寄存器。
执行:CPU 从 CS:IP 指向的位置开始,一条一条执行机器指令。
4.2 源程序
assume cs:codesg ; 告诉编译器:codesg 是代码段
codesg segment ; 定义代码段开始
mov ax,0123H ; 真正执行的程序
mov bx,0456H
add ax,bx
add ax,ax
mov ax,4c00H
int 21H
codesg ends ; 段结束
end ; 整个源程序结束| 名称 | 写法 | 作用 | 谁处理 |
|---|---|---|---|
| 段结束 | codesg ends | 告诉编译器一个段结束 | 编译器 |
| 源程序结束 | end | 告诉编译器整个源程序结束 | 编译器 |
| 程序运行结束(返回 DOS) | mov ax,4C00H``int 21H | 告诉 DOS 程序执行完毕,把控制权还给操作系统 | CPU 执行指令,DOS 响应中断 |
源程序 ≠ 程序。源程序(.asm)中既包含给编译器看的内容(伪指令),也包含给 CPU 执行的内容(汇编指令);只有后者最终会被翻译成机器码并由 CPU 执行。

4.4 编译
从 wathenjiang/masm: debug.exe for DOSBox 整一个 MASM。
让 DOSBOX 连接到真实硬盘上(D:/),创建一个 .asm 文件:
assume cs:codesg
codesg segment
mov ax,0123h
mov bx,0456h
add ax,bx
add ax,ax
mov ax,4c00h
int 21h
codesg ends
end操作 MASM 编译出 1.obj:

4.5 连接

连接的作用有以下几个。
- 当源程序很大时,可以将它分为多个源程序文件来编译,每个源程序编译成为目标文件后,再用连接程序将它们连接到一起,生成一个可执行文件;
- 程序中调用了某个库文件中的子程序,需要将这个库文件和该程序生成的目标文件连接到一起,生成一个可执行文件;
- 一个源程序编译后,得到了存有机器码的目标文件,目标文件中的有些内容还不能直接用来生成可执行文件,连接程序将这些内容处理为最终的可执行信息。所以,在只有一个源程序文件,而又不需要调用某个库中的子程序的情况下,也必须用连接程序对目标文件进行处理,生成可执行文件。
4.6 以简化的方式进行编译和连接
在 masm 后面加上被编译的源程序文件的路径、文件名,在命令行的结尾再加上分号,按 Enter 键后,编译器就对 .asm 进行编译,在当前路径下生成目标文件 1.obj,并在编译的过程中自动忽略中间文件的生成。
4.7 1.exe 的执行
没有输出指令,不会有东西的。
4.8 谁将可执行文件中的程序装载进入内存并使它运行?
程序不会自己启动自己,而是由操作系统中的加载器(通常由 Shell 发起)将可执行文件装入内存,设置 CPU 的入口地址,然后把 CPU 的控制权交给程序;程序结束后,再把控制权交还给操作系统。
4.9 程序执行过程的跟踪
使用 debug 1.exe 即可调试。

因为 command 的程序加载,设置 CS:IP 指向程序的入口的操作是连续完成的,而当 CS:IP 一指向程序的入口,command 就放弃了 CPU 的控制权,CPU 立即开始运行程序,直至程序结束。
Debug 自动把程序长度放进 CX。
| 命令 | 全称 | 功能 |
|---|---|---|
R | Register | 查看或修改寄存器内容 |
U | Unassemble | 将内存中的机器码反汇编为汇编指令 |
T | Trace | 单步执行一条指令,并停下来等待下一步 |
P | Proceed | 执行整个子程序或中断(如 INT 21h),不中途进入其内部 |
Q | Quit | 退出 Debug,返回到 command.com |
Debug 不仅负责像 command.com 一样加载程序,还保留了对 CPU 的控制权,因此可以逐条执行、观察寄存器和内存的变化,从而分析程序的运行过程和定位错误。

DOS 将 EXE 文件加载到内存(前面放 PSP,后面放程序),初始化寄存器,使 CS:IP 指向程序入口,然后把 CPU 控制权交给程序。
PSP(Program Segment Prefix)是 DOS 在加载程序时自动创建的一块 256 字节的内存区域,位于程序前面,用来保存程序与 DOS 通信所需的信息。程序真正的代码紧跟在 PSP 后面。
实验 3
编译连接下面的 t1.asm:
assume cs:codesg
codesg segment
mov ax,2000H
mov ss,ax
mov sp,0
add sp,10
pop ax
pop bx
push ax
push bx
pop ax
pop ax
mov ax,4C00H
int 21H
codesg ends
end
DS 就是 PSP 所在的段地址。
第 5 章 [BX] 和 loop 指令
[bx] 和内存单元的描述
用 [0] 表示一个内存单元时,0 表示单元的偏移地址,段地址默认在 ds 中,单元的长度(类型)可以由具体指令中的其他操作对象(比如说寄存器)指出。
[bx] 同样也表示一个内存单元,它的偏移地址在 bx 中,比如指令:mov ax, [bx] 将一个内存单元的内容送入 ax,这个内存单元的长度为 2 字节(字单元),存放一个字,偏移地址在 bx 中,段地址在 ds 中。
mov al, [bx] 将一个内存单元的内容送入 al,这个内存单元的长度为 1 字节(字节单元),存放一个字节,偏移地址在 bx 中,段地址在 ds 中。
定义描述性的符号 ()
- ax 中的内容为 0010H,可以这样来描述:
(ax)=0010H; 2000:1000处的内容为 0010H,可以这样来描述:(21000H)=0010H;- 对于
mov ax,[2]的功能,可以这样来描述:(ax)=((ds)*16+2); - 对于
mov [2], ax的功能,可以这样来描述:((ds)*16+2)=(ax); - 对于
add ax,2的功能,可以这样来描述:(ax)=(ax)+2; - 对于
add ax, bx的功能,可以这样来描述:(ax)=(ax)+(bx); - 对于
push ax的功能,可以这样来描述:(sp)=(sp)-2(ss*16+(sp))=(ax)
- 对于 pop ax 的功能,可以这样来描述:
(ax)=((ss)*16+(sp))(sp)=(sp)+2
(X) 所表示的数据有两种类型:①字节;②字。是哪种类型由寄存器名或具体的运算决定,比如:
(al)、(bl)、(cl)等得到的数据为字节型;(ds)、(ax)、(bx)等得到的数据为字型。(al)=(20000H),则(20000H)得到的数据为字节型;(ax)=(20000H),则(20000H)得到的数据为字型。
约定符号 idata 表示常量
指令中,在 [...] 里用一个常量 0 表示内存单元的偏移地址。以后,我们用 idata 表示常量。比如:
mov ax,[idata]就代表mov ax,[1]、mov ax,[2]、mov ax,[3]等。mov bx, data就代表mov bx,1、mov bx,2、mov bx,3等。mov ds,idata就代表mov ds,1、mov ds,2等,它们都是非法指令。
5.1 [BX]
-
mov ax, [bx]:bx 中存放的数据作为一个偏移地址 EA,段地址 SA 默认在 ds 中,将SA:EA处的数据送入 ax 中。即:(ax)=((ds)*16+(bx))。 -
mov [bx], ax:bx 中存放的数据作为一个偏移地址 EA,段地址 SA 默认在 ds 中,将 ax 中的数据送入内存SA:EA处。即:((ds)*16+(bx))=(ax)。
5.2 Loop 指令
loop 指令的格式是:loop 标号,CPU 执行 loop 指令的时候,要进行两步操作:
(cx)=(cx)-1;- 判断 cx 中的值,不为零则转至标号处执行程序,如果为零则向下执行。
使用 loop 计算 212。
assume cs:code
code segment
mov ax,2
mov cx,11; 循环 12 次
s: add ax,ax; ax=ax*2
loop s
mov ax,4c00h
int 21h
code ends
end
使用 loop 计算 123*236 结果存在 ax 中。
assume cs:code
code segment
mov ax,0
mov cx,236
s: add ax,123
loop s
mov ax,4c00h
int 21h
code ends
end5.4 Debug 和汇编编译器 masm 对指令的不同处理
| 写法 | Debug 的理解 | MASM 的理解 |
|---|---|---|
mov al,[0] | 访问 DS:0000 内存 | 等价于 mov al,0 |
mov al,ds:[0] | 访问 DS:0000 内存 | 访问 DS:0000 内存 |
mov al,[bx] | 访问 DS:BX 内存 | 访问 DS:BX 内存 |
mov al,ds:[bx] | 访问 DS:BX 内存 | 访问 DS:BX 内存 |
Debug 是一个调试器,它默认把 [...] 看作内存地址;而 MASM 是一个编译器,它需要根据语法判断含义,当 [] 中只有一个数字时,会把它当作立即数处理。只有显式写出段寄存器(如 ds:[0])或使用寄存器间接寻址(如 [bx]),MASM 才明确知道这是在访问内存。
5.5 loop 和 [bx] 的联合应用
计算 ffff:0~ffff:b 单元中数据的和存储在 dx 中。
assume cs:code
code segment
mov ax,0ffffh
mov bx,0
mov dx,0
mov cx,12
s: mov al,[bx]
mov ah,0
add dx,ax
inc bx
loop s
mov ax,4c00h
int 21h
code ends
end| 地址 | 内容 |
|---|---|
FFFF:0000 | CPU 复位入口(Reset Vector),通常是一条跳转指令 |
FFFF:0001 ~ FFFF:000B | BIOS ROM 中的数据/指令,具体由 BIOS 决定 |
| 类型 | ROM,不是 RAM |
| 作用 | 计算机启动时让 CPU 找到 BIOS |

5.6 段前缀
段前缀就是在访问内存时,用 段寄存器: 明确指定段地址。例如 [bx] 默认访问 DS:BX,而 CS:[bx] 则强制访问 CS:BX。
| 写法 | 含义 | 段地址来源 | 偏移地址来源 | 访问的内存地址 | 说明 |
|---|---|---|---|---|---|
mov ax,[bx] | 将内存单元内容送入 AX | 默认 DS | BX | DS:BX | 不写段前缀时,默认使用 DS |
mov ax,ds:[bx] | 将内存单元内容送入 AX | DS | BX | DS:BX | 显式指定 DS 段 |
mov ax,cs:[bx] | 将内存单元内容送入 AX | CS | BX | CS:BX | 显式指定代码段 |
mov ax,ss:[bx] | 将内存单元内容送入 AX | SS | BX | SS:BX | 显式指定栈段 |
mov ax,es:[bx] | 将内存单元内容送入 AX | ES | BX | ES:BX | 显式指定附加段 |
mov ax,ss:[0] | 将内存单元内容送入 AX | SS | 0000H | SS:0000 | 偏移地址直接给出 |
mov ax,cs:[0] | 将内存单元内容送入 AX | CS | 0000H | CS:0000 | 偏移地址直接给出 |
5.7 一段安全的空间
DOS 和其他合法的程序一般都不会使用 0:200~0:2ff 这段空间。
- 我们需要直接向一段内存中写入内容;
- 这段内存空间不应存放系统或其他程序的数据或代码,否则写入操作很可能引发错误;
- DOS 方式下,一般情况,
0:200~0:2ff空间中没有系统或其他程序的数据或代码; - 以后,我们需要直接向一段内存中写入内容时,就使用
0:200~0:2ff这段空间。
5.8 段前缀的使用
将内存 ffff:0~ffff:b 单元中的数据复制到 0:200~0:20b 单元(相当于 0020:0~0020:b)中。
assume cs:code
code segment
mov ax,0ffffh
mov ds,ax
mov ax,0020h
mov es,ax
mov bx,0
mov cx,12
s: mov dl,[bx]
mov es:[bx],dl
inc bx
loop s
mov ax,4c00h
int 21h
code ends
end使用 es 存放目标空间 0020:0~0020:b 的段地址,用 ds 存放源始空间 ffff:0~ffff:b 的段地址。在访问内存单元的指令 mov es:[bx],al 中,显式地用段前缀 “es:” 给出单元的段地址,这样就不必在循环中重复设置 ds。
实验 4 [bx] 和 loop 的使用
编程,向内存 0:200~0:23F 依次传送数据 0~63(3FH),程序中只能使用 9 条指令,9 条指令中包括 mov ax,4c00h 和 int 21h。
assume cs:code
code segment
mov ax,0020h
mov ds,ax
mov bx,0
mov cx,3fh
s: mov [bx],bx
inc bx
loop s
mov ax,4c00h
int 21h
code ends
end
下面的程序的功能是将 mov ax,4c00h 之前的指令复制到内存 0:200 处,补全程序。上机调试,跟踪运行结果。
assume cs:code
code segment
mov ax,cs
mov ds,ax
mov ax,0020h
mov es,ax
mov bx,0
mov cx,???
s: mov al,[bx]
mov es:[bx],al
inc bx
loop s
mov ax,4c00h
int 21h
code ends
end其中,??? 先编译,看 mov ax,4c00h 的地址,把这个地址填入 CX。
第 6 章 包含多个段的程序
在操作系统的环境中,合法地通过操作系统取得的空间都是安全的,因为操作系统不会让一个程序所用的空间和其他程序以及系统自己的空间相冲突。在操作系统允许的情况下,程序可以取得任意容量的空间。
6.1 在代码段中使用数据
使用循环的方法,将数据存在一组地址连续的内存单元中,将 0123h、0456h、0789h、0abch、0defh、0fedh、0cbah、0987h 累加的结果存在 ax 寄存器中。
assume cs:code
code segment
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
mov bx,0
mov ax,0
mov cx,8
s: add ax,cs:[bx]
add bx,2
loop s
mov ax,4c00h
int 21h
code ends
end这样不行,因为 dw 定义的数据会和代码一起装入内存。CPU 会将 dw 部分的数据当成代码跑了。
用 start 来安排程序的框架,这样程序就知道代码的入口:
assume cs:code
code segment
:
:
数据
:
start:
:
代码
:
code ends
end startassume cs:code
code segment
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
start: mov bx,0
mov ax,0
mov cx,8
s: add ax,cs:[bx]
add bx,2
loop s
mov ax,4c00h
int 21h
code ends
end start
6.2 在代码中使用栈
将程序中定义的数据(0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h)逆序存放。
assume cs:codesg
codesg segment
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
;用dw定义16个字型数据,在程序加载后,将取得16个字的
;内存空间,存放这16个数据。在后面的程序中将这段
;空间当作栈来使用
start: mov ax,cs
mov ss,ax
mov sp,30h ;将设置栈顶 ss:sp 指向 cs:30
mov bx,0
mov cx,8
s: push cs:[bx]
add bx,2
loop s ;以上将代码段0~15单元中的8个字型数据依次入栈
mov bx,0
mov cx,8
s0: pop cs:[bx]
add bx,2
loop s0 ;以上依次出栈8个字型数据到代码段0~15单元中
mov ax,4c00h
int 21h
codesg ends
end start ;指明程序的入口在start处
检测点 6.1
6.3 将数据、代码、栈放入不同的段
assume cs:b, ds:a, ss:c
a segment
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
a ends
c segment
dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
c ends
b segment
d: mov ax,c
mov ss,ax
mov sp,20h ;希望用 c 段当作栈空间,设置 ss:sp 指向 c:20
mov ax,a
mov ds,ax
mov bx,0 ;希望用 ds:bx 访问 a 段中的数据,ds 指向 a 段
;ds:bx 指向 a 段中的第一个单元
mov cx,8
s: push [bx]
add bx,2
loop s ;以上将 a 段中的 0~15 单元中的 8 个字型数据依次入栈
mov bx,0
mov cx,8
s0: pop [bx]
add bx,2
loop s0 ;以上依次出栈 8 个字型数据到 a 段的 0~15 单元中
mov ax,4c00h
int 21h
b ends
end d ;d 处是要执行的第一条指令,即程序的入口实验 5 编写、调试具有多个段的程序
用 push 指令将 a 段中的前 8 个字形数据,逆序存储到 b 段中。
assume cs:code
a segment
dw 1,2,3,4,5,6,7,8,9,0ah,0bh,0ch,0dh,0eh,0fh,0ffh
a ends
b segment
dw 0,0,0,0,0,0,0,0
b ends
code segment
start:
mov ax,a
mov ds,ax
mov ax,b
mov ss,ax
mov bx,0
mov cx,7
s: push bx
inc bx
loop s
mov ax,4c00h
int 21h
code ends
end start第 7 章 更灵活的定位内存地址的方法
7.1 and 和 or 指令
| 汇编 | C/C++ | Java | JavaScript | Python | 作用 |
|---|---|---|---|---|---|
and ax,bx | ax &= bx; | ax &= bx; | ax &= bx; | ax &= bx | 按位与 |
or ax,bx | `ax | = bx;` | `ax | = bx;` | `ax |
7.2 关于 ASCII 码

7.3 以字符形式给出的数据
可以用 '...' 的方式指明数据是以字符的形式给出的。
assume cs:code,ds:data
data segment
db 'unIX'
db 'foRK'
data ends
code segment
start: mov al,'a'
mov bl,'b'
mov ax,4c00h
int 21h
code ends
end start
7.4 大小写转换的问题
| 大写 | 十六进制 | 二进制 | 小写 | 十六进制 | 二进制 |
|---|---|---|---|---|---|
| A | 41H | 0100 0001 | a | 61H | 0110 0001 |
| B | 42H | 0100 0010 | b | 62H | 0110 0010 |
| C | 43H | 0100 0011 | c | 63H | 0110 0011 |
| D | 44H | 0100 0100 | d | 64H | 0110 0100 |
| E | 45H | 0100 0101 | e | 65H | 0110 0101 |
| F | 46H | 0100 0110 | f | 66H | 0110 0110 |
-
小写字母的 ASCII 码值比大写字母的 ASCII 码值大 20H。
-
第 5 位(位数从 0 开始计算)外,大写字母和小写字母的其他各位都一样。
- 大写字母 ASCII 码的第 5 位为 0,小写字母的第 5 位为 1。
用 or 和 and 语句可以将一个英文字母字符串全部变成大写字母/小写字母(将第 5 位置 0/1)
assume cs:codesg,ds:datasg
datasg segment
db 'BaSiC'
db 'iNfOrMaTiOn'
datasg ends
codesg segment
start: mov ax,datasg
mov ds,ax ; 设置 ds 指向 datasg 段
mov bx,0 ; 设置 (bx)=0,ds:bx 指向 'BaSiC' 的第一个字母
mov cx,5 ; 设置循环次数 5,因为'BaSiC'有 5 个字母
s:mov al,[bx] ;将 ASCII 码从 ds:bx 所指向的单元中取出
and al,11011111B ; 将 al 中的 ASCII 码的第5位置为 0,变为大写字母
mov [bx],al ;将转变后的 ASCII 码写回原单元
inc bx ; (bx)加 1,ds:bx 指向下一个字母
loop s
mov bx,5 ; 设置 (bx)=5,ds:bx 指向 'iNfOrMaTiOn' 的第一个字母
mov cx,11 ; 设置循环次数 11,因为 'iNfOrMaTiOn' 有 11 个字母
s0:mov al,[bx]
or al,00100000B ; 将 al 中的 ASCII 码的第 5 位置为 1,变为小写字母
mov [bx],al
inc bx
loop s0
mov ax,4c00h
int 21h
codesg ends
end start
7.5 [bx+idata]
用 Debug 查看内存,结果如下:
2000:1000 BE 00 06 00 00 00 .....
写出下面的程序执行后,ax、bx、cx 中的内容。
mov ax,2000H
mov ds,ax
mov bx,1000H
mov ax, [bx]
mov cx, [bx+1]
add cx, [bx+2]-
mov ax, [bx]:访问的字单元的段地址在 ds 中,(ds)=2000H;偏移地址在 bx 中,(bx)=1000H;指令执行后(ax)=00BEH。 -
mov cx, [bx+1]:访问的字单元的段地址在 ds 中,(ds)=2000H;偏移地址=(bx)+1=1001H;指令执行后(cx)=0600H。 -
add cx, [bx+2]:访问的字单元的段地址在 ds 中,(ds)=2000H;偏移地址=(bx)+2=1002H;指令执行后(cx)=0606H。
7.6 用 [bx+idata] 的方式进行数组的处理
mov ax,datasg
mov ds,ax
mov bx,0
mov cx,5
s: mov al,0[bx]
and al,11011111b
mov 0[bx],al
mov al,5[bx]
or al,00100000b
mov 5[bx],al
inc bx
loop schar a[5]="BaSiC";
char b[5]="MinIX";
main()
{
int i;
i=0;
do
{
a[i]=a[i]&0xDF;
b[i]=b[i]|0x20;
i++;
}
while (i<5);
}-
C 语言:
a[i],b[i] -
汇编语言:
0[bx],5[bx]
[bx+idata] 的方式为高级语言实现数组提供了便利机制。
7.7 SI 和 DI
- SI:源地址(Source)
- DI:目的地址(Destination)
程序员逐渐形成习惯:
- SI 用来指向源数据。
- DI 用来指向目标位置。
- BX 更多作为普通基址寄存器使用。
7.10 不同的寻址方式的灵活运用
| 汇编寻址方式 | 地址计算 | C语言类比 | 典型用途 | 示例 |
|---|---|---|---|---|
[bx] | base | *p | 访问一个变量、数组首元素、结构体首地址 | value = *p; |
[bx+8] | base + offset | *(p+8) 或 ((char*)p)+8 | 访问固定偏移的数据(常见于结构体成员) | student.age(假设 age 偏移为 8) |
[si] | index | *src 或 src[i] | 顺序读取数据,如遍历字符串、数组 | ch = src[i]; |
[di] | index | *dst 或 dst[i] | 顺序写入数据,如复制字符串、填充数组 | dst[i] = value; |
[bx+si] | base + index | p[i] | 数组访问(最经典、最常见) | sum += array[i]; |
[bx+di] | base + index | p[j] 或 dst[i] | 访问另一组数据或目标数组 | dst[i] = ...; |
[bx+si+8] | base + index + offset | p[i+8] 或 array[i].member | 访问带固定偏移的数据,如结构体数组成员、二维数组元素 | student[i].age |
[bx+di+8] | base + index + offset | dst[i+8] 或 dst[i].member | 向带固定偏移的位置写数据 | student[i].score = ...; |
| 汇编形式 | 本质 |
|---|---|
[bx] | 基地址(Base) |
[bx+常数] | 基地址 + 固定偏移(Offset) |
[si] / [di] | 索引(Index) |
[bx+si] / [bx+di] | 基地址 + 索引(Base + Index) |
[bx+si+常数] / [bx+di+常数] | 基地址 + 索引 + 固定偏移(Base + Index + Offset) |
*p --> [bx]
p->member --> [bx+offset]
array[i] --> [bx+si]
array[i].member --> [bx+si+offset]将 datamsg 段中每个单词改为大写字母。

assume cs:codesg,ds:datasg,ss:stacksg
datasg segment
db 'ibm '
db 'dec '
db 'dos '
db 'vax '
datasg ends
stacksg segment ; 定义一个段,用来做栈段,容量为16个字节
dw 0,0,0,0,0,0,0,0
stacksg ends
codesg segment
start: mov ax,stacksg
mov ss,ax
mov sp,16
mov ax,datasg
mov ds,ax
mov bx,0
mov cx,4
s0: push cx ; 将外层循环的 cx 值压栈
mov si,0
mov cx,3 ; cx 设置为内层循环的次数
s: mov al, [bx+si]
and al,11011111b
mov [bx+si],al
inc si
loop s
add bx,16
pop cx ; 从栈顶弹出原 cx 的值,恢复 cx
loop s0 ; 外层循环的 loop 指令将 cx 中的计数值减 1
mov ax,4c00H
int 21H
codesg ends
end start进行二重循环,却只用了一个循环计数器,造成在进行内层循环的时候,覆盖了外层循环的循环计数值。应该在每次开始内层循环的时候,将外层循环的 cx 中的数值保存起来,在执行外层循环的 loop 指令前,再恢复外层循环的 cx 数值。
