bt

汇编语言(王爽·第4版)—— 完整教程与复习笔记

· 95 min read · 汇编语言 , 8086 , 期末复习

更新于: 2026年10月20日

面向考试 / 期末复习,章节结构与王爽《汇编语言(第4版)》目录完全一致:17 章 + 各章实验/课程设计 + 综合研究 + 附注 1~5。CPU 模型为 8086(16 位、实模式)。文中所有代码示例与计算均已逐一验算;全部内容已对照第 4 版原书逐章核对补全。

这篇讲知识点,适合当参考手册查。想看怎么把代码跑起来,读环境与 14 课实战;想先搞清楚”学它图啥”,读为什么学汇编;考前冲刺,读期末真题与课程项目。示例源码在 GitHub 的 汇编/ 目录。


目录

  1. 基础知识
  2. 寄存器
  3. 寄存器(内存访问)
  4. 第一个程序
  5. [BX]和loop指令
  6. 包含多个段的程序
  7. 更灵活的定位内存地址的方法
  8. 数据处理的两个基本问题
  9. 转移指令的原理
  10. CALL和RET指令
  11. 标志寄存器
  12. 内中断
  13. int指令
  14. 端口
  15. 外中断
  16. 直接定址表
  17. 使用BIOS进行键盘输入和磁盘读写
  18. 综合研究
  19. 附注
  20. 复习速查(本笔记补充)

1. 基础知识

1.1 机器语言

  • 机器语言:二进制机器指令的集合,CPU 能直接执行,但人几乎无法直接读写。
  • 汇编语言正是在机器语言基础上用符号辅助编写。

1.2 汇编语言的产生

  • 机器语言难以记忆、难以查错,于是用助记符(如 mov、add)代表机器指令——这就是汇编语言的产生动机。

1.3 汇编语言的组成

汇编语言由三类指令组成:

  1. 汇编指令:机器码的助记符,是汇编语言的核心,有对应的机器码;
  2. 伪指令:没有对应的机器码,由编译器执行(如 segment、end);
  3. 其他符号:如 +、-、*、/,由编译器识别,没有对应机器码。

1.4 存储器

  • 存储器:CPU 运行时向其读取/写入数据的部件,由若干存储单元组成。
  • 内存的作用:存放 CPU 执行需要的指令和数据。

1.5 指令和数据

  • 指令和数据在内存/磁盘中没有区别,都是二进制信息;CPU 在工作时把有的信息当作指令、有的当作数据,区别只在于应用场合。

1.6 存储单元

  • 存储器被划分为若干存储单元,每个单元从 0 开始编号,称为地址。
  • 8086 一个存储单元 = 1 字节(Byte)= 8 bit;1 字(Word)= 2 字节。

1.7 CPU对存储器的读写

CPU 要读写数据,必须和外部芯片进行 3 类信息交互:

  1. 地址信息:存储单元的地址(通过地址总线发出);
  2. 控制信息:读还是写(通过控制总线发出);
  3. 数据信息:读/写的内容(通过数据总线传送)。

1.8 地址总线

  • 地址总线宽度决定 CPU 的寻址能力:N 根地址线 → 可寻址 $2^N$ 个存储单元。
  • 8086 有 20 根地址总线 → 寻址 $2^{20}$ = 1 MB 内存空间。

1.9 数据总线

  • 数据总线宽度决定 CPU 一次传送多少数据(bit 数)。
  • 8086 数据总线 16 根 → 一次传送 2 字节;8088 只有 8 根 → 一次 1 字节。

1.10 控制总线

  • 控制总线是一些不同控制线的集合,每根线单独表达一种控制信息。
  • 方向不能一概而论:有的由 CPU 发出(如”读”信号),有的由外部芯片发给 CPU(如中断请求线)。

计算例:地址总线宽度为 20,内存地址空间大小 = $2^{20}\ \text{B} = 1048576\ \text{B} = 1\ \text{MB}$。

1.11 内存地址空间(概述)

  • 每台 PC 中,内存地址空间的大小受地址总线宽度限制;CPU 对它统一寻址。

1.12 主板

  • 主板上有通过总线相连的核心器件:CPU、存储器(RAM)、外围芯片(接口卡)、扩展插槽。

1.13 接口卡

  • CPU 不能直接控制外设(显示器、键盘、音箱),而是通过向接口卡上的**寄存器(端口)**发命令间接控制。
  • CPU 通过总线向接口卡发送命令,接口卡根据命令控制外设工作。

1.14 各类存储器芯片

分类特点举例
随机存储器 RAM可读可写,断电丢失主存、显存
只读存储器 ROM只能读,断电不丢BIOS ROM(其中存有开机程序)

1.15 内存地址空间

  • 内存地址空间:CPU 将各类存储器都看作一个由若干存储单元组成的逻辑存储器,每个物理存储器在地址空间中占有一段地址区间。
  • 8086 PC 机地址空间分配(从低到高):主存储器地址空间 → 显存地址空间 → 各类 ROM 地址空间。对某地址读写,实际上就是对它所归属的物理存储器读写。

2. 寄存器

CPU 的主要部件:运算器进行信息处理、寄存器进行信息存储、控制器控制各器件工作、内部总线在器件之间传送数据。第 1 章的总线相对于 CPU 内部来说是外部总线(连接 CPU 与主板上其他器件)。对汇编程序员来说,CPU 中主要可操作的部件是寄存器——程序员通过改变寄存器中的内容来实现对 CPU 的控制。

2.1 通用寄存器

8086 CPU 有 14 个寄存器(16 位可访问):AX、BX、CX、DX、SI、DI、SP、BP、CS、DS、SS、ES、IP、FLAGS。其中通用寄存器 4 个,通常用于存放一般性数据:

16 位寄存器高 8 位低 8 位
AX(累加)AHAL
BX(基址)BHBL
CX(计数)CHCL
DX(数据)DHDL
  • 为保证兼容,上一代 CPU 中的 8 位寄存器都可独立使用;高 8 位和低 8 位互不影响。

2.2 字在寄存器中的存储

  • 一个字存在一个 16 位寄存器中:高字节放高位、低字节放低位。
  • 例:AX = 01001110 00100000B = 4E20H,则 AH = 4EH,AL = 20H。

2.3 几条汇编指令

mov ax, 18 ; ax = 18
mov ah, 78 ; ah = 78(不影响 AL)
add ax, 8 ; ax = ax + 8
mov ax, bx ; ax = bx
add ax, bx ; ax = ax + bx
  • 指令进行两个操作对象位数一致的运算。
  • 结果超出寄存器宽度时进位(高位)直接丢弃:mov al, 98H 后 add al, 98H,98H+98H = 130H,8 位放不下,AL 中只保留低 8 位 30H(进位在 CF 中,见第 11 章)。
  • 汇编中十六进制以字母开头时前面补 0(如 0C0H),否则编译器会把它当符号。

2.4 物理地址

  • 8086 有 20 位地址总线(寻址 1MB),但 CPU 内部是 16 位结构——一个 16 位地址无法直接给出 20 位地址,所以需要两个 16 位地址合成物理地址。

2.5 16位结构的CPU

  • 16 位结构:运算器一次最多处理 16 位数据、寄存器最大宽度 16 位、寄存器与运算器之间的通路为 16 位。

2.6 8086CPU给出物理地址的方法

$$\textbf{物理地址} = \textbf{段地址} \times 16 + \textbf{偏移地址}$$

  • ×16 在二进制中即左移 4 位(十六进制左移 1 位,后面补 0)。
  • 例:段地址 123CH、偏移地址 0008H → 物理地址 = 123C0H + 0008H = 123C8H。

2.7 “段地址×16+偏移地址=物理地址”的本质含义

  • CPU 用”基础地址(段地址×16)+ 相对于基础地址的偏移地址”给出内存单元的物理地址。
  • 两个 16 位地址(段地址、偏移地址)合成了一个 20 位的物理地址。

2.8 段的概念

  • “段”并非内存真的被划分,而是 CPU 分段寻址方式带来的逻辑概念;可以根据需要安排若干”段”。
  • 一个段的起始地址(段地址×16)一定是 16 的倍数。
  • 偏移地址 16 位 → 一个段最长 64 KB($2^{16}$)。
  • 同一物理地址可由不同的”段地址:偏移地址 ”组合得到,如 21F60H = 2000H:1F60H = 21F6H:0000H 。

2.9 段寄存器

  • 4 个段寄存器:CS、DS、SS、ES。
  • 8086 任何时候都要用”段地址:偏移地址 ”定位指令或数据;段地址就存放在段寄存器中。

2.10 CS和IP

  • CS(代码段寄存器)+ IP(指令指针寄存器):指示 CPU 当前要读取指令的地址。
  • 任意时刻,CPU 将 CS:IP 指向的内容当作指令执行:
    1. 从 CS×16+IP 单元读取指令,送入指令缓冲器;
    2. IP = IP + 所读指令长度(指向下一条指令);
    3. 执行指令,转步骤 1。
  • 8086 加电启动或复位后:CS = FFFFH、IP = 0000H,即从 FFFF0H 单元开始执行——那里是 BIOS ROM 中的开机第一条指令。

2.11 修改CS、IP的指令

  • 8086 大部分寄存器可用 mov 修改,但 mov 不能用于设置 CS、IP。
  • 能修改 CS、IP 的指令统称为转移指令,最简单的是 jmp:
jmp 2AE3:3 ; 同时修改:CS=2AE3H, IP=0003H(段间转移)
jmp ax ; 仅修改 IP = ax(段内转移,功能上类比 mov ip, ax)

