文章目录
- 0. 声明
- 第一章 基础知识
- 第二章 寄存器(CPU工作原理)
- 第三章 寄存器(内存访问)
- 第四章 第一个汇编程序
- 第五章 【bx】和loop指令
- 第六章 包含多个段的程序
- 第七章 更灵活地定位内存地址
- 第八章 数据处理的两个基本问题
- 第九章 转移指令的原理
- 第十章 call和ret指令
- 第十一章 标志寄存器
- 第十二章 内中断
- 第十三章 int指令
- 第十四章 端口
- 第十五章 外中断
- 第十六章 直接定址表
- 第十七章 使用BIOS进入键盘输入和磁盘读写
0. 声明
该内容为B站小甲鱼视频配套笔记,参考了他人笔记集合课程重点以及PPT汇总整理完成 可以帮助学习汇编的朋友节省笔记时间 配套课程可去b站搜索小甲鱼0基础入门汇编
ps: 全套课程学完约25h,加上练习大概30h左右,本人亲测在有代码基础和一些os基础的情况下 学习周期不超过7天!!纯0基础小白 半个月也足矣!
第一章 基础知识
1.1 基础知识
【学习汇编主要是: 学习汇编的编程思想,掌握机器运行的思维】
汇编语言是直接在硬件上工作的编程语言,首先要了解硬件系统的结构,才能有效的应用汇编语言对其编程。
- 汇编课程的研究重点 : 如何利用硬件系统的编程结构和指令集有效灵活的控制系统进行工作
- 汇编语言的主体是
汇编指令- 汇编指令和机器指令的差别在于指令的表示方法上 汇编指令是机器指令便于记忆的书写格式
- 汇编语言是
机器指令的助记符- 汇编语言的组成
- 汇编指令(机器码的助记符)
- 伪指令(由编译器执行)【 只给汇编编译器(MASM/TASM) 看的指令,不会生成任何 CPU 机器码 】
DATA SEGMENT ; 告诉编译器:下面是数据段开始 MSG DB 'Hello',0DH,0AH,'$' DATA ENDS ; 数据段结束 CODE SEGMENT ; 代码段开始 START: CODE ENDS ; 代码段结束
- 其他符号(由编译器识别,如:+ - * /)
汇编语言的核心是汇编指令,他决定了汇编语言的特性
1.2 CPU对存储器的读写
CPU对存储器(一般是内存)的读写 ,CPU要想进行数据的读写,必须和外部器件(即外部芯片)进行三类信息的交互
1. 地址信息: 存储单元的地址(如内存网卡显卡中存储数据的地址)
2. 控制信息: 芯片的选择,读或写命令(选择读or写,怎么读怎么写)
3. 数据信息: 读或写的数据(读什么;写什么的数据本身)
那么CPU是通过什么做到的?
通过电信号,电信号通过导线
1.3 地址总线
CPU是通过地址总线来指定存储单元的。
⚠️:地址总线上能传送多少个不同的信息,CPU就能对多少个存储单元进行寻址。

一个CPU有N根地址总线,则可以说这个CPU的地址总线的宽度为N。这样的CPU最多可以寻找2的N次方个内存单元。(N 根线组合起来,就可以产生 2^N 个唯一的二进制编码,每一个编码对应内存中的一个唯一地址。)
一个内存单元存储一个字节。
1.4 数据总线
看一下 如何通过数据总线传输数据89D8H:
8位总线:
先传低地址再传高地址 传两次:
16位一次传完:
1.5 控制总线


比如说一个根线 是对显卡 1是读命令 第二根对内存 读命令 第三根对x x 写命令
1.6 小结



1.7 内存地址空间

1.8 主板和接口卡


比如显卡控制显示器;网卡控制网线等
1.9 ROM和RAM
ROM(Read-Only Memory)—— 非易失性存储器(
只读存储器)
断电后数据不丢失:即使关闭电源,存储在ROM中的信息依然保留。
用途:用于存储需要“固化”的关键程序和数据
读取速度:相对较慢,但足以满足启动和固件加载的需求。
RAM(Random Access Memory)—— 易失性存储器(
随机存储器)
断电后数据丢失:一旦断电,所有存储在RAM中的数据都会被清空。
用途:作为CPU的“临时工作台”,用于存放当前正在运行的程序、操作系统、打开的文件和计算中的中间结果。
读写速度:极快(纳秒级),是CPU直接访问的高速暂存空间,直接影响系统的多任务处理能力和响应速度。
1.10 BIOS

BIOS(Basic Input/Output System,基本输入输出系统)是一组固化在计算机主板ROM芯片中的程序。它是计算机开机时最先运行的第一个程序,承担着硬件初始化、开机自检以及引导操作系统等基础功能,是连通软件程序与硬件设备之间的枢纽。
BIOS 主要包含以下四大核心功能:
- 上电自检(POST):系统通电后,BIOS 首先对 CPU、内存、显卡、硬盘等内部各个设备进行检查。若发现严重故障则停机,发现非严重故障则会给出提示或鸣笛警告。
- 硬件初始化:创建中断向量、设置寄存器,并对一些外部设备进行初始化和检测。
- 系统启动自举:自检成功后,BIOS 按照预设的启动顺序搜寻硬盘、U盘或光驱等有效启动设备,读取操作系统的引导记录,并将系统控制权移交给操作系统以完成启动。
- BIOS 中断服务与设置: 作为微机系统中软件与硬件之间的可编程接口,实现程序对硬件的直接操作。同时,BIOS 中装有系统设置程序,允许用户通过特定按键进入设置界面,对硬件配置参数进行调整。

1.11 内存地址空间




类似操作系统中 虚拟地址对物理地址的映射机制

第二章 寄存器(CPU工作原理)
2.0 8086 CPU 寄存器基础
CPU = 运算器 + 控制器 + 寄存器,器件之间通过总线相连

8086CPU有14个寄存器,名称分别为:
AX,BX,CX,DX,SI,DI,SP,BP,IP,CS,SS,DS,ES,PSW
2.1 通用寄存器
- 8086CPU所有的 寄存器都是16位的,可以存放2个字节
AX、BX、CX、DX通常用来存放一般性数据,被称为通用寄存器- 8086上一代CPU中的寄存器都是8位的,为了保证兼容性,这四个寄存器都可以拆分为2个独立的8位寄存器使用
- AX = AH + AL
- BX = BH + BL
- CX = CH + CL
- DX = DH + DL
- AX 的低8位(0~7)构成 AL 寄存器;高8位(8~15)构成 AH 寄存器;AH 和 AL 是可独立使用的8位寄存器
- 一个字等于2byte
2.2 字在寄存器中的存储
8086 一个字占16位

2.3 几条汇编指令
2.3.1 基础规则
- 汇编指令不区分大小写
2.3.2 基础汇编指令示例
mov ax,18 ;AX=18
mov ah,78 ;AH=78
add ax,8 ;AX=AX+8
mov ax,bx ;AX=BX
add ax,bx ;AX+=BX
2.3.3 编程练习:最多使用四条指令,计算2的4次方
mov ax,2 ;ax=2
add ax,ax ;ax=4
add ax,ax ;ax=8
add ax,ax ;ax=16
2.4 物理地址
- CPU访问内存单元时,要给出内存单元的地址。
- 所有的内存单元构成的存储空间是一个一维的线性空间
- 我们将这个唯一的地址称为
物理地址
2.5 16位结构的CPU
16位结构描述了一个CPU具有以下几个方面特征:
- 运算器一次最多可以处理16位的数据
- 寄存器的最大宽度为16位
- 寄存器和运算器之间的通路是16位的
2.6 8086CPU给出物理地址的方法
2^20 byte = 1024 × 1024 byte = 1024 KB = 1 MB ; 2^16 byte = 64kb
Intel 8086 CPU 最多只能直接管理 1MB 的内存
- 8086有20位地址总线,可传送20位地址,实际上的寻址能力为
1M - 8086内部为16位结构(寄存器啥都是16位),它只能传送16位的地址,理论上表现出的寻址能力却只有64K
- 问题:8086CPU如何用内部16位的数据转换成20位的地址?
- 8086CPU采用一种在内部用两个16位地址合成的方法,来形成20位的物理地址
即:段地址+偏移地址=物理地址 - 地址加法器合成物理地址的方法:
物理地址=段地址×16+偏移地址 - “段地址×16”即是数据左移4位(二进制位的左移4位,十六进制的左移1位)
- 在地址加法器中,如何完成“段地址×16”? 二进制形式的段地址左移4位
- 8086CPU采用一种在内部用两个16位地址合成的方法,来形成20位的物理地址
2.7 “段地址×16+偏移地址=物理地址”的本质含义
- 即可以用两个16位的二进制数来表示一个20位的二进制数
- 8086CPU中内部为16位结构,但地址线却是20位的,使用地址加法器可以把16位地址变成20位地址
具体操作就是: 段地址×16+偏移地址

2.8 段的概念
- 内存并没有分段,段的划分来自于CPU,由于8086CPU用“
段地址×16+偏移地址=物理地址”的方式给出内存单元的物理地址,使得我们可以用分段的方式来管理内存 - 以后,在编程时可以根据需要,将若干地址连续的内存单元看作一个段,
使用段地址×16定位段的起始地址(基础地址),用偏移地址定位段中的内存单元 - 注意
- 段地址必然是16的倍数,即一个段的起始地址必然是16的倍数
- 偏移地址为16位,16位地址的寻址能力为
64K,所以一个段的长度最大为64K - CPU可以用不同的段地址和偏移地址形成同一个物理地址

2.9 段寄存器
2.9.1 段寄存器作用
- 段寄存器就是提供段地址的
8086CPU有4个段寄存器: - CS(code segment)
- DS(data segment)
- SS(stack segment)
- ES(extra segment)
2.9.2 使用规则
当8086CPU要访问内存时,由这4个段寄存器提供内存单元的段地址
2.10 CS和IP
2.10.1 寄存器定位
CS和IP是8086CPU中最关键的寄存器,他们指示了CPU当前读取指令的地址。
2.10.2 CS与IP含义
- CS: 代码段寄存器
- IP: 指令指针寄存器【专用寄存器】
2.10.3 8086CPU工作过程的简要描述
- 从CS:IP指向内存单元,读取指令,读取的指令进入指令缓冲器
- IP=IP+所读取指令的长度,从而指向下一条指令
- 执行指令,转到步骤1,重复这个过程
2.10.4 开机时的CS和IP
- 在8086CPU加电启动或复位后(即CPU刚开始工作时)CS和IP被设置为CS=FFFFH,IP=0000H
- 即在8086PC机刚启动时,CPU从内存FFFF0H单元中读取指令执行
- FFFF0H单元中的指令是8086PC机开机后执行的第一条指令
2.10.5 修改CS、IP的指令
- 在CPU中,程序员能够【用指令读写】的部件只有【寄存器】,程序员可以通过改变寄存器中的内容实现对CPU的控制
- CPU从何处执行指令是由CS、IP中的内容决定的,程序员可以通过改变CS、IP中的内容控制CPU执行目标指令
- 如何修改CS和IP?
- 通过mov改变AX等,但是不能通过mov改变CS和IP
- 【
jmp 段地址:偏移地址( 冒号 : 只能用于连接“立即数”(即具体的数字) )】 可以用来同时修改CS和IP【该方法只限debug中可以】
指令中的段地址修改CS
偏移地址修改IP - 【
jmp 某一合法的寄存器(只能寄存器)】 仅修改IP的内容
比如:jmp ax 或者 jmp bx(类似于mov IP ax) - jmp是只具有一个操作对象的指令
2.11 代码段
- 可以将长度为N(N<=64KB)的一组代码,存放在一组地址连续、起始地址为16的倍数的内存单元中,这段内存是用来存放代码的,从而定义了一个代码段
- CPU中只认被CS:IP指向的内存单元中的内容为指令


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

实验常用Debug命令(不区分大小写)
- R命令:查看、改变CPU寄存器的内容
- r后面加寄存器的名称可以改变CPU寄存器的内容(cs、ip 也能修改)

- D命令:查看内存中的内容

- E命令:改写内存中的内容
- A命令:以汇编指令的格式在内存中写入一条机器指令
- debug中输入的默认是16位数
- 空格数量任意
- U命令:将内存中的机器指令翻译成汇编指令
- T命令:执行一条机器指令
- Q命令:按Q可以退出
第三章 寄存器(内存访问)
3.1 内存中字的存储

- 任何两个地址连续的内存单元,N号单元和N+1号单元,可以将他们看成两个存储单元,也可以看成一个地址为N的字单元中的高位字节单元和低位字节单元
- 注意:在内存的表示中,地址数值从低到高逐渐变大;书写存储内容时,低位字节存在低地址单元,高位字节存在高地址单元
例: 4E20H 存储规则:0号字节存储20,1号字节存储4E
3.2 DS和[address]
- 8086中有一个DS寄存器,通常用来存放要访问的数据的段地址
- 示例:读取10000H单元内容程序段
mov bx,1000H
mov ds,bx
mov al,[0]
上面三条指令将10000H(1000:0)中的数据读到al中
- mov指令已掌握的两种基础传送功能
- 将数据直接送入寄存器
- 将一个寄存器中的内容送入另一个寄存器中
- mov额外功能: 将内存单元内容送入寄存器
mov指令格式:mov 寄存器名,内存单元地址
[...]代表一个内存单元,括号内数值为内存单元偏移地址
CPU执行时自动取用DS存放的值作为该内存单元段地址 - 段寄存器赋值规则
不能直接将立即数送入段寄存器,必须借助通用寄存器中转
mov ds,1000H属于非法指令
流程:立即数 → 通用寄存器 → 段寄存器 - 编程练习:将AL数据送入内存10000H单元
mov bx,1000H
mov ds,bx
mov [0],al ;al中的字节数据送入1000H:0内存单元
3.3 字的传送
- 8086CPU为16位结构,拥有16根数据线,支持一次性传送16位数据(一个字)
- 示例代码
mov bx,1000H
mov ds,bx
mov ax,[0] ;1000H:0处16位字型数据送入ax
mov [0],cx ;cx中16位字型数据存入1000H:0内存单元
3.4 mov、add、sub指令
3.4.1 mov指令全部可用格式
- mov 寄存器, 数据 ;立即寻址
- 含义:把一个常数(立即数)直接塞进寄存器。
- 示例:
mov ax, 1234h- 注意:数据的大小必须和寄存器匹配(8位对8位,16位对16位)。
- mov 寄存器, 寄存器 ;寄存器寻址
- 含义:把源寄存器的值复制给目的寄存器。
- 示例:
mov ax, bx- 注意:段寄存器之间不能互传!(例如
mov ds, es是非法的,必须借助通用寄存器中转)。
- mov 寄存器, 内存单元 ;直接寻址
- 含义:去内存里取数据给寄存器。
- 示例:
mov ax, [0](表示取 DS:0000 处的字数据)- 补充:这里默认使用 DS 作为段地址。如果要指定其他段,需用段前缀,如
mov ax, es:[0]。
- mov 内存单元, 寄存器 ;直接寻址
- 含义:把寄存器的值存到内存里去。
- 示例:
mov [0], ax- 关键点:段地址默认取自 DS,段前缀 cs:[0] / ss:[0] / es:[0] 可手动更换段寄存器。
- mov 段寄存器, 寄存器 ;寄存器寻址
- 含义:修改段寄存器(CS除外)的值。
- 示例:
mov ds, ax- 铁律:绝对不能直接把立即数送给段寄存器!
- ❌
mov ds, 1000h(错误!)- ✅ 必须先给通用寄存器:
mov ax, 1000h->mov ds, ax
- mov 寄存器, 段寄存器 ;寄存器寻址
- 含义:读取段寄存器的值(通常用于保存现场)。
- 示例:
mov ax, ds
- mov 内存, 数据 ;立即数送内存(也是直接寻址)
- 场景:直接往内存里写死数据。
- 示例:
mov byte ptr [0], 'A'
注意: 同样需要用 byte ptr 或 word ptr 指明大小,否则汇编器会报错,因为它不知道这个 1 是占 1 个字节还是 2 个字节。
DOSBox debug.exe环境中mov ax, [1000:0]这种写法也是可以的。
3.4.2 add指令用法(双操作数)
- add 寄存器,数据 ;立即寻址
- add 寄存器,寄存器 ;寄存器寻址
- add 寄存器,内存单元 ;直接寻址
- add 内存单元,寄存器
- 内存单元默认段地址还是DS
- 8086 汇编语言中,ADD(加)和 SUB(减)指令严格要求操作数的位数(字长)必须保持一致!
| 组合类型 | 示例写法 | 合法性 | 关键点/避坑指南 |
|---|---|---|---|
| 寄存器 + 立即数 | add ax, 10 | ✅ | 最常用,大小由寄存器决定。 |
| 寄存器 + 内存 | add ax, [bx] | ✅ | 大小由寄存器决定,自动匹配。 |
| 内存 + 寄存器 | add [bx], ax | ✅ | 大小由寄存器决定,自动匹配。 |
| 内存 + 立即数 | add [bx], 10 | ❌ 报错 | 必须加修饰符! 改为: add byte ptr [bx], 10 或 word ptr。 |
| 内存 + 内存 | add [bx], [si] | ❌ 禁止 | CPU 不支持双内存操作。 解法:用寄存器中转(先 mov 再 add)。 |
3.4.3 sub指令用法(不带借位减法)
指令格式:sub op1,op2 运算逻辑:op1 = op1 - op2
- sub 寄存器,数据 ;立即寻址
- sub 寄存器,寄存器 ;寄存器寻址
- sub 寄存器,内存单元 ;直接寻址
- sub 内存单元,寄存器
| 指令写法 | 合法性 | 原因解析 |
|---|---|---|
sub [bx], ax | ✅ 合法 | BX 是合法指针;大小由 AX 自动推断;默认使用 DS 段。 |
sub [1], ax | ❌ 非法 | 纯数字缺段前缀;缺大小修饰符。 |
sub ds:[1], ax | ❌ 非法 | 加了段前缀,但依然缺大小修饰符。 |
sub word ptr ds:[1], ax | ✅ 合法 | 段前缀、大小修饰符齐全,符合纯数字寻址的严格语法。 |
3.5 数据段

