第一章

机器语言与汇编语言的基础

冯·诺依曼体系:该体系的核心思想是将程序当作数据存储起来,然后由计算机自动运行。

机器语言:它是机器指令的集合,而机器指令本质上就是一台机器可以正确执行的二进制码或电平脉冲命令。

汇编语言的产生:因为机器指令难以记忆,所以产生了汇编语言。汇编指令是机器指令的助记符,书写格式更接近人类语言,便于阅读和记忆。程序员编写的汇编指令需要通过“编译器”转换成机器码才能被计算机执行。

汇编语言的组成:它的核心是决定了语言特性的汇编指令,此外还包含由编译器执行的伪指令,以及供编译器识别的其他符号。

指令、数据与存储器(内存)

指令与数据的统一性:指令和数据仅仅是应用上的概念区分。在内存或磁盘中,它们没有任何物理区别,全都是二进制信息。

存储器的重要性:CPU要工作,就必须从存储器(内存)中获取指令和数据。磁盘中的程序或数据如果不读入内存,CPU是无法直接使用的。

存储单元与容量:存储器被划分为若干个从0开始顺序编号的存储单元。一个存储单元可以存储8个bit(即8位二进制数,记作1B)。微机中常用的容量计量单位换算为:1KB=1024B,1MB=1024KB,1GB=1024MB,1TB=1024GB。

总线系统 (Bus System)

每一个CPU芯片都有许多管脚,这些管脚和总线相连。

CPU要进行数据读写,必须通过“总线”(一根根导线的集合)与外部器件进行地址、控制和数据信息的交互。

地址总线:CPU通过它来指定需要访问的存储单元。地址总线的宽度(根数N)直接决定了CPU的寻址能力,即最多可以寻找 二的N次方 个内存单元。

数据总线:负责CPU与外部器件之间的数据传送。数据总线的宽度决定了CPU与外界的数据传送速度(即一次传送的数据量)。

控制总线:是各种不同控制线的集合(例如包含读信号和写信号输出控制线),负责向外部器件发送命令。控制总线的宽度决定了CPU对外部器件的控制能力。

内存地址空间

逻辑存储器概念:系统中的物理存储器(如主板RAM、接口卡上的RAM、装有BIOS的ROM等)在物理上是独立的,但它们都和CPU总线相连,受CPU控制。对CPU而言,所有的物理存储器共同构成了一个统一的逻辑存储器,这就是“内存地址空间”。

地址段分配:在这个逻辑空间中,每个物理存储器都占有一段特定的地址空间。CPU向某段地址空间读写数据,实际上就是对映射到该地址的物理存储器进行操作。

8086PC机示例:以8086PC机为例,其内存地址空间被分配给了主存储器地址空间(RAM)、显存地址空间以及各类ROM地址空间等不同部分。

第二章

CPU基础结构与寄存器

CPU组成:典型的CPU由运算器、控制器、寄存器等组成,通过内部总线相连以实现内部通信;外部总线则用于CPU与主板其他器件的联系。

16位结构的特征:8086是一台16位结构的CPU,这意味着它的运算器一次最多处理16位的数据,寄存器最大宽度为16位,且寄存器与运算器之间的通路也是16位的。

8086的寄存器:8086 CPU共有14个寄存器,分别是:AX、BX、CX、DX、SI、DI、SP、BP、IP、CS、SS、DS、ES、PSW。

物理地址与分段机制

物理地址:所有内存单元构成一个一维的线性空间,每个单元在这个空间中唯一的地址被称为物理地址。

地址的生成方式:8086 CPU拥有20位地址总线(寻址能力为1MB),但内部是16位结构。为了寻址,它采用内部地址加法器,用两个16位地址合成一个20位的物理地址。

计算公式:物理地址 = 段地址 ✖16 +偏移地址。其中,“段地址×16”在底层实现上,相当于将以二进制形式存放的段地址左移4位。

段的概念:内存本身并没有分段,段的划分是人为的,来自于CPU用“段地址×16+偏移地址”管理内存的方式。在编程时,可以将若干地址连续的内存单元看作一个段。提供段地址的部件是段寄存器,8086有4个段寄存器:CS、DS、SS、ES。

最关键的寄存器:CS 和 IP

功能定义:CS为代码段寄存器,IP为指令指针寄存器(偏移地址)。

指令的判定:内存中指令和数据本质都是二进制信息,CPU之所以能区分,是因为在任何时候,CPU只将 CS:IP 指向的内存单元中的内容当作指令来执行

CPU的工作过程

从CS:IP指向的内存单元读取指令,进入指令缓冲器。

IP自动增加,更新为 IP = IP + 所读取指令的长度,从而指向下一条指令。

执行指令,随后重复步骤1。

系统启动状态:8086PC机刚开机启动或复位时,CS被设置为FFFFH,IP被设置为0000H,这是开机后执行的第一条指令地址。

基础汇编指令与控制

基本操作:汇编指令不区分大小写。常用指令如 mov ax, bx(将BX数据送入AX)和 add ax, 8(AX的数值加8)。

修改CS与IP:程序员不能用 mov 指令修改CS和IP,8086不提供此功能。必须使用转移指令(jmp)来修改:

同时修改CS和IP:使用 jmp 段地址:偏移地址(如 jmp 2AE3:3)。

仅修改IP:使用 jmp 某一合法寄存器(如 jmp ax,功能类似于把AX的值赋给IP)。

代码段的执行:即使在内存中安排了一组代码作为一个“代码段”(长度小于等于64KB),CPU也不会自动将其作为指令执行。必须通过修改CS和IP的值,将其指向该代码段的第一条指令的首地址,代码才能被执行。

十六进制(Hexadecimal)快速复习

在日常生活中我们用十进制(逢 10 进 1,数字是 0-9)。 计算机为了方便把二进制压缩显示,通常用十六进制(逢 16 进 1)。

因为 0-9 只有十个符号不够用,所以借用了英文字母:

  • 0~9 还是原来的意思。
  • A = 10
  • B = 11
  • C = 12
  • D = 13
  • E = 14
  • F = 15

image-20260506175756099

1
AX = 001AH

拆开来看:高8位 AH = 00H,低8位 AL = 1AH

现在我们把提取出的数值进行相加:1AH + 26H

我们需要按照十六进制(逢16进1)的规则来算:

  • 先算低位(个位): A 代表十进制的 10。 10 + 6 = 16。在十六进制里,满 16 就要向前进 1,当前位留下 0。所以,低位是 0,并产生一个进位 1
  • 再算高位(十位): 原本是 1 + 2 = 3,再加上刚才个位进上来的 1,变成 4

组合起来,运算结果就是:40H

揭秘最后一步 (add al, 93H) 现在我们要执行最后一条指令:将 al 的值加上 93H

  • 当前 AL = C5H
  • 进行十六进制加法:C5H + 93H
    • 个位相加:5 + 3 = 8
    • 十位相加:C (对应十进制的12) + 9 = 21。在十六进制中,21 = 16 + 5,所以留下 5,向前进 1
    • 加法完整结果:158H