2.12 代码段

  • 将一段内存当作代码段,只需把它的段地址放在 CS、把第一条指令的偏移放在 IP,CPU 就会依次执行其中指令。
  • 代码段是人为安排的;CPU 只认 CS:IP 。

实验1 查看CPU和内存,用机器指令和汇编指令编程

用 Debug 直接操作 CPU 和内存,Debug 常用命令:

命令功能
R查看/修改寄存器内容(R ax 改 AX)
D查看内存内容(D 段地址:偏移)
E改写内存(E 1000:0 41 42 43)
U将内存中的机器指令翻译成汇编指令(反汇编)
A以汇编指令格式向内存写入机器指令
T执行一条指令(单步跟踪)
G执行到指定位置(G 0016)
P执行到循环结束/中断返回(遇到 int 指令时不停进中断例程内部)
Q退出 Debug

实验任务要点:①将给定程序段写入内存逐条执行,观察寄存器变化;②用 3 条指令计算 2⁸(mov ax,1 / add ax,ax / jmp 2000:0003 循环);③查看主板 ROM 中的生产日期(内存 FFF00H~FFFFFH 的某几个单元,ROM 只读、试图修改无效——正好验证 1.15 节”写 ROM 无效”);④向 B8100H 起的显存单元填写数据,屏幕上立即出现现象(写显存 = 显示)。


3. 寄存器(内存访问)

3.1 内存中字的存储

  • 小端方式(低对低、高对高——教材称“高高低低”原则):字的低字节存放在低地址单元,高字节存放在高地址单元。
  • 例:把 4E20H 存进 20000H 起的字单元,则 (20000H)=20H、(20001H)=4EH;20000H、20001H 合起来构成一个字单元,值为 4E20H。
  • 字单元:由两个地址连续的内存单元组成,起始单元为低地址单元。

3.2 DS和[address]

  • DS 通常存放要访问数据的段地址;[address] 表示一个内存单元,偏移地址为 address,段地址默认取 DS。
mov bx, 1000H
mov ds, bx ; 注意!8086 不支持 mov ds, 立即数,必须经通用寄存器中转
mov al, [0] ; al = (DS×16+0) 中的字节
mov [0], al ; 将 al 写入该内存单元

3.3 字的传送

  • 字的传送一次完成(16 根数据线同时工作):mov ax, [0] 一次读 0、1 两个单元,ax 中高 8 位来自高地址单元、低 8 位来自低地址单元。

3.4 mov、add、sub指令

指令形式举例
mov/add/sub 寄存器, 数据add ax, 8
mov/add/sub 寄存器, 寄存器mov ax, bx
mov/add/sub 寄存器, 内存单元mov ax, [0]
mov/add/sub 内存单元, 寄存器add [0], ax
mov 段寄存器, 寄存器mov ds, bx
mov 寄存器, 段寄存器mov ax, cs

⚠️ 不能:

  • mov ds, 1000H:段寄存器不能直接给立即数,须经通用寄存器中转;
  • add/sub 不能操作段寄存器;
  • 两个内存单元之间不能直接 mov/add/sub;
  • 立即数不能作为目的操作数(如 mov 8, ax)。

3.5 数据段

把一段内存当作数据段:将段地址放入 DS,用 [偏移] 访问其中单元即可。数据段也是人为安排。

3.6 栈

  • 栈:一种后进先出(LIFO)的结构,8086 提供相关指令以内存的一段空间作为栈。

3.7 CPU提供的栈机制

  • SS:SP 指示栈顶元素:SS 存放栈顶的段地址,SP 存放栈顶的偏移地址。
  • 8086 的入栈/出栈以字(2 字节)为单位。

push ax 执行过程:

  1. SP = SP − 2(栈顶指针先向低地址移动);
  2. 将 ax 内容送入 SS:SP 指向的字单元。

pop ax 执行过程:

  1. 将 SS:SP 指向的字单元数据送入 ax;
  2. SP = SP + 2。
  • 空栈时,SP 指向栈底(栈空间最高地址单元)的下一个单元。
  • 栈增长方向:从高地址向低地址增长。
mov ax, 1000H
mov ss, ax
mov sp, 0010H ; 栈空间 1000:0000~000FH(16 字节),空栈时 SP=0010H 指向栈底下一单元
push ax ; 入栈
pop bx ; 出栈

执行过程演练(栈空间 1000:0000 ~000FH,初始 SP=0010H):

mov ax, 0123H
mov bx, 2266H
mov cx, 1122H
push ax ; SP=000EH,(1000:000EH)=0123H
push bx ; SP=000CH,(1000:000CH)=2266H
push cx ; SP=000AH,(1000:000AH)=1122H
pop ax ; ax=1122H,SP=000CH
pop bx ; bx=2266H,SP=000EH
pop cx ; cx=0123H,SP=0010H

出栈顺序与入栈相反:ax、bx、cx 的值在入栈前后互换。

3.8 栈顶超界的问题

  • push 前 SP 只减 2、pop 后 SP 只加 2,8086 不保证栈操作不会越界,不会因超界而报错。
  • 编程者必须自己操心栈的范围,安排足够大的栈空间。

3.9 push、pop指令

可操作对象:寄存器、段寄存器、内存单元。

push ax ; 寄存器
push ds ; 段寄存器
push [0] ; 内存字单元
pop [2] ; 出栈到内存

3.10 栈段

  • 栈段:把一段内存当作栈段,只需 SS:SP 指向它。
  • 栈段最大 64KB:push/pop 只修改 SP,栈顶变化范围 0FFFFH;从栈空(SP=0)一直压栈到栈满(SP 又回到 0),若再压栈,栈顶将环绕、覆盖原有内容。例:10000H1FFFFH 作栈段(64KB),栈空时 SS=1000H、SP=0。
  • 段综述:一段内存可以既是代码段、又是数据段、还可以是栈段,也可以什么都不是——关键只在于 CPU 中寄存器的设置:CS:IP 指向 → 当指令执行;DS 指向 → 当数据访问;SS:SP 指向 → 当栈使用。

实验2 用机器指令和汇编指令编程

  • 在 Debug 中用 A 命令写汇编指令,体会栈机制与寄存器、内存的读写。
  • 重要现象:用 T 命令单步执行修改 SS 的指令(如 mov ss,ax)时,下一条指令会紧接着被执行(如 mov sp,10 看似没执行,实际和前一条一起执行了)。原因:CPU 执行完设置 SS 的指令后不响应任何中断(含 Debug 依赖的单步中断),正好为连续设置 SS、SP 提供了时机(呼应 12.12 节)。因此设置栈顶的指令应当连续存放。

4. 第一个程序

4.1 一个源程序从写出到执行的过程

编写(.asm)→ 编译 masm(.obj)→ 连接 link(.exe)→ 加载运行

  • 编译:把汇编指令翻译成机器码,生成目标文件 .obj;同时可生成列表文件 .lst。
  • 连接:把目标文件组装成可执行文件 .exe;同时可生成映像文件 .map。

4.2 源程序

assume cs:codesg ; 将代码段 codesg 与段寄存器 CS 关联(假设性说明)
codesg segment ; 定义一个段的开始
mov ax, 0123H
mov bx, 0456H
add ax, bx
add ax, ax
mov ax, 4c00H ; 程序返回:AH=4CH(功能号),AL=00H(返回码)
int 21H ; 调用 DOS 中断完成返回
codesg ends ; 段结束
end ; 程序结束标记(编译器遇 end 结束编译)
伪指令含义
xxx segment / xxx ends定义一个段(成对出现)
end标记整个程序结束(编译器遇 end 结束编译)
assume”假设”某段与某段寄存器相关联

⚠️ 两个”结束”不是一回事:

  • end 是程序结束:汇编期间通知编译器”源程序到此为止”;

  • mov ax,4c00H + int 21H 是程序返回:运行期间把 CPU 控制权交还给系统。没有它,程序执行完会继续跑进后面的内存。

  • 标号:codesg 作为段名,最终被编译处理为一个段地址。

  • 语法错误 vs 逻辑错误:编译时被编译器发现的(伪指令拼写错误、段未闭合等)是语法错误;编译通过、运行时才暴露的(如程序没有返回)是逻辑错误。

4.3 编辑源程序

  • 用文本编辑器编写 .asm 源文件(纯文本,不含格式)。

4.4 编译

  • 用编译器(masm)对源文件编译:生成目标文件 .obj(以及可选列表文件 .lst)。

4.5 连接

  • 用连接器(link)连接目标文件:生成可执行文件 .exe(以及可选映像文件 .map)。程序很大时可分为多个源文件分别编译再连接,也可调用库文件。

4.6 以简化的方式进行编译和连接

  • 简化方式:在命令行结尾加分号——masm c:\1;、link 1;,直接生成 .obj / .exe,并自动忽略中间文件(.lst / .crf / .map)的生成。

4.7 1.exe的执行

  • 在 DOS 中直接输入可执行文件名,command.com 将其装入内存并执行。

4.8 谁将可执行文件中的程序装载进入内存并使它运行?

  • command.com(DOS 的 shell 程序)将 .exe 装入内存:找到一段容量足够的空闲内存 → 建立 PSP(程序段前缀,256 字节) → 在 PSP 之后装入程序。
  • 程序加载后:DS 中存放 PSP 的段地址(程序从 PSP 后 256 字节处开始,即程序段地址 = DS+10H),CS:IP 指向程序入口,SS:SP 指向栈顶。