如何访问数据段中的数据?
将一段内存规划为数据段属于编程层面的自定义安排;
操作方法: DS存放数据段段地址,配合各类寻址指令读写段内内存单元。

3.6 栈
- 8086CPU内置专用指令支持以栈模式访问内存,编程时可自定义一段内存作为栈空间
- 栈基础指令(以字为单位操作)
- push:入栈
- pop:出栈
- push ax:寄存器ax内数据送入栈
- pop ax:从栈顶取出数据存入ax
- push、pop操作统一以16位字为单位

- push、pop可实现寄存器与内存栈空间的数据互传
- 栈定位寄存器
SS段寄存器: 存放栈段段地址SP专用寄存器: 存放栈顶偏移地址- 任意时刻
SS:SP指向当前栈顶元素;栈为空时,SS:SP指向栈最高地址单元的下一个单元

- push、pop底层执行流程
- push ax 执行步骤
① sp = sp - 2
② 将ax数据存入SS:SP指向内存单元,SS:SP更新为新栈顶

- pop ax 执行步骤
① 取出SS:SP指向内存单元数据送入ax(原内存数据不会自动清除,新push会覆盖)
② sp = sp + 2

- push ax 执行步骤
- 栈空时SP指向:栈最高地址单元的下一个单元
3.7 栈顶超界的问题
SS、SP仅记录栈顶地址,仅能定位栈顶,无法自动限制栈边界
- 8086CPU硬件不做栈越界保护
- 两类危险越界场景
- 栈空时继续执行pop出栈
- 栈存满时继续执行push入栈,越界会改写栈外部代码/数据,程序出错,栈顶超界风险极高
- 8086无专用寄存器记录栈上下边界,边界校验需要程序员自行控制
3.8 栈段和push和pop指令格式



⚠️:push后面不能加立即数!!!!
- 将一段内存定义为栈段,仅为编程自定义规划,硬件无强制区分
SS:SP指向自定义栈段栈顶; 栈空时SP指向栈最高地址的下一个单元- 栈段最大容量:64KB
- SP数值变化范围0~FFFFH
栈空初始sp=0(指向FFFFH下一个单元0000H),持续压栈直至sp=0栈存满;再次push会循环覆盖原有栈数据 - 同一段内存可多重用途: 代码段/数据段/栈段,由CS、IP、SS、SP、DS寄存器配置决定用途
3.9 SS和SP的修改
寄存器赋值规则
- sp可直接使用mov传入立即数
- cs、ip、ss、ds不可直接mov立即数赋值
- 修改CS、IP:使用jmp跳转指令
- 修改SS、DS:通用寄存器中转,格式
mov ss,通用寄存器/mov ds,通用寄存器
第四章 第一个汇编程序
4.1 一个源程序从写出到执行的过程
- 一个汇编语言程序从写出到最终执行的简要过程:
编写->编译连接->执行 - 对源程序进行编译连接
- 使用汇编语言编译程序(MASM.EXE)对源程序文件中的源程序进行编译,产生目标文件【.obj文件】
- 再用连接程序(LINK.EXE)对目标文件进行连接,生成可在操作系统中直接运行的可执行文件【.EXE文件】。

- 可执行文件包含两部分内容
- 程序(从源程序的汇编指令翻译过来的机器码)和数据(源程序中定义的数据)
- 相关的描述信息(比如:程序有多大、要占多少内存空间等,
.exe都是ELF文件格式的,其信息就在程序头表、节头表中【了解就行,我是因为学过所以知道】)
- 执行可执行文件中的程序
- 在操作系统(如:MSDOS)中,执行可执行文件中的程序
- 操作系统依照可执行文件中的描述信息,将可执行文件中的机器码和数据加载入内存,并进行相关的初始化(比如:设置CS:IP指向第一条要执行的指令),然后由CPU执行程序
4.2 源程序的主要结构
源程序由 汇编指令+伪指令+宏指令(其他指令) 组成
- 伪指令:
编译器处理 - 汇编指令:编译为机器码

4.2.1 伪指令
- 没有对应的机器码的指令,不能由CPU直接执行
- 伪指令是由编译器来执行的指令,编译器根据伪指令来进行相关的编译工作
4.2.2 segment和ends【定义一个段】
- segment和ends是一对成对使用的伪指令,是编写汇编程序【必须】使用到的指令
- segment和ends的功能是定义一个段
- segment:说明一个段开始
- ends:说明一个段结束
- 一个段必须有一个名称来标识,使用格式为
段名 segment
段名 ends
- 一个汇编程序由多个段组成
这些段用来存放【代码、数据、或当作栈空间】来使用
一个有意义的汇编程序至少要有一个段,这个段用来存放代码。
4.2.3 end【程序结束标记】
- end是一个汇编程序的结束标记
- 编译器在编译汇编程序的过程中,如果碰到了伪指令end,就结束对源程序的编译
- 如果程序写完了,要在结尾处加上伪指令end,否则,编译器无法知道程序在何处结束
- 【切记】不要把end和ends搞混了
- end:汇编程序的结束标记
- ends:与segment成对出现,标记单个段结束
在4.2.6里面还有更详细的解析关于end 标号的作用
4.2.4 assume【寄存器和段的关联假设】
- 它假设某一段寄存器和程序中的某一个用segment…ends定义的段相关联
- 通过assume说明这种关联,在需要的情况下,编译程序可以将段寄存器和某一具体的段相联系

该伪指令写在代码段开头 或者 所有指令的最开头处!不然编译报错!

4.2.5 程序和源程序区分
- 我们将源程序文件中的所有内容称为【源程序】(里面有指令 注释 伪指令等 )
- 将源程序中最终由计算机执行处理的指令或数据称为【程序】
程序最先以汇编指令的形式,存储在源程序中,源程序中的汇编指令经编译、链接转化为机器码并存储于可执行文件中,此过程会剔除注释与伪指令,仅保留CPU可识别的机器指令及实际运算数据,即程序。
4.2.6 标号是什么,标号与段名称有所区别
- 一个标号指代了一个地址
- 段名称放在segment的前面,作为一个段的名称,这个段的名称最终将被汇编、连接程序处理为一个段的段地址
举个例子:
假设我们写了下面这段简单的汇编代码:
codesg segment ; 定义一个段,段名称叫 codesg
assume cs:codesg ; 告诉汇编器,cs 寄存器对应 codesg 段
start: ; 【标号】start 指代了下面mov ax, 1000h这条指令的偏移地址
mov ax, 1000h ; 机器码:B8 00 10
mov bx, 2000h ; 机器码:BB 00 20
jmp start ; 【标号】start 被用作跳转目标
codesg ends ; 段结束
end start ; 程序结束,入口点是 start
当我们按下“编译”和“链接”后,后台发生了以下变化:
第一步:汇编器(计算偏移地址) 汇编器逐行扫描代码,建立符号表:
- 它发现
start:标号位于代码段的第一条指令,所以它的偏移地址(IP)是 0000H。- 当它处理到
jmp start时,它去查表,发现start的偏移是 0000H,于是把这条指令翻译成机器码(比如EB FE,表示向后跳转到偏移 0 的位置)。第二步:链接器(分配段地址) 链接器把这段代码加载到内存中,假设操作系统给它分配的段基址(CS)是 1000H。
- 此时,源程序中的段名称
codesg就真正变成了物理上的段地址 1000H。- 标号
start对应的完整物理地址就是:1000H : 0000H。
CPU 执行 jmp start 时,它根本不知道 start 是什么,它只是忠实地把 IP 寄存器改成了 0000H,然后继续从 CS:IP (即 1000:0000) 处取指令执行。
那么 end start 是什么意思呢
- 告诉汇编器“代码到此为止”:汇编器在翻译代码时,遇到 end 就会停止工作,不再处理后面的任何内容。
- 指定程序的“入口点”:end 后面的 start 是一个标号。它在告诉操作系统:“当这个程序被加载到内存准备运行时,请把 CPU 的指令指针(CS:IP)直接指向 start 标号所在的位置,从那里开始执行。”
程序会从 start: 标号所在的位置 也就是 mov ax, 1000h 开始执行,底层就是它会在代码执行时 os会把 ip改成start 当然你也可以只写个end结尾 只是最后程序执行的时候会从默认的0开始执行!

4.2.7 DOS中的程序运行
- DOS是一个单任务操作系统
- 一个程序结束后,将CPU的控制权交还给使它得以运行的程序,我们称这个过程为:程序返回
4.2.8 程序返回
中断必须使用ah寄存器保存要执行的值

mov ax,4c00H
int 21H
【中断机制】是DOS最伟大的机制,Windows系统上是【消息机制】
这两条指令所实现的功能就是程序返回
DOS汇编程序安全退出机制速记:
mov ax, 4c00h:设置退出参数。AH=4Ch 为DOS终止程序功能号,AL=00h 为返回码(0表示正常退出)。int 21h:触发DOS 21号中断,将控制权交还操作系统,防止程序"跑飞"。- 作用:相当于C语言的
return 0;,是DOS汇编程序必不可少的"安全着陆伞"。
⚠️:在标准的 DOS 汇编程序中,这两行代码是必须加的。
ps: 因为这里涉及到os的知识,所以看不懂正常 说人话就是 这里面涉及os内核中软中断,随后进入内核态 进行进程(即程序)调度,4Ch 就是 DOS 系统规定的“带返回码终止程序”的功能号,会有对应程序写在os内核里面,然后调用该函数 通过等待将0写入父进程中 告诉子进程退出信息!至于写进ax是因为在执行对应程序的时候 其所需信息(调用哪个程序 程序参数等)就在ax里面 通过ax 锁定是哪个程序 里面传什么参数等(博主自己学过os内核 简单解释下 看不懂算了)
4.2.9 几个和结束相关的内容
- 段结束:伪指令,通知编译器一个段的结束【ends】
- 程序结束:伪指令,通知编译器程序的结束【end】
- 程序返回:汇编指令
mov ax,4c00H
int 21H
4.2.10 语法错误和逻辑错误
- 语法错误
- 程序在编译时被编译器发现的错误
- 容易发现
- 逻辑错误
- 在编写时不会表现出来的错误、在运行时会发生的错误
- 不容易发现
4.3 以简化的方式进行汇编和连接
- 汇编使用的程序:masm.exe
- 连接使用的程序:link.exe
- 简化方式进行汇编和连接的程序:ml.exe
我这里没装 ml.exe 就用
masm.exe和link.exe试试吧(添加;可以跳过提示信息hello.asm默认会变成hello.obj和hello.exe,不加;会有提示让你自己命名)
运行:
4.4 汇编和连接的作用
连接的作用
- 当源程序很大时,可以将他们分成多个源程序分别编译,每个源程序编译成为目标文件后,再用连接程序将它们连接在一起,生成一个可执行文件
- 程序中调用了某个库文件中的子程序,需要将这个库文件和该程序生成的目标文件连接到一起,生成一个可执行文件
- 一个源程序编译后,得到了存有机器码的目标文件,目标文件中的有些内容还不能直接用来生成可执行文件,连接程序将这些内容处理为最终的可执行信息。
所以在只有一个源程序文件,而又不需要调用某个库中的子程序的情况下,也必须用连接程序对目标文件进行处理,生成可执行文件
4.5 可执行文件中的程序装入内存并运行的原理
- 在DOS中,可执行文件中的程序P1若要运行,必须有一个正在运行的程序P2将P1从可执行文件中加载入内存,将CPU的控制权交给P1,P1才能得以运行
- 当P1运行完毕后,应该将CPU的控制权交还给使它得以运行的程序
- 操作系统的外壳
- 操作系统是由多个功能模块组成的庞大、复杂的软件系统,任何通用的操作系统,都需要提供一个称为shell(外壳)的程序,用户(操作人员)使用这个程序来操作计算机系统工作
- DOS中有一个程序
command.com(名字不一定哈,我们这里就这么称呼了),这个程序在DOS中称为命令解释器,也就是DOS系统的shell
在Mac中就是这个:
- 执行可执行文件1.exe时相关问题解答
(1)什么程序将CPU的控制权交给了1.exe?
(2)将程序1.exe加载入内存后,如何使程序得以运行?
(3)1.exe程序运行结束后,返回到了哪里?
1. 在DOS中直接执行1.exe时,是正在运行的command.com将1.exe中的程序加载入内存
2. command.com设置CPU的CS:IP指向程序的第一条指令(即,程序的入口),从而使程序得以运行
3. 程序运行结束后,返回command.com中,CPU继续运行command.com
4.6 程序执行过程中的跟踪

结论:如果使用debug将.exe加载到内存中,cx中存放了是程序的长度

里面其实还有一步重定位,这涉及到os内核的一些机制(博主因为学过linux内核所以知道 单纯汇编了解到这里其实就可以了),包括PSP是什么 如果学过OS 就可以理解成 管道(父子进程通信用的)、或者共享内存之类的,里面存放的就是是操作系统用来管理你这个程序的信息(可以理解成PCB)
内存地址(偏移量) 内存区域 说明
┌──────────────────┐
│ 0000H ~ 00FFH │ 程序段前缀 (PSP) DOS 用来和程序通信(256字节)
│ │ 程序启动时 DS 默认指向这里
├──────────────────┤
│ 0100H 及以后 │ 代码段 / 数据段 程序真正的代码和数据从这里开始
│ │ (.COM 文件代码强制从 0100H 开始)
│ │ (.EXE 文件紧随 PSP 之后)
├──────────────────┤
│ ... │ 其他段 / 空闲内存 数据段、堆栈段等依次向后排列
├──────────────────┤
│ 高地址 │ 堆栈段 (Stack) 栈通常位于分配内存的顶部,向下增长
└──────────────────┘
程序刚被加载时,DS 寄存器里存放的是 PSP 的段地址(即 SA)。如果你想在代码里访问自己定义的数据,必须手动把数据段的段地址赋值给 DS
第五章 【bx】和loop指令
5.1 [bx]&&inc指令&&描述性符号
[bx]和[0]类似,[0]表示内存单元,它的偏移地址是0;(ax\cx\dx只能存放数据 不能存放地址)[bx]同样也表示一个内存单元,它的段地址在DS中,偏移地址存放在bx中;
最终操作字节还是字,取决于目标寄存器是8位还是16位。
如果是立即数必须手动告诉大小!!
- 补充: inc指令:相当于C语言中的++运算符



5.2 Loop指令
loop指令用于实现循环
- 指令格式:loop 标号
CPU执行loop指令时执行两步操作:
cx = cx - 1- 判断cx的值,若不为零,则跳转到标号处执行;若为零,则向下顺序执行。
- 常规用法:cx中预先存放循环次数,配合loop实现循环。
- 标号
在汇编语言中,标号代表一个内存地址,用于标记跳转位置。 - 使用cx与loop配合实现循环的三点要求
- 在cx中存入循环次数
- loop后的标号要写在循环段前方
- 需要循环执行的代码放在标号与loop指令之间
- 标准循环程序框架
mov cx,循环次数
S: 循环执行的程序段
loop s【一个名字你实际爱用啥用啥】

5.3 在Debug中跟踪loop循环程序
注意:
- 汇编源程序中数字不能以字母开头,如FFFFH必须写成0FFFFH;Debug中无此限制。
- Debug新增两条调试命令:
G命令、P命令- G命令
- 不带参数:持续运行程序直到程序结束
- 带地址参数:运行到IP等于指定地址时暂停
- P命令
- T命令:单步进入(step into),跟进循环、中断等内部流程
- P命令:单步跳过(step over),一次性执行完loop完整循环、int中断等
- G命令
5.4 Debug和汇编编译器Masm对指令的不同处理
- Debug中
mov ax,[0]可直接读取DS:0内存单元数据送入ax - MASM编译器直接写
mov ax,[0]会被识别为立即数寻址,等价于mov ax,0- 正确写法1:显式添加段前缀
mov ax,ds:[0] - 正确写法2:借助bx间接寻址
- 正确写法1:显式添加段前缀
mov bx,0
mov ax,[bx]
; 等价 mov ax,ds:[bx]
不光是mov ,sub、add等也是 而且不光只针对[0] 只要是 [立即数]都会这样!
5.5 loop和[bx]的联合应用
需求:计算ffff:0~ffff:b共12个字节单元的数据和,结果存放在dx中
- 两个关键问题
- 12个8位数据加,总和可能超过8位,必须使用16位寄存器保存结果
- 8位内存数据不能直接和16位寄存器相加,类型不匹配

- 解决方案: 将8位数据送入al,自动补齐为16位ax后再参与加法!(即咱们找个中介!)
- 完整代码示例
codesg segment
assume cs:codesg
start:
;指定数据段段地址
mov ax,0ffffh
mov ds,ax
;寄存器初始化
mov ax,0
mov dx,0
mov bx,0
;设置循环次数12次
mov cx,0ch
circ:
;取出8位数据放入al,ax高8位自动为0,转为16位数值
mov al,[bx]
;累加至dx
add dx,ax
;偏移地址自增,访问下一字节
inc bx
loop circ
;程序返回DOS
mov ax,4c00h
int 21H
codesg ends
end start
5.6 段前缀
mov ax,[bx]默认段地址取自DS,偏移地址由bx提供- 可在内存寻址指令前手动指定段寄存器,显式声明段地址来源,这类前缀称为段前缀
示例:
mov ax,ds:[0]
mov ax,ds:[bx]
mov ax,cs:[0]
mov ax,cs:[bx]
5.7 一段安全的空间
- 8086DOS模式(
实模式,可以随意操作底层硬件)下,随意写入未知内存十分危险,该区域可能存放系统关键数据、程序代码,覆盖后会造成程序崩溃。 - 常规PC DOS环境下,
0:200 ~ 0:2FF共256字节空间不会被DOS及其他合法程序占用,编写测试程序时可放心读写这段内存。

5.8 es是什么
ES(Extra Segment): 附加段寄存器,顾名思义,它是用来“打辅助”的额外数据段。
第六章 包含多个段的程序
6.1 在代码段中使用数据
dw含义: 定义字型数据(define word),每个dw占2字节- 在数据段中使用dw,数据存放于数据段
- 在代码段中使用dw,数据存放于代码段
- 堆栈段同理
- 在程序第一条指令前添加标号start,并将该标号写在end伪指令后方
作用: 告知编译器程序入口位置,同时标记程序编译结束位置




