8086汇编
简要介绍
总线:
逻辑上划分:地址总线、数据总线、控制总线
+ 地址总线:指定存储单元
地址总线宽度(根数n):决定了可寻址的存储单元大小(2^n)
+ 数据总线:数据传送
数据总线宽度:决定数据传送速度
+ 控制总线:CPU通过控制总线对外部器件进行控制
控制总线宽度:决定对外部器件的控制能力
外部器件
CPU进行数据的读写,必须和外部器件进行三类信息的交互
存储单元的地址(地址信息)
器件的选择、读写命令(控制信息)
读写的数据(数据信息)
CPU地址空间分配
随机存储器(RAM):可读可写、断电清空
只读存储器(ROM):只读的
将各类存储器看做一个逻辑存储器:统一编址
寄存器
CPU中:
运算器:信息处理
寄存器:信息存储
控制器:控制各种器件进行工作
内部总线:连接各种器件,进行数据传送
汇编指令和寄存器名称不区分大小写
在进行数据传送或运算时:要注意指令的两个操作对象的位数应当一致
对于8086CPU
基础地址 + 偏移地址 = 物理地址
段地址x16可看做基础地址
内存并没有分段:
+ 可以通过分段方式管理内存
+ 可根据需要,将若干地址连续的内存单元看做一个段:
+ 用段地址x16定位段的起始地址(基础地址)
+ 用偏移地址定位段中的内存单元
注:
+ 段地址x16必然是16的倍数,因此段的起始地址也一定是16的倍数
+ 偏移地址为16为,16位地址寻址能力为64kb,所以一个段的长度最大为64kb
段寄存器
指示CPU当前要读取指令的地址
CS:代码段寄存器 M
IP:指令指针寄存器 N
8086CPU从内存M x16 + N单元(CS:IP)开始,读取一条指令并执行
读取指令后,IP中的值自动增加(IP = IP + 所读取指令长度),以使CPU可以读取下一条指令
8086CPU启动或复位后(CPU开始工作时)
CS和IP被设为一段地址 从此处读取开机后第一条指令
修改CS IP的指令
jmp指令
同时修改CS、IP的内容:jmp 段地址: 偏移地址
指令功能:用指令中给出的段地址修改CS 偏移地址修改IP
仅修改IP的内容:jmp 合法寄存器
指令功能:用寄存器中的值修改IP
jmp ax 含义上类似 mov IP, ax
(用汇编指令的语法描述 并不存在这种汇编指令)
Debug
Debug:实模式程序的调试工具
可以查看CPU各种寄存器中的内容、内存情况,在机器码级跟踪程序运行
用到的Debug功能:
+ R命令:查看、改变CPU寄存器的内容
+ D命令:查看内存中的内容
+ E命令:改写内存中的内容
+ U命令:将内存中的机器指令翻译成汇编指令
+ T命令:执行一条机器指令
+ A命令:以汇编指令的格式在内存中写入一条机器指令
Debug共有20多个指令,此6个与汇编学习密切相关
寄存器
字单元:存放一个字型数据(16位)的内存单元,由两个地址连续的内存单元组成
高位放高位字节
起始地址为N的字单元:N地址字单元
CPU读写内存单元时,必须先给出内存单元的地址
8086CPU有DS寄存器:通常用来存放要访问的数据的段地址
mov指令可完成两种传送:
将数据直接送入寄存器
将寄存器中的内容送入另一个寄存器
寄存器:用寄存器名来指明
内存单元:用内存单元的地址来指明
[...]
表示一个内存单元,...
表示内存单元的偏移地址
指令执行时,8086CPU自动取ds中的数据为内存单元的段地址
8086CPU不支持:将数据直接送入段寄存器的操作
因此需要用一个一般寄存器作为中转
字的传送
8086CPU是16位结构,可以一次性传送16位数据(一字)
因此,只要在mov指令中给出16位寄存器 就可以进行16为数据传送
定义了一个数据段
编程时,可将一组长度为N(N<=64KB)、地址连续、起始地址为16的倍数的内存单元当做专门存储数据的内存空间
(编程时的安排/ * 我觉得像一种设计方式 * /)
如何访问数据段中的数据
用ds存放数据段的段地址,根据需要,用相关指令访问数据段中的具体单元
CPU提供的栈机制
8086CPU提供相关指令:来以栈方式访问内存空间
基本的指令:PUSH(入栈)、POP(出栈)
push ax:将寄存器ax中的数据入栈
pop ax:将栈顶元素送入ax
8086CPU的入栈出栈操作都以字为单位进行的
字型数据用两个单元存放,高地址高8位 低地址低8位
8086CPU中
段寄存器SS:存放栈顶段地址
寄存器SP:存放偏移地址
任意时刻,SS:SP指向栈顶元素
push和pop指令执行时,CPU从SS和SP中得到栈顶地址
8086CPU中,入栈时,栈顶从高地址向低地址方向增长
栈反向增长?低位:栈顶 高位:栈底 入栈 SP-2 出栈 SP+2
栈越界问题
8086CPU不保证对栈的操作不会越界
只知道栈顶在何处(SS:SP),不知道栈空间大小
同只知道当前指令在何处(CS:IP),不知道要执行指令有多少
8086工作机理:只考虑当前情况
push pop指令可以在寄存器和内存间传送数据的
格式可以是:
push 寄存器 ;将寄存器中的数据入栈
push 段寄存器
push 内存单元
pop同理
push [0] ;内存地址为 ds:0
指令执行时,可以在push pop指令中只给出内存单元的偏移地址,段地址在指令执行时,CPU从ds中取得
push、pop需要两步操作
push:CPU先改变SP,后向SS:SP处传送
pop:CPU先读取SS:SP处的数据,后改变SP
push、pop等栈操作指令,修改的只是SP
栈顶变化范围最大为:0~FFFFH
栈段
将一段内存当做栈段是编程时的安排
访问栈段:通过改变SS:SP指向定义的栈段
段的综述
可以将一段内存定义为一个段
用段地址指示段
用偏移地址访问段内的单元
可以用段存放数据:数据段
将段地市放在DS中
用mov、add、sub等访问内存单元的指令时:CPU将段中内容当做数据访问
可以用段存放代码:代码段
段地址放在CS中,段中第一条指令偏移地址放在IP中
CPU将执行定义的代码段中的指令
可以用段当做栈:栈段
段地址放在SS中,栈顶单元的偏移地址放在SP中
CPU在执行栈操作时(push、pop指令等):将定义的栈段当做栈空间使用
一段内存,可以既是代码段,又是数据段,又是栈空间,也可以都不是
关键在于CPU中寄存器的设置:CS IP SS SP DS的指向
Debug的T命令在执行修改寄存器SS的指令时,下一条指令也紧接着被执行(有关中断机制)
第一个程序
汇编程序的简要过程:
第一步:编写汇编程序
产生了一个储存源程序的文本文件
第二步:对源程序进行编译连接
可执行文件包含两部分内容:
+ 程序(源程序汇编指令翻译来的机器码) 和 数据(源程序中定义的数据)
+ 相关的描述信息(如程序大小,占用内存空间等)
产生了一个可执行文件
第三步:执行可执行文件中的程序
操作系统依照.exe中的描述信息
将机器码和数据加载入内存
进行相关的初始化
CPU执行程序
汇编语言源程序中包含两种指令:
汇编指令:有对应的机器码的指令,最终被CPU执行
伪指令:没有对应的机器码,最终不被CPU执行
是由编译器执行的指令
编译器根据伪指令来进行相关的编译工作
1.伪指令:
(1)segment
和ends
1 | 段名 segment |
segment和ends成对使用的伪指令
功能:定义一个段
segment:说明一个段开始 ends:说明一个段结束
一个段必须有一个名称来标识(段名)
一个有意义的汇编程序至少要有一个段,用来存放代码
(2)end
汇编程序的结束标记
编译器编译汇编程序时,碰到end就结束编译
(3)assume
含义:假设某段寄存器和程序中的某一个用segment...ends
定义的段相关联
通过assume说明这种关联
编译程序可以将段寄存器和某个具体的段相联系
记得用assume将有特定用途的段和相关的段寄存器关联起来即可
2.程序:存储机器码的可执行文件
3.标号
一个标号指代了一个地址
例:segment
前的段名:最终将被编译、连接程序处理为一个段的段地址
4.程序的结构
简单框架:
1 | assume cs:abc ;abc被当做代码来用 应将abc和cs联系起来 |
5.程序返回
可执行文件如何运行:
有一个正在运行的程序P1
将P2从可执行文件中加载入内存后
将CPU控制权交给P2
P2开始运行后,P1暂停运行
P2运行完毕后,将CPU的控制权交还给使它得以运行的程序P1
P1继续运行
程序结束后,将CPU控制权交还的过程被称为:程序返回
如何返回:在程序的末尾添加返回的程序段
6.语法错误和逻辑错误
编译时被编译器发现的错误:语法错误
编译后,在运行时发生的错误:逻辑错误
编译
使用微软的masm5.0汇编编译器:文件名masm.exe
编译过程中 提供了一个输入:源程序文件(.asm)
最多可以得到3个输出:目标文件(.obj)、列表文件(.lst)、交叉引用文件(.crf)
后两个文件为中间结果,可以让编译器忽略对它们的生成
连接
使用微软的Overlay Linker3.60连接器:文件名link.exe
使用汇编语言编程,就要用到编辑器(Edit)、编译器(masm)、连接其(link)、调试工具(Debug)等工具
这些工具程序运行在操作系统之上
连接的作用
(1)源程序很大时,可以分为多个源程序文件来编译
每个源程序编译成为目标文件后,再用连接程序将其连接到一起,生成一个可执行文件
(2)程序中调用了某个库文件中的子程序
需要将这个库文件和该程序生产的目标文件连接到一起,生成一个可执行文件
(3)源程序编译后,得到了存有机器码的目标文件
目标文件中的内容还不能直接用来生成可执行文件,连接程序将这些内容处理为最终的可执行信息
对于连接的过程,可执行文件是要得到的最终结果
操作系统外壳
操作系统都要提供一个称为shell(外壳) 的程序
用户使用这个程序来操作计算机系统进行工作
DOS中 有个程序command.com:在DOS中称为命令解释器:即DOS系统的shell
DOS启动时,先完成其他重要的初始化工作,然后运行command.com执行完其他相关的任务
如果用户要执行一个程序,则输入该程序的可执行文件的名称
command先根据文件名找到可执行文件
然后将这个可执行文件中的程序加载入内存,设置CS:IP指向程序的入口
此后command暂停运行,CPU运行程序
程序运行结束后,返回到command中
在DOS中,command处理各种输入:命令或要执行的程序的文件名
我们通过command来进行工作
可以使用Debug跟踪程序的运行过程
Debug可以将程序加载入内存、设置CS:IP指向程序的入口
但Debug并不放弃对CPU的控制
这样,就可以使用Debug相关命令来单步执行程序,查看每一条指令的执行结果
DOS系统中.exe文件中的程序的加载过程:
- 找到一段起始地址(容量足够的空闲内存区)
- 在这段内存区前256字节中,创建称为程序前缀(PSP)的数据区
DOS要利用PSP来和被加载的程序进行通信 - 在PSP后,将程序装入
注:PSP区和程序区虽然物理地址连续,却有不同的段地址 - 将该内存区的段地址存入ds中,初始化其他相关寄存器后,设置CS:IP指向程序入口
[BX]和loop指令
1.[bx]
完整的描述一个内存单元,需要两种信息:
1.内存单元的地址
2.内存单元的长度(类型)
用[0]
表示一个内存单元时:
0表示单元的偏移地址
段地址默认在ds中
单元的长度(类型)可以由具体指令中的其他操作对象(如寄存器)指出
[bx]
同样也表示一个内存单元
偏移地址在bx中
段地址在ds中
2.loop
该指令与循环有关
3.描述性符号:()
为了简洁的描述:使用()
来表示一个寄存器或内存单元中的内容
()
中的元素可以有三种类型:寄存器名、段寄存器名、内存单元的物理地址(一个20位的数据)
(X)
所表示的数据有两种类型:字节、字
是哪种类型由寄存器名或具体的运算决定
4.约定符号idata表示常量
inc bx 含义:bx中的内容加1
Loop指令
指令格式:loop 标号
CPU执行loop指令时,要进行两步操作
1.(cx)=(cx)-1
2.判断cx中的值
不为零则转至标号出执行程序
为零则向下执行
通常,用loop指令实现循环功能,cx中存放循环次数
例:
1 | assume cs:code |
用cx和loop指令相配合实现循环功能的3个要点:
(1)在cx中存放次数
(2)loop指令中的标号所标识地址要在前面
(3)要循环执行的程序段,要写在标号和loop指令中间
程序框架如下:
1 | mov cx,循环次数 |
注:在汇编源程序中,数据不能以字母开头,所以要在前面加上0
Debug命令 g:
`g 0012`表示执行程序到当前代码段(段地址在CS中)的0012h处
将Debug从当前CS:IP指向的程序执行,一直到(IP)=0012h为止
Debug命令 p:
遇到loop指令时,使用p命令来执行,Debug会自动重复执行循环中的指令,直到(cx)=0为止
对于[idata]
Debug将[idata]
解释为一个内存单元,idata是内存单元的偏移地址
编译器将[idata]
解释为idata
如何在源程序中实现将内存2000:0单元中的数据送入al中
可将偏移地址送入bx寄存器中,用[bx]
的方式来访问内存单元
1 | mov ax,2000h |
麻烦
可以在[]
前显式的给出段地址所在的段寄存器
1 | mov ax,2000h |
(1)在汇编源程序中,如果用指令访问一个内存单元,则在指令中必须用[...]
来表示内存单元
如果[]
里用一个常量idata直接给出内存单元的偏移地址,就要在[]
前显式的给出段地址所在的段寄存器
若未显式给出段寄存器,则[idata]
解释为idata
(2)如果在[]
里用寄存器(间接给出内存单元的偏移地址),则段地址默认在ds中
也可以显式的给出段地址所在的段寄存器
为将8位寄存器 加到16位寄存器上
需要先将8位数据赋值到16位寄存器(如ax)中,再将ax中的数据加到bx上
从而是两个运算对象的类型匹配且结果不会超界
段前缀
用于显式的指明内存单元的段地址的ds: cs: ss: es:
在汇编语言中称为段前缀
一段安全的空间
在不能确定一段内存空间中是否存放重要的数据或代码时,不能随意向其中写入内容
如果要向内存空间写入数据的话,要使用操作系统分配的空间,而不应该直接用地址任意指定内存单元
一般PC机中,DOS方式下一般都不会使用0:200h~0:2ffh(00200h~002ffh)
的256个字节的空间
段前缀的使用
在循环中使用段前缀而不是隐式使用ds 可以减少ds设置次数
包含多个段的程序
规范角度:定义的数据在可执行文件中的程序被加载入内存时,数据同时也被加载入内存中,获得了存储空间
在代码段中使用数据
dw 即 define world
含义:定义字型数据
数据之间以逗号分隔,所占内存空间大小为16字节
可以在源程序中指明程序的入口:
1 | assume cs:code |
将数据、代码、栈放入不同的段
放在一个段里的问题:
1.程序混乱
2.空间不能超过64kb(8086模式)
(1)定义多个段的方法
同定义代码段,对于不同的段 要有不同的段名
(2)对段地址的引用
段名相当于一个标号,代表了段地址
一个段中的数据的段地址可由段名代表,偏移地址要看其在段中的位置
1 | mov ax,data |
更灵活的定位内存地址的方法
(1)and指令:逻辑与指令,按位进行与运算
(2)or指令:逻辑或指令,按位进行或运算
以字符形式给出的数据
可以在汇编程序中,用'...'
的方式指明数据是以字符的形式给出的
编译器将其转换为相对应的ASCII码
1 | db 'unIX' |
小写字母的ASCII码比大写字母的值大20H
大写字母与小写字母的ASCII码区别:大写第五位为0 小写第五位为1
[bx+idata]
更为灵活的方式指明内存单元:
[bx+idata]
:表示一个内存单元
偏移地址为(bx)+idata (bx中的数值加上idata)
1 | mov ax,[bx+200] |
[bx+idata]
的方式为高级语言实现数组提供了便利机制
si和di是8086CPU中和bx功能相近的寄存器
si和di不能分成两个8位寄存器来使用
指明一个内存单元:[bx(si或di)]
和[bx(si或di)+idata]
更灵活的方式:[bx+si]
和[bx+di]
[bx+si]
表示一个内存单元
偏移地址为(bx)+(si)
例:
1 | 指令mov ax,[bx+si] |
[bx+si+idata]
和[bx+di+idata]
[bx+si+idata]
表示一个内存单元
偏移地址:(bx)+(si)+idata
1 | 指令mov ax,[bx+si+idata] |
不同的寻址方式的灵活应用
(1)[idata]
用常量表示地址
可用于直接定位一个内存单元
(2)[bx]
用变量来表示内存地址
可用于间接定位一个内存单元
(3)[bx+idata]
用一个变量和一个常量表示地址
可在一个起始地址的基础上用变量间接定位一个内存单元
(4)[bx+si]
用两个变量表示地址
(5)[bx+si+idata]
用两个变量和一个常量表示一个地址
可以用更加灵活的方式来定位一个内存单元的地址,便于从更加结构化的角度看待所要处理的数据
对于二重循环,loop指令默认cx为循环计数器
为了防止进行内层循环时覆盖外层循环的循环计数值
应该在每次内层循环开始时,将外层循环的cx中的数值保存起来
在执行外层循环的loop指令之前,再恢复外层循环的cx数值
若程序比较复杂,寄存器不够用
可以考虑将需要暂存的数据放到内存单元中
需要使用时再从内存单元中恢复
一般来说,在需要暂存数据的时候,都应该使用栈
1 | //类似的二层循环的C表示 |
数据处理的两个问题
(1)处理的数据在什么地方
(2)要处理的数据有多长
为了描述上的间接,定义的描述性符号:reg和sreg
reg 表示一个寄存器:ax bx cx dx ah al bh bl ch cl dh dl sp bp si di
sreg 表示一个段寄存器:ds ss cs es
bx si di bp
(1)在8086CPU中,只有这四个寄存器可以用在[...]
中来进行寻址
(2)在[...]
中
这四个寄存器可以单个出现
或只能以四种组合出现:
bx 和 si
bx 和 di
bp 和 si
bp 和 di
(3)只要在[...]
中使用寄存器bp,而指令中没有显性的给出段地址,段地址就默认在ss中
机器指令处理的数据在什么地方
数据处理大致分为三类:读取、写入、运算
机器指令层来讲:并不关心数据的值是多少,而关心指令执行前一刻其要处理的数据所在的位置
指令执行前,所要处理的数据可在三个地方:CPU内部、内存、端口
汇编语言中数据位置的表达
汇编语言中用3个概念来表达数据的位置
(1)立即数(idata)
- 对于直接包含在机器指令中的数据(执行前在CPU的指令缓冲器中),在汇编语言中称为:立即数(idata)
- 在汇编指令中直接给出
(2)寄存器
- 指令要处理的数据在寄存器中
- 在汇编指令中给出相应的寄存器名
(3)段地址(SA)和偏移地址(EA)
- 指令要处理的数据在内存中
- 在汇编指令中可用
[X]
的格式给出EA - SA在某个段寄存器中
存放段地址的寄存器也可以是显性给出的
寻址方式
数据存放在内存的时候,可以用多种方式来给定内存单元的偏移地址
这种定位内存单元的方法一般被称为:寻址方式
指令要处理的数据有多长
8086CPU指令可以处理两种尺寸的数据:byte(字节)、word(字)
所以在机器中要指明指令进行的操作数据大小
(1)通过寄存器名指明要处理的数据的尺寸
- 字操作:
ax
- 字节操作:
al
(2)在没有寄存器名的情况下
- 用操作符
X ptr
指明内存单元的长度 - X在汇编指令中可以为word或byte
1
2
3
4
5用word ptr指明了指令访问的内存单元是一个字单元
mov word ptr ds;[0],1
用byte ptr指明了指令访问的内存单元是一个字节单元
mov byte ptr ds:[0],1
在没有寄存器参与的内存单元访问指令中,用word ptr或byte ptr显性指明所要访问的内存单元的长度是很必要的
否则CPU无法得知所要访问的单元是字单元,还是字节单元
(3)其他方法
- 有些指令默认了访问的是字单元还是字节单元
如:push [1000H]
就不用指明访问的是字单元还是字节单元,因为push指令只进行字操作
div
指令
div是除法指令,使用div做除法时应注意一下问题:
(1)除数:有8位 16位两种,在一个reg或内存单元中
(2)被除数:默认放在AX 或 DX和AX中
+ 如果除数位8位,被除数则为16位,默认在AX中存放
+ 如果除数位16位,被除数则为32位,在DX和AX中存放,DX存放高16位,AX存放低16位
(3)结果:
+ 如果除数位8位,则AL存储除法操作的商,AH存储除法操作的余数
+ 如果除数位16位,则AX存储除法操作的商,DX存储除法操作的余数
格式如下:
1 | div reg |
伪指令dd
前面使用db和dw定义字节型数据 和 字型数据
dd用来定义dword(double word 双字)型数据的
dup
dup是一个操作符,在汇编语言中同db dw dd等一样,也由编译器识别处理的符号
和db dw dd等数据定义的伪指令配合使用,用来进行数据的重复
例:
1 | db 3 dup (0) |
转移指令的原理
可以修改IP,或同时修改CS和IP的指令统称为转移指令
转移指令就是可以控制CPU执行内存中某处代码的指令
8086CPU转移行为有以下几类:
- 只修改IP:段内转移:如
jmp ax
- 同时修改CS和IP:段间转移:如
jmp 1000:0
由于转移指令对IP修改范围不同,段内转移又分为:短转移和近转移
- 短转移IP的修改范围为:
-128~127
- 近转移IP的修改范围为:
-32768~32767
8086CPU转移指令分为以下几类
- 无条件转移指令
- 条件转移指令
- 循环指令
- 过程
- 中断
操作符offset
- 在汇编语言中是由编译器处理的符号
- 功能:取得标号的偏移地址
1 | start:mov ax,offset start ;相当于mov ax,0 |
jmp指令
- 无条件转移指令
- 可以只修改IP,也可以同时修改CS和IP
jmp指令要给出两种信息:
(1)转移的目的地址
(2)转移的距离(段间转移、段内短转移、段内近转移)
根据位移进行转移的jmp指令
jmp short 标号(转到标号出执行指令)
此格式的jmp指令:实现段内转移
对IP的修改范围:-128~127
short符号,说明此指令进行短转移
标号:代码段中的标号,指明了指令要转移的目的地
汇编指令中的idata(立即数),不论表示一个数据还是内存单元的偏移地址,都会在对应的机器指令中出现
CPU在执行jmp指令的时候并不需要转移的目的地址
在jmp short 标号
指令所对应的机器码中,并不包含转移的目的地址,而包含的是转移的位移
此位移,是编译器根据汇编指令中的标号计算出来的
(1)8位位移=标号处的地址-jmp指令后的第一个字节的地址
(2)short指明此处的位移为8位位移
(3)8位位移的范围为-128~127,用补码表示
(4)8位位移由编译程序在编译时算出
jmp near ptr 标号
功能:(IP)=(IP)+16位位移
转移的目的地址在指令中的jmp指令
jmp far ptr 标号
:实现段间转移,由称为远转移
(CS)=标号所在段的段地址
(IP)=标号所在段中的偏移地址
far ptr指明了指令用标号的段地址和偏移地址修改CS和IP
所对应的机器码中包含转移的目的地址
转移地址在寄存器中的jmp指令
指令格式:jmp 16位reg
功能:(IP)=(16位reg)
转移地址在内存中的jmp指令
有两种格式:
(1)jmp word ptr 内存单元地址(段内转移)· 功能:从内存单元地址处开始存放着一个字,是转移的目的的偏移地址 内存单元地址可用寻址方式的任一格式给出 (2)
jmp dword ptr 内存单元地址(段间转移) 功能:从内存单元地址处开始存放着两个字,高地址处的字是转移的目的段地址,低地址处是转移的目的偏移地址
(CS)=(内存单元地址+2)
(IP)+(内存单元地址)`
内存单元地址可用寻址方式的任一格式给出
jcxz指令
jcxz指令有条件转移指令
所有的有条件转移指令都是短转移
在对应的机器码中包含转移的位移,而不是目的地址
对IP的修改范围都为:-128~127
指令格式:jcxz 标号(如果(cx)=0,转移到标号处执行)
操作:当(cx)=0时,(IP)=(IP)+8位位移
8位位移=标号处的地址-jcxz指令后的第一个字节的地址
8位位移的范围为-128~127,用补码表示
8位位移由编译程序在编译时算出
当(cx)!=0时,什么也不做(程序向下执行)
loop指令
loop指令为循环指令
所有的循环指令都是短转移
在对应的机器码中包含转移的唯一,而不是目的地址
对IP的修改范围都为:-128~127
指令格式:loop 标号((cx)=(cx)-1,如果(cx)!=0,转移到标号处执行)
操作:
(1)(cx)=(cx)-1
(2)如果(cx)!=0,(IP)=(IP)+8位位移
8位位移=标号处的地址-loop指令后的第一个字节的地址
8位位移的范围为-128~127,用补码表示
8位位移由编译程序在编译时算出
如果(cx)=0,什么也不做(必须向下执行)
根据位位移进行转移的意义
方便了程序段在内存中的浮动装配
在内存中的不同位置都可正确执行
如果包含地址,则就对程序段在内存中的偏移地址有了严格的限制
编译器对转移位移超界的检测
注意:根据位移进行转移的指令,它们的转移范围收到转移位移的限制
如果在源程序中出现了转移范围超界的问题,在编译的时候,编译器将报错
CALL和RET指令
call和ret指令都是转移指令,它们都修改IP,或同时修改CS和IP
它们经常被用来实现子程序的设计
ret和retf
ret指令:用于栈中的数据,修改IP的内容,从而实现近转移
retf指令:用于栈中的数据,修改CS和IP的内容,从而实现远转移
CPU执行ret指令时,进行下面两步操作:
(1)(IP)=((ss)* 16+(sp))
(2)(sp)=(sp)+2
CPU执行retf指令时,进行下面4步操作:
(1)(IP)=((ss)* 16+(sp))
(2)(sp)=(sp)+2
(3)(CS)=((ss)* 16+(sp))
(4)(sp)=(sp)+2
CPU执行ret执行时,相当于进行:pop IP
CPU执行ref指令时,相当于进行:POP IP POP CS
call指令
CPU执行call指令时,进行两步操作:
(1)将当前的IP或CS和IP压入栈中
(2)转移
call指令不能实现短转移,除此之外,call指令实现转移的方法和jmp指令的原理相同
根据位移进行转移的call指令
call 标号(将当前的IP压入栈后,转到标号处执行指令)
CPU执行此中格式的call指令时,进行如下的操作:
(1)(sp)=(sp)-2
((ss)* 16+(sp))=(IP)
(2)(IP)=(IP)+16位位移
相当于对当前IP的转移位移
转移的目的地址在指令中的call指令
call far ptr 标号
实现的是段间转移
CPU执行此种格式的call指令时,进行如下的操作:
(1)(sp)=(sp)-2
((sp)* 16+(sp))=(CS)
(sp)=(sp)-2
((ss)* 16+(sp))=(IP)
(2)(CS)=标号所在段的段地址
(IP)=标号在段中的偏移地址
转移地址在寄存器中的call指令
指令格式:call 16位reg
功能:
(sp)=(sp)-2
((ss)* 16+(sp))=(IP)
(IP)=(16位reg)
转移地址在内存中的call指令
有两种格式:
(1)call word ptr 内存单元地址
相当于:
push IP
jmp word ptr 内存单元地址
(2)call dword ptr 内存单元地址
相当于:
push CS
push IP
jmp dword ptr 内存单元地址
可以写一个具有特定功能的程序段:称其为子程序
在需要的时候,用call指令去执行
可以利用call和ret来实现子程序的机制,子程序框架如下:
标号:
指令
ret
具有子程序的源程序框架如下:
1 | assume cs:code |
mul指令
mul是乘法指令,使用mul做乘法的时候注意以下两点:
(1)两个相乘的数,要么都是8位,要么都是16位
如果是8位,一个默认放在AL中,另一个放在8位reg或内存字节单元中
如果是16位:一个默认在AX中,另一个放在16位reg或内存字单元中
(2)结果:
如果是8位乘法,结果默认放在AX中
如果是16位乘法,结果高位默认在DX中存放,低位在AX中放
格式如下:
1 | mul reg |
内存单元可以用不同的寻址方式给出,如:
1 | mul byte ptr ds:[0] |
模块化程序设计
利用call和ret指令,可以用简洁的方法,实现多个相互联系、功能独立的子程序来解决一个复杂的问题
参数和结果传递的问题
两个问题:
(1)将参数N存储在什么地方
(2)计算得到的数值,存储在什么地方
用寄存器来存储参数和结果是最常用的方法
对于存放参数的寄存器和存放结果的寄存器,调用者和子程序的读写操作恰好相反:
调用者将参数送入参数寄存器,从结果寄存器中取到返回值
子程序从参数寄存器中取到参数,将返回值送入结果寄存器
批量数据的传递
将批量数据放到内存中,然后将其所在的内存空间的首地址放在寄存器中,传递给需要的子程序
对于具有批量数据的返回结果,也可以用同样的方法
注:除了用寄存器传递参数外,还有一种通用的方法是用栈来传递参数
详见附注4
寄存器冲突问题
子程序中使用的寄存器,很可能在主程序中也要使用,造成了寄存器使用上的冲突
我们希望:
(1)编写调用子程序的程序的时候不必关系子程序到底使用了哪些寄存器
(2)编写子程序的时候不必关系调用者使用了哪些寄存器
(3)不会发生寄存器冲突
解决问题的简捷方法:
在子程序的开始将子程序中所有用到的寄存器中的内容都保存起来,在子程序返回前再恢复
可以用栈来保存寄存器中的内容
编写子程序的标准框架如下:
1 | 子程序开始:子程序中使用的寄存器入栈 |
要注意寄存器入栈和出栈的顺序
标志寄存器
CPU内部的寄存器中,有一种特殊的寄存器(对于不同的处理机,个数和结构都可以不同)
具有以下3中作用:
(1)用来存储相关指令的某些执行结果
(2)用来为CPU执行相关指令提供行为依据
(3)用来控制CPU的相关工作方式
这种特殊的寄存器在8086CPU中,被称为标志寄存器
8086CPU的标志寄存器有16位,其中存储的信息通常被称为程序状态字(PSW)
以下简称为:flag
flag和其他寄存器不一样
其他寄存器用来存放数据,都是整个寄存器具有一个含义
flag寄存器是按位起作用的,即:每一位都有专门的含义,记录特定的信息
flag的1、3、5、12、13、14、15
位在8086CPU中没有使用,不具有任何含义
而0、2、4、6、7、8、9、10、11
位都有特殊含义
ZF标志
flag的第6位是ZF,零标志位
记录相关指令执行后,其结果是否为0
如果为0,则zf=1
如果不为0,则zf=0
zf标记相关指令的计算结果是否为0,如果为0,则zf要记录下”是0”这样的肯定信息,若不为零,则要记录下”结果不是0
在计算机中,1表示逻辑真,0表示逻辑假
注意:在8086CPU的指令集中
有的指令的执行是影响标志寄存器的
如add、sub、mul、div、inc、or、and
等,大多都是运算指令(进行逻辑或算数运算)
有的指令的执行对标志寄存器没有影响
如mov、push、pop
等,大都是传送指令
在使用一条指令的时候,要注意这条指令的全部功能
其中包括,执行结果对标志寄存器的哪些标志位造成影响
PF标志
flag的第2位是PF,寄偶标志位
记录相关指令执行后,其结果的所有bit位中1的个数是否为偶数
如果1的个数为偶数,pf=1
如果为奇数,pf=0
SF标志
flag的第7为是SF,符号标志位
记录相关指令执行后,其结果是否为负
如果为负,sf=1
如果非负,sf=0
计算机中通常用补码来表示有符号数据
计算机中的一个数据可以看作是有符号数,也可以看成是无符号数
不管如何看待,CPU在执行add等指令的时候,就已经包含了两种演绎,也将得到用同一种信息来记录的两种结果
关键在于我们的程序需要哪一种结果
即 对一个数字有两种不同的解释
SF标志,CPU对有符号数运算结果的一种记录
在将数据当做有符号数来运算的时候,可以通过它来得知结果的正负
如果将数据当做无符号数来运算,SF的值则没有意义
CF标志
flag的第0位是CF,进位标志位
一般情况下,在进行无符号数运算的时候,记录了运算结果的最高有效位向更高位的进位值,或从更高位的借位值
对于位数为N的无符号数来说,其对应的二进制信息的最高位,即第N-1位,是最高有效位
则 第N位是相对于最高有效位的更高位
两数相加的时候,可能产生从最高有效位向更高位的进位(高位溢出)
溢出的这个进位值
在CPU运算的时候记录在一个特殊的寄存器的某一位上
8086CPU就用CF位来记录这个进位值
而当两数减法时,可能向更高位借位
而flag的CF位也可以用来记录这个借位值
OF标志
在进行有符号运算的时候,如果超过了机器所能表示的范围称为溢出
由于在进行用符号数运算时,可能发生溢出而造成结果的错误
则CPU需要对指令执行后是否产生溢出进行记录
flag的第11位是OF,溢出标志位
一般情况下,OF记录了有符号数运算的结果是否发生了溢出
如果发生溢出,OF=1
如果没有,OF=0
注意:CF和OF的区别:
CF是对无符号运算有意义的标志位
OF是对有符号运算有意义的标志位
CPU在执行add等指令的时候,包含两种含义
对于无符号数运算,CPU用CF位来记录是否产生了进位
对于有符号数运算,CPU用OF位来记录是否产生了溢出
当然,还要用SF位来记录结果的符号
CF和OF所表示的进位和溢出,是分别对无符号数和有符号数运算而言的,它们之间没有任何关系
(说白了不都是溢出吗??)
adc指令
adc是带进位加法指令,利用了CF位上记录的进位值
指令格式:adc 操作对象1,操作对象2
功能:操作对象1=操作对象1 + 操作对象2 + CF
为什么加CF
加法可以分两步来进行:
(1)低位相加
(2)高位相加再加上低位相加产生的进位值
CPU提供的adc指令的目的:进行加法的第二步运算的
adc指令和add指令相配合就可以对更大的数据进行加法运算
sbb指令
sbb是带借位减法指令,它利用了CF位上记录的借位值
指令格式:sbb 操作对象1,操作对象2
功能:操作对象1=操作对象1-操作对象2-CF
sbb指令执行后,将对CF进行设置
利用sbb指令可以对任意大的数据进行减法运算
sbb和adc是基于同样的思想设计的两条指令,在应用思路上和adc类似
cmp指令
cmp是比较指令,功能相当于减法指令,只是不保存结果
cmp指令执行后,将对标志寄存器产生影响
其他相关指令通过识别这些被影响的标志寄存器位来得知比较结果
cmp指令格式:cmp 操作对象1,操作对象2
功能:计算操作对象1-操作对象2,但并不保存结果
仅仅根据计算结果对标志寄存器进行设置
通过cmp指令执行后,相关标志位的值就可以看出比较的结果:
比较指令的设计思路:通过做减法运算,影响标志寄存器,标志寄存器的相关位记录了比较的结果
1 | cmp ax,bx的逻辑含义是比较ax和bx中的值,如果执行后: |
对于有符号数
如果因溢出导致了实际结果为负,那么逻辑上真正的结果必然为正
1 | zf=1,则(ah)=(bh) |
检测比较结果的条件转移指令
转移:指能够修改IP
条件:指可以根据某种条件,决定是否修改IP
这些条件转移指令通常都和cmp相配合使用
就像call和ret指令通常相配合使用一样
因为cmp指令可以同时进行两种比较,无符号数比较和有符号数比较
所以根据cmp指令的比较结果进行转移的指令也分为两种
即:根据无符号数的比较结果进行转移的条件转移指令(检测zf、cf的值)
和 根据有符号数的比较结果进行转移的条件转移指令(检测sf、of、zf的值)
常用的根据无符号数的比较结果进行转移的条件转移指令:
1 | 指令 含义 检测的相关标志位 |
这些指令比较常用,且好记
第一个字母都是j,表示jump
后面的字母意义如下:
e:表示equal
ne:表示not equal
b:表示below
nb:表示not below
a:表示above
na:表示not above
所检测的标志位,都是cmp指令进行无符号数比较的时候,记录比较结果的标志位
虽然可以随便写,但经常在一起配合使用
不必再考虑cmp指令对相关标志位的影响和je等指令对相关标志位的检测
联合使用时表现的功能有些像高级语句中的IF语句
此处主要探讨cmp、标志寄存器的相关位、条件转移指令 三者配合应用的原理,该原理具有普遍性
DF标志和串传送指令
flag的第10位是DF,方向标志位
在串处理指令中,控制每次操作后si、di的增减
df=0 每次操作后si、di递增
df=1 每次操作后si、di递减
一个串传送指令
格式:movsb
功能:执行movsb指令相当于进行下面几步操作
(1)((es)* 16+(di))=((ds)* 16+(si))
(2)如果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
功能:将ds:si指向的内存字单元中的字送入es:di中,然后根据标志寄存器中df位的值,将si和di递增2或递减2
一般来说,movsb和movsw都和rep配合使用,格式如下:rep movsb
即:
1 | s: movsb |
rep的作用是:根据cx的值,重复执行后面的串传送质量
rep movsb可以循环实现(cx)哥字符的传送
由于flag的df位决定着串传送指令执行后,si和di改变的方向
所以CPU应该提供相应的指令来对df位进行设置,从而使程序原能够决定传送的方向
8086CPU提供下面两条指令对df位进行设置:
cld
指令:将标志寄存器的df位置0
std
指令:将标志寄存器的df位置1
pushf和popf
pushf的功能:将标志寄存器的值压栈
popf的功能:从栈中弹出数据,送入标志寄存器中
pushf和popf,为直接访问标志寄存器提供了一种方法
标志寄存器在Debug中的表示
在Debug中,标志寄存器是按照有意义的各个标志位单独表示的
内中断
任何一个通用的CPU都具备的一种能力
可以在执行完当前正在执行的指令之后,检测到从CPU外部发送过来的或内部产生的一种特殊信息,并且可以立即对所接收到的信息进行处理
这种特殊的信息被称为:中断信息
中断:CPU不再接着(刚执行完的指令)向下执行,而是转去处理这个特殊信息
内中断的产生
对于8086CPU,当CPU内部有下面的情况发生的时候,将产生相应的中断信息
(1)除法错误,比如:执行div指令产生的除法溢出
(2)单步执行
(3)执行into指令
(4)执行int指令
中断信息中必须包含识别来源的编码
8086CPU用称为中断类型码的数据来标识中断信息的来源
产生中断信息的事件:简称为中断源
4种中断源,在8086CPU中的中断类型码如下:
(1)除法错误:0
(2)单步执行:1
(3)执行into指令:4
(4)执行int指令
该指令的格式为int n
,
指令中的n为字节型立即数,是提供给CPU的中断类型码
中断处理程序
CPU收到中断信息后,需要对中断信息进行处理
如何处理中断信息可以由编程决定
我们编写的,用来处理中断信息的程序被称为中断处理程序
一般,需要对不同的中断信息编写不同的处理程序
CPU在收到中断信息后,就应该转去执行该中断信息的处理程序
CPU在收到中断信息后,如何根据中断信息确定其处理程序的入口?
CPU的设计者必须在终端信息和其处理程序的入口地址之间建立某种联系,使得CPU根据中断信息可以找到要执行的处理程序
中断信息中包含有标识中断源的类型码
中断类型码的作用:定位中断处理程序
若要定位中断处理程序,需要知道其段地址和偏移地址
而如何根据8位的中断类型码得到中断处理程序的段地址和偏移地址?
中断向量表
CPU用8位的中断类型码 通过 中断向量表 找到相应的中断处理程序的入口地址
中断向量表:中断处理程序入口地址的列表
中断向量表在内存中保存
CPU只要知道了中断类型码
就可以将中断类型码作为中断向量表的表项号
定位相应的表项
从而得到中断处理程序的入口地址
CPU如何找到中断向量表?
中断向量表在内存中存放,对于8086PC机,中断向量表指定放在内存地址0处
从内存0000:0000
到0000:03FF
的1024个单元中存放着中断向量表
在中断向量表中,一个表项存放一个中断向量(中断处理程序的入口地址)
对于8086CPU,这个入口地址包含段地址和偏移地址
所以一个表项占两个字,高地址字存放段地址,低地址字存放偏移地址
中断过程
中断类型码,在中断向量表中找到中断处理程序的入口
找到这个入口地址的最终目的:用它设置CS和IP,使CPU执行中断处理程序
用中断类型码找到中断向量,并通过它设置CS和IP,这个工作由CPU的硬件自动完成
CPU硬件完成这个工作的过程被称为中断过程
CPU收到中断信息后,要对中断信息进行处理,首先将引发中断过程
硬件在完成中断过程后,CS:IP将指向中断处理程序的入口,CPU开始执行中断处理程序
CPU在执行完中断处理程序后,应返回原来的执行段继续执行下面的指令
所以在终端过程中,设置CS:IP之前,还要将原来的CS和IP的值保存起来
类似call指令调用子程序
8086CPU在收到中断信息后,所引发的中断过程
(1)(从中断信息中)取得中断类型码
(2)标志寄存器的值入栈
(因在中断过程中要改变标志寄存器的值,所以先将其保存在栈中)
(3)设置标志寄存器的第8位TF和第9位IF的值为0(后续介绍)
(4)CS的内容入栈
(5)IP的内容入栈
(6)从内存地址为中断类型码* 4和中断类型码* 4+2的两个字单元中读取中断处理程序的入口地址设置IP和CS
CPU在收到中断信息之后,如果处理中断信息,就完成一个由硬件自动执行的中断过程(程序员无法改变这个过程中所要做的工作)
中断过程的主要任务就是用中断类型码在终端向量表中找到中断处理程序的入口地址,设置CS和IP
中断处理程序和iret指令
由于CPU随时都可能检测到中断信息
即:CPU随时都可能执行中断处理程序,所以中断处理程序必须一直存储在内存某段空间之中
而中断处理程序的入口地址(即中断向量)必须存储在对应的中断向量表表项中
中断处理程序的编写方法和子程序的比较相似,下面是常规的步骤:
(1)保存用到的寄存器
(2)处理中断
(3)恢复用到的寄存器
(4)用iret指令返回
iret指令的功能用汇编语法描述为:
1 | pop IP |
iret通常和硬件自动完成的中断过程配合使用
中断过程中,寄存器入栈的顺序与iret出栈顺序刚好相对应
实现了用执行中断处理程序前的CPU现场恢复标志寄存器和CS、IP的工作
iret指令执行后,CPU回到执行中断处理程序前的执行点继续执行程序
除法错误中断的处理
0号中断处理程序的功能:显式提示信息Divide overflow
后,返回到操作系统中
编程处理0号中断
(1)当发生除法溢出时,产生0号中断信息,从而引发中断过程
此时,CPU将进行以下工作
1.取得中断类型码0
2.标志寄存器入栈,TF、IF设置为0
3.CS、IP入栈
4.(IP)=(0* 4),(CS)=(0* 4+2)
(2)可见,当中断0发生时,CPU将转去执行中断处理程序
只需要按如下步骤编写中断处理程序,当中断0发生时,即可显式overflow
1.相关处理
2.向现实缓冲区送字符串overflow
3.返回DOS
我们将这段程序称之为:do0
(3)do0应该放在哪里?
内存0000:0000~0000:03FF
大小为1KB的空间是系统存放中断处理程序的入口地址的中断向量表
8086支持256个中断,但实际上系统中要处理的中断时间远没有达到256个,所以在中断向量表中,有许多单元是空的
中断向量表是PC系统重最重要的内存区,只用来存放中断处理程序的入口地址,DOS和其他应用程序都不会随便使用这段空间
经验:do0长度不可能超过256个字节
结论:可以将do0传送到内存0000:0200
处
(4)将终端处理程序do0放到0000:0200
后
段地址0存放在0000:0002字单元中,偏移地址200H存放在0000:0000字单元中
总结:
(1)编写do0
(2)将do0送入内存0000:0200处
(3)将do0的入口地址0000:0200存储在中断向量表0号表项中
程序框架如下:
1 | assume cs:code |
程序分为两部分:
(1)安装do0,设置中断向量的程序
(2)do0
安装
可以使用movsb指令,将do0的代码送入0:200处
程序如下:
1 | assume cs:code |
用rep movsb指令时需要确定的信息
(1)传送的原始位置:段地址:code,偏移地址:offset do0
(2)传送的目的位置:0:200
(3)传送的长度:do0部分代码的长度
(4)传送的方向:正向
明确的程序如下:
1 | assume cs:code |
可以利用编译器来计算do0的长度:
1 | mov cx,offset do0end-offset do0 ;设置cx为传输长度 |
do0
do0程序的主要任务是显式字符串,程序如下:
1 | do0: 设置ds:si指向字符串 |
可显示的字符串放在哪里?
do0程序随时可能被执行,而要用到字符串,所以该字符串也应该存放在一段不会被覆盖的空间中
正确程序如下:
1 | assume cs:code |
设置中断向量
将do0入口地址0:200,写入中断向量表的0号表项中,使do0成为0号中断的中断处理程序
1 | mov ax,0 |
单步中断
基本上,CPU在执行完一条指令后,如果检测到标志寄存器的TF位为1,则产生单步中断,引发中断过程
单步中断的中断类型码为1,则它所引发的中断过程如下:
(1)取得中断类型码1
(2)标志寄存器入栈,TF、IF设置为0
(3)CS、IP入栈
(4)(IP)=(1* 4),(CS)=(1* 4+2)
CPU在执行程序的时候是从CS:IP指向的某个地址开始,自动向下读取指令执行
即,如果CPU不提供其他功能的话,就按照这种方式工作
只要CPU一加电,就从预设的地址开始一直执行下去,不能有任何程序能控制它在执行完一条指令后停止
Debug提供了单步中断的中断处理程序
功能为:显式所有寄存器中的内容后等待输入命令
然后,在使用t命令执行指令时,Debug将TF设置为1,使得CPU工作于单步中断方式下
则CPU执行完这条指令后就引发单步中断,执行单步中断的中断处理程序
所有的寄存器中的内容被现实在屏幕上,并且等待输入命令
CPU为了防止,当TF=1时,执行中断程序的命令后,TF=1,执行中断命令,陷入循环
解决办法:在进入中断处理程序之前,设置TF=0,从而避免CPU在执行中断处理程序的时候发生单步中断
这就是为什么在中断过程中有TF=0这个步骤
中断过程:
(1)取得中断类型码N
(2)标志寄存器入栈,TF=0、IF=0
(3)CS、IP入栈
(4)(IP)=(N* 4),(CS)=(N* 4+2)
最后,CPU提供单步中断的功能的原因是:为单步跟踪程序的执行过程,提供了实现机制
影响中断的特殊情况
一般情况下,CPU在执行完当前指令后,如果检测到中断信息,就相应中断,引发中断过程
有时,CPU在执行完当前指令后,即使发生中断,也不会相应
对应这些情况,不一一例举,只用一种情况来说明
在执行完向ss寄存器传送数据的指令后,即使发生中断,CPU也不会相应
这样做的主要原因是:ss:sp联合指向栈顶,而对它们的设置应该连续完成
如果在执行完设置ss的指令后,CPU相应中断,引发中断处理,要在栈中压入标志寄存器、CS、IP的值
而ss改变,sp并未改变,ss:sp指向的不是正确的栈顶,将引起错误
所以CPU在执行完设置ss的指令后,不相应中断
这给连续设置ss和sp指向正确的栈顶提供了一个时机
即,应该利用这个特性,将ss和sp的指令连续存放,使得设置sp的指令紧接着设置ss的指令执行
而在此期间,CPU不会引发中断过程
int指令
另一种重要的内中断,由int指令引发的中断
int指令
int指令的格式:int n
n为中断类型码,功能是引发中断过程
CPU执行int n
指令,相当于引发了一个n号中断过程,执行过程如下:
(1)取中断类型码n
(2)标志寄存器入栈,IF=0,TF=0
(3)CS、IP入栈
(4)(IP)=(n* 4),(CS)=(n* 4+2)
从此转去执行n号中断的中断处理程序
可以在程序中使用int指令调用任何一个中断的中断处理程序
一般情况下,系统将一些具有移动功能的子程序,以中断处理程序的方式提供给应用程序调用
在编程时,可以用int指令调用这些子程序,也可以自己编写一些中断处理程序供别人使用
以后,将中断处理程序简称为中断例程
编写供应用程序调用的中断例程
同do0
编写中断例程和编写子程序的时候具有同样的问题,要避免寄存器的冲突
应该注意例程中用到的寄存器的值的保存和恢复
BIOS和DOS所提供的中断例程
系统板的ROM中存放着一套程序,成为BIOS(基本输入输出系统)
BIOS中主要包含以下几部分内容:
(1)硬件系统的检测和初始化程序
(2)外部中断和内部中断的中断例程
(3)用于对硬件设备进行I/O操作的中断例程
(4)其他和硬件系统相关的中断例程
操作系统DOS也提供了中断例程,从操作系统的角度来看,DOS的中断例程就是操作系统向程序员提供的编程资源
BIOS和DOS在所提供的中断历程中包含了许多子程序
这些子程序实现了程序员在编程的时候经常需要用到的功能
程序员在编程的时候,可以用int指令直接调用BIOS和DOS提供的中断例程,来完成某些工作
和硬件设备相关的DOS中断例程中,一般都调用了BIOS的中断例程
BIOS和DOS中断例程的安装过程
BIOS和DOS提供的中断例程是如何安装到内存中的呢?
安装过程:
(1)开机后,CPU加电,初始化(CS)=0FFFFH,(IP)=0,自动从FFFF:0单元开始执行程序
FFFF:0处有一条跳转指令,CPU执行该指令后,转去执行BIOS中的硬件系统检测和初始化程序
(2)初始化程序将建立BIOS所支持的中断向量,即将BIOS提供的中断例程的入口地址登记在中断向量表中
注意:对于BIOS所提供的中断例程,只需要将入口地址登记在中断向量表中即可
因为它们是固化到ROM中的程序,一直在内存中存在
(3)硬件系统检测在初始化完成后,调用int 19h
进行操纵系统的引导
从此将计算机交由操作系统控制
(4)DOS启动后,除完成其他工作外,还将它所提供的中断例程装入内,并建立相应的中断向量
BIOS中断例程应用
int 10h
中断例程是BIOS提供的中断例程,其中包含了多个屏幕输出相关的子程序
一般,一个供程序员调用的中断例程往往包含多个子程序,中断例程内部用传递进来的参数来确定执行哪一个子程序
BIOS和DOS提供的中断例程,都用ah来传递内部子程序的编号
int 10h
中断例程的设置光标位置功能
1 | mov ah,a ;置光标 |
DOS中断例程应用
int 21h
中断历程是DOS提供的中断历程,其中包含了DOS提供给程序原在编程时调用的子程序
int 21h
中断历程的4ch号功能,即程序返回功能,如下:
1 | mov ah,4ch ;程序返回 |
经常写做:
1 | mov ax,4c00h |
int 12h
中断例程在光标位置显示字符串的功能:
1 | ds:dx指向字符串 ;要显示的字符串需用"$"作为结束符 |
$本身并不显示,只起到边界的作用
如果字符串比较长,遇到行为,程序会自动转到下一行开头处继续显示
如果到了最后一行,还能自动上卷一行
DOS为程序原提供了许多可以调用的子程序,都包含在int 21h
中断例程中
端口
各种存储器都和CPU的地址线、数据线、控制线相连
CPU在操作它们的时候,把它们都当做内存来对待
把它们总的看做一个由若干存储单元组成的逻辑存储器
这个逻辑存储器称其为:内存地址空间
在PC机系统中,和CPU通过总线相连的芯片除各种存储器外,还有以下3中芯片
(1)各种接口卡(如,网卡、显卡)上的接口芯片,它们控制接口卡进行工作
(2)主板上的接口芯片,CPU通过它们对部分外设进行访问
(3)其他芯片,用来存储相关的系统信息,或进行相关的输入输出处理
在这些芯片中都有一组可以由CPU读写的寄存器
这些寄存器,在物理上可能处于不同的芯片中,但在以下两点上相同
(1)都和CPU的总线相连,当然这种连接是通过它们所在的芯片进行的
(2)CPU对它们进行读或写的时候都通过控制线向它们所在的芯片发出端口读写命令
可见,从CPU的角度,将这些寄存器都当做端口,对它们进行统一编址,从而建立了一个统一的端口地址空间
每个端口在地址空间中都有一个地址
CPU可以直接读写以下3个地方的数据:
(1)CPU内部的寄存器
(2)内存单元
(2)端口
端口的读写
访问端口的时候,CPU通过端口地址来定位端口
因为端口所在的芯片和CPU通过总线相连,所以,端口地址和内存地址一样,通过地址总线来传送
在PC系统中,CPU最多可以定位64KB个不同的端口
则端口地址的范围为:0~65535
对端口的读写不能用mov、push、pop等内存读写指令
端口的读写指令只有两条:
in和out
分别用于从端口读取数据和往端口写入数据
CPU执行内存访问指令和端口访问指令时,总线上的信息
(1)访问内存:
mov ax,ds:[8] ;假设执行前(ds)=0
执行时与总线相关的操作如下所示
1.CPU通过地址线将地址信息8发出
2.CPU通过控制线发出内存读命令,选中存储器芯片,并通知它,将要从中读取数据
3.存储器将8号单元中的数据通过数据线送入CPU
(2)访问端口
in al,60h ;从60h号端口读入一个字节
执行时与总线相关的操作如下
1.CPU通过地址线将地址信息60h发出
2.CPU通过控制吸纳发出端口读命令,选中端口所在的芯片,并通知它,将要从中读取数据
3.端口所在的芯片将60h端口中的数据通过数据线送入CPU
注意:在in和out指令中,只能使用ax或al来存放从端口中读入的数据或要发送到端口中的数据
访问8位端口时用al,访问16位端口时用ax
对0~255
以内端口进行读写时:
1 | in al,20h |
对256~65535
的端口进行读写时,端口号放在dx中
1 | mov dx,3f8h |
CMOS RAM芯片
PC机中,有一个CMOS RAM芯片,一般简称CMOS,此芯片的特征如下
shl和shr指令
shl和shr是逻辑位移指令
shl是逻辑左移指令,功能为:
(1)将一个寄存器或内存单元中的数据向左移位
(2)将最后移出的一位写入CF中
(3)最低位用0补充
如果移动位数大于1时,必须将移动位数放在cl中
1 | shl al,cl |
shr是逻辑右移指令,和shl所进行的操作刚好相反
(1)将一个寄存器或内存单元中的数据向右位移
(2)将最后移出的一位写入CF中
(3)最高位用0补充
如果移动位数大于1时,必须将移动位数放在cl中
外中断
CPU除了有运算能力外,还要有I/O能力
要及时处理外设的输入,显然需要解决两个问题;
(1)外设的输入随时可能发生,CPU如何得知
(2)CPU从何处得到外设的输入
接口芯片和端口
CPU通过端口和外部设备进行联系
外中断信息
外设随时都有可能发生需要CPU及时处理的事件,CPU如何及时得知并进行处理?
CPU提供中断机制来满足这种需要
当CPU内部有需要处理的事情发生的时候,将产生中断信息,引发中断过程
这种中断信息来自CPU的内部
当CPU外部有需要处理的事情发生的时候
CPU在执行完当前的指令后,可以检测到发送过来的中断信息,引发中断过程,处理外设的输入
PC系统中,外中断源一共有以下两类
1.可屏蔽中断
可屏蔽中断是CPU可以不相应的外中断
CPU是否相应可屏蔽中断,要看标志寄存器的IF位的设置
+ 当CPU检测到可屏蔽中断信息时:
+ 如果IF=1,则CPU在执行完当前指令后相应中断,引发中断过程
+ 如果IF=0,则不相应可屏蔽中断
可屏蔽中断所引发的中断过程,除了第一步的实现有所不同,基本上和内中断过程相同
因为可屏蔽中断信息来自于CPU外部,中断类型码是通过数总线送入CPU的
而内中断的中断类型码是在CPU内部产生的
将IF值0的原因:在进入中断处理程序后,禁止其他的可屏蔽中断
如果在中断处理程序中需要处理可屏蔽中断,可以用指令将IF置1
8086CPU提供的设置IF的指令如下:
1 | sti,设置IF=1 |
2.不可屏蔽中断
- 不可屏蔽中断是CPU必须相应的外中断
- 当CPU检测到不可屏蔽中断信息时,则在执行完当前指令后,立即相应,引发中断过程
对于8086CPU,不可屏蔽中断的中断类型码固定为2,所以中断过程中,不需要取中断类型码
则不可屏蔽中断的中断过程为:
(1)标志寄存器入栈,IF=0,TF=0
(2)CS、IP入栈
(3)(IP)=(8),(CS)=(0AH)
几乎所有由外设引发的外中断,都是可屏蔽中断
当外设有需要处理的时间(比如键盘输入)发生时,相关芯片向CPU发出可屏蔽中断信息
不可屏蔽中断是在系统中有必须处理的紧急情况发生时用来通知CPU的中断信息
PC机键盘的处理过程
PC机处理外设输入的基本方法
1.键盘输入
键盘中有个芯片对键盘上的每个键的开关状态进行扫描
按下产生的扫描码称为通码
松开产生的扫描码称为断码
断码=通码+80h
2.引发9号中断
键盘输入到达60h端口时,芯片向CPU发出中断类型码为9的可屏蔽中断信息
若IF=1,响应中断,转去执行int 9
中断例程
3.执行int 9
中断例程
(1)读60h端口中的扫描码
(2)如果是字符键扫描码,转为ASKII码送入BIOS键盘缓冲区
如果是控制键和切换键扫描码,则转变为状态字节(二进制记录状态的字节)写入内存中存储状态字节的单元
(3)对键盘系统进行相关的控制,如,向相关芯片发出应答信息
BIOS键盘缓冲区是系统启动后,BIOS用于存放int 9终端例程所接收的键盘输入的内存区
高位字节存放扫描码,低位字节存放字符码
直接定址表
如何有效合理的组织数据,以及相关的编程技术
描述了单元长度的标号
1 | assum cs:code |
还可以使用一种标号
这种标号不但表示内存单元的地址,还表示了内存单元的长度,即:表示在此标号处的单元,是一个字节单元,还是字单元,还是双字单元
1 | assume cs:code |
标号a、b后没有:
是同时描述内存地址和单元长度的标号
标号a,描述了地址code:0,和 从这个地址开始,以后的内存单元都是字节单元
因为这种标号包括了对单元长度的描述,所以在指令中,可以代表一个段中的内存单元,如b dw 0
指令: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]
可见,使用这种包含单元长度的标号,可以以简洁的形式访问内存中的数据
以后,将这种标号称为数据标号,标记了存储数据的单元的地址和长度
不仅仅表示地址的地址标号
在其他段中使用数据标号
一般来说,不在代码段中定义数据,而是将数据定义到其他段中
在其他段中,也可以使用数据标号来描述存储数据的单元的地址和长度
注意:在后面加有:
的地址标号,只能在代码段中使用,不能在其他段中使用
注意:如果想在代码段中直接用数据标号访问数据,泽东需要用伪指令assume将标号所在的段和一个段寄存器联系起来
否则编译器在编译的时候,无法确定标号的段地址在哪个寄存器中
这种联系是编译器需要的,但绝对不是说,因为编译器工作需要,段寄存器中就会真的存放该段地址
在程序中还要使用指令对段寄存器进行设置
1 | assume cs:code,ds:data |
直接定址表
讨论用查表的方法编写相关程序的技巧
可以利用数值和字符之间原本存在的映射关系,通过高4位和低4位值得到对应的字符码
但由于映射关系的不同,在程序中必须进行一些比较
如果希望用更简捷的算法,就要考虑用同一种映射关系从数值得到数字码
建立新的映射关系:具体做法,建立一张表,表中依次存储字符0~F
,可以通过数值0~15
直接查找到对应的字符
利用标,在两个数据集合之间建立一种映射关系,使我们可以用查表的方法根据给出的数据得到其在另一个集合中的对应数据
这样做的目的一般有以下3个:
(1)为了算法的清晰和间接
(2)为了加快运算速度
(3)为了使程序易于扩充
编程的时候要注意程序的容错性,即对错误的输入要有处理能力
像这种可以通过依据数据,直接计算出所要查找的元素的位置的表,称其为直接定址表
程序入口地址的直接定址表
可以将子程序的入口地址存储在一个表中,在表中的位置和功能号相对应
1 | setscreen: jmp short set |
当然,也可以将子程序反复用cmp 和 je实现
显然,用通过比较功能号进行转移的方法,程序结构比较混乱,不利于功能的扩充
用根据功能号查找地址表的方法,程序的结构清晰,便于扩充
如果加入一个新的功能子程序,那么只需要在地址表中加入它的入口地址就可以了
使用BIOS进行键盘输入和磁盘读写
BIOS为键盘和磁盘这两种外设的I/O提供了最基本的中断例程
int 9
中断例程对键盘输入的处理
其实键盘缓冲区是用环形队列结构管理的内存区
使用int 16h
中断例程读取键盘缓冲区
BIOS提供了int 16h中断例程供程序员调用
int 16h中断例程中包含的一个最重要的功能是:从键盘缓冲区中读取一个键盘输入,该功能的编号为0
1 | 从键盘缓冲区中读取一个键盘输入,并且将其从缓冲区中删除 |
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在应用程序对其进行调用时
在编写一般的处理键盘输入的程序的时候,可以调用int 16h从键盘缓冲区中读取键盘的输入
字符串的输入
用户通过键盘输入的通常不仅仅是单个字符而是字符串
最基本的字符串输入程序,需要具备下面的功能
(1)在输入的同时需要显示这个字符串
(2)一般在输入回车符后,字符串输入结束
(3)能够删除已经输入的字符串
字符串输入的过程中,字符的输入和输出是按照栈的访问规则进行的,即后进先出
这样就可以用栈的方式来管理字符串的存储空间
即,字符串的存储空间实际上是一个字符栈
字符栈中的所有字符,从栈底到栈顶,组成一个字符串
应用int 13h中断例程对磁盘进行读写
以3.5英寸软盘为例
BIOS提供了对扇区进行读写的中断例程,这些中断例程完成了许多复杂的和硬件相关的工作
可以通过调用BIOS中断例程来访问磁盘
BIOS提供的访问磁盘的中断例程为int 13h
直接向磁盘扇区写入数据是很危险的,很可能覆盖掉重要的数据
如果向软盘的0面0道1扇区中写入了数据,要使软盘在现有的操作系统下可以使用,必须要重新格式化
在编写相关的程序之前,必须要找一张空闲的软盘
在使用int 13h中断例程时一定要注意驱动器号是否正确,千万不要随便对硬盘中的扇区进行写入
用面好、磁道号、扇区号来访问磁盘不太方便
可以考虑对于位不同的磁道、面上的所有扇区进行统一编号
附录 A
C语言中,用指针类型数据来表示内存空间的地址和空间存储数据的类型
如:向偏移地址为2000h、存储一个字节的内存空间写入一个字符’a’
用如下方法:*(char *)0x2000='a';
第一个*
表示要访问的是一个内存空间
这不对指针解引用吗?(char *)
里的*
指明了这个数据表示一个内存空间的地址char
指明了这个地址是存储char类型数据的内存空间的地址
也可以用给出段地址和偏移地址的方法法访问内存空间
如:要向地址为2000:0、存储一个字节的内存空间写入字符'a'
,如下:*(char far *)0x20000000='a';
far
指明内存空间的地址是段地址和偏移地址
0x2000给出了段地址、0000给出了偏移地址
不过这样直接用地址访问内存空间的方式是不安全的
用malloc开辟内存空间
C程序必须从main函数开始,是C语言的规定
不是在编译、连接的时候保证的,而是用如下的机制保证的
C开发系统提供了用户写的应用程序正确运行所必须的初始化和程序返回等相关程序,这些程序存放在相关的.obj文件中
其次,需要将这些文件和用户.obj文件一起进行连接,才能生成可正确运行的.exe文件
第三,连接在用户.obj文件前面的由C语言开发环境提供的.obj文件里的程序压迫对main函数进行调用
基于这种机制,只要改写.obj,让它调用其他函数,编程时就可以不写main函数了
附录 B
用栈传递参数
由调用者将需要传递给子程序的参数压入栈中,子程序从栈中取得参数
因为用栈传递参数,所以调用者在调用程序的时候要向栈中压入参数,子程序在返回的时候可以用ret n
指令将栈顶指针修改为调用前的值
(完)
- 标题: 8086汇编
- 作者: GuangYing
- 创建于 : 2024-10-21 22:05:05
- 更新于 : 2024-10-21 23:18:56
- 链接: http://quebo.cn/2024/10/21/8086汇编/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。