4.9 程序执行过程的跟踪

  • Debug 中把 .exe 作为参数载入后跟踪:用 T 逐条执行,遇到 int 21H 用 P 正常结束返回。
  • Debug 加载后 CX 存放程序的长度(机器码字节数,如 15 字节的程序 CX=000FH);PSP 的头两个字节是 CD 20(int 20H 的机器码),可用 d ds:0 查看。
  • 装载与返回链:command 加载 Debug → Debug 加载程序;程序运行结束返回 Debug,Q 退出 Debug 返回 command。

实验3 编程、编译、连接、跟踪

  • 完整走一遍”编写源程序 → masm 编译 → link 连接 → Debug 跟踪”流程。

5. [BX]和loop指令

约定符号:( ) 表示寄存器或内存单元中的内容,如 (ax)、(20000H)、((ds)*16+(bx));idata 表示常量。例如 mov ax,[bx] 描述为 (ax)=((ds)*16+(bx));push ax 描述为 (sp)=(sp)-2、((ss)*16+(sp))=(ax)。完整描述一个内存单元需要两种信息:地址 + 长度(类型)。

5.1 [BX]

  • [bx] 表示内存单元,其偏移地址在 bx 中,段地址默认 DS。
mov bx, 0
mov ax, [bx] ; 等价于 mov ax, (DS:0)
inc bx ; bx = bx + 1
  • inc 寄存器:加 1;dec 寄存器:减 1(两条指令后面会常用来移动指针)。

5.2 Loop指令

  • CPU 执行 loop 标号:
    1. (cx) = (cx) − 1;
    2. 判断 cx:不为 0 则转至标号处执行;为 0 则向下执行。
  • cx 中存放循环次数。

经典例子:计算 $2^{12}$(不用乘法):

mov ax, 2
mov cx, 11 ; 循环 11 次,每次翻倍
s: add ax, ax ; ax = ax × 2
loop s ; cx-- ≠0 就跳到 s

执行结果:AX = $2^{12}$ = 4096 = 1000H。

5.3 在Debug中跟踪用loop指令实现的循环程序

  • 用 G 偏移 直接执行到循环外(跳过循环体),避免 T 逐条执行 12 次;或用 P 一次执行完 loop。

5.4 Debug和汇编编译器masm对指令的不同处理

写法Debug(A 命令)masm 源程序
mov ax, [0]访问内存单元 DS:0 被当作 mov ax, 0(立即数)!
访问偏移为 idata 的内存[0]必须写成 ds:[0],或先把偏移送入 bx 用 [bx]

在源程序中用 idata 表示偏移地址,要显式写段前缀 ds:[0]。

5.5 loop和[bx]的联合应用

典型套路:累加一段连续内存(ffff:0 ~ffff:b 共 12 个字节单元求和到 dx):

mov ax, 0ffffH
mov ds, ax
mov bx, 0 ; 偏移指针,从 0 单元开始
mov dx, 0 ; 累加结果
mov cx, 12 ; 12 个单元
s: mov al, [bx]
mov ah, 0 ; 字节扩展为字,避免 add dx, ax 时误带高位
add dx, ax
inc bx
loop s

5.6 段前缀

  • 段前缀:显式给出段地址,如 ds:[bx]、cs:[bx]、ss:[bx]、es:[bx]。
  • 出现在访问内存单元的指令中,用于显式指定内存单元的段地址。

5.7 一段安全的空间

  • 在一般 PC 机中,0:200H ~0:2FFH 的 256 字节空闲安全,Debug 实验可直接使用;不要随意向未知内存写数据。

5.8 段前缀的使用

  • 把数据放到其他段(如代码段、栈段)时,用段前缀访问;没有前缀时默认 DS。
  • 经典例:把 ffff:0 ffff:b 的 12 个字节复制到 0:200 0:20b (等价于 0020:0 ~0020:b ,选用后者是为了让源和目标的偏移都从 0 开始)。改进:用 ES 存放目标段地址(mov ax,0020H / mov es,ax),循环内 mov dl,[bx] + mov es:[bx],dl,避免每次循环重复设置 DS。

实验4 [bx]和loop的使用

  • 用 loop 和 [bx] 完成对连续内存单元的访问(如累加、复制)。

6. 包含多个段的程序

6.1 在代码段中使用数据

  • 程序取得内存空间的两种方式:加载时由系统分配(在源程序中定义段来说明所需空间,编译连接后随程序一起装入)与执行中向系统申请(本课程不讨论)。
  • 程序中定义的数据放在代码段最前面时,要让 CS:IP 跳过数据,用 end 标号 指定程序入口:
assume cs:code
code segment
dw 0123H,0456H,0789H,0abcH,0defH,0fedH,0cbaH,0987H ; define word,8 个字数据
start: ; 程序入口标号
mov bx, 0
mov ax, 0
mov cx, 8
s: add ax, cs:[bx] ; 数据在代码段,用 cs 段前缀访问
add bx, 2
loop s
mov ax, 4c00H
int 21H
code ends
end start ; end 后标号指示程序从 start 开始执行

执行结果:AX = 8 个字之和 = 4BDBH。

  • dw(define word)定义字型数据;db(define byte)定义字节型数据。

6.2 在代码段中使用栈

用 dw 在段中开辟栈空间,再把 SS:SP 指向它:

dw 0,0,0,0,0,0,0,0 ; 8 个字作为栈空间,占 16 字节
start:
mov ax, cs
mov ss, ax
mov sp, 10H ; 空栈 SP 指向栈底(最高单元)下一单元(16)

6.3 将数据、代码、栈放入不同的段

  • 一个段容量 ≤ 64KB,且数据/代码/栈混放显得混乱;可以定义多个段:
assume cs:code, ds:data, ss:stack
data segment
dw 0123H, 0456H
data ends
stack segment
dw 0,0,0,0,0,0,0,0
stack ends
code segment
start:
mov ax, stack
mov ss, ax
mov sp, 16
mov ax, data
mov ds, ax ; 访问 data 段前,先把段地址送 DS
; ...业务指令...
mov ax, 4c00H
int 21H
code ends
end start

关键:assume 只是给编译器的”假设”,CPU 并不会因此自动把段地址送入 DS/SS,段地址必须由指令自己设置;只有 CS:IP 由加载程序根据 end 入口标号 自动设置。

  • 段名 = 标号:指令中的段名(如 mov ax,data)被编译器处理为一个表示段地址的数值,所以同样不能直接写 mov ds,data(不能给段寄存器立即数)。段名只存在于源程序中,CPU 并不知道;把哪个段当代码/数据/栈,完全由程序中的具体指令和对 CS:IP 、DS、SS:SP 的设置决定。

实验5 编写、调试具有多个段的程序

  • 编写包含数据段、栈段、代码段的程序并调试。

7. 更灵活的定位内存地址的方法

7.1 and和or指令

  • and:按位与(两 1 才 1),常用于把指定位清零;
  • or:按位或(有 1 就 1),常用于把指定位置 1。
mov al, 01100011B
and al, 11011111B ; 第 5 位清零,其余不变 → 01000011B
or al, 00100000B ; 第 5 位置 1,其余不变

7.2 关于ASCII码

字符ASCII(十六进制)
'0'~'9'30H~39H
'A'~'Z'41H~5AH
'a'~'z'61H~7AH
  • 同一字母大小写 ASCII 码相差 20H,区别只在第 5 位(从 0 数)。

7.3 以字符形式给出的数据

  • 源程序中字符常量用引号:db 'unIX' 等价于 db 75H,6EH,49H,58H。

7.4 大小写转换的问题

  • 大写 → 小写:第 5 位置 1(or xx, 00100000B);
  • 小写 → 大写:第 5 位清零(and xx, 11011111B)。

应用:将 data 段中字符串 ‘BaSiC’ 转为大写、‘MinIX’ 转为小写:

assume cs:code, ds:data
data segment
db 'BaSiC'
db 'MinIX'
data ends
code segment
start: mov ax, data
mov ds, ax
mov bx, 0
mov cx, 5
s: mov al, [bx] ; 第一个字符串 (偏移 0~4)
and al, 11011111B ; 转大写
mov [bx], al
mov al, [bx+5] ; 第二个字符串 (偏移 5~9)
or al, 00100000B ; 转小写
mov [bx+5], al
inc bx
loop s
mov ax, 4c00H
int 21H
code ends
end start

7.5 [bx+idata]

mov ax, [bx+200] ; 偏移地址 = (bx)+200,等价写法:
mov ax, 200[bx]
mov ax, [bx].200

7.6 用[bx+idata]的方式进行数组的处理

  • 两个数组分别从 [bx] 和 [bx+idata] 开始,同一 bx 同步定位(如上面 7.4 的例子)。

7.7 SI和DI

  • SI(源变址)、DI(目的变址):功能与 bx 相近,但不能拆成两个 8 位寄存器。
mov si, 0
mov ax, [si]
mov di, 0
mov ax, [di+10]
  • 经典例:把字符串 ‘welcome to masm!’(16 字节)复制到它后面的数据区——si=0、di=16,字传送 8 次(mov ax,[si] / mov [di],ax / add si,2 / add di,2);用 [bx+idata] 思想可再简化为 mov ax,0[si] / mov 16[si],ax,只用一个变址寄存器。

7.8 [bx+si]和[bx+di]

  • 用两个变址/基址寄存器表示偏移:mov ax, [bx+si] = mov ax, [bx][si]。

7.9 [bx+si+idata]和[bx+di+idata]

  • 偏移 = 基址 + 变址 + 常数:mov ax, [bx+si+idata]。