我们要在程序的第一条指令处加上一个标号start 表示第一条指令的位置

修改后:

如果不这么写 CPU默认从所有代码中的0开始读,这里开头部分就是dw 数据 ,数据... 然后会将其变成一些2进制码 被CPU理解成一些奇奇怪怪的指令!
dw 是以字开通 db就是以字节开通 规则是一样的!!!
6.2 在代码段中使用栈



补充:题目要求逆序存储数据,优先使用栈结构(FILO先进后出)
使用dw预留内存空间,将这片空间当作栈段使用
6.3 将数据、代码、栈放入不同的段
6.1、6.2将数据、栈、代码混在同一个段存在缺陷,编程时需要手动区分代码、数据、栈区域- 混在单段的两大问题
- 代码、数据、栈混杂,程序结构混乱,可读性差
- 8086单个段最大容量64KB,若三者总空间超过64KB,单段无法存放
- 解决方式: 仿照代码段定义格式,单独定义数据段、栈段,在对应段内存放数据或开辟栈空间
- 多段程序核心知识点
- 自定义段名仅给人阅读区分,CPU无法识别data/code/stack这类名称
- data:存放数据的段
- code:存放指令的代码段
- stack:用作栈的段
assume cs:code,ds:data,ss:stack仅为伪指令,只给编译器做关联标记,CPU无法识别该指令; 执行后不会自动修改CS、DS、SS寄存器,不会自动绑定对应段

- 想要CPU正确区分三段,必须手写汇编指令修改段寄存器
- 代码段起始位置由end后标号指定(这个会自动修改CS:IP值)
- 手动给DS、SS赋值段地址(DS、SS必须手动进行修改)

- assume伪指令
不能省略,作用需要后续学习逐步理解
- 自定义段名仅给人阅读区分,CPU无法识别data/code/stack这类名称
【实验五】【包含start作用的二次解析】
- 段内存占用规则:
若段内数据总字节数为N,程序载入内存后段实际占用空间; 若N是16的倍数则占用N字节,否则向上补齐到16的整数倍
原理: 段起始地址必须是16的倍数,段最小占用16字节 - 源码中调换data、stack、code各段书写顺序,程序加载后CS、DS、SS对应的段基地址会改变
start标号作用- 省略start时编译器从上至下编译执行
- 若第一个定义的段是代码段,程序可正常运行;首段为数据/栈段则程序出错
- 代码示例1:交换data段内2个字数据顺序
assume cs:code,ds:data,ss:stack
;数据段
data segment
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
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
push ds:[0]
push ds:[2]
pop ds:[2]
pop ds:[0]
;程序返回
mov ax,4c00h
int 21h
code ends
end
- 代码示例2:a、b两段字节数据相加,结果存入data段
assume cs:code, ds:a, es:b
a segment
db 1,2,3,4,5,6,7,8
a ends
b segment
db 1,2,3,4,5,6,7,8
b ends
data segment
db 0,0,0,0,0,0,0,0
data ends
code segment
start:
; 1. 初始化段寄存器
mov ax, a
mov ds, ax ; DS 指向 a 段
mov ax, b
mov es, ax ; ES 指向 b 段
mov ax, data
; 注意:这里我们不能直接把 data 给 DS,因为 DS 已经被 a 占用了。
; 我们可以把结果先暂存,或者利用 SS 指向 data(只要确保不使用堆栈指令)
; 为了绝对安全,我们换个思路:用 BX 做偏移,手动计算物理地址太麻烦。
; 在简单实验中,我们可以让 SS 指向 data,但绝对不用 PUSH/POP/CALL/INT(除了最后的退出)
mov ss, ax ; 让 SS 暂时指向 data 段(仅用于寻址,严禁压栈!)
mov bx, 0
mov cx, 8
circ:
; 2. 取出 a 段和 b 段的字节,用 16 位寄存器相加防止溢出
mov al, ds:[bx] ; 取 a 段的数
mov ah, 0 ; 清零高8位,扩展成16位
mov dl, es:[bx] ; 取 b 段的数
mov dh, 0 ; 清零高8位
add ax, dx ; 16位相加,结果在 AX 中
; 3. 将结果存入 data 段(通过 SS 寻址)
; 因为两个8位最大255+255=510,不会超过16位,所以低8位存在 data 段即可
mov ss:[bx], al ; 把结果的低8位存入 data 段
inc bx
loop circ
; 4. 程序返回
mov ax, 4c00h
int 21h
code ends
end start
- 代码示例3:将a段前8个字型数据逆序存入b段
assume cs:code, ds:a, ss:b
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:
; 1. 初始化段寄存器
mov ax, a
mov ds, ax ; DS 指向 a 段(数据源)
mov ax, b
mov ss, ax ; SS 指向 b 段(作为栈)
mov sp, 10H ; 栈顶指针初始化为 16(即 10H),指向 b 段的末尾
mov bx, 0 ; BX 作为 a 段的偏移地址
mov cx, 8 ; 循环 8 次
circ:
; 2. 将 a 段的数据压入 b 段(栈)
push ds:[bx] ; 从 a 段取出一个字,压入栈(b 段)
add bx, 2 ; 偏移地址加 2(因为是字型数据 dw)
loop circ
; 3. 程序返回
mov ax, 4c00h
int 21h
code ends
end start
第七章 更灵活地定位内存地址
本章主要讲解一些更灵活的定位内存地址的方法和相关的编程方法
7.1 and和or指令
7.1.1 and 逻辑与指令
功能:按二进制位执行与运算
- 示例
mov al,01100011B
and al,00111011B
;执行后 al=00100011B
- 作用: 将指定位置0,其余位不变
- 将al第6位清0:
and al,10111111B - 将al第7位清0:
and al,01111111B - 将al第0位清0:
and al,11111110B
7.1.2 or 逻辑或指令
功能: 按二进制位执行或运算
- 示例
mov al,01100011B
or al,00111011B
;执行后 al=01111011B
- 作用: 将指定位置1,其余位不变
- 将al第6位置1:
or al,01000000B - 将al第7位置1:
or al,10000000B - 将al第0位置1:
or al,00000001B
7.2 关于ASCII码
ASCII是计算机通用的字符编码方案,单个字符占用8位(1字节)。

7.3 以字符形式给出的数据
- 汇编中可用单引号
'xxx'表示字符数据 - 编译器会自动把字符转换为对应ASCII码存储
- 示例
db 'unIX' ;等价 db 75H,6EH,49H,58H
mov al,'a' ;等价 mov al,61H
- 大小写字母ASCII规律
- 小写字母 = 对应大写字母 + 32(20H)
- 大写起始:
A=41H;小写起始:a=61H
| 大写 | 二进制 | 小写 | 二进制 |
|---|---|---|---|
| A | 01000001 | a | 01100001 |
| B | 01000010 | b | 01100010 |
| C | 01000011 | c | 01100011 |
| D | 01000100 | d | 01100100 |
7.4 大小写转换的问题
方案一(需要条件判断,暂未学习)
- 判断当前字符是大写还是小写
- 根据判断执行
+20H转小写 /-20H转大写
方案二(位运算,无需判断)
- 全部转大写:第5位清0
and al,11011111B
- 全部转小写:第5位置1
or al,00100000B
7.5 [bx+常数] 寻址方式
指令示例:mov ax,[bx+200]
- 含义: 读取一个字单元送入ax;段地址默认DS,偏移地址 = bx数值 + 200
- 等价写法
mov ax,[200+bx]
mov ax,200[bx]
mov ax,[bx].200
7.6 [bx+idata] 处理数组
需求: 将第一个字符串全部大写,第二个字符串全部小写
- 字符串1起始偏移0,字符串2起始偏移5
[bx]操作第一串,[bx+5]操作第二串,单循环完成处理
assume cs:codesg,ds:datasg
datasg segment
db 'BaSiC'
db 'MinIX'
datasg ends
codesg segment
start:
mov ax,datasg
mov ds,ax
mov bx,0
mov cx,5 ;循环5次
circ:
mov al,[bx]
and al,11011111b
mov [bx],al
mov al,[bx+5]
or al,00100000b
mov 5[bx],al
inc bx
loop circ
mov ax,4c00h
int 21h
codesg ends
end start
对比C语言数组:
- C:
a[i]、b[i],a、b为地址常量 - 汇编:
0[bx]、5[bx],[bx+idata]、[bx].5模拟数组下标机制,这么做不会更改bx的值
7.7 SI和DI寄存器
已学寄存器:AX、BX、CX、DX、DS、CS、SS、ES、IP、SP
SI、DI功能和BX近似,BX不足时使用(当然也能存单纯的数字)- 区别: SI、DI只能整体16位使用,不能拆分为8位寄存器
- 等价寻址示例:
;写法1 bx
mov bx,0
mov ax,[bx]
;写法2 si
mov si,0
mov ax,[si]
;写法3 di
mov di,0
mov ax,[di]
带常量寻址同样通用:
mov bx,0
mov ax,[bx+123]
mov si,0
mov ax,[si+123]
mov di,0
mov ax,[di+123]
- 实战:

assume cs:code,ds:data
data segment
db 'welcome to masm!'
db '................'
data ends
code segment
start:
mov ax,data
mov ds,ax
mov si,0
mov di,16
mov cx,8
circ:
mov ax,0[si]
mov [di],ax
inc di
inc di
inc si
inc si
loop circ
mov ax,4c00h
int 21h
code ends
end start
7.8 [bx+si] 和 [bx+di]
- 以
[bx+si]为例: 偏移地址 = bx内容 + si内容,段地址默认DS - 等价书写:
[bx][si]
7.9 [bx+si+常数] / [bx+di+常数]
- 偏移地址 = bx + si + 常量数值
mov ax,[bx+si+200]等价写法
mov ax, [si+bx+200] ; 交换基址和变址
mov ax, [200+bx+si] ; 位移量放在最前面
mov ax, [bx+200+si] ; 位移量夹在中间
mov ax, 200[bx][si] ; 极其常见的写法,表示在 bx+si 的基础上偏移 200
mov ax, 200[si][bx] ; 同样合法
mov ax, [bx+si].200
mov ax, [bx].200[si] ; 混合使用点号和加号也是允许的
200[bx+si]这么写违法
7.10 不同寻址方式的灵活应用
7.10.1 五种内存寻址总结
ds:[常数]直接寻址:固定常量偏移[bx]寄存器间接寻址:寄存器存放偏移[bx+常数]寄存器相对寻址:基址+固定偏移[bx+si]基址变址寻址[bx+si+常数]相对基址变址寻址
7.10.2 编程1:每个单词首字母转大写

assume cs:code,ds:data
data segment
db '1. file ';都是16位
db '2. edit '
db '3. search '
db '4. view '
db '5. options '
db '6. help '
data ends
code segment
start:
mov ax,data
mov ds,ax
mov bx,0
mov cx,6
circ:
mov al,[bx+3]
and al,11011111b
mov [bx+3],al
add bx,16
loop circ
mov ax,4c00h
int 21h
code ends
end start
7.10.3 编程2:全部单词字母转大写(双重循环)
核心要点:loop执行时先cx=cx-1再判断;
双重循环需保存外层cx
两种方案: 通用寄存器暂存 / 栈暂存(推荐栈)
方案1:寄存器保存外层cx
assume cs:code, ds:data
data segment
db 4,4,6,4,7,4 ; 定义长度表:记录每个菜单单词的字母个数
db ' ' ; 补齐占位:预留10个字节,防止访问越界或对齐
db '1. file ' ; 菜单项1:偏移地址 16,单词 "file" 从偏移 19(16+3) 开始
db '2. edit ' ; 菜单项2:偏移地址 32,单词 "edit" 从偏移 35(32+3) 开始
db '3. search ' ; 菜单项3:偏移地址 48,单词 "search" 从偏移 51(48+3) 开始
db '4. view ' ; 菜单项4:偏移地址 64,单词 "view" 从偏移 67(64+3) 开始
db '5. options ' ; 菜单项5:偏移地址 80,单词 "options" 从偏移 83(80+3) 开始
db '6. help ' ; 菜单项6:偏移地址 96,单词 "help" 从偏移 99(96+3) 开始
data ends
code segment
start:
; --- 1. 初始化段寄存器与指针 ---
mov ax, data
mov ds, ax ; 将数据段基地址加载到 DS 寄存器
mov bx, 16 ; BX 作为基址寄存器,初始指向第一个菜单项的起始偏移(16)
mov si, 0 ; SI 作为变址寄存器,初始指向单词内的第0个字母
mov di, 0 ; DI 作为长度表的索引,初始指向长度表的第0个字节
; --- 2. 外层循环:遍历6个菜单项 ---
mov cx, 6 ; 设置外层循环次数为 6(共6个菜单)
outer:
mov dx, cx ; 【关键】将外层循环次数暂存到 DX,防止被内层循环破坏
; --- 3. 内层循环:遍历当前菜单的单词字母 ---
mov cx, 0 ; 清空 CX 高8位,防止残留数据
mov cl, [di] ; 从长度表中取出当前单词的字母个数,作为内层循环次数
inner:
; 取出当前字母并转为大写
mov al, [bx][si+3] ; 寻址:当前行基址(BX) + 单词内偏移(SI) + 跳过前缀(3)
and al, 11011111b ; 位运算:将 ASCII 码第5位清零,小写转大写
mov [bx][si+3], al ; 将转换后的大写字母写回原内存位置
inc si ; SI 自增,指向单词的下一个字母
loop inner ; 内层循环控制:CX减1,若不为0则跳转至 inner
; --- 4. 准备处理下一个菜单项 ---
add bx, 16 ; BX 增加 16,定位到下一行菜单的起始位置
mov si, 0 ; 重置 SI,下一个单词重新从第0个字母开始遍历
inc di ; DI 自增,指向长度表中的下一个长度值
mov cx, dx ; 【关键】从 DX 恢复外层循环的剩余次数
loop outer ; 外层循环控制:CX减1,若不为0则跳转至 outer
; --- 5. 安全退出程序 ---
mov ax, 4c00h
int 21h
code ends
end start
方案2:栈保存外层cx(推荐)
assume cs:code, ds:data, ss:stack
data segment
db 4,4,6,4,7,4 ; 定义长度表:记录每个菜单单词的字母个数
db ' ' ; 补齐占位:预留10个字节,防止访问越界或对齐
db '1. file ' ; 菜单项1:偏移地址 16,单词 "file" 从偏移 19(16+3) 开始
db '2. edit ' ; 菜单项2:偏移地址 32
db '3. search ' ; 菜单项3:偏移地址 48
db '4. view ' ; 菜单项4:偏移地址 64
db '5. options ' ; 菜单项5:偏移地址 80
db '6. help ' ; 菜单项6:偏移地址 96
data ends
stack segment
dw 0,0,0,0,0,0,0,0 ; 定义栈段,分配 16 个字节(8个字)的空间
stack ends
code segment
start:
; --- 1. 初始化段寄存器与指针 ---
mov ax, data
mov ds, ax ; 将数据段基地址加载到 DS 寄存器
mov ax, stack
mov ss, ax ; 将栈段基地址加载到 SS 寄存器
mov sp, 16 ; 栈顶指针指向栈底(16字节处),栈从高地址向低地址生长
mov bx, 16 ; BX 作为基址寄存器,初始指向第一个菜单项的起始偏移(16)
mov si, 0 ; SI 作为变址寄存器,初始指向单词内的第0个字母
mov di, 0 ; DI 作为长度表索引,必须初始化为 0!
; --- 2. 外层循环:遍历6个菜单项 ---
mov cx, 6 ; 设置外层循环次数为 6(共6个菜单)
outer:
push cx ; 【栈技巧】将外层循环次数压入栈中保存,防止被内层循环破坏
; --- 3. 内层循环:遍历当前菜单的单词字母 ---
mov cx, 0 ; 清空 CX 高8位,防止残留数据
mov cl, [di] ; 从长度表([di])中取出当前单词的字母个数,作为内层循环次数
inner:
; 取出当前字母并转为大写
mov al, [bx][si+3] ; 寻址:当前行基址(BX) + 单词内偏移(SI) + 跳过前缀(3)
and al, 11011111b ; 位运算:将 ASCII 码第5位清零,小写转大写
mov [bx][si+3], al ; 将转换后的大写字母写回原内存位置
inc si ; SI 自增,指向单词的下一个字母
loop inner ; 内层循环控制:CX减1,若不为0则跳转至 inner
; --- 4. 准备处理下一个菜单项 ---
add bx, 16 ; BX 增加 16,定位到下一行菜单的起始位置
mov si, 0 ; 重置 SI,下一个单词重新从第0个字母开始遍历
inc di ; DI 自增,指向长度表中的下一个长度值
pop cx ; 【栈技巧】从栈中弹出之前保存的外层循环次数,恢复到 CX
loop outer ; 外层循环控制:CX减1,若不为0则跳转至 outer
; --- 5. 安全退出程序 ---
mov ax, 4c00h
int 21h
code ends
end start
第八章 数据处理的两个基本问题
本章对前面所有内容具有总结性
计算机是处理、运算数据的机器,包含两个核心基础问题:
- 待处理的数据存放在哪里?
- 待处理的数据长度是多少?
机器指令必须显式或隐式说明这两点,否则CPU无法执行运算
8.1 bx、si、di、bp 寻址寄存器规则(bp是什么)
| 特性 | BX (Base Register) | BP (Base Pointer)本质上就是一个普通的 16 位通用寄存器 |
|---|---|---|
| 默认段寄存器 | DS (数据段) | SS (栈段) |
| 主要用途 | 访问普通内存数据、数组 | 访问栈数据、函数参数传递 |
| 寻址能力 | 可作为基址寄存器 | 可作为基址寄存器 |
- 8086CPU仅4个寄存器可放在
[]内做内存偏移寻址:bx、bp、si、di []内寄存器允许的合法组合(单个/成对)- 单独使用:
bx / bp / si / di - 成对合法组合:
bx+si、bx+di、bp+si、bp+di
- 单独使用:
- 非法组合
[bx+bp][si+di]
- bp专属段寻址规则
若[]中出现bp,且无段前缀显式指定段寄存器,则默认段地址取自SS栈段
示例:mov ax,[bp] mov ax,[bp+常数] mov ax,[bp+si] mov ax,[bp+si+常数]
8.2 机器指令处理的数据所在位置
- 数据处理指令分为三类: 读取、写入、运算
- 机器指令不关心数据具体数值,只关注指令执行前数据存放位置
- 数据仅能存放于三处: CPU内部寄存器、内存、端口(端口后面讲)