计算结果是 158H,需要三个十六进制位(也就是 12 个二进制位)才能存下。 但是,我们的目标寄存器是 al,它是一个 8位寄存器,最多只能容纳两位十六进制数(最大是 FFH)。 所以,最高位的这个进位 1 会发生溢出。它不会跑到 ah 里去,而是直接从 al 中掉出去了(实际上是去改变了 CPU 的进位标志位 CF )。

  • al 最终保留的是低两位:58H
  • ah 没有参与运算,保持原来的 00H 不变。

拼合起来,最终 AX 的值就是 0058H

第三章

内存中字的存储

存储规则:8086CPU 在存储字型数据(16位)时,需要用两个地址连续的内存单元来存放 。

高低对应:字的低位字节存放在低地址单元中,高位字节存放在高地址单元中 。在内存和寄存器之间传送字型数据时,高地址单元对应高8位寄存器,低地址单元对应低8位寄存器 。

DS寄存器与数据访问 ([address])

DS(数据段寄存器):通常用来存放要访问的数据的段地址 。

寻址方式:使用 [address] 表示一个内存单元,括号内的数值代表偏移地址 。当执行指令(如 mov al, [0])时,CPU会自动默认取 DS 寄存器中的数据作为该内存单元的段地址 。

eg:

1
2
3
4
5
mov bx,1000H
mov ds,bx
mov al,[0] ; 访问 1000:0 内存单元


mov ds,1000H 是非法的

需要先传给通用寄存器,再传给段寄存器

字的传送

  • 8086 CPU是16位结构,一次可传送16位(word)。
  • mov 指令可以:
    • 寄存器 ↔ 数据
    • 寄存器 ↔ 寄存器
    • 寄存器 ↔ 内存
    • 内存 ↔ 寄存器
  • 高位字节对应高地址或寄存器高8位,低位字节对应低地址或寄存器低8位。

基本指令:mov、add、sub

  • mov:
    • mov 寄存器, 数据
    • mov 寄存器, 寄存器
    • mov 寄存器, 内存
    • mov 内存, 寄存器
    • mov 段寄存器, 寄存器
  • add/sub:
    • 同样是双操作数指令,可对寄存器和内存操作。
  • 注意:
    • 段寄存器操作需要通过通用寄存器间接传送。
类型 示例 特点
数据传送(立即数) mov ax, 1234H 直接把固定值放入寄存器,不访问内存,最快
寄存器传送 mov bx, ax 寄存器之间直接移动,不访问内存,速度最快
内存读取到寄存器 mov ax, [1000H] CPU读取内存单元的数据放入寄存器,速度慢一些
寄存器写入内存 mov [1000H], ax CPU把寄存器数据写入内存,速度慢,可能跨字节(16位)

eg:

1
mov ax, [1000H]
  • 含义:把内存地址 1000H 处的 一个字(16位) 读到寄存器 AX。
  • 假设内存:
地址 数据
1000H 34H
1001H 12H
  • 执行步骤:
    1. CPU读取 DS:1000H → 内存低字节 34H
    2. CPU读取 DS:1001H → 内存高字节 12H
    3. AX = 1234H(AL=34H,AH=12H)

eg:

1
mov [1000H], ax
  • 含义:把 AX 寄存器的值写入内存地址 1000H
  • AX = 1234H
  • 执行步骤:
    1. CPU把 AL=34H 写入 DS:1000H
    2. CPU把 AH=12H 写入 DS:1001H
  • 内存示意
地址 数据
1000H 34H
1001H 12H

image-20260507192924569

数据段

  • 内存段可以定义为数据段:
    • 连续内存,长度 ≤ 64KB
    • 起始地址为16的倍数
    • 用DS寄存器指向段地址
  • 数据存放规则:
    • 字:低字节存低地址,高字节存高地址
    • mov指令访问时,可只提供偏移地址(段地址默认在DS)

栈的机制与 SS、SP 寄存器

栈的特性:栈是一种具有特殊访问方式的存储空间,遵循 LIFO(Last In First Out,后进先出)规则 。

工作机制:8086CPU 提供了栈操作机制,方案是在 SS 寄存器中存放栈顶的段地址,在 SP 寄存器中存放栈顶的偏移地址 。任意时刻,SS:SP 始终指向当前的栈顶元素 。

入栈 (push) 与 出栈 (pop)

push 执行步骤:首先 SP = SP - 2,然后将数据送入 SS:SP 指向的新字单元中 。

pop 执行步骤:首先从 SS:SP 指向的字单元中读取数据,然后 SP = SP + 2

注意:出栈后,原栈顶元素的数据依然残留在内存中,但它已不在栈内,直到下次执行 push 指令时才会被新数据覆盖 。

image-20260507203028021

image-20260507203055973

栈顶超界与栈的容量限制