7.10 不同的寻址方式的灵活应用

  • 8086 中能用在 [...] 里的寄存器只有 bx、bp、si、di(且 bx/bp 不同时出现,si/di 不同时出现):
形式名称例子
[idata]直接寻址[0]
[bx]寄存器间接寻址[bx]
[bx+idata]寄存器相对寻址[bx+8]
[bx+si]基址变址寻址[bx+si]
[bx+si+idata]相对基址变址寻址[bx+si+8]
  • 正确组合:bx/si、bx/di、bp/si、bp/di;错误组合:[bx+bp]、[si+di]。
  • 默认段规则:寻址中出现 bp 时默认段寄存器为 SS;其余默认 DS。可用段前缀改变。
  • 二重循环与 cx 冲突:loop 只认 cx,内外两层循环共用 cx 会互相覆盖。演进方案:①用另一个寄存器(如 dx)暂存外层 cx;②寄存器不够用时暂存到内存单元;③通用方案——用栈:内层循环前 push cx,外层 loop s0 前 pop cx。“需要暂存数据时用栈”由此而来。
  • 结构化数据定位:把 4 行 × 16 列的数据看做二维数组,用 [bx+si] 定位(bx 定位行、si 定位列),用 [bx+3+si] 定位行内偏移 3 起的字段——CPU 提供多种寻址方式正是为了从结构化的角度看待数据。

实验6 实践课程中的程序

  • 综合练习本章寻址方式:完成问题 7.9(将每个单词的前 4 个字母改为大写,[bx+3+si] 寻址)等课程程序。

8. 数据处理的两个基本问题

约定符号:reg 表示寄存器(ax、bx、cx、dx、ah/al/bh/bl/ch/cl/dh/dl、sp、bp、si、di);sreg 表示段寄存器(ds、ss、cs、es)。

8.1 bx、si、di和bp

  • 8086 中可用于 [...] 的寄存器:bx、bp、si、di;bx/bp 是”基址”,si/di 是”变址”。
  • bp 默认与 SS 搭配,其余默认与 DS 搭配。

8.2 机器指令处理的数据在什么地方

机器指令处理的数据可在三个地方:①指令内部(立即数)②寄存器 ③内存(段地址 SA:偏移地址 EA)。

8.3 汇编语言中数据位置的表达

位置表达例子
立即数 idata直接写在指令中mov ax, 1
寄存器寄存器名mov ax, bx
段地址+偏移SA:EA mov ax, [0]
  • 立即数只能作为源操作数,不能作为目的。

8.4 寻址方式

寻址方式偏移地址 EA常用例子典型用途
直接寻址idata[idata]直接定位一个单元
间接寻址BX/BP/SI/DI[bx]用变量间接定位
相对寻址BX/BP/SI/DI + idata[bx+idata]结构体 [bx].idata;数组 idata[si]
基址变址BX/BP + SI/DI[bx+si]二维数组 [bx][si]
相对基址变址BX/BP + SI/DI + idata[bx+si+idata]结构体中的数组项 [bx].idata[si]

8.5 指令要处理的数据有多长

8086 可以处理 byte(8 位)和 word(16 位)两种尺寸,由以下方式指明:

  1. 寄存器名指明:mov ax, [0] 是字操作,mov al, [0] 是字节操作。
  2. 操作符 word ptr / byte ptr 指明:
mov word ptr [bx], 0 ; 向 [bx]、[bx+1] 组成的字单元送 0
mov byte ptr [bx], 0 ; 只向 [bx] 字节单元送 0
  1. 其他方式:如 push/pop 默认只进行字操作。

8.6 寻址方式的综合应用

  • 经典例(DEC 公司记录,1982→1988):记录存于 seg:60 处——公司名 +0(3 字节)、总裁名 +3(9 字节)、排名 +0C(字)、收入 +0E(字)、产品 +10(3 字节)。修改排名/收入/产品:bx 定位整条记录(mov bx,60h),idata 定位字段(mov word ptr [bx+0ch],38、add word ptr [bx+0eh],70),si 定位数组项中的字符(mov byte ptr [bx+10h+si],'V')——与 C 语言 dec.pm=38; dec.sr+=70; dec.cp[i]='V' 一一对应。
  • 总结:bx 定位结构体,idata 定位结构体中的数据项,si 定位数组项中的元素。

8.7 div指令

  • 除数:8 位或 16 位,在寄存器或内存中(不能是立即数)。
  • 被除数默认放在 AX(8 位除法)或 DX 和 AX(16 位除法,DX 存高 16 位)。
除数宽度被除数商余数
8 位AXALAH
16 位DX(高):AX (低)AXDX
; 8 位除法:1001 / 100
mov ax, 1001
mov bl, 100
div bl ; AL = 商(10 = 0AH), AH = 余(1)
; 16 位除法:100001 / 100
mov dx, 1
mov ax, 86A1H ; DX:AX = 186A1H = 100001
mov bx, 100
div bx ; AX = 商(03E8H = 1000), DX = 余(1)

⚠️ 除法溢出:若商超出寄存器容量(8 位除法商 > 255,16 位除法商 > 65535),CPU 产生除法溢出,触发 0 号中断(详见第 12 章)。编程时要保证商放得下。

8.8 伪指令dd

  • db 定义字节、dw 定义字、dd 定义双字(dword,4 字节):
dd 12345678H ; 占 4 字节

8.9 dup

  • dup 用来重复定义数据:
db 3 dup (0) ; 等价 db 0,0,0
db 3 dup (0,1,2) ; 等价 db 0,1,2,0,1,2,0,1,2
db 200 dup (?) ; 预留 200 个字节的空间(不确定值)

实验7 寻址方式在结构化数据访问中的应用

  • 任务:把 Power idea 公司 1975~1995 共 21 年的数据(data 段:21 个年份字符串 db、21 个总收入 dd、21 个雇员数 dw)写入 table 段(db 21 dup ('year summ ne ?? '),每行 16 字节:年份 4 + 空格 + 收入 4 + 空格 + 雇员 2 + 空格 + 人均收入 2 + 空格),并计算人均收入(取整)。
  • 思路:data 段看作 3 个数组,table 段看作结构体数组——bx 定位每条记录(每行 +10H)、si 定位数组元素,用 [bx].idata、[bx].idata[si] 访问;人均收入 = 收入(dd) ÷ 雇员(dw),收入最大 5937000 > 65535,要用 dx:ax 联合存放被除数再 div。

9. 转移指令的原理

9.1 操作符offset

  • offset 标号 取得标号的偏移地址(由编译器处理):
start: mov ax, offset start ; ax = start 处的偏移地址

9.2 jmp指令

  • jmp 是无条件转移指令:可以只修改 IP,也可以同时修改 CS 和 IP。

9.3 依据位移进行转移的jmp指令

jmp short 标号 ; 段内短转移:IP = IP + 8 位位移;范围 −128 ~ 127
jmp near ptr 标号 ; 段内近转移:IP = IP + 16 位位移;范围 −32768 ~ 32767
  • 机器码中不含转移目的地址,而含相对当前 IP 的位移:位移 = 标号处偏移 − jmp 指令后第一条指令的偏移,便于程序在内存中浮动装配。

9.4 转移的目的地址在指令中的jmp指令

  • 段间转移(远转移,far):机器码中直接含目的段地址和偏移地址,格式为 EA + 偏移地址(低字)+ 段地址(高字),共 5 字节。
jmp far ptr 标号 ; CS = 标号所在段地址,IP = 标号偏移地址

9.5 转移地址在寄存器中的jmp指令

jmp ax ; IP = ax(不改变 CS)

9.6 转移地址在内存中的jmp指令

jmp word ptr [bx] ; 段内:从内存字单元取偏移送 IP
jmp dword ptr [bx] ; 段间:低字 → IP,高字 → CS

9.7 jcxz指令

  • jcxz = jump if cx is zero:cx 为 0 时转移,否则向下执行(段内短转移,位移 −128~127)。
jcxz 标号 ; 等价于:if ((cx)==0) jmp short 标号

9.8 loop指令

  • loop 标号:cx 减 1,cx≠0 则转移;也是段内短转移,机器码中存 8 位位移。

9.9 根据位移进行转移的意义

  • 转移指令中记录的是”相对位移”而非绝对地址,程序被装载到内存任意位置都能正确执行。

9.10 编译器对转移位移超界的检测

  • 编译器会对转移位移超界进行检测,超界直接编译错误(如 jmp short 位移超出 −128~127)。
  • ⚠️ 形如 jmp 2000:0100 的”段地址:偏移地址 ”写法只能在 Debug 中使用,masm 编译器不认识,源程序中使用会报错。

实验8 分析一个奇怪的程序

  • 跟踪一个”奇怪”程序,深入理解位移转移的机器码含义。

实验9 根据材料编程

  • 任务:在屏幕中间分别显示绿色、绿底红色、白底蓝色的字符串 ‘welcome to masm!’。
  • 80×25 彩色字符模式显示缓冲区:内存 B8000HBFFFFH 共 32KB;分 8 页、每页 4KB,通常显示第 0 页(B8000HB8F9FH,4000 字节 = 25 行 × 160 字节)。每行 80 字符、每字符占 2 字节:偶地址存 ASCII 码、奇地址存属性;第 n 行第 m 列的字符偏移 = n×160 + m×2。
  • 属性字节格式:7 6 5 4 3 2 1 0 = 闪烁 BL、背景 RGB、高亮、前景 RGB。例:黑底绿字 02H、红底绿字 42H、白底蓝字 71H、红底高亮闪烁绿字 CAH。闪烁效果必须在全屏 DOS 方式下才能看到。