8.3 汇编语言中数据位置的三种表达
- 立即数: 指令中直接给出数值

寄存器: 数据存放于内部寄存器- 段地址(SA)+偏移地址(EA) 访问内存
- 默认段寄存器规则
- 无bp:默认DS
- 含bp:默认SS
- 当然可手动添加段前缀显式指定段寄存器
mov ax,ds:[bp] mov ax,es:[bx] mov ax,ss:[bx+si] mov ax,cs:[bx+si+8] - 默认段寄存器规则
8.4 寻址方式总结(含一些疑难点的例子)
指令会写入指令缓冲寄存器内 直接寻址、寄存器相对寻址和相对基址变址寻址 都会直接读该寄存器内的值(具体过程看b站小甲鱼的课程39)
还要⚠️:指令的二进制读取是和数据读取反着来的 如图8B07(存储该值的就是指令缓冲寄存器 里面存了 mov ax,[bx]的机器码)部分


一些容易错的例子
- ✅
mov ax, [0](正确)
- 含义:去内存地址
DS:0的地方取一个字(Word)放入 AX。- 解释:方括号里只有一个数字,这叫直接寻址。因为数字前面没有段寄存器,CPU 默认去数据段(DS)里找。
- ✅
mov ax, ds:[0](正确,且强烈推荐)
- 含义:去内存地址
DS:0的地方取一个字放入 AX。- 解释:这里你显式地加上了
ds:前缀。它和上一行的效果完全一模一样。- 💡 为什么要这么写? 原因在
5.4部分
- ✅
mov ax, [9](正确)
- 含义:去内存地址
DS:9的地方取一个字放入 AX。- 解释:和第一行同理,只是偏移量变成了 9。
mov ax,ds:[9](正确)mov ax,ds[0]、ds[9]是 错误的!
这么写是合理(寄存器)的:
mov ax, ds:[bx] ; ✅ 显式指定段寄存器,最严谨
mov ax, [bx] ; ✅ 省略 ds:,因为 BX 默认就是配合 DS 的
mov ax, ds:100[bx] ; ✅ 如果 BX 是 0,这种带偏移的写法也可以

8.5 指令要处理的数据有多长?
- 8086仅支持两种操作尺寸:byte(字节8位)、word(字16位),指令必须区分操作长度
- 三种判断数据长度的方式
方式1:通过寄存器位数区分
mov al,1 ;al为8位,字节操作
方式2:无寄存器时,使用byte ptr/word ptr显式声明内存单元长度
- byte ptr 字节单元
mov byte ptr ds:[0],1
inc byte ptr [bx]
inc byte ptr ds:[0]
add byte ptr [bx],2
- word ptr 字单元
mov word ptr ds:[0],1
inc word ptr [bx]
inc word ptr ds:[0]
add word ptr [bx],2
方式3:指令自带固定操作长度
push、pop 固定为16位字操作
关键说明: 仅内存寻址、无寄存器参与时,必须用ptr指明长度,CPU无法自动区分字节/字单元
8.6 寻址方式的综合应用
结合前面所有基址、变址、相对寻址完成复杂内存遍历、批量数据处理(这一部分就知识点而言并不重要 感兴趣的可以直接看b站小甲鱼的第40节课 直接跳过也没事)
8.7 div 除法指令
8.7.1 除法运算规则
- 除数:8位/16位,存放于寄存器或内存单元
- 被除数默认存放规则
除数位数 被除数存储位置 8位 AX(16位) 16位 DX(高16位)+AX(低16位) 组合32位 - 运算结果存放
除数 商存放 余数存放 8位 AL AH 16位 AX DX
8.7.2 div 两种格式
- div 寄存器(除数) 寄存器可以通过8位(如bl、bh都可以)、16位的寄存器位数来自行区分
- div 内存单元(除数)(因为无寄存器所有要显示声明)

8.7.3 指令示例
;8位除数,16位被除数AX
div byte ptr ds:[0]
;商=AX / 内存字节 → AL
;余数=AX % 内存字节 → AH
;16位除数,32位被除数DX:AX
div word ptr es:[0]
;商=(DX<<16 + AX) / 内存字 → AX
;余数=(DX<<16 + AX) % 内存字 → DX
8.7.4 例题:计算100001 ÷ 100
100001 > 65535,需要DX+AX存放32位被除数,除数100使用16位寄存器bx
100001 = 186A1H → DX=0001H,AX=86A1H
mov dx,1;这个1是十进制的
mov ax,86A1H
mov bx,100;这个100是10进制的
div bx
8.8 伪指令 dd
dd: define double word,定义32位双字型数据- db/dw/dd对比
data segment
db 1 ;1字节
dw 1 ;2字节字
dd 1 ;4字节双字
data ends
- 例题: data段第一个双字 ÷ 第二个字,商存入第三个字单元
assume cs:code, ds:data
data segment
dd 100001 ; 定义双字(32位)被除数:100001。低16位在前,高16位在后
dw 100 ; 定义字(16位)除数:100
dw 0 ; 定义字(16位)用于存放计算结果(商)
data ends
code segment
start:
; --- 1. 初始化数据段寄存器 ---
mov ax, data
mov ds, ax
; --- 2. 准备被除数 (DX:AX) ---
mov bx, 0 ; BX 作为基址指针,初始指向数据段的起始偏移地址 0
mov ax, [bx] ; 取出双字的低16位(100001的低16位),放入 AX
mov dx, [bx+2] ; 取出双字的高16位(100001的高16位),放入 DX
; --- 3. 执行除法运算 (32位 ÷ 16位) ---
; 此时被除数是 DX:AX (32位),除数是内存中的字数据 [bx+4] (16位)
; 运算规则:商 -> AX,余数 -> DX
div word ptr [bx+4]
; --- 4. 保存运算结果 ---
mov [bx+6], ax ; 将计算得到的商(AX),存入数据段偏移地址 6 的字单元中
; --- 5. 安全退出程序 ---
mov ax, 4c00h
int 21h
code ends
end start
8.9 dup 重复操作符
-
dup属于编译器识别的伪操作符,配合db/dw/dd批量重复定义数据(编译阶段处理) - 使用格式:
数据定义符 次数 dup(数据列表) - 示例
db 3 dup(0) ;3个0,3字节
db 3 dup(0,1,2) ;0,1,2循环3次,共9字节
db 3 dup('abc','ABC') ;'abcABC'重复3次,共18字节


- 通用格式
db N dup(字节数据)
dw N dup(字型数据)
dd N dup(双字型数据)
【实验七】表格数据整理程序
assume cs:code, ds:data, ss:stack, es:table
stack segment
dw 8 dup(0) ; 开辟 16 字节栈空间
stack ends
data segment
; --- 21个年份字符串,每个4字节 (共 21 * 4 = 84 字节) ---
db '1975','1976','1977','1978','1979','1980','1981','1982','1983'
db '1984','1985','1986','1987','1988','1989','1990','1991','1992'
db '1993','1994','1995'
; --- 21个总收入,双字(4字节) (共 21 * 4 = 84 字节) ---
dd 16,22,382,1356,2390,8000,16000,24486,50065,97479,140417,197514
dd 345980,590827,803530,1183000,1843000,2759000,3753000,4649000,5937000
; --- 21个雇员人数,字(2字节) (共 21 * 2 = 42 字节) ---
dw 3,7,9,13,28,38,130,220,476,778,1001,1442,2258,2793,4037,5635,8226
dw 11542,14430,15257,17800
data ends
table segment
; 21行表格,每行16字节:'year summ ne ?? '
db 21 dup('year summ ne ?? ')
table ends
code segment
start:
; --- 1. 初始化各个段寄存器 ---
mov ax, data
mov ds, ax ; DS 指向数据段(存放原始数据)
mov ax, table
mov es, ax ; ES 指向表格段(存放目标结果)
mov ax, stack
mov ss, ax ; SS 指向栈段
mov sp, 16 ; 初始化栈顶指针 SP
; --- 2. 初始化寻址用的指针寄存器 ---
mov si, 0 ; SI 将作为 data 段中“雇员人数(dw)”的偏移指针
mov di, 0 ; DI 将作为 table 段中“每行内部的偏移量”
mov bx, 0 ; BX 将作为 table 段中“行号”的偏移指针 (每行16字节)
mov bp, 0 ; BP 将作为 data 段中“年份(db)”和“收入(dd)”的偏移指针
mov cx, 21 ; 循环计数器,共21条数据
outer:
; ================= 第一步:搬运“年份” (4字节) =================
push si ; 【保护现场】因为下面要用 SI 做变址,先压栈保护外层 SI
add si, si ; SI = SI * 2 (因为年份是4字节,这里配合后面的寻址巧妙取值)
; 从 data 段取年份的低16位和高16位,放入 table 对应位置
mov ax, ds:[bp] ; 取年份前两个字符 (如 '19')
mov es:[bx][di], ax ; 存入 table 的 'year' 位置 (偏移0)
mov ax, ds:84[bp] ; 取年份后两个字符 (如 '75'),84是年份段的总长度
mov es:[bx][di+2], ax ; 存入 table 的 'year' 位置 (偏移2)
; ================= 第二步:搬运“总收入” (4字节) =================
; 收入段紧跟在年份段之后,偏移量也是基于 bp 的
mov ax, ds:84[bp] ; 取收入的高16位 (注意:这里原文逻辑需结合大小端序理解)
mov es:[bx][di+5], ax ; 存入 table 的 'summ' 位置 (偏移5)
; *注:原文这里只搬运了2个字节到 summ,实际上双字需要4字节空间,这里按你的原逻辑保留*
pop si ; 【恢复现场】恢复 SI 的值
; ================= 第三步:搬运“雇员人数” (2字节) =================
mov al, ds:168[si] ; 168 = 84(年份) + 84(收入)。取雇员人数的低8位
mov es:[bx][di+10], al ; 存入 table 的 'ne' 位置 (偏移10)
; --- 更新指针,准备处理下一行 ---
inc si ; SI 指向下一个雇员人数 (dw 占2字节,但这里用了巧妙偏移)
add di, 2 ; DI 在 table 行内偏移 +2
add bx, 16 ; BX 指向 table 的下一行 (每行固定 16 字节)
loop outer ; 循环 21 次
mov ax, 4c00h
int 21h
code ends
end start
第九章 转移指令的原理
8086CPU的转移指令分为以下5类:
- 无条件跳转指令(如:jmp)
- 条件跳转指令
- 循环指令(如:loop)
- 过程调用,等价于C语言中的函数
- 中断指令
9.1 操作符offset(伪指令)&& nop
offset是由编译器处理的操作符,作用: 获取标号对应的偏移地址
示例:
s: mov ax,offset s 相当于 mov ax,s对应的地址


nop就是机器码占一个字节 没具体含义 理解成占字符就行
9.2 jmp 无条件跳转指令
- 功能:无条件转移,分为两种修改寄存器的情况
jmp 段地址:偏移地址:同时修改CS与IP- 段地址赋值给CS,偏移地址赋值给IP
- 该写法仅Debug可用,MASM编译器不识别
jmp 16位通用寄存器:仅修改IP
示例:jmp ax、jmp bx,等价于把寄存器内容送入IP
- jmp指令需要包含两类信息
- 跳转目标地址
- 跳转距离分类:段间转移、段内短转移、段内近转移
9.3 基于位移量的jmp指令
9.3.1 jmp short 标号(段内短转移)&&机器码与jmp汇编指令对应不匹配问题
标号翻译后的地址既可以是 8 位,也可以是 16 位。这主要取决于你使用的指令类型以及标号与当前指令之间的“距离”。
- 修改IP的位移范围:
-128 ~ 127 - 向前跳转最大128字节(负数用补码存储),向后跳转最大127字节
- 机器码特点: 不存放目标绝对地址,仅存放相对当前IP的8位位移量,CPU靠位移修改IP

一个结论:

但是例外来了:

■ jmp 0008 (Debug 中的表示)或jmp short s (汇编语言中的表示)所对应的机器码为EB 03,注意,这个机器码中竟不包含转移的目的地址。
■ 这意味着,CPU 在执行EB 03的时候,并不知道转移目的地址。


那么CPU是根据什么进行转移的呢?

这张图展示了 CPU 执行“短跳转”(jmp short)的相对寻址原理,核心逻辑如下:
- 取指与自动后移
CPU 读取指令EB 03后,IP 会自动增加指令长度(2字节),指向下一条指令的地址(此时 IP = 0008)。- 基于当前位置计算
CPU 并不直接跳转到绝对地址,而是将指令中的偏移量(03)加到当前的 IP 上。- 最终落点
计算公式为:当前 IP (0008) + 偏移量 (3) = 目标 IP (000B)。CPU 并不直接知道目的地是 000B,它是先走到下一条指令的位置(0008),然后根据指令里给的提示(+3),再往前跨了 3 步,最终落在了 000B 这个位置(inc ax)。这就是所谓的相对跳转。
9.3.2 jmp near ptr 标号(段内近转移)
- 采用16位位移量,
IP = IP + 16位位移 - short是8位位移,near ptr是16位位移,跳转范围更大

9.4 目标地址直接存于机器码:jmp far ptr 标号
- far ptr 实现段间远转移
- 机器码内直接存放目标
段地址+偏移地址 - 执行逻辑:
CS = 标号所在段的段地址 IP = 标号在段内的偏移地址
| 跳转类型 | 汇编写法 | 机器码特征 | 核心逻辑 | 形象比喻 |
|---|---|---|---|---|
| 相对跳转 | jmp short s | EB + 1字节位移 | 当前IP + 偏移量 | 往前走3步 |
| 绝对跳转 | jmp far ptr s | EA + 2字节IP + 2字节CS | 直接修改CS和IP | 直接去1001号房间 |
- 在汇编里写成:
jmp far ptr 标号 - 对应的机器码是:
EA + 目标偏移地址(IP) + 目标段地址(CS)
9.5 转移地址存放在寄存器中
指令格式:jmp 16位寄存器
功能: 将16位寄存器内的值直接送入IP,完成段内跳转
这个早就学过
9.6 转移地址存放在内存单元的两种jmp格式
9.6.1 jmp word ptr 内存单元(段内转移)
- 从内存读取一个字作为新IP,仅修改IP
- 寻址方式可使用任意合法内存寻址写法

9.6.2 jmp dword ptr 内存单元(段间转移)
- 内存双字:低字为偏移地址(送入IP),高字为段地址(送入CS)
IP = 内存单元地址处的字 CS = 内存单元地址+2 处的字 - 等价功能:
jmp 段地址:偏移地址

9.7 jcxz 条件短跳转指令
- 所有条件跳转均为短转移,机器码只存8位位移,范围-128~127,
loop同样属于短转移指令 - 格式:
jcxz 标号 - 执行逻辑:
- 若CX = 0:
IP = IP + 8位位移(可正可负),跳转到标号- 8位位移 = “标号”处的地址 - jcxz指令后的第一个字节的地址
- 若CX ≠ 0:不跳转,顺序向下执行

- 若CX = 0:
9.8 loop 循环短转移指令
- 循环指令全部为短转移,机器码存储8位位移量
- 格式:
loop 标号 - 内部执行步骤
- CX = CX - 1
- 若CX ≠ 0: IP加上8位位移,跳转至标号循环
- 若CX = 0:停止循环,顺序向下执行
- CX专门用于存放循环次数

9.9 相对位移跳转的设计意义
- 依靠位移跳转支持代码浮动装配,程序段在内存任意位置加载都可正常运行,方便代码复用

- 若机器码直接写死绝对段地址:偏移地址,程序搬家后地址失效,运行出错
- 四类基于位移跳转的指令:
1. jmp short 标号 2. jmp near ptr 标号 3. jcxz 标号 4. loop 标号
9.10 编译器对转移超界的检测
短转移、近转移都有固定位移范围限制; 若源程序中跳转距离超出对应位移上限,编译时编译器直接报错。