超界危险(栈满的时候再使用push或者栈空的时候再使用pop:8086 CPU 内部没有记录栈上下限的寄存器,它只知道栈顶在哪里(由 SS:SP 指示),而不知道程序员安排的栈空间有多大 。因此,CPU 不会自动保证不发生栈顶超界。程序员必须自己管理栈的大小,防止过度入栈或出栈导致覆盖其他重要数据或代码 。

最大容量:因为 pushpop 等栈操作指令只修改 SP 的值 ,而 SP 是 16 位寄存器,变化范围是 0 ~ FFFFH 。因此,一个栈段的最大容量限制为 64KB 。

栈段

  • 栈段定义:
    • 一段连续内存,用作栈
    • 长度 ≤ 64KB,起始地址为16的倍数
  • CPU执行栈操作指令时,必须通过 SS:SP 指向栈段
  • 栈段最大容量:
    • 64KB(SP范围0~FFFFH)

一段内存可同时是:

  • 代码段(CS:IP指向)
  • 数据段(DS指向)
  • 栈段(SS:SP指向)

核心概念:

  • CPU的行为完全取决于寄存器设置(CS, IP, DS, SS, SP)

第四章

汇编程序的基本流程

完整流程:编写源程序 → 编译 → 连接 → 执行。

程序组成

  • 汇编指令:可被编译为机器码的指令。
  • 伪指令:如 segment, ends, assume, end,供编译器使用,不生成机器码。

程序段

  • 每个段有标号(段名)如 codesg,用于标识内存位置。
  • 段通过 assume 与段寄存器关联,例如 assume cs:codesg

程序返回:一个程序运行结束后,必须将CPU的控制权交还给使它得以运行的程序,这个过程称为程序返回 。在源程序末尾,通常通过添加两条汇编指令 mov ax, 4c00Hint 21H 来实现程序返回功能 。

编译与连接

编译:将汇编指令和伪指令处理为机器码,生成目标文件(.obj)。

错误类型:在开发过程中会遇到两类错误。一类是语法错误,在程序编译时由编译器发现,相对容易排查 。另一类是逻辑错误,编译时无法表现出来,只在运行时发生,较难发现 。

连接的作用

当源程序很大时,可以将多个编译好的目标文件连接到一起,生成一个可执行文件 (.exe)。

如果程序调用了库文件中的子程序,连接器负责将库文件与目标文件连接 。

即使只有一个源程序文件且不调用外部库,也必须使用连接程序将目标文件处理为最终的可执行信息,从而生成可执行文件 。

程序的加载与执行(DOS环境)

在DOS系统中,程序的运行必须由一个正在运行的程序(如 command)将其从可执行文件中加载入内存,并将控制权交给它 。

内存分配:系统会寻找一段起始地址的偏移地址为0的空闲内存区(例如段地址为SA) 。

PSP(程序段前缀):在该内存区的前256个字节中,系统会创建一个称为程序前缀(PSP)的数据区,DOS通过它与被加载程序进行通信 。

物理地址的计算

  • 程序装入在PSP之后,即从这段内存的256字节处开始装入 。
  • PSP的地址安排为 SA:0,由于PSP占256(100H)个字节,程序所在的物理地址计算公式为:SA✖16+0+256=(SA+16)✖16+0。
  • 因此,程序的入口地址被设定为 SA+10H : 0 。

执行控制:加载完成后,段寄存器 DS 会存放内存区的段地址,系统设置 CS:IP 指向程序的入口,从而使程序得以运行 。

程序的跟踪与调试

为了观察程序的运行过程,可以使用 Debug 工具将程序加载入内存并进行单步跟踪 。

常用 Debug 命令

  • 使用 R命令 查看各个寄存器的设置情况 。

  • 使用 U命令 查看程序中指令的执行内容 。

  • 使用 T命令 单步执行程序中的每一条指令,并观察结果 。

  • 当遇到 int 21H 时,必须使用 P命令 来执行,执行后会显示 “Program terminated normally” 表示程序正常结束 。

  • 使用 Q命令 可以退出 Debug 。

加载与返回顺序:在跟踪调试时,加载顺序是:command 加载 Debug,然后 Debug 加载程序文件(如 1.exe) 。程序运行结束后的返回顺序则是:从 1.exe 返回到 Debug,再由 Debug 返回到 command 。

在代码段中使用数据

在代码段中,可以通过 dw(define word)伪指令来定义字型数据 。

如果数据定义在代码段的最开始,它们的偏移地址将从0开始算起(例如 CS:0, CS:2 等) 。

将包含数据的程序编译连接成可执行文件后,直接运行可能会出现问题,因为程序的入口处可能并不是期望执行的指令(CPU可能会将数据当作指令执行) 。

为了解决入口问题,可以在源程序中使用标号(如 start)配合 end start 来明确指明程序的真正入口所在 。

伪指令 end 的作用除了通知编译器程序结束外,还可以指明程序的执行入口 。

eg:

1
2
3
4
5
6
7
8
9
10
11
12
13
assume cs:code
code segment
:
数据
:
start:
:
:
代码
:
:
code ends
end start

编程计算以下8个数据的和,结果存在ax 寄存器中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
assume cs:codesg        ; 告诉编译器,将代码段寄存器 CS 与名为 codesg 的段关联起来

codesg segment ; 定义一个名为 codesg 的代码段开始


; 在代码段的起始位置定义 8 个字型(16位)数据。
; 因为是代码段的最开始,所以它们的偏移地址依次是 0, 2, 4, 6...
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h

start: ; 程序的真正执行入口标号
mov bx,0 ; 初始化 bx 寄存器为 0。这里 bx 被用作数据的偏移地址指针
mov ax,0 ; 初始化 ax 寄存器为 0。ax 被用作累加器,保存最后的求和结果
mov cx,8 ; 设置 cx 寄存器为 8。cx 是循环计数器,代表要循环加 8 次

s: ; 循环体开始的标号 's'
add ax,cs:[bx] ; 核心指令:取出段地址为 cs、偏移地址为 bx 的内存单元中的数据,加到 ax 中
add bx,2 ; 将 bx 加 2,使指针向后移动到下一个字型数据(一个字占2个字节)
loop s ; 循环指令:自动将 cx 减 1,如果 cx 不为 0,则跳转回标号 's' 继续执行

; 以下两条指令是 DOS 系统下标准的程序退出方式
mov ax,4c00h ; 将 4c00h 放入 ax 中,表示程序正常结束的功能号
int 21h ; 调用 21h 号中断,安全退出程序并将控制权交还给操作系统

codesg ends ; 标志 codesg 段结束

end start ; 通知编译器源程序结束,并指明程序的执行入口是标号 start

在代码段中使用栈

  • 栈需要内存空间,程序可以通过在内部定义一段数据来取得内存空间,然后将这段空间当作栈来使用 。

  • 利用栈先进后出的特性,可以实现特定的功能,例如将数据依次入栈再依次出栈,从而实现数据的逆序存放 。

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

  • 将数据、代码和栈全部放在同一个段中会使程序显得混乱,且代码段的可用空间是有限的 。

  • 源程序中的 assume 是一条伪指令,它仅用于通知编译器,CPU 并不知晓它的存在 。

  • 若要让 CPU 按照程序员的意图行事,必须使用机器指令(汇编指令)来控制它 。

  • 可执行文件加载到内存后,CPU 会根据文件描述信息中的入口地址设置 CS:IP,从而开始执行代码段的第一条指令 。

  • CPU 究竟将内存段当作指令执行、数据访问还是栈空间,完全取决于程序里具体的汇编指令以及对 CS:IP、SS:SP、DS 等寄存器的设置 。

  • 访问数据段需通过指令将段地址存入 DS 寄存器 。

  • 使用栈空间需通过指令设置 SS 寄存器指向栈段,并设置 SP 寄存器指向栈顶 。

第五章

寄存器间接寻址与描述符号

[bx] 寻址[bx] 表示一个内存单元,其偏移地址存放在 bx 寄存器中,而段地址默认存放在 ds 寄存器中 。

**()符号**:这是一个描述性符号,用于表示一个寄存器或内存单元中所包含的内容,例如(ax)代表ax` 中的数据 。

idata 符号:这是一个约定符号,用于在描述指令时代表一个常量 。

loop 指令与循环控制

基本功能loop 指令通常用于实现循环控制,它需要与 cx 寄存器配合使用,cx 中存放的是循环的总次数 。

执行步骤:CPU 在执行 loop 指令时,首先会将 cx 寄存器中的值减去 1 。

跳转判断:减 1 操作后,CPU 会判断 cx 的值,如果不为 0 则跳转至 loop 指令指定的标号处继续循环,如果为 0 则向下执行紧邻的下一条指令 。

用cx和loop指令相配合实现循环功能的程序框架如下:

1
2
3
4
5
6
mov cx,循环次数 
s:
循环执行的程序段
loop s


编程计算2∧12。
程序代码:

1
2
3
4
5
6
7
8
9
10
11
12
assume cs:code
code segment
mov ax,2
mov cx,11
s: add ax,ax
loop s
mov ax,4c00h
int 21h
code ends
end


loop 和 [bx] 的联合应用

要求:计算 ffff:0~ffff:b 单元中的数据的和,并将结果存储在 dx 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
; 【初始化阶段】
mov ax, 0ffffh
mov ds, ax ; 设置段地址 ds = ffffh
mov bx, 0 ; 设置初始偏移地址 bx = 0
mov dx, 0 ; 累加器 dx 清零
mov cx, 12 ; 设置循环次数 cx = 12

; 【循环阶段】
s: mov al, ds:[bx] ; 取出 [bx] 指向的 8 位内存数据,放入 al
mov ah, 0 ; 将 ah 清零,把 8 位数据安全扩展为 16 位
add dx, ax ; 累加到 dx 中
inc bx ; bx 加 1,指针移动到下一个内存单元 (等同于 add bx, 1)
loop s ; cx 减 1,如果不为 0 则跳回标号 s


内存中的安全空间

写入风险:在 8086 模式中随意向内存空间写入内容是非常危险的操作,因为可能会无意中覆盖掉操作系统存放的重要数据或代码,引发程序错误甚至死机 。

安全空间:在一般的 PC 机纯 DOS 环境(实模式)下,DOS 及其它合法程序通常不会使用 0:200~0:2FF 这一段共 256 字节的内存空间 。

实验建议:当程序员需要绕开操作系统直接向内存写入数据以进行硬件编程体验时,应当优先使用 0:200~0:2FF 这段被认为是安全的空间 。

逻辑运算与大小写转换

1)and 指令:逻辑与指令,按位进行与运算。
如mov al, 01100011B
and al, 00111011B
执行后:al = 00100011B
通过该指令可将操作对象的相应位设为0,其他位不变。

2)or指令:逻辑或指令,按位进行或运算。
如mov al, 01100011B
oral, 00111011B
执行后:al = 01111011B
通过该指令可将操作对象的相应位设为1,其他位不变。

ASCII 码大小写转换规律:大写字母(如 ‘A’, 41H)和小写字母(如 ‘a’, 61H)的 ASCII 码在二进制表示上只有第 5 位(从右往左数第 6 位)不同。

  • 将第 5 位置 0(使用 and),字符必然变为大写
  • 将第 5 位置 1(使用 or),字符必然变为小写

灵活的寻址方式

为了更结构化地处理内存数据,汇编语言提供了多种递进的寻址方式:

  1. 直接寻址 [idata]:使用常量表示地址,直接定位单个内存单元 。
  2. 寄存器间接寻址 [bx]:使用变量表示地址,常用于循环中遍历连续数据 。
  3. 寄存器相对寻址 [bx+idata]:使用变量加常量。非常适合处理一维数组,例如同时遍历两个数组时,可利用常量(如 [5+bx])作为相对偏移量定位第二个数组的元素。
  4. 基址变址寻址 [bx+si][bx+di]:使用两个变量。其中 bx 常作基址(如行首地址),si/di 常作变址(如列偏移)。
  5. 相对基址变址寻址 [bx+si+idata]:使用两个变量加一个常量。这是处理类似二维数组或复杂数据结构的强大工具(如:bx 定位行,idata 定位特定的起始列,si 定位起始列之后的动态偏移) 。

(注:si (Source Index) 和 di (Destination Index) 是功能类似 bx 的 16 位寄存器,但它们不能拆分成两个 8 位寄存器使用。)

eg1:

1
2
3
4
5
6
7
8
9
10
11
12
在codesg中填写代码,将datasg中定义的第一个字符串,转化为大写,第二个字符串转化为小写。
assume cs:codesg,ds:datasg
datasg segment
db 'BaSiC'
db 'MinIX'
datasg ends
codesg segment
start: ……
codesg ends
end start


1
2
3
4
5
6
7
8
9
10
11
12
13
mov ax,datasg
mov ds,ax
mov bx,0
mov cx,5
s:mov al,[bx];定位第一个字符串的字符
and al,11011111b
mov [bx],al
mov al,[5+bx];定位第二个字符串的字符
or al,00100000b
mov [5+bx],al
inc bx
loop s

eg2:

1
2
3
4
5
6
7
8
用寄存器SI和DI实现将字符串‘welcome to masm!’复制到它后面的数据区中。
assume cs:codesg,ds:datasg
datasg segment
db 'welcome to masm!'
db '................'
datasg ends

###
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
codesg segment
start: mov ax,datasg
mov ds,ax
mov si,0
mov cx,8
s: mov ax,0[si]
mov 16[si],ax
add si,2
loop s
mov ax,4c00h
int 21h
codesg ends
end start

###

eg3:

1
内存中有 4 行字符串(如 `'1. display      '`,每行 16 字节),要求将每个单词的**前 4 个字母改为大写** 。  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
assume cs:codesg, ds:datasg, ss:stacksg

; 1. 定义栈段:专门开辟一段内存用于保存临时数据
stacksg segment
dw 0,0,0,0,0,0,0,0 ; 申请 8 个字(16 个字节)的空闲空间作为栈
stacksg ends

; 2. 定义数据段:模拟 4 行带有序号的二维数组
datasg segment
db '1. display ' ; 每行 16 个字节
db '2. brows '
db '3. replace '
db '4. modify '
datasg ends

; 3. 定义代码段
codesg segment
start:
; 【第一步:初始化系统环境】
mov ax, datasg
mov ds, ax ; 让 ds 寄存器指向我们的数据段


mov ax, stacksg
mov ss, ax ; 让 ss 寄存器指向我们的栈段
mov sp, 16 ; 设置栈顶指针 SP。因为栈空间是 16 字节,所以 SP 初始指向最底部的 16

; 【第二步:外层循环准备】
mov bx, 0 ; bx 用作行指针,初始指向第 0 行
mov cx, 4 ; 外层循环计数器,总共 4 行

s0:
; 【保护现场】
push cx ; 进入内层前,把外层的 4、3、2、1 压入栈中保护起来


; 【第三步:内层循环准备】
mov si, 0 ; si 用作列游标,初始为 0
mov cx, 4 ; 内层循环计数器,每个单词改 4 个字母

s:
; 【第四步:核心寻址与修改】
; 终极寻址法:bx 负责找到行,3 负责跨过前面的 "1. ",si 负责在单词内部移动
mov al, [bx+si+3] ; 把目标字母取到 al 中
and al, 11011111b ; 使用 and 指令,将第 5 位置 0,强制转换为大写字母
mov [bx+si+3], al ; 将转换后的大写字母放回原来的内存位置


inc si ; 列游标 si 向后移动一格(加 1)
loop s ; 内层循环,处理下一个字母。循环结束后,cx 会变成 0

; 【第五步:行指针移动与恢复现场】
add bx, 16 ; 当前行处理完毕,bx 加 16,指针跳到下一行的行首
pop cx ; 从栈中弹出之前保护的外层循环计数值,覆盖掉那个没用的 0

loop s0 ; 外层循环,处理下一行

; 【第六步:安全退出程序】
mov ax, 4c00h ; 正常结束程序
int 21h

codesg ends
end start

###

二重循环与栈的应用

寄存器冲突问题:在处理二维数组(如修改多行字符串中的部分字符)时,需要用到嵌套的二重循环。由于 loop 指令默认且只能使用 cx 寄存器作为计数器,内层循环会覆盖外层循环的 cx 值 。

解决方案:必须在进入内层循环前,保存外层循环的 cx 值;在内层循环结束后,恢复外层循环的 cx 值 。

为什么使用栈:虽然可以使用其他空闲寄存器(如 dx)来暂存 cx 的值,但在复杂的程序中,可用的寄存器数量非常有限。因此,使用栈内存空间(配合 pushpop 指令)来暂存数据是最通用、最合理的解决方案

进入内层循环前:把当前外层循环的 CX 值压入栈中(push cx)安全保护起来。

执行内层循环:随便怎么折腾 CX,用它来控制内层循环。

内层循环结束后:把当初存进去的那个值从栈里弹出来(pop cx,原封不动地还给 CX

一个带有自定义栈段的安全二重循环长这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
assume cs:codesg, ds:datasg, ss:stacksg

; 1. 开辟一段内存当做栈
stacksg segment
dw 0,0,0,0,0,0,0,0 ; 申请 16 个字节的栈空间
stacksg ends

codesg segment
start:
; ... 省略设置 ds 和 ss 的代码 ...


mov cx, 4 ; 外层循环 4 次

s0:
push cx ; 【关键动作1:保护现场】将外层循环的 CX 压栈


mov cx, 3 ; 设置内层循环 3 次

s:
; ... 这里写修改数据的具体指令 ...
loop s ; 内层循环结束时,CX 会变成 0


pop cx ; 【关键动作2:恢复现场】从栈中弹出之前存的 4、3、2... 覆盖掉那个没用的 0
loop s0 ; 外层循环根据恢复好的 CX 继续完美运转

codesg ends
end start

第六章

标志寄存器基础结构

  • 在8086CPU中,标志寄存器有多个位具有特殊含义,其中第0、2、4、6、7、8、9、10、11位被使用 。

  • 标志寄存器的第1、3、5、12、13、14、15位在8086CPU中没有被使用,不具有任何含义 。

核心标志位及其功能

-

ZF(零标志位,第6位):记录相关指令执行后的结果 。若结果为0,ZF=1 ;若结果不为0,ZF=0 。

-

PF(奇偶标志位,第2位):记录指令执行后,结果的所有二进制位中1的个数 。若为偶数,PF=1 ;若为奇数,PF=0 。

-

SF(符号标志位,第7位):用于记录有符号数运算结果的正负 。若结果为负,SF=1 ;若结果为正,SF=0 。如果将数据当作无符号数运算,SF的值没有意义 。

如果结果的最高位是 1,CPU 就认为结果为负,把 SF = 1

如果结果的最高位是 0,CPU 就认为结果为正(或 0),把 SF = 0

-

CF(进位标志位,第0位):一般用于无符号数运算,记录运算结果的最高有效位向更高位的进位值,或从更高位的借位值 。

只有当你在做无符号数的运算时,去关注 CF 才有意义 。只要加法的结果往外“进位”了,或者减法“借位”了,CF 就会变成 1,否则就是 0。

-

OF(溢出标志位,第11位):用于记录有符号数运算的结果是否发生了溢出(即超出了机器所能表示的范围) 。如果发生溢出,OF=1 ;如果没有,OF=0 。

特征 CF (进位标志) OF (溢出标志)
服务对象 无符号数 有符号数
物理含义 关注最高有效位是否向“假想的更高位”发生进位或借位。 关注次高位向最高位的进位,与最高位向外的进位是否不一致(异或运算)。
通俗理解 寄存器物理空间装不下了,溢出到外面了。 数学逻辑出错了(比如两正相加得负)。
后续常接指令 adc (带进位加), sbb (带借位减), ja/jb (无符号跳转) jg/jl (有符号跳转), jo/jno (溢出跳转)

与标志位相关的运算与比较指令

-

adc指令:利用进位值进行加法运算,配合add指令可以对更大的数据进行加法运算 。

应用场景: 当相加的数据位数大于寄存器容量(如 16 位)时,我们将计算分步进行:先将低 16 位用 add 指令相加,这可能会产生进位(CF=1);然后再用 adc 指令将高 16 位相加,此时 CPU 会自动把刚才低位产生的 CF 值一起加进去 。这样配合使用,就能对更大的数据进行加法运算 。

add al,bl

adc ah,bh

-

sbb指令:带借位减法指令,利用了CF位上记录的借位值,可用于对任意大的数据进行减法运算 。功能为:操作对象1 = 操作对象1 - 操作对象2 - CF 。

-

cmp指令:比较指令,功能相当于减法指令,但不保存结果,仅根据计算结果对标志寄存器进行设置 。

-

**无符号数比较**:通过ZF和CF的值判断比较结果。例如:ZF=1说明两数相等;CF=1说明操作对象1小于操作对象2 。  

-

**有符号数比较**:需要综合考察SF和OF的值。例如:若SF=1且OF=0,说明无溢出且实际结果为负,因此操作对象1小于操作对象2 。

DF标志与串传送指令

-

DF(方向标志位,第10位):在串处理指令中,控制每次操作后si、di寄存器的增减 。DF=0时递增(正向搬运),DF=1时递减(反向搬运) 。

si (Source Index - 源变址寄存器):它永远指向“数据源”。在串传送指令中,它默认与 ds(数据段寄存器)绑定,构成源内存地址 ds:si

**di(Destination Index - 目的变址寄存器)**:它永远指向“目的地”。它默认与es(附加段寄存器)绑定,构成目标内存地址 es:di` 。

每次搬运时,CPU 就会把 ds:si 指向的内存数据,复制到 es:di 指向的内存单元中 。具体的物理地址计算公式为:((es)*16+(di)) = ((ds)*16+(si))

-

修改DF指令cld 指令将DF位置0,std 指令将DF位置1 。

  • 串传送指令

    movsb (Move String Byte):以字节(8位)为单位传送 。执行一次,传送一个字节,然后根据 DF 的值,将 sidi 加 1 或减 1 。

    movsw (Move String Word):以(16位,即 2 个字节)为单位传送 。传送后,将 sidi 加 2 或减 2 。

    rep:与movsb或movsw配合使用,根据cx的值重复执行后面的串传送指令,实现连续多个字符的传送 。

    rep movsb 用汇编语法来描述rep movsb的功能就是:s : movsb loop s

    rep movsw用汇编语法来描述rep movsw的功能就是:s : movsw loop s

其他相关指令与Debug显示

-

栈操作指令pushf 将标志寄存器的值压栈;popf 从栈中弹出数据送入标志寄存器,这为直接访问标志寄存器提供了方法 。

-

Debug中的表示方法

  • OF: OV (值为1) / NV (值为0)
  • SF: NG (值为1) / PL (值为0)
  • ZF: ZR (值为1) / NZ (值为0)
  • PF: PE (值为1) / PO (值为0)
  • CF: CY (值为1) / NC (值为0)
  • DF: DN (值为1) / UP (值为0)

第七章

转移指令概述

8086CPU的转移指令主要分为以下几大类 :

无条件转移指令(如:jmp)

条件转移指令

循环指令(如:loop)

过程

中断

操作符 offset

-

offset 操作符由编译器在编译阶段处理 。

  • 它的主要功能是取得汇编语言中标号的偏移地址 。
1
2
3
4
5
6
assume cs:codesg
codeseg segment
start:mov ax,offset start ; 相当于mov ax,0
s:mov ax,offset s ; 相当于mov ax,3
codesg ends
end start

注:1个字节(8位 / Byte):比如存放在 ALAHBL 等 8 位寄存器中的数据。

2个字节(16位 / Word):即一个“字”,比如存放在 AXBXCX 等 16 位寄存器中的数据。

mov ax, 0(或者说 mov ax, 立即数)这条指令在 8086 CPU 的机器码中,占用了 3 个字节 的内存空间。

这 3 个字节分别是:1 个字节的操作码(告诉 CPU 要执行 mov ax 的操作),加上 2 个字节的数据(即 00 00)。

eg:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
assume cs:codesg        ; 伪指令:告诉编译器,将代码段寄存器 CS 与名为 codesg 的段关联起来

codesg segment ; 定义一个名为 codesg 的代码段(段的开始)

s: mov ax,bx ; 【标号 s】这是我们要复制的“源指令”。
; 作用是将 bx 的值赋给 ax。(注意:这条指令的机器码刚好占 2 个字节)

mov si,offset s ; 将标号 s 的偏移地址存入 SI 寄存器。
; (SI 现在指向了源数据所在的内存位置)

mov di,offset s0 ; 将标号 s0 的偏移地址存入 DI 寄存器。
; (DI 现在指向了我们要把数据复制过去的“目的地”内存位置)

mov ax,cs:[si] ; 【关键操作:读】
; 从 CS 段(代码段)中,读取偏移地址为 SI 处的一个字(2个字节)的数据,存入 AX。
; 因为 SI 指向标号 s,所以这步实际上是把 `mov ax,bx` 的机器码读到了 AX 中。

mov cs:[di],ax ; 【关键操作:写】
; 将 AX 中的数据(也就是刚才读到的机器码),写入到 CS 段中偏移地址为 DI 的内存处。
; 因为 DI 指向标号 s0,所以这步把机器码覆盖写到了两个 nop 指令的位置。

s0: nop ; 【标号 s0】这是复制的目的地。
; nop 是空操作指令,什么都不做,其机器码占 1 个字节。
nop ; 第二个 nop 指令。
; 两个 nop 加起来刚好腾出 2 个字节的空间,完美接纳复制过来的 `mov ax,bx` 的机器码。

codesg ends ; codesg 代码段结束

end ; 整个汇编程序结束(通常规范写法是 end 标号,代表程序入口点并结束)

无条件转移指令 (jmp)

jmp 指令可以只修改IP寄存器,也可以同时修改CS和IP寄存器 。根据转移的距离和地址来源,可分为以下几种:

  • 依据位移进行转移 (段内转移)

    短转移 (jmp short 标号):对IP的修改范围为 -128-127(8位位移,一个字节) 。机器码中不包含转移的目的地址,只包含计算出的位移量 。

    eg:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    assume cs:codesg        ; 声明 codesg 段为代码段 
    codesg segment ; 代码段开始

    start: ; 程序的起始入口标号
    mov ax,0 ; 将寄存器 AX 置 0
    mov bx,0 ; 将寄存器 BX 置 0

    jmp short s ; 【核心跳转】无条件跳转到标号 s 处执行
    ; 这是一个段内短转移,机器码中不包含目的地址,
    ; 而是包含从下一条指令起始处到标号 s 的位移量

    add ax,1 ; 这条指令被跳过了,永远不会被执行

    s: inc ax ; 【跳转目的地】AX 的值加 1
    ; 执行完 jmp short s 后,CPU 的 IP 寄存器会加上
    ; 编译时算出的位移量,直接指向这条指令

    codesg ends ; 代码段结束
    end start ; 指定程序入口并结束编译

    近转移 (jmp near ptr 标号):功能为 (IP)=(IP)+16 位位移 。位移范围为 -32769-32767 。

  • 转移目的地址在指令中 (段间远转移)

    -

    远转移 (jmp far ptr 标号):实现段间转移 。指令会同时修改CS(标号所在段的段地址)和IP(标号所在段的偏移地址) 。

    eg:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    assume cs:codesg
    codesg segment

    start:
    mov ax,0 ; 将 AX 初始化为 0
    mov bx,0 ; 将 BX 初始化为 0

    jmp far ptr s ; 【核心:远转移】
    ; 修改 CS 为标号 s 的段地址,修改 IP 为 s 的偏移地址
    ; 这种指令的机器码中直接存储了目标地址的绝对值

    db 256 dup (0) ; 定义 256 个字节的数据(占位),由于 jmp 的存在,这段空间会被直接跳过

    s: ; 跳转的目标位置
    add ax,1 ; 执行加法,AX = 1
    inc ax ; 执行自增,AX = 2

    codesg ends
    end start

    机器码特征:它通常是一个 5 字节的指令。例如机器码 EA 0B 01 BD 0B 中,EA 是操作码,低地址的 0B 01 是偏移地址(即010BH),高地址的 BD 0B 是段地址(即0BBDH) 。

  • 转移地址在寄存器中

    -

    jmp 16位寄存器:功能是将IP修改为该16位寄存器中的值 。

    jmp 16位寄存器 (例如:jmp ax, jmp bx

    原理:(IP) = (16位寄存器)。

    特点:仅仅修改 IP,所以也是段内转移。

  • 转移地址在内存中

    -

    jmp word ptr 内存单元地址:段内转移,从指定内存中读取一个字作为转移的目的偏移地址 。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    mov ax,0123H              ; 将 16 位立即数 0123H 赋值给 AX 寄存器

    mov [bx],ax ; 将 AX 的值放入以 BX 为偏移地址的内存中。
    ; [bx] 存放 23H,[bx+1] 存放 01H。
    ; 这构成了一个字:0123H。

    jmp word ptr [bx] ; 【段内转移】
    ; word ptr 告诉 CPU 仅从 [bx] 开始读取一个字(2 个字节)
    ; 规则:读取的这个字直接送入 IP
    ; 结果:(IP) = 0123H,CS 保持当前代码段不变
    执行后,(IP)=0123H

    -

    jmp dword ptr 内存单元地址:段间转移,从指定内存中读取两个字,高地址处为目的段地址(CS),低地址处为目的偏移地址(IP) 。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    mov ax,0123H              ; 将 16 位立即数 0123H 赋值给 AX 寄存器

    mov [bx],ax ; 将 AX 的值放入以 BX 为偏移地址的内存中。
    ; [bx] 存放 23H,[bx+1] 存放 01H。
    ; 这两个字节(低位字)将作为转移的 偏移地址(IP)

    mov word ptr [bx+2],0 ; 在 [bx+2] 处连续写入 2 个字节的 0(即 0000H)。
    ; [bx+2] 存放 00H,[bx+3] 存放 00H。
    ; 这两个字节(高位字)将作为转移的 段地址(CS)

    jmp dword ptr [bx] ; 【段间远转移】
    ; dword ptr 告诉 CPU 从 [bx] 开始读取双字(4 个字节)
    ; 规则:低地址字送入 IP,高地址字送入 CS
    ; 结果:(IP) = 0123H,(CS) = 0000H
    执行后,
    (CS)=0
    (IP)=0123H
    CS:IP指向0000:0123。

位移与字节

16位(bit)等于 2个字节(Byte)

条件转移指令

  • 所有的有条件转移指令都是短转移,对应的机器码中只包含8位位移(范围 -128-127),不包含目的地址

jcxz 标号:当 (cx)=0时,(IP)=(IP)+8位位移 。若 (cx) 不等于0,则程序继续向下执行 。

  • 文档还列举了基于标志位(如CF, ZF, SF, OF)的无符号数转移指令(如JA, JB)和有符号数转移指令(如JG, JL) 。

循环指令 (loop)

-

loop 指令属于循环指令,也是短转移,修改IP范围为 -128-127 。

-

指令操作:执行时首先 (cx)=(cx)-1 。如果 (cx) 不等于0,则进行位移转移到标号处 ;如果 (cx)=0,则什么也不做,程序向下执行 。

位移转移的意义与越界检测

-

浮动装配jmp shortjmp near ptrjcxzloop 等指令根据位移来进行转移,机器码中不包含目的地址,这种设计极大地方便了程序段在内存中的浮动装配 。

-

超界报错:如果源程序中出现了转移范围超界的情况(例如 jmp short 向后转移超过127个字节),编译器在编译时会进行检测并报错 。

下面的程序将引起编译错误:

1
2
3
4
5
6
7
assume cs:code
code segment
start: jmp short s
db 128 dup(0)
s: mov ax,0ffffh
code ends
end start

jmp short s的转移范围是-128~127,IP最多向后移动127个字节。

retretf 指令

这两条指令都是转移指令,常用于子程序设计中,通过修改指令指针来控制程序执行流 。

-

ret 指令:利用栈中的数据来修改 IP(指令指针寄存器)的内容,从而实现近转移 。

-

retf 指令:利用栈中的数据同时修改 CS(代码段寄存器)和 IP 的内容,从而实现远转移 。

call 指令

CPU 执行 call 指令时分为两步操作:首先将当前的 IP 或同时将 CS 和 IP 压入栈中,然后再进行转移 。需要注意的是,call 指令不能实现短转移 。根据目的地址的提供方式,call 指令分为以下几种:

-

依据位移转移call 标号。CPU 会将当前 IP 压栈,然后加上由编译程序算出的 16 位位移量(范围用补码表示为 -32768 到 32767),这相当于执行了 push IPjmp near ptr 标号

-

目的地址在指令中call far ptr 标号。用于实现段间转移,CPU 会依次将 CS 和 IP 压栈,然后跳转,相当于执行 push CSpush IPjmp far ptr 标号

-

目的地址在寄存器中call 16位寄存器。将当前 IP 压栈后,将 IP 的值修改为指定的 16 位寄存器中的值 。

-

目的地址在内存中:分为 call word ptr 内存单元地址call dword ptr 内存单元地址 两种格式 。其中 dword ptr 会实现段间调用,相当于压入 CS 和 IP 后执行段间跳转 。

callret 的配合使用

  • 它们被共同用来实现子程序的机制 。

  • 在执行子程序前,call 指令会将它下一条指令的地址保存在栈中 。

  • 在子程序末尾使用 ret 指令,可以从栈中弹出该数据并设置到 IP,使得 CPU 能够准确回到 call 指令后面的代码处继续向下执行 。

mul 指令(乘法)

用于执行乘法运算,格式分为 mul reg(寄存器)和 mul 内存单元

-

8 位乘法:与 al 寄存器相乘,最终结果保存在 ax 寄存器中 。

1
2
3
4
5
6
mov al,100  ; 步骤1:将隐式被乘数 100 放入 al 
mov bl,10 ; 步骤2:将 8 位乘数 10 放入 bl
mul bl ; 步骤3:执行 8 位乘法
; 结果:(ax) = 1000 (十六进制为 03E8H)

因为 100 和 10 都小于 255,所以可以做 8 位乘法 。

-

16 位乘法:与 ax 寄存器相乘,结果的高 16 位保存在 dx 寄存器中,低 16 位保存在 ax 寄存器中 。

1
2
3
4
5
mov ax, 100    ; 步骤1:将隐式被乘数 100 放入 ax 
mov bx, 10000 ; 步骤2:将 16 位乘数 10000 放入 bx
mul bx ; 步骤3:执行 16 位乘法
; 真实结果:1000000 (十六进制为 F4240H)
; 寄存器结果:高16位 (dx) = 000FH,低16位 (ax) = 4240H

mul byte ptr ds:[0] 。它的含义是:将 al 的值与数据段偏移地址 0 处的一个字节相乘,结果存入 ax

mul word ptr [bx+si+8] 。它的含义是:将 ax 的值与对应内存地址的一个字(16位)相乘 ,结果分别存入 dxax

dup 操作符

-

dup 是一个由编译器识别和处理的操作符,与 dbdwdd 等数据定义伪指令配合使用,用于进行数据的重复 。

  • 例如,db 3 dup (0) 等同于 db 0,0,0 ;它也支持重复字符串,如 db 3 dup ('abc','ABC')

  • 在实际应用中,常被用来快速分配内存空间,例如定义一个容量为 200 个字节的纯零堆栈段 。

伪指令 英文全称 中文含义 内存大小 对应寄存器示例 主要用途
db Define Byte 定义字节 1 字节 (8位) al, bl, cl 存小数、存字符、存字符串
dw Define Word 定义字 2 字节 (16位) ax, bx, si 存大数、存 16 位偏移地址
dd Define Double word 定义双字 4 字节 (32位) dx:axeax 存超大数、存 32 位完整地址 (段:偏移)

第八章

子程序的定义与格式

  • 子程序通过 PROCENDP 伪指令进行定义 。
  • 可以分为段内调用(NEAR)和段间调用(FAR)两种格式 。
  • 子程序的末尾需要使用 RET 指令以便返回调用处 。

image-20260511141251755

现场保护

  • 为了防止子程序改变主程序中寄存器的值,需要在子程序开始时使用 PUSH 指令保存寄存器状态 。
  • 在子程序执行完毕、调用 RET 返回前,必须使用 POP 指令按相反的顺序恢复这些寄存器的状态 。

image-20260511141400165

子程序的调用机制

  • 将经常使用或无规律重复的代码段设计成独立的、可反复调用的程序段 。

  • 使用 CALL 指令进行调用,子程序执行完毕后会自动返回到调用处的下一条指令继续执行 。

参数传递方法

-

寄存器传送:通过 CPU 内部寄存器传递变量 。

-

堆栈传送:通过堆栈传递变量或变量地址 。

-

存储器传送:通过内存传递 。如果在同一个程序模块中,子程序可以直接访问模块内的变量 ;如果不在同一模块,可以通过建立公共数据区或使用外部符号来进行传送 。

eg:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
; =================================================================
; 完整代码:模块化设计演示
; =================================================================
CODE SEGMENT
ASSUME CS:CODE ; 假定 CS 寄存器指向 CODE 段

START: ; 主程序入口标签
; --- 【主程序部分】 ---


MOV CX, 10 ; 【传参】将要打印的星号数量 10 存入 CX 寄存器
CALL STAR ; 【调用】执行子程序 STAR,屏幕将输出 10 个 '*' 并换行

MOV CX, 5 ; 【传参】修改循环次数为 5
CALL STAR ; 【调用】再次执行子程序 STAR,屏幕将输出 5 个 '*' 并换行

; (规范的主程序结尾,需要加上返回 DOS 操作系统的代码)
MOV AH, 4CH ; 准备调用 DOS 中断 4CH 功能(结束程序并返回系统)
INT 21H ; 执行中断


; =================================================================
; 子程序名称:STAR
; 功能描述:连续显示 N 个 '*',然后显示一个回车换行符。
; 参数传递:寄存器 CX (表示打印星号的次数 N)
; =================================================================
STAR PROC NEAR ; 声明一个名为 STAR 的段内子程序 (NEAR 表示在同一代码段内)

; --- 1. 现场保护(修复教科书遗漏) ---
PUSH AX ; 压栈:备份 AX 寄存器 (因为后面用到了 AH)
PUSH DX ; 压栈:备份 DX 寄存器 (因为后面用到了 DL)

; --- 2. 核心逻辑:循环打印星号 ---

AGAIN:
MOV DL, '*' ; 将星号的 ASCII 码放入 DL 寄存器 (DOS 2号功能的参数)
MOV AH, 2 ; 将功能号 2 放入 AH (表示控制台输出单字符)
INT 21H ; 执行 DOS 中断,屏幕输出一个 '*'
LOOP AGAIN ; 循环指令:自动执行 CX = CX - 1。若 CX 不为 0,则跳回 AGAIN 继续

; --- 3. 核心逻辑:打印回车换行 ---
; (1) 打印回车符 (Carriage Return,光标移到行首)
MOV DL, 0DH ; 将回车符的 ASCII 码 (0D十六进制/13十进制) 放入 DL
MOV AH, 2 ; 确认功能号为 2
INT 21H ; 执行 DOS 中断

; (2) 打印换行符 (Line Feed,光标下移一行)
MOV DL, 0AH ; 将换行符的 ASCII 码 (0A十六进制/10十进制) 放入 DL
MOV AH, 2 ; 功能号仍为 2
INT 21H ; 执行 DOS 中断

; --- 4. 恢复现场(注意:出栈顺序必须与入栈严格相反!) ---
POP DX ; 出栈:恢复原先 DX 寄存器里的值
POP AX ; 出栈:恢复原先 AX 寄存器里的值

; --- 5. 返回主程序 ---
RET ; 返回指令:跳回调用处 (即执行刚才那句 CALL STAR 下面的代码)

STAR ENDP ; 结束名为 STAR 的子程序定义

CODE ENDS ; 结束 CODE 代码段
END START ; 汇编结束,并指定程序的执行入口点为 START

第九章

中断的基本概念与分类

-

中断的定义:中断是指CPU不再接着执行刚执行完的下一条指令,而是暂停并转而去处理一个特殊信息

-

中断源分类:引起中断的事件被称为中断源 。主要分为以下两类:

-

**内中断(软中断)**:由CPU内部情况产生,如执行 `INT` 指令、执行 `INTO` 指令、单步执行,或者是CPU执行出错(如除法错误、溢出),以及为调试程序设置的中断等 。  

-

**外中断(硬中断)**:由外部设备引起,包括外设的I/O请求(可屏蔽中断),以及电源掉电或奇偶校验错(非屏蔽中断) 。

中断处理与中断向量表

-

定位中断处理程序:中断信息中包含标识中断源的类型码,CPU根据该中断类型码来定位要执行的中断处理程序 。

-

中断向量表:这是一个保存在内存中的列表,里面存放着256个中断源所对应的中断处理程序入口地址 。

-

存放位置:对于8086PC机,中断向量表固定存放在内存地址0处,具体范围是从内存 0000:00000000:03FF,总共占据1024个字节单元 。

CPU的中断处理过程

当发生中断时,CPU的硬件会自动完成以下几个步骤(被称为中断过程):

  1. 从中断信息中取得中断类型码 N 。

  2. 将标志寄存器的值压入栈(pushf),以保存中断前标志寄存器的状态 。

  3. 将标志寄存器的第8位TF 和第9位 IF的值均设置为0 。

  4. CS 的内容压入栈(push CS) 。

  5. IP 的内容压入栈(push IP) 。

  6. 从内存地址为 (N✖4)和 (N✖4+2) 的两个字单元中读取中断处理程序的入口地址,并分别设置给 IPCS

中断处理程序的编写与返回

-

常规编写步骤:保存用到的寄存器,处理中断逻辑,恢复用到的寄存器,最后使用 iret 指令返回 。

-

iret 指令机制iret 的功能相当于依次执行 pop IPpop CSpopf,以此恢复被中断的程序现场 。

关于 INT 指令

-

调用逻辑INT n 指令引发了中断类型码为 n 的中断过程,它的最终功能类似于 call 指令,都是去调用一段具有特定功能的子程序 。

-

系统应用:系统通常会将一些具有一定功能的子程序,封装成中断处理程序的形式提供给应用程序去调用 。

eg:

目标任务:编写一个自定义的0号中断处理程序。当发生除法溢出时,不要使用系统默认的处理方式,而是让它在屏幕中间显示“overflow!”,随后安全返回到DOS操作系统 。

第一步:编写安装程序(搬运 do0 代码)

CPU 执行这个主程序时,第一件事就是要把 do0 的代码完好无损地复制到内存 0000:0200 处。这里使用了串传送指令 movsb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
assume cs:code
code segment
start: mov ax, cs
mov ds, ax
mov si, offset do0 ; 设置 ds:si 指向源地址(准备搬运的代码起点)

mov ax, 0
mov es, ax
mov di, 200h ; 设置 es:di 指向目的地址(0000:0200)

; 重点:如何知道 do0 有多长?
mov cx, offset do0end - offset do0 ; 设置 cx 为传输长度

cld ; 设置传输方向为正向
rep movsb ; 开启循环搬运,直到 cx 为 0

第二步:修改中断向量表

代码搬完了,接下来要告诉 CPU:以后发生 0 号中断,去 0:200 找代码。0 号表项的地址为 0:0,其中 0:0 单元存放偏移地址,0:2 单元存放段地址 。

1
2
3
4
5
6
7
8
9
; 设置中断向量表 
mov ax, 0
mov es, ax
mov word ptr es:[0*4], 200h ; 将偏移地址 200h 写入 0 号表项低位字
mov word ptr es:[0*4+2], 0 ; 将段地址 0 写入 0 号表项高位字

; 安装完毕,主程序可以功成身退了
mov ax, 4c00h
int 21h ; 返回 DOS 系统 [cite: 267, 268]

至此,安装工作全部结束。虽然主程序退出了,但是我们写的 do0 代码已经安全地驻留在内存 0000:0200 处,并且中断向量表已经被我们篡改。

第三步:被调用的 do0 程序

如果在随后的任何时间里,有程序执行除法发生了溢出,CPU 就会自动去查中断向量表的 0 号表项,然后跳转到我们写好的 do0 处执行。这段程序的主要任务是显示字符串 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
do0:    ; (此处省略了设置 ds:si 指向要打印的字符串 "overflow!" 的步骤) 

mov ax, 0b800h ; b800h 是彩色字符模式的显存段首地址
mov es, ax

; 计算屏幕正中间的内存偏移地址
mov di, 12*160 + 36*2 ; 设置 es:di 指向显存空间的中间位置

mov cx, 9 ; "overflow!" 共有 9 个字符,设置循环次数

s: mov al, [si] ; 从源字符串拿一个字符放入 al
mov es:[di], al ; 将字符写入显存地址,它就会显示在屏幕上
inc si ; 指向下一个待打印字符
add di, 2 ; 显存中每个字符占2个字节(1字节ASCII+1字节颜色属性)
loop s ; 循环 s,直到全部打印完毕

; 报错打印完毕,优雅退出
mov ax, 4c00h
int 21h ; 返回 DOS

do0end: nop ; 标记 do0 代码块结束的空指令,用于给前面计算长度
code ends
end start