10. CALL和RET指令

10.1 ret和retf

指令等价操作用途
retpop IP段内近返回
retfpop IP;pop CS段间远返回

10.2 call指令

  • CPU 执行 call 时先把返回地址(当前 IP 或 CS:IP )压栈,再转移。
  • call 不能实现短转移(没有 8 位位移的格式);除此之外,实现转移的方法和 jmp 相同。

10.3 依据位移进行转移的call指令

  • call 标号(近):push IP;jmp near ptr 标号(16 位位移)。

10.4 转移的目的地址在指令中的call指令

  • call far ptr 标号(远):push CS;push IP;jmp far ptr 标号。
  • 段间 call 先压 CS 再压 IP,正好对应 retf 先弹 IP 再弹 CS。

10.5 转移地址在寄存器中的call指令

  • call reg:push IP;jmp reg(IP = 16 位寄存器)。

10.6 转移地址在内存中的call指令

  • call word ptr 内存:push IP;jmp word ptr 内存单元。
  • call dword ptr 内存:push CS;push IP;jmp dword ptr 内存单元。

10.7 call和ret的配合使用

; 主程序
mov ax, 0
call s ; 调用子程序 s:push 返回地址后跳转
mov bx, ax ; 返回后继续
mov ax, 4c00H
int 21H
s: add ax, 1 ; 子程序
ret ; 返回:pop IP 回到 call 的下一条指令

10.8 mul指令

  • mul 的操作数只能是寄存器或内存单元(不能是立即数)。
乘数位数被乘数乘数结果
8 位ALr8/m8AX
16 位AXr16/m16DX(高 16 位):AX (低 16 位)
mov al, 64
mov bl, 64
mul bl ; AX = 64×64 = 4096 = 1000H
mov ax, 100
mov bx, 100
mul bx ; DX:AX = 100×100 = 10000 = 2710H(DX=0, AX=2710H)

10.9 模块化程序设计

  • 子程序(过程)= 具有一定功能的程序段,用 call 调用、ret 返回,避免重复代码。

10.10 参数和结果传递的问题

  • 调用子程序前需要把参数传给子程序,子程序返回后需要把结果交给主程序——这就是参数和结果传递问题。

10.11 批量数据的传递

  • 参数多(批量数据)时用**内存单元(数据表)**传递:把参数放到内存区,用 ds:si / ds:bx 传地址。
  • 三种传递方式汇总:
方式适用
寄存器传递参数少
内存单元(数据表)传递参数多(批量数据)
栈传递通用、规范(见附注4)

寄存器传参示例:子程序 cube 计算 ax 的三次方,结果 dx:ax :

cube: mov bx, ax
mul bx ; dx:ax = N × N
mul bx ; dx:ax = N² × N = N³
ret

10.12 寄存器冲突的问题

  • 子程序中使用的寄存器可能与主程序正在使用的冲突(如主程序用 bx 做指针,cube 却改了 bx)。
  • 标准解决框架:子程序开头把用到的寄存器全部压栈,返回前出栈恢复(注意出栈顺序相反):
sub: push ax ; 保存现场
push bx
push cx
; ... 子程序功能,随意使用 ax/bx/cx ...
pop cx ; 恢复现场(逆序)
pop bx
pop ax
ret
  • 经典例 capital:将以 0 结尾的字符串转大写——mov cl,[si] / mov ch,0 / jcxz ok(检测到 0 结束)/ and byte ptr [si],11011111B / inc si / jmp short change。主程序用 cx 控制”处理 4 个字符串”的循环,capital 内部也用 cx,冲突由此产生;改写为开头 push cx、push si,结尾 pop si、pop cx 即可。

实验10 编写子程序

编写 3 个后面课程要反复用到的经典子程序:

子程序功能参数要点
show_str在指定位置、以指定颜色显示 0 结尾的字符串dh=行(024)、dl=列(079)、cl=颜色、ds:si →字符串行列 → 显存偏移 dh×160+dl×2;字符写偶地址、颜色写奇地址;子程序内部屏蔽显存结构,向外提供通用接口
divdw不会溢出的除法(dword ÷ word)ax=低 16 位、dx=高 16 位、cx=除数公式 $X/N = \text{int}(H/N)×65536 + [\text{rem}(H/N)×65536+L]/N$(证明见附注5);返回 dx:ax =商、cx=余数
dtocword 型数据转十进制字符串(0 结尾)ax=数据、ds:si →缓冲区反复除以 10 记下余数,余数 +30H 转为字符;用 jcxz 检测商为 0 结束(程序员无法预知循环次数)

课程设计1

  • 任务:将实验 7 中 Power idea 公司的数据按”年份 收入 雇员数 人均收入”四列的表格显示在屏幕上。
  • 综合运用:实验 7 的寻址方式整理数据 + show_str 显示 + dword 版 dtoc(收入最大 5937000 > 65535,除 10 时须用 divdw 防止除法溢出)。

11. 标志寄存器

标志寄存器(FLAGS)按位起作用,每一位有专门含义。8086 的 16 位中使用了 9 位(另:AF 辅助进位标志在第 4 位,用于 BCD 运算,教材不展开;TF/IF 在第 8/9 位,见第 12、15 章)。

11.1 ZF标志

  • ZF(第 6 位)零标志:运算结果为 0 时 ZF=1,否则 ZF=0。
mov ax, 1
sub ax, 1 ; 结果 0 → ZF=1

11.2 PF标志

  • PF(第 2 位)奇偶标志:结果二进制中 1 的个数为偶数时 PF=1。
mov ax, 1
sub ax, 1 ; 结果 0,0 个 1(偶数)→ PF=1

11.3 SF标志

  • SF(第 7 位)符号标志:结果为负(有符号运算)时 SF=1,即 SF = 结果的最高位。
  • SF 只对有符号数运算有意义:把结果当有符号数时,SF 指示正负。

11.4 CF标志

  • CF(第 0 位)进位标志:无符号运算时最高位向外有进位(加)/向最高位有借位(减)→ CF=1。
mov al, 98H
add al, 98H ; 98H+98H = 130H,8 位放不下 → CF=1,AL=30H

11.5 OF标志

  • OF(第 11 位)溢出标志:有符号运算结果超出机器能表示的范围 → OF=1。
  • 8 位有符号范围 −128127;16 位 −3276832767。

CF 与 OF 的区别:

CF 进位标志OF 溢出标志
视角无符号数运算有符号数运算
为 1最高位向外有进位(加)/向最高位有借位(减)结果超出机器能表示的范围
典型FFH+01H=100H → CF=1,OF=0(有符号看是 −1+1=0,没溢出)97H+88H=11FH → CF=1 且 OF=1(有符号看是 −105−120=−225,超出 −128~127)
  • CF 和 OF 互不相关,各自记录不同视角下的结果。

11.6 adc指令

  • adc ax, bx:ax = ax + bx + CF(加上进位位)。
  • 用途:对超出寄存器宽度的大数分高低位依次运算,低位产生的进位由 CF 带入高位。
; 16 位寄存器实现 32 位加法:1EF000H + 201000H = 3F0000H
mov ax, 001EH
mov bx, 0F000H
add bx, 1000H ; 低 16 位相加:F000H+1000H = 0000H,CF=1
adc ax, 0020H ; 高 16 位相加再加 CF:1EH+20H+1 = 3FH
; 结果 AX(高)=003FH, BX(低)=0000H
  • 任意大数相加:最低位用 add,其后每个 16 位都用 adc 链接进位。经典例 add128(两个 128 位整数相加,ds:si / ds:di 指向两数的 8 个字单元,结果存回第一个数的空间):循环 mov ax,[si] / adc ax,[di] / mov [si],ax;循环开始前先 sub ax,ax 清 CF;循环内用 inc 递增指针(inc 不影响 CF,若换成 add si,2 会破坏进位链)。

11.7 sbb指令

  • sbb ax, bx:ax = ax − bx − CF(减去借位位),用于大数减法的高位借位传播。

11.8 cmp指令

  • cmp ax, bx 做 ax − bx 运算但不保存结果,只影响标志寄存器,供后续条件转移指令判断。

无符号数比较结果判断:

cmp a,b 后关系标志
ZF=1a = b—
CF=1a < b产生借位
CF=0 且 ZF=0a > b—

有符号数比较:看 SF 与 OF 是否相同(a>b ⇔ SF=OF 且 ZF=0;a<b ⇔ SF≠OF)。

11.9 检测比较结果的条件转移指令

条件转移指令都是段内短转移(位移 −128~127),通常紧跟 cmp 使用。

指令含义判断条件等价指令
je相等则转移ZF=1jz
jne不等则转移ZF=0jnz
jb低于则转移(无符号)CF=1jnae/jc
jnb不低于则转移CF=0jae/jnc
ja高于则转移(无符号)CF=0 且 ZF=0jnbe
jna不高于则转移CF=1 或 ZF=1jbe
jl小于则转移(有符号)SF≠OFjnge
jge大于等于则转移SF=OFjnl
jg大于则转移(有符号)SF=OF 且 ZF=0jnle
jle小于等于则转移SF≠OF 或 ZF=1jng

记忆:equal 相等;below/above 无符号高低;less/greater 有符号大小;n = not。另有 js/jns(按 SF)、jp/jnp(按 PF)。

cmp ax, bx
je equal ; ax==bx 才跳
; ...
equal:

11.10 DF标志和串传送指令

  • DF 决定串操作后变址寄存器的增减方向:
    • cld:DF=0,递增;std:DF=1,递减。