第十章 call和ret指令
10.1 ret 和 retf 指令
10.1.1 ret 近返回(段内返回)
依靠栈内数据修改IP,完成段内近转移,执行两步:
IP = SS×16 + SP取出栈顶字送入IPSP = SP + 2栈顶上移
等价汇编:pop ip
10.1.2 retf 远返回(段间返回)
同时修改CS、IP实现段间远转移,执行四步:
IP = SS×16 + SPSP = SP + 2CS = SS×16 + SPSP = SP + 2
栈内两个字:低字给IP,高字给CS
等价汇编:
pop ip
pop cs
10.2 call 调用指令
- call常与ret配对实现子程序调用,执行固定两步:
- 将当前IP(段内调用)或CS+IP(段间调用)压栈保存返回地址
- 跳转到目标地址
- call不支持短转移,其余跳转逻辑和jmp一致
10.2.1 call 标号(段内近调用,16位位移)
执行流程:
SP = SP - 2[SS:SP] = IP保存当前IPIP = IP + 16位位移跳转标号
等价:
push ip
jmp near ptr 标号
; 段内调用示例
mov ax, 0 ; 初始化 AX
call s ; 【核心】将下一条指令的 IP 压入栈,然后跳转到标号 s
inc ax ; 子程序返回后会继续执行这里
s:
pop ax ; 将栈中保存的 IP 弹出到 AX(此时 AX 等于 inc ax 的偏移地址)
ret ; 弹出栈顶的 IP 赋值给 IP 寄存器,返回主程序
10.4 call far ptr 标号(段间远调用)
- 实现跨段转移
- CPU执行步骤:
SP = SP - 2;栈顶移动[SS:SP] = CS;先把cs压栈SP = SP - 2;栈顶移动;[SS:SP] = IP;然后把ss压栈
等价:
push cs
push ip
jmp far ptr 标号
; 段间调用示例
mov ax, 0 ; 初始化 AX
call far ptr s ; 【核心】将当前的 CS 和下一条指令的 IP 依次压入栈中,并跳转到 s
inc ax ; 子程序返回后会继续执行这里
s:
pop ax ; 弹出栈顶的 IP 赋值给 AX(此时 AX 等于 inc ax 的偏移地址)
add ax, ax ; AX 自乘
pop bx ; 弹出栈顶的 CS 赋值给 BX(此时 BX 等于调用前的 CS 段地址)
add ax, bx ; AX 加上 BX
retf ; 注意:远转移必须用 retf 返回,它会依次弹出 IP 和 CS
10.5 call 16位寄存器
- 格式:
call 通用16位寄存器 - 执行步骤:
SP = SP - 2[SS:SP] = IPIP = 寄存器值
等价:
push ip
jmp 寄存器
mov bx, offset s ; 将标号 s 的偏移地址传送给 bx 寄存器
call bx ; 【核心】1. 将下一条指令的 IP 压栈 2. 把 bx 的值赋给 IP,跳转到 s
inc ax ; 子程序返回后,继续执行这里
s:
add ax, 1 ; 子程序代码
ret ; 从栈中弹出之前压入的 IP,返回主程序
10.6 转移地址在内存中的call指令
10.6.1 call word ptr 内存单元(段内调用)
相当于:
push ip
jmp word ptr 内存单元地址
mov sp, 10h ; 初始化栈顶指针 SP 为 10H
mov ax, 0123h ; 准备跳转的目标偏移地址 0123H
mov ds:[0], ax ; 将 0123H 存入内存单元 ds:[0] 中
call word ptr ds:[0] ; 【核心】1. 将下一条指令的 IP 压栈 2. 从 ds:[0] 取出 0123H 赋给 IP
10.6.2 call dword ptr 内存单元(段间调用)
内存双字:低字IP,高字CS
push cs
push ip
jmp dword ptr 内存单元地址
mov sp, 10h ; 初始化栈顶指针 SP 为 10H
mov word ptr ds:[0], 0123h ; 在 ds:[0] 处放入目标偏移地址 IP = 0123H
mov word ptr ds:[2], 1000h ; 在 ds:[2] 处放入目标代码段地址 CS = 1000H
call dword ptr ds:[0] ; 【核心】1. 将当前的 CS 和 IP 压栈 2. 从内存取出新 CS 和 IP
10.7 call和ret配合使用
call保存返回地址入栈,子程序末尾用ret/retf弹出地址回到原调用处,实现子程序调用与返回。

10.8 mul 乘法指令
相乘两数位数必须相同: 同为8位 或 同为16位
- 8位乘法
- 被乘数: AL
- 乘数: 8位寄存器 / 字节内存单元
- 结果: AX(16位)
- 16位乘法
- 被乘数: AX
- 乘数: 16位寄存器 / 字内存单元
- 结果: DX存高16位,AX存低16位
如果8位*16位 就将8位补0成16位即可!
3. 指令格式
mul reg(reg表示任意一个 16 位通用寄存器)
mul byte ptr [bx]
mul word ptr [bx]


10.9 模块化程序设计
利用call+ret划分独立子程序,实现代码复用、结构清晰,对应高级语言函数。

10.10 批量数据传递三种方式
- 寄存器传递


2. 内存全局数据段传递




当然用栈也是同理的
3. 栈传递参数
10.11 三道试验题和一些补充内容
这一部分的程序设计 感兴趣的可以自己在b站搜小甲鱼 第51、52 、53、54节课
第十一章 标志寄存器
11.0 标志寄存器
8086CPU的标志寄存器有16位,其中存储的信息通常被称为程序状态字(PSW)
本章中的标志寄存器(以下简称为flag)是我们要学习的最后一个寄存器
flag寄存器是按位起作用的,也就是说,它的每一位都有专门的含义,记录特定的信息
寄存器位结构说明
- flag的
1、3、5、12、13、14、15位共7位在8086CPU中没有使用,不具有任何含义
而0、2、6、7、8、9、10、11位共9位都具有特殊的含义 - 配套位示意图

注意,在8086CPU的指令集中,有的指令的执行会影响标志寄存器
比如:add、sub、mul、div、inc、or、and等,它们大都是运算指令(逻辑运算或者算术运算)
有的指令的执行对标志寄存器没有影响,比如:mov、push、pop等,它们大都是传送指令
11.1 ZF标志(第6位)
- flag的第6位是ZF,零标志位。
它记录相关指令执行后:- 结果为0,ZF=1
- 结果不为0,ZF=0
- 示例:
mov ax,1
sub ax,1
;指令执行后,结果为0,则ZF=1
mov ax,2
sub ax,1
;指令执行后,结果不为0,则ZF=0
11.2 PF标志(第2位)
flag的第2位是PF,奇偶标志位
它记录指令执行后,结果的所有二进制位中1的个数
- 1的个数为偶数,PF=1
- 1的个数为奇数,PF=0

11.3 SF标志(第7位)
- flag的第7位是SF,符号标志位
- 它记录指令执行后:
- 结果为负,sf=1
- 结果为正或者0,sf=0
- sf标志,就是CPU对有符号数运算结果的一种记录,它记录数据的正负
- sf标志把所有数当作有符号数,如果把数据当作无符号数运算,sf的值则没有意义,虽然相关指令会影响它的值
- 也就是说,CPU在执行add等指令时,是必然要影响sf标志位的值;至于我们需不需要这种影响,那就看我们如何看待指令所进行的运算

11.4 CF标志(第0位)
- flag的第0位是CF,进位标志位
一般情况下,在进行无符号数运算的时候,它记录了运算结果的最高有效位向更高位的进位值,或从更高位的借位值,代表假想的更高位 - CPU在运算时,不会丢弃进位值,而是记录在一个特殊的寄存器的某一位上


8086CPU就用flag的cf位来记录这个进位值,借位也一样

- CF在debug中有对应显示标识

- CF仅用于无符号数运算场景记录进位、借位结果(自己主动决定)
⚠️: inc 和 loop 不影响CF的值!
11.5 OF标志(第11位)
flag中的第11位为OF溢出标志位
进行有符号数运算的时候,如果结果超过了机器所能表示的范围称为溢出
- 这里所讲的溢出,只是对有符号数运算而言,就像进位只是相对于无符号数而言!