指令操作
movsb以字节传送:es:[di] = ds:[si];si、di 各 ±1
movsw以字传送:es:[di] = ds:[si];si、di 各 ±2
rep movsb根据 cx 重复执行 movsb(cx 次)
mov ax, data
mov ds, ax
mov ax, dest
mov es, ax ; 目的段用 ES
mov si, 0
mov di, 0
mov cx, 8
cld ; 递增方向
rep movsb ; 连续传送 8 个字节
  • 逆向传送:如把 F000H 段最后 16 个字符复制到 data 段——si=0FFFFH、di=15,std 置 DF=1 后 rep movsb,si/di 每次递减。

11.11 pushf和popf

  • pushf:将标志寄存器压栈;popf:从栈中弹出送入标志寄存器。
  • 配合实现标志寄存器的暂存与设置。

11.12 标志寄存器在Debug中的表示

标志=1=0
OFOVNV
DFDNUP
IFEIDI
SFNGPL
ZFZRNZ
PFPEPO
CFCYNC

实验11 编写子程序

  • 综合练习子程序与标志位判断。

12. 内中断

12.1 内中断的产生

CPU 内部发生以下情况时产生内中断:

类型码来源
0除法错误(div 溢出、除以 0)
1单步执行(TF=1)
4溢出(into 指令检测 OF=1)
n执行 int n 指令

12.2 中断处理程序

  • 中断处理程序(中断例程):CPU 收到中断信息后转去执行的处理程序。

12.3 中断向量表

  • 中断向量表:建立”中断类型码 → 中断处理程序入口地址”的映射。
    • 8086 的中断向量表放在内存 0000:0000 ~ 0000:03FF ,共 1024 字节;
    • 每个表项 4 字节:低字存偏移 IP,高字存段地址 CS;
    • 类型码为 N 的中断,其入口地址存放在 0:N ×4(IP)和 0:N ×4+2(CS)。

例:类型码 9 的中断(键盘),向量位于 0:9 ×4 = 0:24H 起的 4 个字节。

12.4 中断过程

CPU 硬件自动完成(中断过程):

  1. 取得中断类型码 N;
  2. pushf(标志寄存器压栈);
  3. TF = 0、IF = 0(中断处理过程中不响应单步中断、可屏蔽外中断);
  4. push CS、push IP;
  5. (IP) = (N×4),(CS) = (N×4+2) —— 转去执行中断例程。

12.5 中断处理程序和iret指令

中断例程末尾用 iret 返回,等价于:pop IP → pop CS → popf,与中断过程的压栈正好对应。

中断例程标准结构:

doit: push ax ; 保护现场
; ... 处理 ...
pop ax ; 恢复现场
iret ; 中断返回

12.6 除法错误中断的处理

  • 除法溢出时 CPU 产生 0 号中断;默认的 0 号中断处理程序显示 “Divide overflow” 后返回 DOS。

12.7 编程处理0号中断

  • 完整套路:①编写中断例程 do0 → ②安装:把 do0 复制到不会被覆盖的内存(0:200H 起)→ ③设置中断向量表:把 0 号表项改为指向 0:200H 。

12.8 安装

  • 用 rep movsb 把例程机器码从代码段复制到安全内存(0:200H )。
  • 传送长度用编译器算:在例程末尾放一个标号 do0end: nop,写 mov cx, offset do0end-offset do0(编译器可以处理常量表达式)——这样例程内容变化也不必重新数字节数。

12.9 do0

; do0 例程:显示 "overflow!" 后结束程序
do0: jmp short do0start
db "overflow!" ; 字符串放在 0:202H 处(jmp short 占 2 字节)
do0start:
mov ax, cs
mov ds, ax
mov si, 202H ; 指向字符串(除法溢出发生时 CS 必为 0,安装后偏移不变)
mov ax, 0b800H ; 显存段
mov es, ax
mov di, 12*160+36*2 ; 屏幕中间
mov cx, 9 ; 9 个字符
s: mov al, [si]
mov es:[di], al ; 偶地址写字符(不写属性即默认黑底白字)
inc si
add di, 2
loop s
mov ax, 4c00H ; 例程功能是"报错后结束程序",故用 4c00H 返回
int 21H
do0end: nop

注意:①do0 结尾用 mov ax,4c00H; int 21H 结束程序而不是 iret——因为除法溢出说明程序已无法继续,处理完就应退出。若例程处理完后还要返回原程序继续运行,才用 iret。②字符串必须定义在 do0 内部(用 jmp short do0start 跳过它),随例程一起安装到 0:200 ——若放在安装程序的 data 段,安装程序返回后内存被系统释放,例程执行时字符串很可能已被覆盖。

12.10 设置中断向量

; 设置 0 号中断向量:0:0 = 200H,0:2 = 0
mov ax, 0
mov es, ax
mov word ptr es:[0], 200H
mov word ptr es:[2], 0

12.11 单步中断

  • TF=1 时,CPU 每执行一条指令就触发类型 1 的单步中断,Debug 正是利用它实现单步跟踪。
  • 中断过程中自动把 TF 置 0,避免中断例程自身被单步;进入例程前 pushf 保存了原 TF。

12.12 响应中断的特殊情况

  • 有些情况下(如设置 SS 的指令之后紧接着设置 SP),CPU 不会在两条指令之间响应中断,以保证 SS:SP 成对正确设置。

实验12 编写0号中断的处理程序

  • 完整实现:编写 do0 → 安装 → 设置向量 → 用除法溢出触发验证。

13. int指令

13.1 int指令

  • int n 触发类型码为 n 的中断,执行过程等价于:取 n → pushf → TF=0、IF=0 → push CS、push IP → 读 n×4 表项跳转。
  • int 指令使程序员可以主动调用系统提供的中断例程;例程末尾用 iret 返回。

13.2 编写供应用程序调用的中断例程

编写、安装中断例程的过程与 0 号中断相同:编写例程 → 安装到安全内存(0:200H )→ 改写对应中断向量表项。

示例:编写并安装 7ch 中断例程,功能是计算 word 数据的平方(参数 ax,结果 dx:ax ):

; 例程本体
lp: mul ax ; dx:ax = ax × ax
iret
; 安装:复制到 0:200H(略),设置 7ch 向量(7CH×4 = 1F0H)
mov ax, 0
mov es, ax
mov word ptr es:[1F0H], 200H
mov word ptr es:[1F2H], 0
; 调用
mov ax, 3456
int 7ch ; 返回后 dx:ax = 3456 的平方

13.3 对int、iret和栈的深入理解

  • int n 与 call 的异同:都要压返回地址后跳转,但 int 压的是标志寄存器 + CS + IP 共 3 个字(call 压 1~2 个字),且 int 会先清 TF、IF。
  • iret 与 retf 的区别:iret 多弹一次标志寄存器(pop IP → pop CS → popf)。
  • 栈平衡:中断例程入口/出口的压栈出栈必须一一对应,才能保证 iret 弹出正确的内容。
  • 经典问题:用 7ch 中断例程实现 loop 指令的功能。参数:cx=循环次数、bx=转移位移(mov bx,offset s-offset se)。例程核心:push bp / mov bp,sp / dec cx / jcxz lpret / add [bp+2],bx / lpret: pop bp / iret——int 压栈后栈中从顶向下依次是 IP(se 的偏移)、CS、FLAGS,[bp+2] 即 se 的偏移地址,加上 bx 中的位移就变成标号 s 的偏移,iret 返回即”跳”到 s。直接修改栈中的返回地址,是理解 int/iret/栈三者关系的最好练习。

13.4 BIOS和DOS所提供的中断例程

  • BIOS(基本输入输出系统):存放在系统板 ROM 中,主要包含 4 部分内容:①硬件系统的检测和初始化程序;②外部中断和内部中断的中断例程;③用于对硬件设备进行 I/O 操作的中断例程;④其他和硬件系统相关的中断例程。
  • DOS 的中断例程是操作系统向程序员提供的编程资源;和硬件设备相关的 DOS 中断例程中,一般都调用了 BIOS 的中断例程。
  • 应用程序用 int 指令直接调用它们完成硬件相关操作,不必关心硬件细节。

13.5 BIOS和DOS中断例程的安装

  • 开机过程中:CPU 加电后从 FFFF0H 执行 BIOS 初始化程序 → BIOS 安装自己的中断例程(如 int 9、int 10H、int 13H)→ BIOS 引导 DOS 启动 → DOS 安装自己的中断例程(如 int 21H)。

13.6 BIOS中断例程应用

int 10H(屏幕/显示相关),通过 ah 指定功能号:

; 在屏幕第 5 行第 12 列显示一个黑底绿色的字符 '!'
mov ah, 2 ; 2 号功能:置光标位置
mov bh, 0 ; 第 0 页
mov dh, 5 ; 行
mov dl, 12 ; 列
int 10H
mov ah, 9 ; 9 号功能:在光标位置显示字符
mov al, '!'
mov bl, 2 ; 颜色属性(绿色)
mov bh, 0
mov cx, 1 ; 显示次数
int 10H

13.7 DOS中断例程应用

int 21H(程序功能):

ah功能
4CH程序返回(mov ax,4c00H + int 21H)
02H显示一个字符(dl=字符)
09H显示以 ’$’ 结尾的字符串(ds:dx 指向串首)
01H读取一个键盘输入并回显,等待到有键(结果在 al)
07H读取一个键盘输入不回显(结果在 al)
0AH读取一个字符串到缓冲区

0AH 号功能的缓冲区格式(ds:dx 指向缓冲区):

  • 第 1 字节:缓冲区可容纳的最大字符数(事先填好);
  • 第 2 字节:实际读入的字符数(DOS 返回时填写,不含回车);
  • 第 3 字节起:读入的字符串(以回车结束输入)。
; int 21h 9 号功能显示字符串
assume cs:code
data segment
str db 'Hello, Assembly!', '$' ; 必须以 $ 结束
data ends
code segment
start:
mov ax, data
mov ds, ax
mov dx, offset str
mov ah, 9
int 21h
mov ax, 4c00h
int 21h
code ends
end start

实验13 编写、应用中断例程

  • 自编 int 7ch 中断例程并调用。

课程设计2

  • 综合应用:用中断例程实现多功能的综合程序。

14. 端口

14.1 端口的读写

  • 各种接口芯片中的寄存器称为端口,CPU 将它们统一编址、当作端口来访问。至此 CPU 可以直接读写 3 个地方的数据:CPU 内部的寄存器、内存、端口。
  • 访问端口只有两条指令:in(读)/ out(写);**数据只能用 al(8 位端口)或 ax(16 位端口)**存放。
in al, 60H ; 从 60H 端口读一个字节到 al
out 20H, al ; 把 al 写入 20H 端口
  • 端口地址 0~FFH 可直接写在指令中;超过 FFH(范围 0~FFFFH)必须先放入 dx:
mov dx, 3F8H
in al, dx
out dx, al

14.2 CMOS RAM芯片

  • CMOS RAM 包含一个实时钟和 128 字节 RAM,靠电池供电,关机后时钟继续走。
  • CPU 通过两个端口访问:
    • 70H(地址端口):写入要访问的 CMOS 单元地址;
    • 71H(数据端口):读写该单元内容。
mov al, 0
out 70H, al ; 选中 0 号单元(秒)
in al, 71H ; 读出秒

14.3 shl和shr指令

  • shl(逻辑左移):各位左移 X 位,最低位补 0,最后移出的一位进入 CF;移位位数只能是 1 或 cl。
  • shr(逻辑右移):各位右移,最高位补 0,最后移出位进 CF。
mov al, 01001000B
shl al, 1 ; 左移 1 位 = ×2 → 10010000B,CF=0
mov cl, 4
shr al, cl ; 右移 4 位 → 00001001B(常用于取 BCD 高 4 位的十位)
  • 左移一位相当于 ×2,右移一位相当于 ÷2(对无符号数)。

14.4 CMOS RAM中存储的时间信息

  • CMOS 中时间信息以 BCD 码存放(一个字节两位十进制,高 4 位十位、低 4 位个位)。
单元内容单元内容
0秒7日
2分8月
4时9年
  • BCD 值转 ASCII 显示:十位 = 高 4 位(右移 4 位)+30H,个位 = 低 4 位(and 0FH)+30H。

实验14 访问CMOS RAM

  • 读出 CMOS 中的时间并显示。

15. 外中断

15.1 接口芯片和端口

  • CPU 通过接口芯片提供的端口与外设联系;接口芯片向外设/CPU 传递信息并产生中断请求。

15.2 外中断信息

外设输入/输出到达时,相关芯片向 CPU 发出外中断。分两类:

类型特点
可屏蔽中断CPU 是否响应取决于 IF:IF=1 响应、IF=0 忽略;sti 置 IF=1,cli 置 IF=0
不可屏蔽中断必须响应,类型码固定为 2(如紧急硬件故障)

可屏蔽外中断的中断过程与内中断一致(进入中断过程时 IF 会被置 0)。

15.3 PC机键盘的处理过程

  1. 键盘按下/松开产生扫描码,送入主板上接口芯片的 60H 端口(通码为按下产生的扫描码,断码 = 通码 + 80H,松开时产生);
  2. 接口芯片向 CPU 发出类型 9 的可屏蔽中断;
  3. CPU 若 IF=1,执行 int 9 中断例程:
    • 从 60H 端口读入扫描码;字符键的扫描码与对应 ASCII 码写入键盘缓冲区(16 个字单元、可存 15 个输入,高位字节=扫描码、低位字节=ASCII 码);控制键/切换键(Ctrl、Shift、CapsLock 等)转变为状态字节,写入 0040:17 单元(8 个二进制位依次记录右 Shift、左 Shift、Ctrl、Alt、ScrollLock、NumLock、CapsLock、Insert 状态);
    • 对键盘系统进行应答等控制,处理完成后向 20H 端口发送中断结束指令 EOI(值 20H):
mov al, 20H
out 20H, al ; 向外设中断控制器发送 EOI

15.4 编写int 9中断例程

套路(以”显示扫描码”为例):

  1. 保存原 int 9 入口(向量表 0:24H 起 4 字节)到安全内存(如 0:200H );
  2. 编写新例程:先 call dword ptr 调用原例程(它负责把键处理进缓冲区并发送 EOI),再加自定义功能;
  3. 把新例程安装到安全内存(如 0:204H );
  4. 把 0:24H 表项改为新例程入口。

15.5 安装新的int 9中断例程

; 新 int 9 例程(已安装到 0:204H,故 cs=0;原入口保存在 0:200H)
newint9:
push ax
push bx
push es
in al, 60h ; 读扫描码
pushf ; 模拟 int 的中断现场(iret 要弹标志)
call dword ptr cs:[200H] ; 调用原 int 9 例程(它发 EOI)
; ---- 自定义功能:在屏幕中间显示扫描码 ----
mov ax, 0b800H
mov es, ax
mov bx, 160*12+40*2
mov byte ptr es:[bx], 'a'
; ----
pop es
pop bx
pop ax
iret

自写外中断例程若不调用原 int 9 例程(自含处理、不经过原例程结束中断),则结束前必须自己向 20H 端口发 EOI,再 iret 返回;若例程通过 call dword ptr 调用原例程,EOI 由原例程发送,无需重复发送。

  • 改向量表项的竞态:设置新 int 9 入口的两条指令(改偏移、改段地址)之间若恰逢键盘中断,CPU 会跳到”半新半旧”的错误入口——应先 cli 屏蔽可屏蔽中断,两条设置指令执行完再 sti 恢复。
  • 教材 15.5 完整示例:安装新 int 9 到 0:204H (原入口存 0:200H ),按 F1(通码 3BH) 将全屏 2000 个属性字节逐个加 1 改变屏幕颜色,其他键 call dword ptr cs:[200H] 照常处理。

实验15 安装新的int 9中断例程

  • 完整安装新的 int 9 例程并验证(如:按 A 键、松开后满屏显示 ‘A’——判断断码 = 通码 + 80H)。

指令系统总结(教材第 15 章末)

类别代表指令说明
数据传送mov、push、pop、pushf、popf、xchg寄存器/内存/段寄存器间单个数据传送,不影响标志位
算术运算add、sub、adc、sbb、inc、dec、cmp、mul、div影响标志寄存器的 sf、zf、of、cf、pf(/af)
逻辑and、or、not、xor、test、shl、shr除 not 外都影响标志位
转移无条件 jmp;条件 jcxz、je、jb、ja…;循环 loop;过程 call/ret/retf;中断 int/iret修改 IP,或同时修改 CS、IP
处理机控制cld、std、cli、sti、nop 等对标志寄存器或其他处理器状态进行设置
串处理movsb、movsw(配合 rep 等前缀)对内存中的批量数据进行处理

16. 直接定址表

16.1 描述了单元长度的标号

代码段中的标号有两种:

  • 地址标号:仅表示段地址:偏移地址 (如 s:),无长度属性;
  • 数据标号:同时表示地址和单元长度:
a db 1,2,3,4,5,6 ; a 既代表地址(偏移 0),又代表字节单元
b dw 0 ; b 代表一个字单元的地址(偏移 6)
mov al, a[0] ; = mov al, cs:[0],编译器知道按字节取
mov ax, b ; = mov ax, cs:[6],按字取

16.2 在其他段中使用数据标号

  • 用 assume 把段与段寄存器关联后,编译器在访问该段中的数据标号时会自动加上相应的段前缀;若 assume 与实际不符,程序会出错,仍须自己在代码中设置段寄存器。
  • 段前缀也可手动显式给出(如 ds:b)。
  • 标号可以作为数据来定义:c dw a,b 等价于 c dw offset a,offset b(在 c 处存放标号 a、b 的偏移地址);c dd a,b 则存放每个标号的偏移地址 + 段地址。seg 标号 操作符取标号的段地址(offset 取偏移地址)。

16.3 直接定址表

直接定址表:以数据(如编号、字符码)为索引,通过查表直接得到目标数据/地址,避免一长串 cmp 条件分支。

数值查表:如用十六进制数码 0~F 作下标,查一张字符表直接得到对应字符:

table db '0123456789ABCDEF'
; al 中为 0~F 的数值
mov bl, al
mov bh, 0
mov al, table[bx] ; 直接查出对应字符的 ASCII
  • 查表的 3 个目的:①算法清晰简洁(避免”≤9 加 30H、否则加 37H”的分支判断);②用空间换时间加快运算——如 sin(x) 只取 0°~180° 的 7 个特殊角度,把结果字符串存表,用 角度/30 直接查出,免去麦克劳林公式的多次乘除;③程序易于扩充。

16.4 程序入口地址的直接定址表

  • 把一组子程序的入口偏移依次存入表中,按编号 call word ptr table[bx] 直接分派,是”多功能中断例程”的标准写法。

实验16 编写包含多个功能子程序的中断例程

  • 用入口地址表实现多功能中断例程。

17. 使用BIOS进行键盘输入和磁盘读写