c - 一定要注意cf和of的区别
- 当需要把机器码看成有符号数则使用of
- 当需要把机器码看成无符号数则使用cf
补充:对cf、sf、of、三个容易搞混的寄存器的分析
1. CF (Carry Flag) —— 进位标志
- 核心作用:专门管无符号数(Unsigned的运算。
- 触发条件:
- 加法时:如果最高位产生了进位(比如 8 位运算中,结果超过了 255),CF = 1。
- 减法时:如果不够减,需要向最高位借位,CF = 1。
- 一句话记忆:“无符号数看 CF,进位借位它做主。”
2. OF (Overflow Flag) —— 溢出标志
- 核心作用:专门管有符号数(Signed的运算。
- 触发条件:如果运算结果超出了该数据类型能表示的范围,OF = 1。
- 比如 8 位有符号数的范围是 -128 到 +127。如果你算
127 + 1,结果变成了 -128,这就叫“溢出”,OF = 1。
- 比如 8 位有符号数的范围是 -128 到 +127。如果你算
- 一句话记忆:“有符号数看 OF,正负突变就溢出。”
3. SF (Sign Flag) —— 符号标志
- 核心作用:记录运算结果的正负。
- 触发条件:它直接等于运算结果的最高位(MSB)。
- 如果结果的最高位是 1,SF = 1(表示负数)。
- 如果结果的最高位是 0,SF = 0(表示正数)。
- 一句话记忆:“结果正负看 SF,最高位是几它就是几。”
CF 和 OF 到底有什么区别?
- 换个更清晰的例子:
0111 1111 + 0000 0001(即 127 + 1)
二进制结果是1000 0000。- 无符号数视角:
127 + 1 = 128。没超过 255,没进位。👉 CF = 0。 - 有符号数视角:
127 + 1 = 128。但 8 位有符号数最大是 127!正数加正数,结果变成了负数(1000 0000是 -128 的补码)。👉 OF = 1(溢出了!)。 - 符号视角:结果最高位是 1。👉 SF = 1。
- 无符号数视角:
11.6 adc 带进位加法指令
adc是带进位的加法指令,它利用了cf上记录的进位值
- 格式:
adc 操作对象1,操作对象2 - 功能: 操作对象1=操作对象1+操作对象2+cf
示例:adc ax,bx实现的功能是:(ax)=(ax)+(bx)+cf



- 执行adc指令的时候,加上的cf的值的含义,由adc指令前的指令决定;也就是说,关键在于所加上的cf值是被什么指令设置的
- 如果cf是被sub指令设置的,那么它的含义就是借位值
- 如果是被add指令设置的,那么它的含义就是进位值
- 下面的指令和add ax,bx具有相同的结果
add al,bl
adc ah,bh
CPU提供adc指令的目的,就是来进行加法的第二步运算的
adc指令和add指令相配合就可以对更大的数据进行加法运算



当寄存器不够用的时候 我们就可以将其放到内存或者栈中!
11.7 sbb 带借位减法指令
sbb是带借位减法指令,它利用了cf位上记录的借位值
- 格式:
sbb 操作对象1,操作对象2 - 功能:
操作对象1=操作对象1-操作对象2-cf - 利用sbb指令,我们可以对任意大的数据进行减法运算
- sbb和adc是基于相同的思想设计的两条指令,在应用思路上和adc类似
计算003E1000H–00202000H,结果放在ax,bx中,程序如下:
mov bx, 1000H ; 低16位被减数
mov ax, 003EH ; 高16位被减数
sub bx, 2000H ; 第一步:低16位相减 (1000H - 2000H)
; 因为不够减,CPU会自动将 CF 置为 1
sbb ax, 0020H ; 第二步:高16位带借位相减
; 实际计算:003EH - 0020H - CF(1)
11.8 cmp 比较指令
- cmp是比较指令,功能相当于减法指令,只是不保存结果;cmp指令执行后,将对标志寄存器产生影响 其他相关指令通过识别这些被影响的标志寄存器,来得知比较结果
- cmp指令格式:
cmp 操作对象1,操作对象2 - 功能:
计算操作对象1-操作对象2,但并不保存结果,仅仅根据计算结果对标志寄存器进行设置 - 示例:cmp ax,ax
做(ax)-(ax)的运算,结果为0,但并不在ax中保存,仅影响flag的相关位
指令执行后:
zf=1 ;结果为0
pf=1 ;结果的1的个数为偶数
sf=0 ;结果为正号
cf=0 ;结果没有产生进位或借位
of=0 ;结果没有溢出
- 可通过flag标志位组合,判断
无符号数cmp比较结果

- cmp既可以对无符号数进行比较,也可以对有符号数进行比较
cmp 操作数1,操作数2 ;操作数1、操作数2都是有符号数
- of=0,说明没有溢出,
逻辑上真正结果的正负=实际结果的正负of=0,sf=1则 操作数1比操作数2小of=0,sf=0则 操作数1比操作数2大- of=1,说明有溢出,
逻辑上真正结果的正负与实际结果的正负相反of=1,sf=1则 操作数1比操作数2大of=1,sf=0则 操作数1比操作数2小
11.9 检测比较结果的条件转移指令
- 这些条件转移指令通常和cmp相配合使用
- 因为cmp指令可以同时进行两种比较,无符号数和有符号数的比较
所以,这些转移指令也分为两种,即:- 根据【无符号数】的比较结果进行转移的条件转移指令,它们检测zf、cf的值
- 根据【有符号数】的比较结果进行转移的条件转移指令,它们检测sf、of和zf的值
- 无符号比较,条件转移指令小结【无符号,6个】



11.10 DF标志(第10位)和串传送指令
- flag的第10位DF,方向标志位
在串处理指令(movsb,movsw)中,控制每次操作后si、di的增减- df=0:每次操作后si,di递增
- df=1:每次操作后si,di递减
- 指令格式:
movsb - movsb功能(以字节为单位传送)
((es)*16+(di))=((ds)*16+(si))- 如果df=0,则:(si)=(si)+1,(di)=(di)+1
如果df=1,则:(si)=(si)-1,(di)=(di)-1
- 文字描述: movsb的功能是将ds:si指向的内存单元中的字节送入es:di中,然后根据标志寄存器df位的值,将si和di递增或递减
- movsw:
传送一个字单元数据
movsb和movsw都和rep配合使用
格式:rep movsb
rep的作用根据cx的值,重复执行后面的串传送指令(跟loop类似)

- cld指令和std指令
- cld指令: 将标志寄存器的df置为0【c:clear】
- std指令: 将标志寄存器的df置为1【s:set】


11.11 pushf和popf
pushf: 将标志寄存器的值压栈
popf: 从栈中弹出数据,送入标志寄存器中
pushf和popf为直接访问标志寄存器提供了一种方法
第十二章 内中断
引言和简介
- 中断是CPU处理外部突发事件的一个重要技术
- 它能使CPU在运行过程中对外部事件发出的中断请求及时地进行处理,处理完成后又立即返回断点,继续进行CPU原来的工作。
- 引起中断的原因【即:发出中断请求的来源叫作中断源】
- 根据中断源的不同,可以把中断分为:【软件中断】和【硬件中断】两大类
而硬件中断又可以分为【外部中断】和【内部中断】两类
12.1 中断的产生
- 外部中断一般是指计算机外设发出的中断请求,如: 键盘中断、打印机中断、定时器中断。
外部中断是可以屏蔽的中断,也就是说,利用中断控制器可以屏蔽这些外部设备的中断请求。 - 内部中断是指因硬件出错(如突然掉电、奇偶校验错等)或运算出错(除数为零、运算溢出、单步中断)所引起的中断。
内部中断是不可屏蔽的中断 - 软件中断其实并不是真正的中断,他们只是可被调用执行的一般程序,
DOS的系统功能调用(int 21h)都是软件中断 - CPU为了处理并发的中断请求,规定了中断的优先权,优先权由高到低的顺序是:
- 除法错、溢出中断、软件中断
- 不可屏蔽中断
- 可屏蔽中断
- 单步中断
12.2 中断处理程序简介
- CPU的设计者必须在中断信息和其处理程序的入口地址之间建立某种联系,使得CPU根据中断信息可以找到要执行的处理程序。
- 中断信息中包含有表示中断的类型码。根据CPU的设计,中断类型码的作用就是用来定位中断处理程序的。
- CPU用8位的中断类型码通过
中断向量表找到相应的中断处理程序的入口地址,即中断类型码是中断向量在中断向量表中的索引
12.3 中断向量表【中断向量表就是中断向量的列表】
- 中断向量表在内存中保存,其中存放着256个【2^8,8位中断类型码】中断源所对应的中断处理程序的入口
对于8086PC机,中断向量表指定放在内存地址0处 - 从
0:0~0:03ffh共1024个字节(256*4,物理地址由段地址+偏移地址组成,每个向量占用4字节)存放中断向量表

12.4 中断过程
- 可以用中断类型码,在中断向量表中找到中断处理程序的入口找到这个入口地址的最终目的是用它设置
cs和ip,使CPU执行中断处理程序 - 用中断类型码找到中断向量,并用它设置cs和ip,这个工作是由CPU的硬件自动完成的, CPU硬件完成这个工作的过程被称为【中断过程】
- 8086CPU完整中断过程
- 从中断信息中取得中断类型码
- 标志寄存器的值入栈(保护标志位)
- 将标志寄存器的第8位
TF和第9位IF设置为0 - cs内容入栈
- ip内容入栈
- 从内存地址为中断类型码✖️4 和 中断类型码✖️4+2 的两个字单元中读取中断处理程序入口地址,设置cs和ip

- 汇编语言等效描述中断过程
- 取得中断类型码N
- pushf
- TF=0,IF=0
- push cs
- push ip
- ip = (N✖️4),cs = (N✖️4+2)
12.5 中断处理程序
- 由于CPU随时都可能检测到中断信息,随时都可能执行中断处理程序,所以中断处理程序必须一直存储在内存某段空间中
- 中断处理程序的入口地址,即【中断向量】,必须存储在对应的中断向量表表项中
- 中断处理程序编写步骤(类似子程序)
- 保存用到的寄存器
- 处理中断业务逻辑
- 恢复用到的寄存器
- 使用iret指令返回
iret指令等效汇编:
pop ip
pop cs
popf
iret和CPU硬件自动执行的中断过程配套使用,执行后CPU回到中断前断点继续运行
12.6 除法错误中断的处理
当CPU执行div除法指令发生除法溢出时,产生中断类型码为0的中断信息,CPU检测后触发0号中断,执行对应0号中断处理程序
示例溢出代码:
mov ax,1000h
mov bh,1
div bh
程序运行会触发除法溢出中断

这里是因为除数为8位,存放在bh内,被除数为16位存放在ax里,商为1000h存在al里,存不下所以溢出!
12.7 编程处理0号中断
需求: 自定义0号中断处理程序,在屏幕中间输出“Welcome to here!”,执行完毕返回操作系统
要求: 将自定义中断处理程序放置到安全内存空间,提供中断程序标准框架




do0的最后两行代码是按照规范返回DOS的!
12.8 程序设计思路


- 计算中断程序占用长度: offset 结束标号 - offset 起始标号
OFFSET 标号 的作用是获取该标号在当前段内的偏移地址




do1看不懂正常 得学完17章后再看!s是设置颜色 不懂也没事
只需要在代码段中存放程序所需常量、数据就可以解决这个问题了!!!
在代码段中存放程序所需常量、数据
12.9 单步中断
若CPU检测到标志寄存器TF位为1,则触发单步中断,完整执行一次标准中断过程







12.12 响应中断的特殊情况




第十三章 int指令
13.1 int指令
- int格式:
int n;n为中断类型码
功能:主动触发完整中断过程 - CPU执行
int n的完整流程- 获取中断类型码n
- 标志寄存器入栈,IF=0,TF=0
- CS、IP依次入栈保存断点
- 根据n去中断向量表读取入口,执行n号中断处理程序
- 程序中可通过int调用任意中断例程; 既可系统自带,也可自定义中断处理程序供外部调用
13.2 编写供应用程序调用的中断例程
实例1:中断7ch,实现word数据求平方
- 功能:计算16位字型数据平方
- 参数:AX = 待计算数字
- 返回:DX存放结果高16位,AX存放结果低16位
- 使用示例:计算 2 × 3456²
;调用程序
code segment
assume cs: code
start:
mov ax,3456 ;(ax)=3456
int 7ch ;调用7ch中断,计算ax平方
add ax,ax
adc dx,dx ;结果 × 2
mov ax,4c00h
int 21h
code ends
end start
; 安装7ch中断例程程序
; 整体思路分为三步:
; 1. 将中断处理代码(sqr到sqrend)复制到内存的安全区域(0000:0200h)
; 2. 修改中断向量表(IVT)中 7ch 对应的表项,使其指向安全区域的代码
; 3. 退出安装程序,以后程序只需执行 `int 7ch` 即可调用平方功能
code segment
assume cs:code
start:
; ================= 第一步:复制中断程序到 0000:0200h =================
; 设置源数据段寄存器 DS,指向当前代码段(因为 sqr 代码就在当前 CS 段中)
mov ax, cs
mov ds, ax
; 设置源变址寄存器 SI,指向要复制的代码起始位置(sqr 标号的偏移地址)
mov si, offset sqr
; 设置目的附加段寄存器 ES,指向内存绝对地址 0000:0200h(安全空间)
mov ax, 0
mov es, ax
; 设置目的变址寄存器 DI,指向目的偏移地址 200h
mov di, 200h
; 计算要复制的代码长度(字节数):结束标号地址 - 起始标号地址
mov cx, offset sqrend - offset sqr
cld ; 清除方向标志位 DF,使 SI 和 DI 在每次复制后自动递增
rep movsb ; 循环执行字节传送指令,将 DS:SI 的内容复制到 ES:DI,直到 CX=0
; ================= 第二步:修改 7ch 中断向量表项 =================
; 重新设置 ES 指向内存首地址 0000h(中断向量表 IVT 位于 0000:0000h ~ 0000:03FFh)
mov ax, 0
mov es, ax
; 修改 IVT 中 7ch 号中断对应的 4 个字节:
; 中断向量表项地址 = 中断类型码 * 4。
; 低 2 字节存放偏移地址(IP),高 2 字节存放段基址(CS)。
mov word ptr es:[7ch*4], 200h ; 将偏移地址 IP 修改为 0200h
mov word ptr es:[7ch*4+2], 0 ; 将段基址 CS 修改为 0000h
; 至此,当 CPU 执行 `int 7ch` 时,会自动跳转到 0000:0200h 执行 sqr 代码
; ================= 第三步:安全退出安装程序 =================
mov ax, 4c00h ; DOS 系统调用号 4Ch(带返回码终止程序)
int 21h ; 调用 DOS 中断,将控制权交还给操作系统
; ================= 中断处理子程序(将被复制到 0000:0200h) =================
sqr:
mul ax ; 计算 AX = AX * AX(实现平方功能,结果存在 DX:AX 中)
iret ; 中断返回指令,自动从堆栈中弹出 IP 和 CS,返回主程序
sqrend:
nop ; 空操作,仅作为代码结束的标志(用于计算复制长度)
code ends
end start
实例2:中断7ch,以0结尾字母字符串全部转大写

code segment
assume cs:code
start:
; ================= 第一步:复制中断程序到 0:200h =================
; 设置源数据段寄存器 DS,指向当前代码段(因为 capital 代码就在当前 CS 段中)
mov ax, cs
mov ds, ax
; 设置源变址寄存器 SI,指向要复制的代码起始位置(capital 标号的偏移地址)
mov si, offset capital
; 设置目的附加段寄存器 ES,指向内存绝对地址 0000h(安全空间)
mov ax, 0
mov es, ax
; 设置目的变址寄存器 DI,指向目的偏移地址 200h
mov di, 200h
; 计算要复制的代码长度(字节数):结束标号地址 - 起始标号地址
mov cx, offset capitalend - offset capital
cld ; 清除方向标志位 DF,使 SI 和 DI 在每次复制后自动递增
rep movsb ; 循环执行字节传送指令,将 DS:SI 的内容复制到 ES:DI,直到 CX=0
; ================= 第二步:设置 7ch 中断向量 =================
; 重新设置 ES 指向内存首地址 0000h(中断向量表 IVT 位于 0000:0000h ~ 0000:03FFh)
mov ax, 0
mov es, ax
; 修改 IVT 中 7ch 号中断对应的 4 个字节:
; 中断向量表项地址 = 中断类型码 * 4。
; 低 2 字节存放偏移地址(IP),高 2 字节存放段基址(CS)。
mov word ptr es:[7ch*4], 200h ; 将偏移地址 IP 修改为 0200h
mov word ptr es:[7ch*4+2], 0 ; 将段基址 CS 修改为 0000h
; 至此,当 CPU 执行 `int 7ch` 时,会自动跳转到 0000:0200h 执行 capital 代码
; ================= 第三步:安全退出安装程序 =================
mov ax, 4c00h ; DOS 系统调用号 4Ch(带返回码终止程序)
int 21h ; 调用 DOS 中断,将控制权交还给操作系统
; ================= 中断处理子程序(将被复制到 0000:0200h) =================
capital:
push cx ; 保护现场:将 CX 压入堆栈(因为后面要用 CX 做循环计数器)
push si ; 保护现场:将 SI 压入堆栈(因为 SI 指向字符串,不能破坏)
change:
mov cl, [si] ; 取 SI 指向的内存单元中的字符,放入 CL
mov ch, 0 ; 将 CH 清零,组成完整的 16 位计数器 CX
jcxz ok ; 如果 CX=0(说明遇到了字符串结束符 '\0'),跳转到 ok 结束循环
; ASCII 码小写转大写:将第 5 位(从0开始)清零。
; 小写字母 'a'(61h) = 01100001b,大写字母 'A'(41h) = 01000001b
and byte ptr [si], 11011111b
inc si ; SI 指针后移,指向下一个字符
jmp short change ; 无条件跳转回 change,继续处理下一个字符
ok:
pop si ; 恢复现场:从堆栈中弹出 SI 的原始值
pop cx ; 恢复现场:从堆栈中弹出 CX 的原始值
iret ; 中断返回指令,自动从堆栈中弹出 IP 和 CS,返回主程序
capitalend:
nop ; 空操作,仅作为代码结束的标志(用于计算复制长度)
code ends
end start
13.3 对int、iret和栈的深入理解(用中断模拟loop)
问题: 使用7ch中断例程模拟loop指令功能
补充说明:禁止随意修改SP,如需访问栈内数据可借助BP间接寻址




[bp+2]保存的就是 ip地址 而bx是s-se(负数)的偏移量 意思就是把偏移的地址减回去
lp: push bp ; 1. 保存旧的 BP,这是标准函数开头,这算是个格式,算是个规范了 原因看下图
mov bp, sp ; 2. 让 BP 指向当前栈顶。此时 [BP] 是 IP,[BP+2] 是 CS
dec cx ; 3. 计数器减 1 (模拟 LOOP 的计数功能)
jcxz lpret ; 4. 如果 CX=0,说明循环结束,跳转到 lpret 直接退出
add [bp+2], bx ; 5. 【核心魔法】修改栈中的 IP (返回地址)
; 原来的 IP + BX = 新的 IP
; 因为 BX 是负数,所以实际上是 IP - |BX|,实现了“回跳”
lpret: pop bp ; 6. 恢复 BP
iret ; 7. 中断返回。CPU 会弹出修改后的 IP,跳转回循环开始处


13.4 BIOS和DOS中断例程的安装过程

- 开机上电,CPU初始
CS=0FFFFH,IP=0,从FFFF:0执行跳转指令,转入BIOS硬件检测初始化程序

- BIOS初始化程序构建中断向量表,将自带中断例程入口写入对应表项
- 硬件检测完成后调用
int 19h引导操作系统,系统接管硬件 - DOS启动后,加载自身提供的中断子程序到内存,并更新对应中断向量
举个简单的例子:

13.6 BIOS中断例程的应用
int 10h中断例程是BIOS提供的中断例程,其中包含了多个和屏幕输出相关的子程序。
一般来说,一个供程序员调用的中断例程中往往包括多个子程序,中断例程内部用传递进来的参数来决定执行哪个子程序。
BIOS 和DOS 提供的中断例程,都用ah来传递内部子程序的编号。
int 10h屏幕操作中断,包含多组屏幕输出子程序,单个中断类型码下划分多个功能子程序,由传入参数区分执行逻辑

一个字符占2字节 一个是ASCII码 另一个是这个字符的属性(按位区分),一共是32k(即8页显存 )一般显示第一页(即前4k字节的内容!!)



直接写入显存会直接输出到屏幕上!!- BIOS、DOS所有中断例程统一使用
AH寄存器存放内部子程序功能编号
13.7 DOS中断例程应用
int 21h DOS系统功能调用中断,内置大量编程可用系统子程序



第十四章 端口
0. CPU可直接读写的三类数据位置
- CPU内部的寄存器
- 内存单元
- 端口(通过端口计算机才能访问硬件,才能与网络进行传输)
14.1 端口的读写
- 端口不能使用mov、push、pop等内存读写指令,仅有两条专用端口指令:
in:从端口读取数据
out:向端口写入数据 - 访问内存与访问端口的总线操作对比
(1)访问内存示例 mov ax,ds:[8],ds=0
- CPU地址线输出地址8
- 控制线发送内存读信号,选中内存芯片
- 内存将8号单元数据通过数据线送入CPU
(2)访问端口示例 in al,60h
- CPU地址线输出端口号60h
- 控制线发送端口读信号,选中对应硬件芯片
- 硬件把60h端口数据通过数据线送入CPU

补充规则:in/out只能使用al(8位端口)、ax(16位端口)存放传输和读的数据。
- 端口号0~255:直接写立即数端口号
in al,20h ;读取20h端口一字节
out 20h,al ;向20h端口写入一字节
- 端口号256~65535:端口号存入dx间接访问
mov dx,3f8h。;将端口号3f8送入dx
in al,dx ;从3f8h端口读一个字节
out dx,al ;从3f8h端口写一个字节
14.2 CMOS RAM芯片
- PC机CMOS RAM芯片特性
- 内置实时时钟 + 128字节RAM(早期机型64字节)
- 电池独立供电,关机时钟持续运行,配置数据不丢失
- RAM分区:0~0dh存时间,其余存储系统配置;BIOS开机可读取修改配置
补充BIOS说明: BIOS即基本输入输出系统,PC启动第一个加载固件程序 - 内置一对专用端口:70h(地址端口)、71h(数据端口)
- 70h:写入要访问的CMOS单元偏移地址
- 71h:读写对应单元存储的数据
- 示例:读取CMOS RAM 2号单元
mov al,2
out 70h,al
in al,71h
14.3 shl 和 shr 逻辑移位指令
shl 逻辑左移- 寄存器/内存数据整体左移
- 最高移出位送入CF标志位
- 最低位补0
mov al,01001000B
shl al,1
;执行后 al=10010000B,CF=0
- shr 逻辑右移,规则与shl相反:最低位移入CF,最高位补0
移位次数大于1时,移位次数必须存入cl(最多移动)。

| 指令 | 全称 | 移动方向 | 空位填充 | 最高/最低位去向 | 数学意义 |
|---|---|---|---|---|---|
| SHL | Shift Logical Left | 向左 | 最低位补 0 | 最高位移入 CF | 无符号数 ×2^n |
| SHR | Shift Right | 向右 | 最高位补 0 | 最低位移入 CF | 无符号数 ÷2^n |
再来个例子:
MOV AX, 62H ; AX = 0110 0010B
SHL AX, 1 ; AX = 1100 0100B, CF=0 (原最高位0)
SHR AX, 1 ; AX = 0110 0010B, CF=0 (原最低位0)


14.4 CMOS RAM中的时间信息
时间对应单元地址(单字节BCD码存储)
秒:00h
分:02h
时:04h
日:07h
月:08h
年:09h
存储规则: 一字节存两位BCD十进制数,高4位十位,低4位个位。
这6个信息的长度都为1个字节
这些数据以BCD码的方式存放,一个字节可以表示两个BCD码
CMOS RAM存储时间信息的单元中存储了用两个BCD码表示的两个十进制数
高4位的BCD码表示十位,低四位的BCD码表示个位
1.CMOS RAM芯片回顾:
1. 70h为地址端口,存放要访问的CMOS RAM单元的地址
2. 71h为数据端口,存放从选定的CMOS RAM单元中【读取】的数据,或【写入】其中的数据
2. 分析
这个程序主要做两部分工作
1. 从CMOS RAM的8号单元读取当前月份的BCD码
要读取CMOS RAM的信息,我们首先要向地址端口70h写入要访问的单元的地址:
mov al,8
out 70h,al
然后从数据端口71h中取得指定单元中的数据:
in al,71h
2. 将用BCD码表示的月份以十进制的形式显示到屏幕上
编程需求:屏幕中间显示当前月份
逻辑分两步:
- 读取CMOS 08h单元的月份BCD码
mov al,8
out 70h,al
in al,71h
- 拆分十位、个位,转ASCII输出到显存
; 功能:从CMOS RAM中读取当前月份,并将其以十进制形式显示在屏幕正中央
code segment
assume cs:code
start:
; ----------------------------
; 第一步:通过 70h/71h 端口读取 CMOS RAM 中的月份数据
; ----------------------------
mov al, 8 ; CMOS RAM 中,月份数据存储在索引号为 8 的单元中
out 70h, al ; 向 70h 端口写入索引号 8,指定要读取的 CMOS 单元
in al, 71h ; 从 71h 端口读取数据(此时 AL 中存放的是月份的 BCD 码)
; ----------------------------
; 第二步:分离 BCD 码的十位和个位
; ----------------------------
mov ah, al ; 将月份数据备份到 AH 中(AL 和 AH 现在都保存着原始数据)
mov cl, 4 ; 设置移位次数为 4
shr ah, cl ; AH 逻辑右移 4 位,将高 4 位(十位)移到低 4 位,得到十位数字
and al, 00001111b ; AL 与 0FH 做与运算,屏蔽高 4 位,只保留低 4 位(个位数字)
; ----------------------------
; 第三步:将数字转换为 ASCII 码
; ----------------------------
add ah, 30h ; 十位数字加上 30h,转换为对应的 ASCII 码字符
add al, 30h ; 个位数字加上 30h,转换为对应的 ASCII 码字符
; ----------------------------
; 第四步:将月份字符写入显存(屏幕中间位置)
; ----------------------------
mov bx, 0b800h ; 80x25 彩色字符模式下的显存段基址
mov es, bx ; 将显存段基址加载到附加段寄存器 ES 中
; 屏幕第 12 行(从0开始),第 40 列(从0开始,即屏幕正中央)
mov byte ptr es:[160*12+40*2], ah ; 写入月份的十位 ASCII 码
mov byte ptr es:[160*12+40*2+2], al ; 写入月份的个位 ASCII 码(+2 是因为每个字符占2字节:ASCII+属性)
; ----------------------------
; 第五步:安全退出程序,返回 DOS
; ----------------------------
mov ax, 4c00h ; 设置 DOS 系统调用功能号 4Ch(带返回码终止程序)
int 21h ; 调用 DOS 中断,将控制权交还给操作系统
code ends
end start
第十五章 外中断
CPU除了有运算能力,还有I/O能力
15.1 接口芯片和端口
- 在PC系统的接口卡和主板上,装有各种接口芯片,这些外设接口芯片的内部装有若干寄存器,CPU将这些寄存器当做【
端口】访问 - 外设的输入不直接送入内存和CPU,而是送入相关的接口芯片的【
端口】中 - CPU向外设的输出也是要先送入【
端口】中,再由相关芯片送入到外设 - CPU可以向外设输出控制命令,这些控制命令也是先送到【
端口】中,然后相关芯片根据命令进行相关工作 - 可见:CPU与外部设备的交流是通过【
端口】进行的
CPU在执行完当前指令后,可以检测到发送过来的中断信息,引发中断过程,处理外设的输入
15.2 外中断信息
- 在PC系统中,外中断源一共有两类
- 存储、 可屏蔽中断
- 不可屏蔽中断
- 可屏蔽中断是CPU可以不响应的外中断。CPU是否响应可屏蔽中断, 要看标志寄存器的IF位的设置
当CPU检测到可屏蔽中断信息时:- 若IF=1,则CPU在执行完当前指令后相应中断,引发中断过程
- 若IF=0,则不响应可屏蔽中断
- 可屏蔽中断所引发的中断过程,除在第一步的实现上与内中断有所不同外,基本上和内中断的中断过程相同
- 因为可屏蔽中断信息来自于CPU外部,中断类型码是通过数据总线送入CPU的,而内中断的中断码是在CPU内部产生的
- IF设置为0的原因: 在进入中断处理程序后,禁止其他的可屏蔽中断; 当然,如果中断处理程序中需要处理可屏蔽中断,可以用指令将IF设置为1
- 8086CPU提供的设置IF的指令如下:
sti ;用于设置IF=1
cli ;用于设置IF=0
- 不可屏蔽中断是CPU必须相应的外中断。
当CPU检测到不可屏蔽中断信息时,则在执行完当前指令后
立即响应,应发中断过程 - 8086CPU不可屏蔽中断的中断类型码
固定为2,所以中断过程中,不需要取中断类型码

- 不可屏蔽中断的中断过程
- 标志寄存器入栈,IF=0,TF=0
- CS,IP入栈
- (IP)=(8),(CS)=(0AH) ;固定地址
- 几乎所有由外设引起的外中断,都是可屏蔽中断。当外设有需要处理的事件发生时
相关芯片向CPU发出可屏蔽中断信息。
不可屏蔽中断是系统中有必须处理的紧急情况发生时用来通知CPU的中断信息,本门课程中,主要讨论可屏蔽中断
15.3 PC机键盘的处理过程
- 下面看一个键盘输入的处理过程,并以此来体会PC机处理外设输入的基本方法
- 键盘输入
- 引发9号中断
- 执行int 9中断例程
- PC机键盘的处理过程
- 键盘上每一个键相当于一个开关,键盘中有一个芯片对键盘上的每一触键的开关状态进行扫描。
- 按下一个键时,开关接通,该芯片就产生一个扫描码,扫描码说明按下的键在键盘上的位置,扫描码被送入主板上的相关接口芯片的寄存器中,该寄存器的端口地址为
60H - 松开控下的键时,也产生一个
扫描码,扫描码说明了松开的键在键盘上的位置,松开按键时
产生的扫描码也被送入60H端口中。
一般按下一个键时,产生的扫描码称为通码,松开一个键产生的扫描码称为断码
扫描码长度为一个字节,通码的第七位为0,断码的第七位为1
即:断码=通码+80H
BIOS提供了int9中断例程,用来进行基本的键盘输入处理,主要的工作如下:- 读出60H端口中的扫描码
- 如果是字符键的扫描码,将该扫描码对应的字符码(即:ASCII码)送入内存中的
BIOS键盘缓冲区(前8字节字符码,后8字节描述码) - 如果是控制键和切换键的扫描码,则将其转变为状态字节,写入内存中存储状态字节的单元
- 键盘的输入到达60H端口时,相关的芯片会向CPU发出中断类型码为9的可屏蔽中断信息。
- CPU检测到中断信息后,如果IF=1,则相应中断,同时将IF设置为0(不让其他可屏蔽中断进行干扰),引发中断过程,转去执行int9中断例程
- BIOS键盘缓冲区是系统启动后,BIOS用于存放int9中断例程所接受的键盘输入的内存区
- 该内存区可以存储15个键盘输入,int9中断例程除了接收扫描码外,还要产生和扫描码对应的字符码,
所以在BIOS键盘缓冲区中,一个键盘输入用一个字单元存放,高字节存放扫描码,低字节存放字符码 0040:17单元存储键盘状态字节,该字节记录了控制键和切换键的状态。键盘状态字节各位记录的信息如下:
在这里插入图片描述

15.4 编写int9中断例程,并安装(挺难的说实话)
梳理键盘输入的处理过程
- 键盘产生扫描码
- 扫描码送入60H端口
- 一旦侦测到60H端口有动静,引发9号中断
- CPU执行int9中断例程处理输入
以上的过程,前三步都由硬件系统自动完成,能够修改的只有第四步,修改int9中断程序
【任务演示】在屏幕中依次显示“a”~“z”并可以让人看清。在显示过程中,按下Esc键后,该表显示的颜色
; 程序1:实现连续显示“a”~“z”
; 功能:在屏幕中间依次显示“a”~“z”,并可以让人看清。
stack segment
db 128 dup (0) ; 定义一个 128 字节的栈空间,用于暂存数据
stack ends
code segment
assume cs:code
start:
; ----------------------------
; 初始化栈段
; ----------------------------
mov ax, stack
mov ss, ax
mov sp, 128 ; 栈顶指针指向栈空间的最高处
; ----------------------------
; 初始化显存段地址
; ----------------------------
mov ax, 0b800h ; 80x25 彩色文本模式的显存基地址
mov es, ax ; 将显存地址加载到附加段寄存器 ES
mov ah, 'a' ; 初始化要显示的字符为 'a'(放在 AH 中)
s:
; 将 AH 中的字符写入显存(屏幕第12行,第40列,即正中央)
mov es:[160*12+40*2], ah
call delay ; 调用延时子程序,让人眼能看清字符的变化
inc ah ; 字符 ASCII 码加 1('a' -> 'b' -> 'c' ...)
cmp ah, 'z' ; 判断是否已经显示完 'z'
jna s ; 如果 AH <= 'z',则跳转回 s 继续循环
; ----------------------------
; 安全退出程序,返回 DOS
; ----------------------------
mov ax, 4c00h
int 21h
; ----------------------------
; 延时子程序:通过巨大的循环消耗 CPU 时间
; ----------------------------
delay:
push ax ; 保护现场,将 AX 压栈
push dx ; 将 DX 压栈
mov dx, 0010h ; 设置 DX 高位初值
mov ax, 0 ; 设置 AX 低位初值,组合成 32 位计数器 (DX:AX)
s1:
sub ax, 1 ; 32位减法:AX 减 1
sbb dx, 0 ; 带借位减法:如果 AX 借位了,DX 也减 1
cmp ax, 0 ; 判断 AX 是否为 0
jne s1 ; 如果 AX 不为 0,继续内层循环
cmp dx, 0 ; 判断 DX 是否为 0
jne s1 ; 如果 DX 不为 0,继续外层循环
pop dx ; 恢复现场
pop ax
ret ; 子程序返回
code ends
end start

; 程序2:实现改变颜色
; 功能:在屏幕中间依次显示“a”~“z”。在显示的过程中,按下 'Esc' 键后,改变显示的颜色。
stack segment
db 128 dup (0) ; 定义栈空间
stack ends
data segment
dw 0, 0 ; 定义两个双字(4字节),用于暂存原 Int 9 中断的入口地址
data ends
code segment
assume cs:code
start:
; ----------------------------
; 初始化栈段和数据段
; ----------------------------
mov ax, stack
mov ss, ax
mov sp, 128
mov ax, data
mov ds, ax ; 初始化 DS,因为后续要用 DS:[0] 暂存原中断向量
; ----------------------------
; 【核心步骤1】备份并替换 Int 9 中断向量
; ----------------------------
mov ax, 0
mov es, ax ; ES 指向中断向量表所在的 0 段
; 将原来的 int 9 中断例程的入口地址(偏移地址+段地址)备份到 ds:[0] 和 ds:[2] 中
push es:[9*4]
pop ds:[0]
push es:[9*4+2]
pop ds:[2]
; 在中断向量表中设置新的 int 9 中断例程的入口地址(指向我们自定义的 int9 标签)
mov word ptr es:[9*4], offset int9
mov es:[9*4+2], cs
; ----------------------------
; 主循环:与程序1相同,依次显示 a~z
; ----------------------------
mov ax, 0b800h
mov es, ax ; ES 重新指向显存
mov ah, 'a'
s:
mov es:[160*12+40*2], ah ; 显示当前字符
call delay ; 延时
inc ah ; 切换到下一个字符
cmp ah, 'z'
jna s ; 循环直到显示完 z
; ----------------------------
; 【核心步骤4】恢复 Int 9 中断向量(程序退出前必须做!)
; ----------------------------
mov ax, 0
mov es, ax
; 将备份在 ds:[0] 和 ds:[2] 中的原中断地址,写回中断向量表
push ds:[0]
pop es:[9*4]
push ds:[2]
pop es:[9*4+2]
; ----------------------------
; 安全退出程序
; ----------------------------
mov ax, 4c00h
int 21h
; ----------------------------
; 延时子程序(与程序1完全相同)
; ----------------------------
delay:
push ax
push dx
mov dx, 0010h
mov ax, 0
s1:
sub ax, 1
sbb dx, 0
cmp ax, 0
jne s1
cmp dx, 0
jne s1
pop dx
pop ax
ret
; ----------------------------
; 【核心步骤2 & 3】自定义的 Int 9 键盘中断例程
; ----------------------------
; int9中断例程是在进行键盘输入之后,由系统自动调用
int9:
; 保护现场
push ax
push bx
push es
in al, 60h ; 从键盘 60h 端口读取扫描码
; 【模拟调用原中断】:为了不破坏系统的正常键盘功能,我们必须手动调用原来的 Int 9
pushf ; 将标志寄存器压栈(模拟 int 指令自动做的第一步)
pushf
pop bx ; 将标志寄存器弹出到 BX,以便修改
and bh, 11111100b ; 清除 IF(中断允许标志)和 TF(陷阱标志),防止嵌套中断
push bx
popf ; 将修改后的标志压回栈中
call dword ptr ds:[0] ; 远调用(Call Far),跳转到备份的原 Int 9 入口地址执行
; 执行完后,原中断处理完毕,返回到这里继续执行
; 【检测 Esc 键】:Esc 键的扫描码是 01h
cmp al, 1
jne int9ret ; 如果扫描码不是 1(不是 Esc),直接跳转退出
; 如果是 Esc 键,修改屏幕中央字符的属性(颜色)
mov ax, 0b800h
mov es, ax
; 显存中,偶数地址存 ASCII,奇数地址存属性。+1 即指向属性字节
inc byte ptr es:[160*12+40*2+1]
int9ret:
; 恢复现场并中断返回
pop es
pop bx
pop ax
iret ; 中断返回指令,自动弹出 IP、CS 和 Flags
code ends
end start
第十六章 直接定址表
16.1 描述了单元长度的标号
- 本章讨论如何有效合理地组织数据,以及相关的编程技术
- 前面的课程中,我们一直在代码段中使用标号来标记指令、数据、段的起始地址
- 还可以使用一种标号,这种标号不但可以表示内存单元的地址,还表示了内存单元的长度
即: 表示在此标号处的单元,是一个字节单元,还是字单元还是双字单元
- 示例
- 标号1
a : db 1,2,3,4,5,6,7,8
b : dw 0
此种标号只能标记地址,此种加有“:”的地址标号,只能在代码段中使用,不能在其他段中使用
- 标号2
a db 1,2,3,4,5,6,7,8 ;标号a,描述了地址code:0,和从这个地址开始,以后的内存单元都是字节单元
b dw 0 ;标号b描述了地址code:8,和从这个地址开始,以后的内存单元都是字单元
此种标号既可以标记地址,也可以表示此标号处的单元:

- 使用这种包含单元长度的标号,可以使我们以简洁的形式访问内存中的数据
此种标号此后称为数据标号,它标记了存储数据的单元的地址和长度 - 数据标号的用法
mov ax,b ;相当于:mov ax,cs:[8]
mov b,2 ;相当于:mov word ptr cs:[8],2
inc b ;相当于:inc word ptr cs:[8]
mov al,a[si] ;相当于:mov al,cs:0[si]
mov al,a[3] ;相当于:mov al,cs:0[3]
mov al,a[bx+si+3] ;相当于:mov al,cs:0[bx+si+3]


16.2 在其他段中使用数据标号
- 注意,如果想在代码段中,直接用数据标号访问数据,
则需要用伪指令assume 将标号所在的段和一个段寄存器联系起来。
否则编译器在编译的时候,无法确定标号的段地址在哪一个寄存器中。 - 当然,这种联系是编译器需要的,但绝对不是说,我们因为编译器的工作需要,
用assume指令将段寄存器和某个段相联系,段寄存器中就会真的存放该段的地址。

- 我们可以将数据标号当作数据来定义,此时,编译器将标号所表示的地址当作数据的值。
- 把数据标号当做数据来定义时,使用【dw】定义数据
data segment
a db 1,2,3,4,5,6,7,8
b dw 0
c dw a,b ;数据标号c处存储的两个字型数据为标号a、b 的偏移地址。
data ends
等价写法:
data segment
a db 1,2,3,4,5,6,7,8
b dw 0
c dw offset a, offset b
data ends
2.把数据标号当做数据来定义时,使用【dd】定义数据
data segment
a db 1,2,3,4,5,6,7,8
b dw 0
c dd a,b ;数据标号c处存储的两个双字型数据为标号a的偏移地址和段地址、标号b 的偏移地址和段地址。
data ends
等价写法:
data segment
a db 1,2,3,4,5,6,7,8
b dw 0
c dw offset a, seg a, offset b, seg b ;seg操作符,功能为取得某一标号的段地址。
data ends
seg操作符,功能为取得某一标号的段地址。
16.3 直接定址表(重点,考试常考)
我们将使用“查表”的方法,编写相关程序的技巧
【任务】编写子程序,以十六进制的形式在屏幕中间显示给定的byte型数据
code segment
assume cs:code
start:
; ----------------------------
; 主程序:准备数据并调用子程序
; ----------------------------
mov al, 0eh ; 将待显示的十六进制字节数据 0EH 放入 AL 寄存器
call showbyte ; 调用子程序,将 AL 中的数据以十六进制字符形式显示在屏幕中央
; ----------------------------
; 安全退出程序,返回 DOS
; ----------------------------
mov ax, 4c00h
int 21h
; ----------------------------
; 子程序:showbyte
; 功能:将 AL 中的 8 位数据拆分为高 4 位和低 4 位,并转换为对应的十六进制 ASCII 字符显示
; 传入参数:AL(要显示的字节数据)
; ----------------------------
showbyte:
jmp short show ; 无条件短跳转到 show 标签,巧妙地绕过了后面的字符表数据
table db '0123456789ABCDEF' ; 定义一个十六进制字符查找表(16个字节)
show:
; ----------------------------
; 保护现场
; ----------------------------
push bx ; BX 寄存器稍后会被用作查表偏移量,先压栈保护
push es ; ES 寄存器稍后会被用作显存段地址,先压栈保护
; ----------------------------
; 第一步:拆分 AL 中的高 4 位和低 4 位
; ----------------------------
mov ah, al ; 将 AL 的数据备份到 AH 中
shr ah, 1 ; 以下 4 条指令将 AH 逻辑右移 4 位
shr ah, 1 ; 此时 AH 中只剩下原始数据的高 4 位(例如 0EH -> 00H)
shr ah, 1
shr ah, 1
and al, 00001111b ; AL 与 0FH 做与运算,屏蔽高 4 位,只保留低 4 位(例如 0EH -> 0EH)
; ----------------------------
; 第二步:查表获取“高 4 位”对应的十六进制字符,并写入显存
; ----------------------------
mov bl, ah ; 将高 4 位的值(0~15)放入 BL,作为查表的偏移量
mov bh, 0 ; 将 BH 清零,确保 BX 是一个合法的 16 位偏移地址
mov ah, table[bx] ; 【查表核心】以 BX 为偏移,从 table 中取出对应的 ASCII 字符放入 AH
; 例如:如果 BX=0,取出 '0' (30H);如果 BX=14(E),取出 'E' (45H)
mov bx, 0b800h ; 80x25 彩色文本模式的显存段基址
mov es, bx ; 将显存段基址加载到附加段寄存器 ES 中
mov es:[160*12+40*2], ah ; 将高 4 位对应的字符写入屏幕正中央(第12行,第40列)
; ----------------------------
; 第三步:查表获取“低 4 位”对应的十六进制字符,并写入显存
; ----------------------------
mov bl, al ; 将低 4 位的值(0~15)放入 BL,作为查表的偏移量
mov bh, 0 ; 将 BH 清零
mov al, table[bx] ; 【查表核心】以 BX 为偏移,从 table 中取出对应的 ASCII 字符放入 AL
mov es:[160*12+40*2+2], al ; 将低 4 位对应的字符写入屏幕正中央的下一个字符位置(+2字节)
; ----------------------------
; 恢复现场并返回
; ----------------------------
pop es ; 恢复 ES 寄存器原来的值
pop bx ; 恢复 BX 寄存器原来的值
ret ; 子程序返回,回到主程序
code ends
end start
16.4 程序入口地址的直接定址表(感兴趣可以自己看课程完成,本人跳过了)
【编程】实现一个子程序setscreen,为显示输出提供如下功能:
- 清屏
- 设置前景色
- 设置背景色
- 向上滚动一行
- 入口参数说明:
- 用ah寄存器传递功能号
0:清屏;1:设置前景色;2:设置背景色;3:向上滚动一行 - 对于2、3号功能,用al传递颜色值
al∈{0,1,2,3,4,5,6,7}
- 用ah寄存器传递功能号
- 各种功能如何实现
- 清屏:将显存中当前屏幕中的字符设为空格符;
- 设置前景色:设置显存中当前屏幕中处于奇地址的属性字节的第0、1、2位;012位存放前景色
- 设置背景色:设置显存中当前屏幕中处于奇地址的属性字节的第4、5、6位;456位存放背景色
- 向上滚动一行:依次将第 n+1行的内容复制到第n行处:最后一行为空。
各独立功能子程序代码
;功能子程序1:清屏
sub1:
push bx ;保护现场,调用子程序的时候,注意要保护现场,运行子程序的时候,可能会修改一些寄存器的值
push cx
push es
mov bx,0b800h
mov es,bx
mov bx,0
mov cx,2000
sub1s:
mov byte ptr es:[bx],' ' ;循坏2000次
add bx,2
loop sub1s
pop es ;恢复现场
pop cx
pop bx
ret
;功能子程序2:设置前景
sub2:
push bx
push cx
push es
mov bx,0b800h
mov es,bx
mov bx,1
mov cx,2000
sub2s:
and byte ptr es:[bx],11111000b
or es:[bx],al
add bx,2
loop sub2s
pop es
pop cx
pop bx
ret
;功能子程序3:设置背景色
sub3:
push bx
push cx
push es
mov cl,4
shl al,cl
mov bx,0b800h
mov es,bx
mov bx,1
mov cx,2000
sub3s:
and byte ptr es:[bx],10001111b
or es:[bx],al
add bx,2
loop sub2s
pop es
pop cx
pop bx
ret
;功能子程序4:向上滚动一行
sub4:
push cx
push si
push di
push es
push ds
mov si,0b800h
mov es,si
mov ds,si
mov si,160 ;ds:si指向第n+1行,第1行
mov di,0 ;es:di指向第n行,第0行
cld
mov cx,24;共复制24行
sub4s:
push cx
mov cx,160
rep movsb ;复制
pop cx
loop sub4s
mov cx,80
mov si,0
sub4s1:
mov byte ptr es:[160*24+si],' ' ;最后一行清空
add si,2
loop sub4s1
pop ds
pop es
pop di
pop si
pop cx
ret ;sub4 ends
- 可以将这些功能子程序的入口地址存储在一个表中,他们在表中的位置和功能号相对应
;编程:实现一个子程序setscreen,为显示输出提供如下功能:
;(1) 清屏。
;(2) 设置前景色。
;(3) 设置背景色。
;(4) 向上滚动一行。
;
;入口参数说明:
;(1) 用 ah 寄存器传递功能号:0 表示清屏,1表示设置前景色,2 表示设置背景色,3 表示向上滚动一行;
;(2) 对于2、3号功能,用 al 传送颜色值,(al) ∈{0,1,2,3,4,5,6,7}
setscreen:
jmp short set
table dw sub1,sub2,sub3,sub4
set:
push bx
cmp ah,3 ;判断传递的是否大于 3
ja sret
mov bl,ah
mov bh,0
add bx,bx ;根据ah中的功能号计算对应子程序的地址在table表中的偏移
call word ptr table[bx] ;调用对应的功能子程序,学会本句代码,是本章节的【精髓】
sret:
pop bx
ret
;功能子程序1:清屏
sub1:
push bx
push cx
push es
mov bx,0b800h
mov es,bx
mov bx,0
mov cx,2000
sub1s:
mov byte ptr es:[bx],' '
add bx,2
loop sub1s
pop es
pop cx
pop bx
ret ;sub1 ends
;功能子程序2:设置前景色
sub2:
push bx
push cx
push es
mov bx,0b800h
mov es,bx
mov bx,1
mov cx,2000
sub2s:
and byte ptr es:[bx],11111000b
or es:[bx],al
add bx,2
loop sub2s
pop es
pop cx
pop bx
ret ;sub2 ends
;功能子程序3:设置背景色
sub3:
push bx
push cx
push es
mov cl,4
shl al,cl
mov bx,0b800h
mov es,bx
mov bx,1
mov cx,2000
sub3s:
and byte ptr es:[bx],10001111b
or es:[bx],al
add bx,2
loop sub2s
pop es
pop cx
pop bx
ret ; sub3 ends
;功能子程序4:向上滚动一行
sub4:
push cx
push si
push di
push es
push ds
mov si,0b800h
mov es,si
mov ds,si
mov si,160 ;ds:si指向第n+1行
mov di,0 ;es:di指向第n行
cld
mov cx,24;共复制24行
sub4s:
push cx
mov cx,160
rep movsb ;复制
pop cx
loop sub4s
mov cx,80
mov si,0
sub4s1:
mov byte ptr es:[160*24+si],' ' ;最后一行清空
add si,2
loop sub4s1
pop ds
pop es
pop di
pop si
pop cx
ret ;sub4 ends
第十七章 使用BIOS进入键盘输入和磁盘读写
- 大多数有用的程序都需要处理用户的输入,键盘输入是最基本的输入。
- 程序和数据通常需要长期存储,磁盘是最常用的存储设备。
- BIOS 为这两种外设的I/O提供了最基本的中断例程,在本章中,我们对它们的应用和相关的问题进行讨论。
17.1 int9 中断例程对键盘输入的处理
CPU 在9 号中断发生后,执行int 9中断例程,从60h 端口读出扫描码,并将其转化为相应的ASCII 码或状态信息,存储在内存的指定空间(键盘缓冲区或状态字节)中。
17.2 使用int16h中断例程读取键盘缓冲区
- 提供了
int 16h中断例程供程序员调用。 int 16h中断例程中包含的一个最重要的功能是从键盘缓冲区中读取一个键盘输入,该功能的编号为0。- 下面的指令从键盘缓冲区(缓冲区的最低位)中读取一个键盘输入,并且将其从缓冲区中删除:
mov ah,0
int 16h
结果: (ah)=扫描码,(al)=ASCII码。
- int 16h 中断例程的 0 号功能,进行如下的工作:
(1)检测键盘缓冲区中是否有数据;
(2)没有则继续做第1 步;(缓冲区随时有可能输入数据)
(3)读取缓冲区第一个字单元中的键盘输入;
(4)将读取的扫描码送入ah,ASCII 码送入al;
(5)将己读取的键盘输入从缓冲区中删除。 - 可见,BIOS 的
int 9 中断例程和int 16h 中断例程是一对相互配合的程序,int 9中断例程向键盘缓冲区中写入,int 16h中断例程从缓冲区中读出。
它们写入和读出的时机不同,int 9 中断例程在有键按下的时候向键盘缓冲区中写入数据;
而int 16h 中断例程是在应用程序对其进行调用的时候,将数据从键盘缓冲区中读出。
【编程】接收用户的键盘输入,输入“r”,将屏幕上的字符设置为红色:输入“g”,
将屏幕上的字符设置为绿色;输入“b ”,将屏幕上的字符设置为蓝色。
code segment
assume cs:code
start:
; ----------------------------
; 第一步:读取用户键盘输入
; ----------------------------
mov ah, 0
int 16h ; 调用 BIOS 16h 中断的 0 号功能:从键盘缓冲区读取一个字符
; 读取成功后,AL 中存放的是字符的 ASCII 码,AH 中存放的是扫描码
; ----------------------------
; 第二步:判断输入的字符(A 处的技巧)
; ----------------------------
mov ah, 1 ; 【A处】将 AH 初始化为 1(对应 VGA 文本模式下的“红色”属性值)
cmp al, 'r' ; 判断用户是否输入了 'r'
je red ; 如果是 'r',跳转到 red 标签
cmp al, 'g' ; 判断用户是否输入了 'g'
je green ; 如果是 'g',跳转到 green 标签
cmp al, 'b' ; 判断用户是否输入了 'b'
je blue ; 如果是 'b',跳转到 blue 标签
jmp short sret ; 如果都不是,直接跳转到程序退出
red:
shl ah, 1 ; 【B处】将 AH 左移 1 位。AH 从 00000001b (1) 变为 00000010b (2)
green:
shl ah, 1 ; 【C处】将 AH 左移 1 位。
; 如果是从 red 跳过来的,AH 从 2 变为 4 (00000100b,即“绿色”)
; 如果是从 green 直接跳过来的,AH 从 1 变为 2 (00000010b,即“红色”)
blue:
; ----------------------------
; 第三步:遍历显存,批量修改屏幕字符颜色
17.3 字符串的输入(int 21输入输出、int 16 输入字符串)
int 21h的0a号功能可以实现字符串的输入 ;
INT 21H 的 0AH 号功能是 DOS 系统提供的 “带缓冲区的字符串输入” 功能。它允许用户从键盘输入一整串字符,并将其安全地存入内存中。
要正确使用这个功能,你需要掌握以下三个核心要点:
1. 调用前的准备(入口参数)
- 设置功能号:将
AH寄存器赋值为0AH。 - 设置缓冲区地址:将
DS:DX指向一个特定格式的输入缓冲区首地址。
2. 输入缓冲区的特定格式
这是 0AH 号功能最关键的部分。你必须在数据段中提前定义好一个缓冲区,它由三部分组成:
- 第 1 个字节:由程序员预先填入,表示允许输入的最大字符数(包含最后的回车符)。
- 第 2 个字节:由系统自动填充。输入结束后,这里会存放实际输入的字符个数(不包括回车符)。
- 第 3 个字节及以后:用于存放实际输入的字符串内容(ASCII 码)。
缓冲区定义示例:
BUF DB 20, ?, 20 DUP(0) ; 最大输入20个字符,?用于存储实际输入数,预留20字节空间,但是做多输入19个 最后一位得留给回车键!
在汇编语言的数据定义伪指令(DB)中,这个 ? 代表“未初始化数据”或“预留空间”。
3. 调用后的结果与注意事项
- 回车符处理:当用户按下回车键(Enter)时,输入结束。回车符的 ASCII 码(0DH)会被存入缓冲区,但它不计入实际输入的字符个数(即第 2 个字节的数值不包含回车符)。
- 溢出处理:如果用户输入的字符数超过了第 1 个字节定义的最大长度,多出的字符会被系统直接丢弃
实战代码演示
以下是一个完整的汇编代码片段,演示了如何调用 0AH 号功能接收用户输入:
DATA SEGMENT
; 定义输入缓冲区:最大长度20,实际长度由系统填入,预留20个字节存数据
BUF DB 20, ?, 20 DUP(0)
DATA ENDS
CODE SEGMENT
ASSUME CS:CODE, DS:DATA
START:
; 初始化数据段寄存器 DS
MOV AX, DATA
MOV DS, AX
; 将缓冲区的首地址偏移量加载到 DX 寄存器
LEA DX, BUF ; 或者使用 MOV DX, OFFSET BUF
; 调用 DOS 0AH 号功能,等待用户输入字符串
MOV AH, 0AH
INT 21H
; 输入结束后,实际输入的字符数存放在 BUF+1 处
; 输入的字符串内容从 BUF+2 处开始存放
; 安全退出程序
MOV AH, 4CH
INT 21H
CODE ENDS
END START
进阶提示:如果你后续想使用 INT 21H 的 09H 号功能(字符串显示)来输出刚才输入的这段文字,需要注意 09H 要求字符串必须以 $ 符号结尾。因此,在调用 0AH 输入结束后,你通常需要手动将用户输入的实际字符数(BUF+1)对应的位置替换为 $,才能正确打印出该字符串。
stack segment
db 128 dup(0)
stack ends
data segment
; 定义输入缓冲区:最大允许输入 19 个字符,? 存放实际长度
BUF DB 20, ?, 20 DUP(0)
; 定义一个换行回车符,方便打印时换行
CRLF DB 0DH, 0AH, '$'
data ends
code segment
assume cs:code, ds:data, ss:stack
start:
; ----------------------------
; 初始化数据段和栈段
; ----------------------------
mov ax, data
mov ds, ax
mov ax, stack
mov ss, ax
mov sp, 128
; ----------------------------
; 第一步:调用 INT 21H 0AH 接收用户输入
; ----------------------------
lea dx, BUF ; 将缓冲区首地址加载到 DX
mov ah, 0ah
int 21h ; 等待用户输入(例如输入 Hello 然后按回车)
; ----------------------------
; 第二步:【核心操作】手动给字符串末尾添加 '$'
; ----------------------------
; 获取实际输入的字符个数(BUF+1 处的值)
mov bl, BUF+1 ; 把实际长度(比如 5)放到 BL 中
mov bh, 0 ; 将 BH 清零,组成合法的 16 位偏移量 BX
mov byte ptr BUF+2[bx], '$' ; 【关键指令】在字符串末尾写入 '$'
; ----------------------------
; 第三步:调用 INT 21H 09H 打印刚才输入的字符串
; ----------------------------
lea dx, BUF+2 ; DX 指向真正的字符串内容(BUF+2)
mov ah, 09h
int 21h ; 屏幕打印出:Hello
; ----------------------------
; 第四步:打印一个换行回车,让光标移到下一行
; ----------------------------
lea dx, CRLF
mov ah, 09h
int 21h
; ----------------------------
; 安全退出程序
; ----------------------------
mov ax, 4c00h
int 21h
code ends
end start

| 寄存器 | 在 09H 功能中的角色 | 通俗理解 |
|---|---|---|
| DS | 存放段基址 | 告诉系统:去哪个“房间”找数据 |
| DX | 存放偏移地址 | 告诉系统:在这个房间的“第几个柜子”找数据 |
16h(选看)
也可以用int 16h,通过显示键盘缓冲区中的内容,实现字符串的显示(选看 ):
- 使用int 16h显示字符串程序的处理过程如下
① 调用int 16h读取键盘输入;
② 如果是字符,进入字符栈,显示字符栈中的所有字符;继续执行① ;
③ 如果是退格键,从字符栈中弹出一个字符,显示字符栈中的所有字符;继续执行① ;
④ 如果是Enter 键,向字符栈中压入0,返回。
- 子程序:字符栈的入栈、出栈和显示
参数说明
(ah)=功能号,0表示入栈,1表示出栈,2表示显示;
ds : si 指向字符栈空间;
对于0 号功能:(al)=入栈字符;
对于1 号功能:(al)=返回的字符;
对于2 号功能:(dh)、(dl) =字符串在屏幕上显示的行、列位置。
; ============================================================
; 子程序名称:charstack
; 功能:一个支持回显、退格、回车结束的字符串输入子程序
; 参数:(dh) = 显示行号, (dl) = 显示列号
; ds:si = 指向字符串的存储空间(以 0 为结尾符)
; ah = 功能号:0=压栈(输入), 1=出栈(退格), 2=显示字符串
; ============================================================
charstack:
jmp short charstart ; 【安全跳转】直接跳到初始化部分,防止程序误入数据区
table dw charpush, charpop, charshow ; 【直接定址表】存放3个子程序的偏移地址
top dw 0 ; 【栈顶指针】记录当前栈中有多少个字符(初始为0)
charstart:
; ----------------------------
; 保护现场(因为子程序会修改这些寄存器,退出前必须还原)
; ----------------------------
push bx
push dx
push di
push es
; ----------------------------
; 核心调度:根据 ah 中的功能号,跳转到对应的子程序
; ----------------------------
cmp ah, 2 ; 判断功能号是否大于 2(合法功能号只有 0, 1, 2)
ja sret ; 如果 ah > 2,说明参数非法,直接恢复现场并返回
mov bl, ah ; 将功能号放入 bl
mov bh, 0 ; bh 清零,组成完整的 16 位 bx
add bx, bx ; 【关键算法】bx = bx * 2。因为每个地址占 2 个字节,所以偏移量要翻倍
jmp word ptr table[bx] ; 【查表跳转】跳转到 table[bx] 指向的子程序
; ============================================================
; 功能 0:压栈(charpush)
; 作用:将 al 中的字符压入 ds:si 指向的栈中,并让栈顶指针 +1
; ============================================================
charpush:
mov bx, top ; 获取当前的栈顶指针
mov [si][bx], al ; 将 al 中的字符存入 ds:si+bx 的位置
inc top ; 栈顶指针 +1(表示栈里多了一个字符)
jmp sret ; 处理完毕,跳转到统一返回处
; ============================================================
; 功能 1:出栈(charpop)
; 作用:模拟退格键,将栈顶指针 -1(字符实际上还在内存里,但逻辑上被删除了)
; ============================================================
charpop:
cmp top, 0 ; 检查栈是否为空
je sret ; 如果栈已经空了(top=0),不能继续退格,直接返回
dec top ; 栈顶指针 -1
mov bx, top ; 获取新的栈顶指针
mov al, [si][bx] ; 【可选操作】把刚弹出的字符读回 al(方便调用者知道删了谁)
jmp sret ; 处理完毕,跳转到统一返回处
; ============================================================
; 功能 2:显示字符串(charshow)
; 作用:将 ds:si 栈中从 0 到 top-1 的所有字符,刷新显示到屏幕指定位置
; ============================================================
charshow:
; ----------------------------
; 第一步:计算显存中的目标偏移地址 (di)
; 公式:显存偏移 = 160 * 行号 + 列号 * 2
; ----------------------------
mov bx, 0b800h ; 显存段基址 0B800H
mov es, bx ; 将显存段基址放入 ES 寄存器
mov al, 160 ; 准备乘以行号
mov ah, 0 ; 将 ah 清零,防止 mul 运算出错
mul dh ; ax = 160 * dh (行号)
mov di, ax ; 将行的起始偏移存入 di
add dl, dl ; 【巧妙算法】dl * 2 等价于 add dl, dl(计算列的字节偏移)
mov dh, 0 ; 将 dh 清零
add di, dx ; di = 行偏移 + 列偏移,现在 di 精准指向了目标光标位置
; ----------------------------
; 第二步:循环遍历栈中的字符,逐个写入显存
; ----------------------------
mov bx, 0 ; bx 作为循环计数器,从 0 开始遍历栈
charshows:
cmp bx, top ; 判断:当前遍历的位置是否等于栈顶?
jne noempty ; 如果 bx != top,说明还有字符没打印,跳转到 noempty
; 【情况一:栈空或遍历结束】
; 如果用户按了退格,栈变空了,我们需要把原来位置上的字符擦除(用空格覆盖)
mov byte ptr es:[di], ' '
jmp sret ; 显示完毕,跳转到统一返回处
noempty:
; 【情况二:还有字符需要显示】
mov al, [si][bx] ; 从栈中取出第 bx 个字符
mov es:[di], al ; 将字符 ASCII 码写入显存的偶数地址
mov byte ptr es:[di+2], ' ' ; 【防重叠残留】在当前字符的下一个位置写入空格。
; 防止上一次打印的长字符串残留(比如把 "Hello" 退格成 "He" 时,清除 'l')
inc bx ; 计数器 +1,准备处理下一个字符
add di, 2 ; 显存指针 +2,指向下一个字符的位置
jmp charshows ; 继续循环
; ============================================================
; 统一返回出口:恢复现场并返回
; ============================================================
sret:
pop es
pop di
pop dx
pop bx
ret
17.4 应用int13h中断例程对磁盘进行读写(选看)
- 磁盘的实际访问由磁盘控制器进行,我们可以通过控制磁盘控制器来访问磁盘。
- 注意,我们只能以扇区为单位对磁盘进行读写。
在读写扇区的时候,要给出面号、磁道号和扇区号。面号和磁道号从0开始,而扇区号从1开始。 - BIOS提供了对扇区进行读写的中断例程,这些中断例程完成了许多复杂的和硬件相关的工作。
- 我们可以通过调用BIOS中断例程来访问磁盘。
BIOS 提供的访问磁盘的中断例程为int 13h 。
如下,读取0面0道1扇区的内容到0:200:

返回参数:
操作成功:(ah)=0,(al)=读入的扇区数
操作失败:(ah)=出错代码
将0:200中的内容写入0面0道1扇区示例
返回参数:

操作成功: (ah)=0,(al)=写入的扇区数
操作失败: (ah)=出错代码
5.注意:使用int 13h 中断例程对软盘进行读写。直接向磁盘扇区写入数据是很危险的,很可能覆盖掉重要的数据。
【编程】将当前屏幕的内容保存在磁盘上
分析:1 屏的内容占4000个字节,需要8 个扇区(一个扇区512B),我们用0面0道的1~8扇区存储显存中的内容。
code segment
assume cs:code
start:
mov ax,0b800h
mov es,ax
mov bx,0 ;es:bx 指向将写入磁盘的数据的内存区
mov al,8 ;写入的扇区数
mov ch,0 ;磁道号,从0开始
mov cl,1 ;扇区号 从1开始
mov dl,0 ;驱动器号0:软驱A, 1:软驱B,硬盘从80h开始, 80h:硬盘C,81h:硬盘D
mov dh,0 ;磁头号,(对于软盘即面号,因为一个面用一个磁头来读写)
mov ah,3 ;传递 int 13h 写入数据的功能号
int 13h
;返回参数
;操作成功:(ah) = 0,(al) = 写入的扇区数
;操作失败:(ah) = 出错代码
return:
mov ax,4c00h
int 21h
code ends
end start
















【汇编全解】&spm=1001.2101.3001.5002&articleId=162546663&d=1&t=3&u=cb9d869ac0d6460189b3119bfdf5a516)
822

被折叠的 条评论
为什么被折叠?