17.1 int 9中断例程对键盘输入的处理

int 9 例程把按键转换后放入键盘缓冲区(环形队列,最多存 15 个按键),供后续程序读取;Shift/CapsLock 等状态键改变状态字节。

17.2 使用int 16h中断例程读取键盘缓冲区

BIOS 提供 int 16h 键盘中断:

ah功能
0读取一个键盘输入(从缓冲区取出一个键并移除;缓冲区空则等待)。结果:ah=扫描码,al=ASCII 码
1查询缓冲区(不取出):有键 ZF=0,无键 ZF=1
2读键盘状态字节到 al(Shift/Ctrl/CapsLock 等状态位)
mov ah, 0
int 16H ; 结果:ah = 扫描码,al = ASCII 码

17.3 字符串的输入

实现字符串输入需要处理:字符入栈/缓存、显示回显、退格(删除栈顶并擦除屏幕字符)、回车结束。核心循环(结构参照教材 17.4,charstack 负责栈缓冲与显示):

getstr: push ax
getstrs:
mov ah, 0
int 16h
cmp al, 20h ; ASCII < 20H 的控制键
jb nochar
mov ah, 0
call charstack ; 字符入栈
mov ah, 2
call charstack ; 显示
jmp getstrs
nochar: cmp ah, 0eh ; 退格键的扫描码
je backspace
cmp ah, 1ch ; 回车键的扫描码
je enter
jmp getstrs
backspace:
mov ah, 1
call charstack ; 出栈
mov ah, 2
call charstack ; 显示
jmp getstrs
enter: mov al, 0
mov ah, 0
call charstack ; 0 入栈
mov ah, 2
call charstack ; 显示
pop ax
ret

17.4 应用int 13h中断例程对磁盘进行读写

BIOS int 13h 访问软盘/硬盘,参数约定:

寄存器内容
ah功能号:2 读扇区、3 写扇区
al读写的扇区数
ch磁道号(柱面号)
cl扇区号(从 1 开始)
dh磁头号(面号)
dl驱动器号(软盘 A 为 0,硬盘从 80H 起)
es:bx 数据缓冲区首地址
; 从 0 号软驱 0 面 0 道 1 扇区读 1 个扇区到 es:bx
mov ax, data
mov es, ax
mov bx, 0
mov ah, 2
mov al, 1
mov ch, 0
mov cl, 1
mov dh, 0
mov dl, 0
int 13H
  • 操作成功:CF=0,AH=0,AL=实际读写扇区数;失败:CF=1,AH 为错误码。
  • 磁盘访问三要素:磁道(柱面)、磁头(面)、扇区,扇区是磁盘读写的最小单位;面号、磁道号从 0 开始,扇区号从 1 开始。
  • 3.5 英寸软盘:2 面 × 80 磁道 × 18 扇区 × 512B = 1.44MB。直接写 0 面 0 道 1 扇区(引导扇区)会使软盘在现有操作系统下不可用(需重新格式化);实验务必用空闲软盘、核对驱动器号。

实验17 编写包含多个功能子程序的中断例程

  • 综合键盘输入与磁盘读写的多功能中断例程。

综合研究

教材结尾的 5 个研究试验围绕”C 语言与汇编的接口”展开(在 C 环境下直接使用汇编能力)。

研究试验1 搭建一个精简的C语言开发环境

  • 用汇编实现最小运行支持,搭建一个精简的 C 语言开发环境。

研究试验2 使用寄存器

  • C 语言中直接使用 8086 寄存器(寄存器变量/内联汇编)。

研究试验3 使用内存空间

  • 在 C 语言中直接读写指定的内存地址空间。

研究试验4 不用main函数编程

  • 不写 main 函数、直接指定程序入口的编程方式。

研究试验5 函数如何接收不定数量的参数

  • 结合栈传参机制,理解 C 函数如何接收不定数量的参数(与附注4 呼应)。

附注

附注1 Intel系列微处理器的3种工作模式

模式说明
实模式8086 工作方式,段地址×16+偏移形成 20 位物理地址,直接访问物理内存(本课程全部内容)
保护模式80286 起引入,段寄存器存放段选择子,经段描述符转换地址,支持特权级与分页
虚拟 8086 模式80386 起引入,保护模式下模拟实模式,使多个 8086 程序得以并发运行

附注2 补码

  • 负数 $-X$ 的补码 = $2^N - X$(N 为位数);求补运算:取反加 1。
  • 8 位补码范围 −128127;16 位 −3276832767。CPU 内部的有符号减法统一用补码加法实现。

附注3 汇编编译器(masm.exe)对jmp的相关处理

  • jmp short 标号 / jmp near ptr 标号:masm 计算位移 = 标号处偏移 − jmp 指令后第一条指令的偏移,写入机器码;
  • jmp far ptr 标号:masm 直接把标号的段地址和偏移地址写入机器码;
  • 位移超出 short 范围(−128~127)却写了 jmp short,编译报错;超出 near 范围同理;
  • jmp 标号 不带类型说明时,masm 依据位移范围自动选择 short/near。

附注4 用栈传递参数

高级语言(C)调用函数时常用栈传参:调用者把参数从右向左依次压栈后 call;子程序用 bp 访问参数([bp+4] 为第一个参数,[bp+2] 是返回 IP,[bp+0] 是保存的旧 bp),返回后调用者平衡栈(或用 ret n 由被调用方平衡)。


复习速查

以下内容是本笔记的补充整理,不属于教材目录,供考试前快速过一遍。

寄存器总表

类别寄存器作用
通用AX/BX/CX/DX(各可拆 H/L)数据;BX 基址、CX 计数、DX 配合乘除/端口
变址SI、DI源/目的变址,串操作、寻址
指针SP、BPSP 栈顶;BP 基址指针(默认 SS)
段寄存器CS、DS、SS、ES代码/数据/栈/附加段
控制IP、FLAGS指令指针、标志寄存器

指令对标志位的影响

指令影响标志位?
add/sub/adc/sbb/cmp影响 ZF/PF/SF/CF/OF/AF
inc/dec影响 ZF/PF/SF/OF/AF,不影响 CF(考试陷阱)
mul/div标志位无确定含义,编程时不要依据
mov、push、pop、lea、xchg、not不影响任何标志位
and/or/xor影响 ZF/PF/SF;CF=OF=0
shl/shr最后移出位进 CF;SF/ZF/PF 按结果设置
cld/std只改 DF;sti/cli 只改 IF

考试重点速查

知识点频率题型
物理地址 = 段地址×16+偏移⭐⭐⭐⭐⭐计算
寻址方式判断([bx+si+idata] 等)⭐⭐⭐⭐⭐选择/读程序
push/pop 与 SS:SP 变化⭐⭐⭐⭐⭐画图/填空
loop + [bx] 循环读程序写结果⭐⭐⭐⭐⭐大题
jmp 转移原理(位移 vs 绝对地址)⭐⭐⭐⭐选择/分析
call/ret 子程序执行流程⭐⭐⭐⭐⭐读程序
标志位 ZF/CF/OF/SF 判断⭐⭐⭐⭐⭐填空
cmp + 条件转移(je/jb/ja/jl/jg)⭐⭐⭐⭐⭐读程序
div/mul 的隐含寄存器约定⭐⭐⭐⭐计算
中断向量表(N×4、低 IP 高 CS)⭐⭐⭐⭐⭐填空/编程
中断过程与 iret⭐⭐⭐⭐简答
int 10H / int 21H / int 16H / int 13H⭐⭐⭐⭐编程
端口 in/out、CMOS 70H/71H⭐⭐⭐编程
shl/shr、BCD 码转换⭐⭐⭐编程
直接定址表⭐⭐⭐编程

易混点最后过一遍

  1. 指令和数据在内存中无差别,区别在 CPU 当前如何使用。
  2. 段是逻辑概念,段起始地址必为 16 的倍数,段长上限 64KB。
  3. 小端:字的低字节在低地址;栈从高地址向低地址增长,push 先 SP−2。
  4. mov 不能给段寄存器立即数,要经通用寄存器中转;两个内存单元不能直接传送。
  5. 源程序中 [0] 是立即数,要写 ds:[0];Debug 中 [0] 才是内存。
  6. assume 不负责填段寄存器,DS/SS 要自己 mov,CS:IP 由 end 入口 自动设置。
  7. CF 看无符号进位,OF 看有符号溢出,两者独立;inc/dec 不影响 CF。
  8. 条件转移:无符号用 b/a(below/above),有符号用 l/g(less/greater)。
  9. 中断向量表每项 4 字节:低字 IP、高字 CS,N 号中断在 0:N ×4。
  10. 中断过程硬件自动:pushf → TF=0、IF=0 → push CS、push IP → 跳转;iret 正好反向。
  11. div 商超出范围 → 除法溢出,触发 0 号中断;mul/div 的操作数不能是立即数。
  12. 8086 移位位数只能是 1 或 cl;断码 = 通码 + 80H。
  13. EOI 由(原)int 9 例程发送;自写例程不调用原例程时才需要自己发。
  14. int 16H ah=0 读键后缓冲区中该键被移除;int 21H 01H 等待并回显、07H 不回显。

💡 全书主线:寄存器 → 内存分段寻址 → 栈 → 程序结构 → 灵活寻址 → 转移与子程序 → 标志位 → 中断体系(内中断 → int → 端口 → 外中断)。抓住”CPU 如何用 CS:IP 取指、用 DS/bx 寻址、用 SS:SP 管理栈、用中断向量表找例程”这四条线,所有章节都能串起来。