Assembly Note——我的汇编笔记
《汇编语言》(第三版) 王爽著
第 1 章 基础知识
汇编语言包含三种指令:汇编指令(核心)、伪指令、其他符号
8086PC机内存地址空间分配情况:
00000~9FFFF 主存储器地址空间(RAM)
A0000~BFFFF 显存地址空间
C0000~FFFFF 各类ROM地址空间
第 2 章 寄存器
8086Pc机有14个寄存器(均为16位)
通用寄存器
通用寄存器有4个:AX、BX、CX、DX、,均可分为两个可独立使用的8位寄存器使用(例如AX可分为AH和AL)
物理地址
8086CPU采用一种在内部用两个16位地址合成的方法来形成20位的物理地址
地址加法器是采用 物理地址 = 段地址 × 16(基础地址) + 偏移地址(通过将段地址左移4位来实现 × 16)
注:一个数据的十六进制形式左移1位,相当于乘以16;一个数据的十进制形式左移一位,相当于乘以10;一个X进制的数据左移一位,相当于乘以X
段寄存器
段寄存器用于存放段地址(基础地址)
8086Pc机一共有4个段寄存器:CS、DS、SS、ES
CS 和 IP
CS 为代码寄存器, IP 为指令指针寄存器——任意时刻下,8086Pc 机的 CPU 将 CS:IP 指向的内容当作指令执行
读取指令过程简述:
(1)从 CS:IP 指向的内存单元读取指令,读取的指令存入指令缓存器
(2)IP = IP + 所读取指令的长度,从而指向下一条指令
(3)执行指令,转到(1),重复
修改 CS、IP 的指令
同时修改 CS、IP:“ jmp 段地址:偏移地址 ”(例如 jmp 2AE3:3,执行后 CS = 2AE3H,IP = 0003H)
只修改 IP ::” jmp 某一合法寄存器 “(例如 jmp ax,jmp bx)
Debug 指令
Debug是 DOS、Windows 都提供的实模式(8086 方式)程序的调试工具,可以查看各种寄存器中的内容、内存的情况和在机器码级别跟踪程序的运行
debug 指令:
-r:查看、改变寄存器的内容
-d:查看内存中的内容(格式 ”—d 段地址:起始偏移地址 结尾偏移地址 “)
-e:改写内存中的内容(格式 “ —e 段地址:偏移地址 ”,然后对于数据,空格直接跳过不修改,改写后空格向后移动,enter 键结束)
-u:将内存中的机器指令翻译为汇编指令
-t:执行一条机器指令(注:当执行的指令是对栈寄存器 ss 进行修改时,下一条指令也会紧接着被执行,如 pop ss)
-a:以汇编指令的格式在内存中写入一条机器指令
-p:在即将执行的命令为 loop 指令时,可以用 p 命令来将循环全部执行,debug会自动重复循环中的指令,直到(cx)=0
-g:在用 debug 加载程序后,通过 “g 偏移地址” 来一次性执行完偏移地址前的所有指令
第 3 章 寄存器(内存访问)
字单元:存放一个字型数据(16位)的内存单元,由两个地址连续的内存单元组成。高地址内存单元存放字型数据的高位字节,低地址内存单元中存放字型数据的低位字节
mov 指令
mov 寄存器,数据 比如:mov ax,8
mov 寄存器,寄存器 比如:mov ax, bx
mov 寄存器,内存单元 比如:mov ax,[ 0 ] 在masm中,此为 mov ax,0,可将0改为bx,或前面加段寄存器
mov 内存单元, 寄存器 比如:mov [ 0 ],ax
mov 段寄存器, 寄存器 比如:mov ds, ax
不合法的指令:mov [ 0 ],[ 2 ] mov [ 0 ],2(缺少参数)
8086CPU 提供的栈机制
push ax(将ax内的数据入栈)
pop ax(将栈中数据弹出到ax)
8086Cpu 的入栈与出栈操作都是以 字 为单位进行的
任意时刻,SS:IP 指向栈顶元素
第 4 章 第一个程序
可执行文件包括两部分内容:程序和数据,相关的描述信息
Debug 将程序从可执行文件载入内存后,cx 中存放的是程序的长度,ds 中存放着程序所在内存区的段地址,在这个内存区的前256个字节中存放的是 PSP(段前缀),DOS用来和程序进行通信,从256个字节处向后的空间存放的是程序
第 5 章 [ bx ] 和 loop 指令
要想完整地描述一个内存单元,需要两种信息:1、内存单元地地址 2、内存单元的长度
inc 指令
jinc bx 地含义是 bx 中地内容加1
loop 指令
格式:loop 标号
CPU 进行两步操作:1、(cx)= (cx)- 1 2、判断 cx 中值(若不为0则跳转至标号处,为0则向下执行)
所以,cx 中应该存放的是循环次数
在汇编源程序中,数据不能以字母开头,所以要在前面加0,比如 0e34dh
一段安全的空间
DOS方式下,DOS 和其他合法的程序一般不会使用 0:200 ~ 0:2ff(00200h ~ 002ffh)的256个字节的空间,所以这段空间是安全的,不过为了谨慎起见,在进入 DOS 后, 我们可以先用 Debug 查看一下,如果 0:200 ~ 0:2ff 单元的内容都是0的话,则证明 DOS 和其他合法的程序没有使用这里,我们可以直接对内存进行写入
第 6 章 包含多个段的程序
系统取得所需空间的方法有两种,一是在加载程序的时候为程序分配,再就是程序在执行的过程中向系统申请
我们通过在源程序中定义段来进行内存分配
end 除了通知编译器程序结束外,还可以通知编译器程序的入口在什么地方(end start)
将数据、代码、栈放入到不同的段
1、将其放在一个段中使程序显得混乱
2、数据较大时,一个段的容量最大是64kb,这是在8086模式下的限制
在程序中,段名就相当于一个标号,它代表了段地址(编译器将其解释为数值)
第 7 章 更灵活的定位内存地址的方法
and 和 or 指令
and 指令:逻辑与指令,按位进行与运算,通过该指令可将操作对象的相应位设为0,其他位不变
or 指令:逻辑或指令,按位进行或指令,通过该指令可将操作对象的相应位设为1,其他位不变
用 [ bx + data ] 的方式进行数组的处理
mov ax, [ bx ] 也可以写成:mov ax, 0[ bx ]
mov ax, [ 5 + bx ] mov ax, [ bx ] . 5
mov ax. [ bx + si ] mov ax, [ bx ] [ si ]
[ bx + si + idata ] 和 [ bx + si + idata ]
mov ax, [ bx + si + idata]
mov ax, [ bx + 200 + si ]
也可以写成:
mov ax, 200 [ bx ] [ si ]
mov ax, [ bx ] . 200 [ si ]
mov ax, [ bx ] [ si ] . 200
第 8 章 数据处理的两个基本问题
在本书中,定义用 reg 表示一个寄存器,包括:ax、bx、cx、dx、ah、al、bh、bl、ch、cl、dh、dl、sp、bp、si、di
定义用 sreg 表示一个段寄存器:ds、ss、es、cs
可以在 “ […] ” 中进行内存寻址的寄存器
在 8086CPU 中,只有4个寄存器可以用在 “ […] ”中来进行内存单元的寻址:bx、bp、si、di
在 “ […] ” 中,这4个寄存器可以单个出现,或只能以4种组合出现:bx 和 si、bx 和 di、bp 和 si、bp 和 di
而下面的指令是错误的:mov ax, [ bx + bp ]、mov ax, [ si + di ]
只要在 […] 种使用寄存器bp,而指令种没有显性地给出段地址,段地址就默认在 ss 中
机器指令处理的数据在什么地方
指令在执行前,所要处理数据可以在3个地方:CPU 内部、内存、端口
汇编语言中数据位置的表达
1、立即数( idata ):对于直接在机器指令中的数据(执行前在 CPU 的指令缓存器中),在汇编语言中称为立即数,在汇编指令中直接给出;例如:mov ax,1;add bx,2000h;or bx,00010000b;mov al,’ a ‘;
2、寄存器:数据在寄存器中。在汇编指令中,直接给出相应的寄存器名;
3、段地址( SA )和偏移地址( EA ):指令要处理的数据在内存中,在汇编指令中可用 [ X ] 的格式给出 EA, SA 在某个段寄存器中;当使用 bp 寄存器时,默认的段寄存器是 ss ;
不同寻址方式
寻址方式:定位内存的方法
寻址方式小结
寻址方式 | 含义 | 名称 | 常用格式举例 |
---|---|---|---|
[ idata ] | EA = idata SA = ( ds ) | 直接寻址 | [ idata ] |
[ bx ] | EA = ( bx ) SA = ( ds ) | 寄存器间接寻址 | [ bx ] |
[ si ] | EA = ( si ) SA = ( ds ) | 寄存器间接寻址 | ~ |
[ di ] | EA = ( di ) SA = ( ds ) | 寄存器间接寻址 | ~ |
[ bp ] | EA = ( bp ) SA = ( ss ) | 寄存器间接寻址 | ~ |
[ bx + idata ] | EA = ( bx ) + idata SA = ( ds ) | 寄存器相对寻址 | 用于结构体 [ bx ] . idata |
[ si + idata ] | EA = ( si ) + idata SA = ( ds ) | 寄存器相对寻址 | 用于(二维)数组 |
[ di + idata] | EA = ( di ) + idata SA = ( ds ) | 寄存器相对寻址 | idata [ si ] , idata [ di ] |
[ bp + idata ] | EA = ( bx ) + idata SA = ( ss ) | 寄存器相对寻址 | [ bx ] [ idata ] |
[ bx + si ] | EA = ( bx ) + ( si ) SA = ( ds ) | 基址变址寻址 | 用于二维数组 |
[ bx + di ] | EA = ( bx ) + ( di ) SA = ( ds ) | 基址变址寻址 | [ bx ] [ si ] |
[ bp + si ] | EA = ( bp ) + ( si ) SA = ( ss ) | 基址变址寻址 | ~ |
[ bp + di ] | EA = ( bp ) + ( di ) SA = ( ss ) | 基址变址寻址 | ~ |
[ bx + si + idata ] | EA = ( bx ) + ( si ) + idata SA = ( ds ) | 相对基址变址寻址 | 用于结构体中的数组项 |
[ bx + di + idata ] | EA = ( bx ) + ( di ) + idata SA = ( ds ) | 相对基址变址寻址 | [ bx ] . idata[ si ] |
[ bp + si + idata ] | EA = ( bp ) + ( si ) + idata SA = ( ss ) | 相对基址变址寻址 | 用于二维数组 |
[ bp + di + idata ] | EA = ( bp ) + ( di ) + idata SA = ( ss ) | 相对基址变址寻址 | idata [ bx ] [ si ] |
指令要处理的数据要多长
8086CPU 的指令,可以处理两种尺寸的数据,byte 和 word ,所以在机器指令中要指明,指令进行的是字操作还是字节操作,在汇编语言中,用以下方法处理:
1、通过寄存器名指明要处理的数据的尺寸:mov ax,1(字操作)mov al,1(字节操作)
2、在没有寄存器名存在的情况下,用操作符 X ptr 指明内存单元的长度, X 在汇编指令中可以为 word 或 byte
3、其他方法:有些指令默认了访问的是字单元还是字节单元,比如 push [ 1000H ] 就不用指明访问的是字单元还是字节单元,因为 push 指令只进行字操作
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 存储除法操作的余数
4、注意溢出:要提前预估计算的结果的最大值,比如16位的除法,除数是10,则最大的结果是0ffffh,即65535 * 10 = 655350,被除数要小于这个值
例如:div reg div 内存单元
div byte ptr ds : [ 0 ] div word ptr es : [ 0 ]
伪指令 dd:前面我们用 db 和 dw 定义字节型数据和字型数据,dd 是用来定义 dword( double word,双字 )型数据
dup 是一个操作符,在汇编语言中同 db、dw、dd 等一样,也是由编译器识别处理的符号,与 db、dw、dd 等数据定义伪指令配合使用,用来进行数据的重复
db 3 dup( 0 )定义了3个字节,它们的值都是0,相当于 db 0,0,0
db 3 dup ( 0,1,2 )定义了9个字节,它们是 0、1、2、0、1、2、0、1、2,相当于 db 0 , 1 , 2 , 0 , 1 , 2 , 0 , 1 , 2
第 9 章 转移指令的原理
可以修改 IP,或同时修改 CS 和 IP 的指令称为转移指令,概括的讲,转移指令就是可以控制 CPU 执行内存中某处代码的指令
8086CPU 的转移行为有以下几类
1、只修改 IP 时,称为段内转移,比如:jmp ax
2、同时修改 CS 和 IP 时,称为段间转移,比如:jmp 1000:0
由于转移指令对 IP 的修改范围不同,段内转移又分为:短转移和近转移
短转移:IP 的修改范围为 -128 ~ 127
近转移:IP 的修改范围为 -32768 ~ 32767
8086CPU 的转移指令分为以下几类
1、无条件转移指令(如:jmp)
2、条件转移指令
3、循环指令(如:loop )
4、过程
5、中断
操作符 offset
操作符 offset 在汇编语言中是由编译器处理的符号,它的功能是取得标号的偏移地址
注:nop 的机器码占一个字节
jmp 指令
jmp 为无条件转移指令,可以只修改 IP,也可以同时修改 CS 和 IP
jmp 指令要给出两种信息:
1、转移的目的地址
2、转移的距离(段间转移、段内短转移、段内近转移)
jmp short 标号(转到标号处执行指令)
这种格式的 jmp 指令实现的是段内短转移,它对 IP 的修改范围为 -128 ~ 127,“ short ” 说明进行的是短转移,转移指令结束后,CS:IP 应该指向标号处的指令
CPU 在执行 jmp 指令的时候并不需要转移的目的地址,“ jmp short 标号 ” 指令所对应的机器码中,并不包含转移的目的地址,而包含的是转移的位移,这个位移,是编译器根据汇编指令中的 “ 标号 ” 计算出来的,计算方法如下
实际上,jmp short 标号 的功能是:( IP )= ( IP )+ 8 位位移(补码)
8 位位移 = 标号处的地址 - jmp 指令后的第一字节的地址
jmp near ptr 标号
功能与 “ jmp short 标号 ” 相近,实现的是段内近转移
实际上,jmp near ptr 标号 的功能是:( IP )= ( IP )+ 16 位位移(补码)
16 位位移 = 标号处的地址 - jmp 指令后的第一字节的地址
转移的目的地址在指令中的 jmp 指令
前面讲的 jmp 指令,其对应的机器指令中并没有转移的目的地址,而是相对于当前 IP 的转移位移
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
指令格式:jxcz 标号(如果 (cx)= 0,转移到标号处执行 )
8位位移(由编译器计算出,用补码表示) = 标号处的地址 - jxcz 指令后的第一个字节的地址
当(cx)!= 0 时,程序向下执行
jxcz 指令的功能相当于:if((cx)== 0 )jmp short 标号;
dec 指令
与 inc 指令功能相反,dec bx 进行的操作是:( bx )= ( bx )- 1
loop 指令
loop 指令为循环指令,所有的循环指令都是短转移,在对于的机器码中包含转移的位移,而不是目的地址,对 IP 的修改范围是 -128 ~ 127
loop 指令的功能相当于:( CX )=( CX )- 1;if(( CX )!= 0 )jmp short 标号;
根据位移进行转移的意义
jmp short 标号
jmp near 标号
jxcz 标号
loop 标号
它们对 IP 的修改是根据转移目的地址和起始地址之间的位移来进行的。在它们对应的机器码中不包含转移的目的地址,而包含的是到目的地址的位移,这种设计,方便了程序员在内存中的浮动装配
编译器对转移位移越界的检测
根据位移进行的转移指令,它们的转移范围受到转移位移的限制,如果在源程序中出现转移范围越界的问题,在编译的时候,编译器就会报错
显存字符属性与基本颜色的 RGB 值
一个字符在显存中占2个字节:低字节是字符的 ASCII 码,高字节是字符属性
字符属性的格式
2进制位 7 6 5 4 3 2 1 0
含义 BL R G B I R G B
闪烁 背景 高亮 前景(字符本身颜色)
基本颜色的 RGB 值(二进制):黑( 000 )蓝( 001 )绿( 010 )青( 011 )红( 100 )紫( 101 )黄( 110 )白( 111 )
第 10 章 CALL 和 RET 指令
ret 和 retf 指令
ret 指令用栈中的数据,修改 IP 的内容,从而实现近转移
retf 指令用栈中的数据,修改 CS 和 IP 的内容,从而实现远转移
CPU 执行 ret 时:
1、( IP )=(( ss )* 16 + ( sp ))
2、( sp )=( sp )+ 2
相当于进行:pop IP
CPU 执行 retf 时:
1、( IP )=(( ss )* 16 + ( sp ))
2、( sp )=( sp )+ 2
3、( CS )=(( ss )* 16 + ( sp ))
4、( sp )=( sp )+ 2
相当于进行:pop IP
pop CS
call 指令
CPU 指令 call 指令时,进行两步操作:
1、将当前的 IP 或 CS 压入栈中
2、转移
call 指令不能实现短转移,除此之外,call 指令实现转移的方法和 jmp 指令的原理相同
依据位移进行转移的 call 指令
call 标号 (将当前的 IP 压入栈中,转到标号处执行指令)
CPU 执行时:
1、( sp )=( sp )- 2 (( ss )* 16 + ( sp ))=( IP )栈顶指针上移,将当前 IP 入栈
2、( IP )=( IP )+ 16 位位移( 16 位位移 = 标号处的地址 - call 指令后的第一个字节的地址 )
相当于:push IP
jmp near ptr 标号
转移的目的地址在指令中的 call 指令
call far ptr 标号(实现的是段间转移)
CPU 执行时:
1、将当前的 CS 和 IP 压入栈中:
( sp )=( sp )- 2
(( ss )* 16 + ( sp ))= ( CS )
( sp )=( sp )- 2
(( ss )* 16 + ( sp ))= ( IP )
2、将标号处的段地址与偏移地址赋给 CS 和 IP
( CS )= 标号处的段地址
( IP )= 标号处的偏移地址
相当于:push CS
push IP
jmp far ptr 标号
转移地址在寄存器中的 call 指令
call 16位 reg
先将当前 IP 入栈,再将 reg 中的值赋给 IP
相当于 :push IP jmp 16 位 reg
转移地址在内存中的 call 指令
转移地址在内存中的 call 指令有两种格式 :
1、call word ptr 内存单元地址,相当于:push IP jmp word ptr 内存单元地址
2、call dword ptr 内存单元地址,相当于:push CS push IP jmp dword ptr 内存单元地址
mul 指令
mul 指令是法指令
注意:
1、两个相乘的数,要么都是 8 位,要么都是 16 位
(1)如果都是 8 位:一个默认在 AL 中,另一个放在 8 位 reg 或者内存字节单元中
(2)如果都是 16 位:一个默认在 AX 中, 另一个在 16 位 reg 或者在内存字单元中
2、结果
(1)如果是 8 位乘法:结果默认在 AX 中
(2)如果是 16 位乘法:结果高位在 DX 中,低位在 AX 中
注:其中内存单元可以用不同的寻址方式给出
call 和 ret 指令共同支持了汇编语言编程中的模块化设计
子程序一般都需要根据提供的参数处理一定的事务,处理后,将结果(返回值)提供给调用者,其实,我们讨论参数和返回值传递的问题,实际上就是在讨论,应该如何存储子程序需要的参数和产生的返回值
批量数据的处理
我们将批量数据放到内存中,让偶将它们所在的内存空间的首地址放到寄存器中,传递给需要的子程序
除了用存储器传递参数外,还有一种通用的方法是用栈来传递参数
存储器冲突的问题
解决这个问题的简捷方法是,在子程序的开始将子程序中所有用到的寄存器中的内容都保存起来,在子程序返回前再恢复,可以用栈来保存寄存器中的内容
以后,我们编写子程序的标准框架如下:
子程序开始:子程序所用到的寄存器入栈
子程序内容
子程序中所用到的寄存器出栈
返回( ret、retf )
要注意寄存器入栈和出栈的顺序
第 11 章 标志寄存器
CPU 内部的寄存器中, 有一种特殊的寄存器(对于不同的处理及,个数和结构可能不同)具有以下 3 种作用
(1)用来存储相关指令的某些执行结果
(2)用来为 CPU 执行相关指令提供行为依据
(3)用来控制 CPU 的相关工作方式
这种特殊的寄存器在 8086PC 中,被称为标志寄存器,以下简称 flag
8086PC 的标志寄存器中有 16 位,其中存储的信息通常被称为程序状态字(PSW)
flag 与其他寄存器不一样,其他寄存器是用来存放数据的,都是整个寄存器具有一个含义,而 flag 是按位起作用的,也就是说,它的每一位都有专门的含义
flag 的 1、3、5、12、13、14、15 位在 8086CPU 中没有使用,不具有任何含义
flag 的 0、2、4、6、7、8、9、10、11 位都具有特殊的含义
注意:在 8086CPU 中,有些指令的执行是影响标志寄存器的,比如 add、sub、mul、div、inc、or、and 等,它们大都是运算指令(进行逻辑或算术运算),有的指令的执行对标志寄存器没有影响,比如 mov、push、pop 等,它们大都是传送指令,在使用一条指令的时候,要注意这条指令的全部功能,其中包括,执行结果对标志寄存器的哪些标志位造成影响
ZF 标志(零标志位)
flag 的第 6 位是 ZF,零标志位
记录相关指令执行后,其结果是否为 0,是,则 ZF 为 1,否则 ZF 为 0
PF 标志(奇偶标志位)
flag 的第 2 位是 PF,奇偶标志位
记录相关指令执行后,其结果的所有 bit 位中 1 的个数是否为偶数
如果 1 的个数为偶数,则 PF 为 1,如果为奇数,那么 PF 为 0
SF 标志(符号标志位)
flag 的第 7 为是 SF,符号标志位
记录相关指令执行后,其结果是否为负
如果结果为负,则 SF 为 1 ,如果非负,则 SF 为 0
CF 标志(进位标志位)
flag 的第 0 位是 CF,进位标志位
一般情况下,在进行无符号数的运算时,记录运算结果最高有效位向更高位的进位值,或从更高位的借位值
比如一个字节是 8 位,当两个8位数相加后溢出时,将产生进位,CF 位来记录这个进位值,相减时,将可能产生借位值
OF 标志(溢出标志位)
flag 的第 11 位是 OF,溢出标志位
一般情况下,OF 记录了有符号数运算的结果是否发生了溢出
如果发生溢出,则 OF 为 1,如果没有溢出,则 OF 为 0
一定要注意 OF 与 CF 的区别:
CF 是对无符号数运算有意义的标志位
OF 是对有符号数运算有意义的标志位
例子:
mov al, 98
add al, 99
add指令执行后,CF = 0,OF = 1
对于无符号数运算:CPU 用 CF 位来记录是否产生了进位
对于有符号数运算:CPU 用 OF 位来记录是否产生了溢出,当然还要用 SF 来记录结果的符号
所以对于无符号数运算,98+99 没有进位, CF = 0
对于有符号数运算,98+99产生溢出,OF = 1
我们可以看出,CF 和 OF 所表示的进位和溢出,是分别对无符号数和有符号数运算而言的,它们之间没有任何关系
adc 指令
adc 指令是带进位加法指令,它利用了 CF 位上记录的进位值
指令格式:adc 操作对象 1,操作对象 2
功能:操作对象 1 = 操作对象 1 + 操作对象 2 + CF
比如:adc ax,bx 实现的是:( ax )=( ax )+( bx )+ CF
例:
mov ax, 2
mov bx, 1
sub bx, ax
adc ax, 1
执行后,( ax )= 4,adc 执行时,相当于计算:( ax )+ 1 + CF = 2 + 1 + 1 = 4
mov ax, 1
add ax, ax
adc ax, 3
执行后,( ax )= 5,adc 执行时,相当于计算( ax )+ 3 + CF = 2 + 3 + 0 = 5
sbb 指令
sbb 指令是带位减法指令,它利用了 CF 位上的记录值
指令格式:sbb 操作对象 1,操作对象 2
功能:操作对象 1 = 操作对象 1 - 操作对象 2 - CF
比如:sub ax,bx 实现的功能是:( ax )= ( ax )-( bx )- CF
adc 与 sbb 指令实现了对任意大的数据进行相加或相减
cmp 指令
cmp 指令是比较指令
cmp 的功能相当于减法指令,只是不保存结果,cmp 指令执行后,将对标志寄存器产生影响,其他相关指令通过识别这些被影响的标志寄存器位来得知比较结果
指令格式:cmp 操作对象 1, 操作对象 2
功能:计算操作对象 1 - 操作对象 2 但不保存结果,仅仅根据计算结果对标志寄存器进行设置
比如:cmp ax,ax 做( ax )-( ax )的运算,结果为 0,但并不在 ax 中保存,仅影响 flag 的相关各位,指令执行后,ZF = 1,PF = 1, SF = 1,CF = 0,OF = 0
根据执行后标志寄存器的值来判断比较结果
比如 cmp ax,bx
对于无符号数的运算来说,cmp 指令执行后:
ZF = 1,说明( ax )=( bx )
ZF = 0,说明( ax )≠( bx )
CF = 1,说明( ax )<( bx )
CF = 0,说明( ax )≥( bx )
CF = 0 并且 ZF = 0,说明( ax )>( bx )
CF = 1 或者 ZF = 1,说明( ax )≤( bx )
对于有符号数的运算来说,cmp 指令执行后:
OF = 0,则说明逻辑上的真正结果的正负 = 实际结果的正负
OF = 1,则说明逻辑上的真正结果的正负 ≠ 实际结果的正负
(1)OF = 0,SF = 1,说明:( ax )<( bx )
(2)OF = 0,SF = 0,说明:( ax )≥( bx )
(3)OF = 1,SF = 1,说明:( ax )>( bx )
(4)OF = 1,SF = 0,说明:( ax )<( bx )
检测比较结果的条件转移指令
常用的根据无符号数的比较结果进行转移的条件转移指令
指令 | 含义 | 检测的相关标志位 |
---|---|---|
je | 等于则转移 | ZF = 1 |
jne | 不等于则转移 | ZF = 0 |
jb | 低于则转移 | CF = 1 |
jnb | 不低于则转移 | CF = 0 |
ja | 高于则转移 | CF = 0 && ZF = 0 |
jna | 不高于则转移 | CF = 1 | | ZF = 1 |
注意:前提是在 cmp 指令执行后,使用这些指令会有对应的含义,若前面没有 cmp 指令,则不具有此些含义,仅仅是通过相应的标志位检测来进行是否转移,而不具有特殊含义,总之,一切取决于如何使用
DF 标志(方向标志位)和串传送指令
flag 的第 10 位是 DF,方向标志位
在串处理指令中,控制每次操作后 si、di 的增减
DF = 0,每次操作后 si、di 递增
DF = 1,每次操作后 si、di 递减
对 DF 位进行设置的指令
cld 指令:将标志寄存器的 DF 位设置为 0
std 指令:将标志寄存器的 DF 位设置为 1
串传送指令 movsb 和 movsw
movsb 指令(传送字节)
功能:
(1)(( es )* 16 +( di ))=(( ds )* 16 +( si ))
(2)如果 DF = 0,则 si、di 同时 + 1,如果 DF = 1,则 si、di 同时 -1
也即是,将 ds:si 指向的内存单元的字节送入 es:di 中,然后根据 DF 的值同时递增或递减 si 和 di
movsw 指令(传送字)
功能:
(1)(( es )* 16 +( di ))=(( ds )* 16 +( si ))
(2)如果 DF = 0,则 si、di 同时 + 2,如果 DF = 1,则 si、di 同时 -2
也即是,将 ds:si 指向的内存单元的字送入 es:di 中,然后根据 DF 的值同时递增或递减 si 和 di
rep 指令
movsb 和 movsw 进行的是串传送操作中的一个步骤,一般来说,movsb 和 movsw 都是和 rep 指令配合使用的
格式:rep movsb
用汇编语言描述其功能:
s:movsb
loop s
可见 rep 指令的作用是根据 cx 的值,重复执行后面的串传送指令,由于每次执行 movsb 指令 si 和 di 都会递增或递减指向后一个单元或前一个单元,则 rep 指令就可以循环实现 ( cx )个字符的传送,movsw 指令同理
组合拳:
mov cx,idata
cld (或 std )
rep movsb (或 movsw)
pushf 和 popf 指令
pushf 的功能是将标志寄存器的值压栈,而 popf 是从栈中弹出数据,送入标志寄存器中
标志寄存器在 Debug 中的表示
标记:NV UP EI PL NZ NA PO NC(标志位均为 0 时的标记)
⬆ ⬆ ⬆ ⬆ ⬆ ⬆
含义:OF DF SF ZF PF CF
OV DN NG ZR PE CY(标志位均为 1 时的标记)
第 12 章 内中断
当 CPU 内部有下面的情况发生时,将产生相应的中断信息
(1)除法错误,比如,执行 div 指令产生的除法溢出
(2)单步执行
(3)执行 into 指令
(4)执行 int 指令
中断源和中断类型码
8086 CPU 用称为中断类型码的数据来标识中断信息的来源,一个字节型的数据,可以表示 256 种中断信息的来源
我们将产生中断信息的事件,即中断信息的来源,简称为 中断源
上述 4 种中断源,在 8086CPU 中的中断类型码如下:
(1)除法错误:0
(2)单步执行:1
(3)执行 into 指令:4
(4)执行 int 指令,该指令的格式为 int idata,指令中的 idata 为字节型立即数,是供给 CPU 的中断类型码
中断处理程序
CPU 收到中断信息后,需要对中断信息进行处理,而如何对中断信息进行处理,可以由我们编程决定,我们编写的用来处理中断信息的程序被称为中断处理程序,一般来说,需要对不同的中断信息写不同的处理程序
CPU 设计者必须在中断信息和其处理程序的入口地址之间建立某种联系,使得 CPU 可以根据中断信息找到要执行的处理程序
中断向量表
CPU 用 8 位的中断类型码通过中断向量表找到相应的中断处理程序的入口地址
中断向量表就是中断向量的列表
中断向量就是中断处理程序的入口地址
所以,中断向量表就是中断处理程序的入口地址的列表
中断向量表在内存中保存,存放有 256 个中断源所对应的中断处理程序的入口
CPU 知道了中断类型码,就可以将中断类型码作为中断向量表的表项号,定位相应的表项,从而得到中断处理程序的入口地址
对于 8086PC 机,中断向量表被指定放在内存地址 0 处,在内存 0000:0000 到 0000:03FF 的1024个内存单元中存放着中断向量表,这是 8086PC 机的规定
其中一个表项占有 2 个字,高地址字存放段地址,低地址字存放偏移地址
中断过程
(1)(从中断信息中)获取中断类型码
(2)标志寄存器的值入栈(因为在中断过程中要哦改变标志寄存器的值,所有要先将其保存在栈中)
(3)设置标志寄存器的第 8 位 TF 和第 9 位 IF 的值为 0
(4)CS 的值入栈
(5)IP 的值入栈
(6)从内存地址为中断类型码(N)* 4 和中断类型码(N)* 4 + 2的两个字单元中读取中断处理程序的入口地址设置 IP 和 CS
简述该过程如下:
(1)取得中断类型码 N
(2)pushf
(3)TF = 0,IF = 0
(4)push CS
(5)push IP
(6)( IP )= ( N * 4),( CS )= ( N * 4 + 2)
接着,CPU开始执行由程序员编写的中断处理程序
中断处理程序的编写和 iret 指令
常规步骤:
(1)保存所用到的寄存器
(2)处理中断
(3)恢复用到的寄存器
(4) 用 iret 指令返回
iret 指令的功能相当于:
pop IP
pop CS
popf
iret 通常和硬件自动完成的中断过程配合使用(包括栈的出入顺序)
编程处理 0 号中断
在中断向量表中,由于系统要处理的中断事件远没有达到 256 个,所以很多的单元是空的
中断向量表是 PC 系统中最重要的内存区,只能用来存放中断处理程序的入口地址,可以利用中断向量表中的空闲单元来存放我们的程序,一般情况下,从 0000:0200 至 0000:02FF 的256个字节的空间对用的中断向量表项都是空的,操作系统和其它的应用程序都不占用
编程实现:
(1)编写 0 号中断处理程序;do0
(2)将 do0 送入到内存 0000:0200 处
(3)将 do0 的入口地址 0000:0200 存储在中断向量表 0 号表项中
框架如下:
start:
do0 安装程序
设置中断向量表
mov ax,4c00h
int 21h
do0:编写想要实现的功能
mov ax,4c00h
int 21h
设置中断向量
mov ax,0
mov es,ax
mov word ptr es:[ 0 * 4 ],200h
mov word ptr es:[ 0 * 4 + 2 ],0
单步中断
基本上,CPU 在执行完一条指令后,如果检测到标志寄存器的 TF 位为 1,则产生单步中断,引发中断过程
在 Debug 中,Debug 提供了单步中断的中断处理程序,功能为显示所有寄存器的内容后等待输入命令,然后在使用 t 命令时执行指令时,Debug 将 TF 设置为 1,使得 CPU 工作于单步中断方式下,则在 CPU 执行完这条指令后就引发单步中断,执行单步中断的中断处理程序,所有寄存器中的内容显示在屏幕上,并且等待输入命令
为了防止在执行单步中断的中断处理程序时,也出现单步中断,从而在引发单步中断时,在进入中断处理程序之前,将 TF 设置为 0,中断过程如下:
(1)取得中断类型码 1
(2)标志寄存器入栈,TF = 0, IF = 0
(3)CS、IP 入栈
(4)( IP )=( N * 4 )( CS )=( N * 4 + 2 )
CPU 提供单步中断功能的原因就是,为单步追踪程序的执行过程,提供了实现机制
响应中断的特殊情况
举例说明
在执行完向 ss 寄存器传送数据的指令后,即使是发生中断,CPU 也不会响应,这样做的只要原因是,ss:sp 联合指向栈顶,而对它们的设置应该连续完成
所以 CPU 在执行完设置 ss 的指令后,不响应中断,这给连续设置 ss 和 sp 指向正确的栈顶提供了一个时机,我们应该利用这个特性,将设置 ss 和 sp 的指令连续存放
第 13 章 int 指令
中断信息可以来自 CPU 的内部和外部,当 CPU 的内部有需要处理的事情发生的时候,将产生需要马上处理的中断信息,引发中断过程,这一章主要是由 int 指令引发的一种内中断
int 指令
int 指令的格式:int n,其中 n 为中断类型码,它的功能是引发中断过程
CPU 执行 int 指令过程:
(1)去中断类型码 n
(2)标志寄存器入栈,IF = 0,TF = 0
(3)CS、IP 入栈
(4)( IP )=( n * 4 )( CS )=( n * 4 + 2 )
可见,int 指令的最终功能和 call 指令相似,都是调用一段程序
以后,我们可以将中断处理程序简称为中断例程
对 int、iret 和栈的深入理解
用 int 7ch 中断例程实现 loop 指令的功能
应用举例:在屏幕中间显示 80 个 !
start:
mov ax,0b800h
mov ex,ax
mov di,160 * 12
mov bx,offset s - offset se
mov cx, 80
s:
mov byte ptr es:[ di ],’ !’
add di,2
int 7ch
se:
nop
mov ax,4c00h
int 21h
7ch例程如下
lp:
push bp
mov bp,sp
dec cx
jcxz lpret
add [ bp + 2 ],bx
lpret:
pop bp
iret
bx 中为子程序开始的位置 - 子程序结束的位置,为负值,通过加法来实现将位置返回到子程序开始
从而通过修改栈中的数据,再利用 iret 来间接的修改 IP,从而修改执行的位置
BIOS 和 DOS 苏哦提供的中断例程
在系统板的 ROM 中存放着一套程序,称为 BIOS(基本输入输出系统),其中包含:
(1)硬件系统的检测和初始化程序
(2)外部中断和内部中断的中断例程
(3)用于对硬件设备进行 I/O 操作的中断例程
(4)其他和硬件系统相关的中断例程
操作系统也提供了中断例程,从操作系统的角度来看,DOS 的中断例程就是操作系统向程序员提供的编程资源
和硬件设备相关的 DOS 中断例程中,一般都调用了 BIOS 的中断例程
BIOS 和 DOS 中断例程的安装过程
(1)开机后,CPU 一加电,初始化( CS )= OFFFFh,( IP )= 0,自动从 FFFF:0 单元开始执行程序。FFFF:0 处有一条跳转指令,CPU 执行这条指令后,转去执行 BIOS 中的硬件系统检测和初始化程序
(2)初始化程序将建立 BIOS 所支持的中断例程,即将 BIOS 提供的中断例程的入口地址登记在中断向量表中。注意,对于 BIOS 苏哦提供的中断例程,只需将入口地址登记在中断向量表中即可,因为它们是固化到 ROM 中的程序,一直都在内存中存在
(3)硬件系统检测和初始化完成后,调用 int 19h 进行操作系统的引导,从此将计算机交到操作系统控制
(4)DOS 启动后,除完成其他工作外,还将它所提供的中断例程装入内存,并建立相应的中断向量
一般来说,一个供程序员调用的中断例程中往往包含了多个子程序,中断例程内部用传递进来的参数决定执行哪一个子程序,BIOS 和 DOS 提供的中断例程,都用 ah 来传递内部子程序的编号
显存页号的含义:内存地址空间中,B8000h ~ BFFFF 共 32 kb 的空间,为 80 * 25 彩色字符模式的显示缓存区
显示缓存区分为 8 页,每页 4 kb(≈ 4000B ),显示器可以显示任意一页的内容,一般情况下,显示 0 页的内容,也就是 B8000h ~ B8F9F 中的4000个字符的内容显示在显示器上
第 14 章 端口
在 PC 机中,和 CPU 通过总线相连的芯片除各种存储器之外,还有下面3中芯片
(1)各种接口卡(比如:网卡、显卡)上的接口芯片,它们是控制接口卡进行工作
(2)主板上的接口芯片,CPU 通过它们对部分外设进行访问
(3)其他芯片,用来存储相关的系统信息,或进行相关的输入输出处理
在这些芯片中,都有一组可以由 CPU 读写的寄存器,这些寄存器,在物理上可能处于不同的芯片中,但是它们都在以下两点上相同:
(1)都和 CPU 的总线相连,当然这种连接是通过它们所在芯片进行的
(2)CPU 对它们进行读或写的时候都通过控制线向它们所在的芯片发出端口读写的命令
可见,从 CPU 的角度,将这些寄存器都当作端口,对它们进行统一编址,从而建立了一个统一的端口地址空间,每一个端口在地址空间中都有一个地址
CPU 可以直接读写以下 3 个地方的寄存器
(1)CPU 内部的寄存器
(2)内存单元
(3)端口
端口的读写
在访问端口时,CPU 通过端口地址来定位端口,因为端口所在的芯片和 CPU 通过总线相连,所以端口地址和内存地址一样,通过总线来传送,在 PC 系统种,CPU 最多可以定位 64KB 个不同的端口,则端口地址的范围是 0 ~ 65535
端口的读写指令只有两条:in 和 out ,分别用于从端口读取数据和往端口写入数据
访问端口时,CPU 执行时与总线的操作如下:
in al,60h
① CPU 通过地址线将地址信息 60h 发出
② CPU 通过控制线发出端口读命令,选中端口所在的芯片,并通知它,将要从中读取数据
③ 端口所在的芯片将 60h 端口中的数据通过数据线送入 CPU
注意:在 in 和 out 指令中,只能使用 ax 或 al 存放从端口读入的数据或要发送到端口中的数据,访问 8 位端口时用 al,访问 16 位端口时用 ax
shl 和 shr 指令
shl 和 shr 是逻辑移位指令
shl(shr) 是逻辑左(右)移指令,它的功能为:
(1)将一个寄存器或内存单元中的数据向左(右)移位
(2)将最后移出的一位写入 CF 中
(3)最低位用 0 补充
注:如果移动的位数大于 1 时,必须将移动数放在 cl 中
CMOS RAM 芯片和其存储的时间信息
在 PC 机中,有一个 CMOS RAM 芯片,一般简称为 CMOS,其特征如下:
(1)包含一个实时钟和一个有 128 个存储单元的 RAM 存储器(早期计算机为 64 个字节)
(2)该芯片靠电池供电,所以,关机后其内部的实时钟仍可正常工作,RAM 中的信息不丢失
(3)128 个字节的 RAM 中,内部实时钟占用 0 ~ 0dh 单元来保存时间信息,其余大部分单元用于保存系统配置,供系统启动时 BIOS 程序读取,BIOS 也提供了相关的程序,使我们可以在开机的时候配置 CMOS RAM 中的系统信息
(4)该芯片内部有两个端口,端口地址为 70h 和 71h,CPU 通过这两个端口来读写 CMOS RAM
(5)70h 端口为地址端口,存放要访问的 CMOS RAM 单元的地址;71h 为数据端口,存放从选定的 CMOS RAM 单元中读取的数据,或要写入到其中的数据,可见,CPU 对 COMS RAM 的读写分为两步进行,
比如读取 CMOS RAM 的 2 号单元:
① 将 2 送入端口 70h
② 从端口 71h 中读出 2 号单元的内容
CMOS RAM 中的时间信息
在 CMOS RAM 中,存放着当前的时间:年、月、日、时、分、秒,这 6 个信息的长度都是 1 个字节,存放单元为:
秒:0 分:2 时:4 日:7 月:8 年:9
这些数据都是以 BCD 码的方式存放的
BCD 码:以 4 位二进制数表示十进制数码的编码方式,比如 26,用 BCD 码表示为:0010 0110
可见,一个字节可以表示两个二进制码,则在 CMOS RAM 存储时间信息的单元中,存储了用两个 BCD 码来表示的两位十进制数,其中,高 4 位的 BCD 码表示十位,低 4 位的 BCD 码表示个位
第 15 章 外中断
CPU 除了具有运算能力以外,还要有 I/O( Input/Outpu )能力
要及时处理外设的输入,显然需要解决两个问题
①外设的输入随时可能发生,CPU 如何得知
②CPU 从何处得到外设的输入
接口芯片和端口
PC 系统的接口卡和主板上,装有各种接口芯片,这些外设接口芯片的内部有若干寄存器,CPU 将这些寄存器当作端口来访问
外设的输入不直接送入内存和 CPU,而是送入相关的接口芯片的端口中,CPU 向外设的输入也不是直接送入外设,而是先送入端口中,再由相关的芯片送到外设,CPU 还可以向外设输出控制命令,这些控制命令也是先送到相关的芯片的端口中,再由相关的芯片根据命令对外实施控制
可见,CPU 通过端口和外部设备进行联系
cli 和 sti 指令
8086CPU 提供的设置 IF 的指令如下
cli 设置 IF = 0
sti 设置 IF = 1
外中断信息
还有一种中断信息来自 CPU 的外部,当 CPU 外部有需要处理的事情发生的时候,比如外设的输入到达,相关芯片价格向 CPU 发出相应的中断信息,CPU 在执行完当前指令后,可以检测到发送过来的中断信息,引发中断过程,处理外设的输入
在 PC 系统中,外中断源一共有以下两类:
1、可屏蔽中断
可屏蔽中断是 CPU 可以不响应的外中断,CPU 是否相应可屏蔽中断,要看标志寄存器的 IF 位的设置,当 CPU 检测到可屏蔽中断信息时,则 CPU 在执行完当前的指令后相应中断,引发中断过程,如果 IF = 0,则不响应可屏蔽中断
现在,我们可以解释中断过程中将 IF 设置为 0 的原因了,将 IF 设置为 0 的原因是,在进入中断处理程序后,禁止其他的可屏蔽中断
2、不可屏蔽中断
不可屏蔽中断是 CPU 必须响应的外中断,当 CPU 检测到不可屏蔽中断信息时,则在执行完当前指令后,立即响应,引发中断过程
对于 8086CPU 来说,不可屏蔽中断的中断类型码固定为 2,所以中断过程中,不需要取中断类型码,则其中断过程为
(1)标志寄存器入栈,IF = 0,TF = 0
(2)CS、IP 入栈
(3)( IP )=(8)( CS )=( 0Ah )
几乎所有由外设引发的外中断,都是可屏蔽中断,不可屏蔽中断是在系统中有必须处理的紧急情况发生时用来通知 CPU 的中断信息,我们主要讨论可屏蔽中断
PC 机键盘的处理过程
1、键盘输入
键盘上的没一个键相当于一个开关,键盘的每一个芯片对键盘上的每一个键的开关状态进行扫描
当按下一个键时,开关接通,该芯片就产生一个扫描码,扫描码说明了按下的键在键盘上的位置,扫描码被送入主板上的相关接口芯片的寄存器中,该寄存器的端口地址为 60h
当松开按下的键时,也产生一个扫描码,扫描码说明了松开的键在键盘上的位置,松开按键时产生的扫描码也被送入到 60h 端口中
一般将按下一个键时产生的扫描码称为通码,松开一个键所产生的扫描码为断码,扫描码长度为一个字节,通码的第 7 位为 0,断码的第 7 位为 1,即
断码 = 通码 + 80h
键盘上部分键的扫描码(通码)
键 | 通码 | 键 | 通码 | 键 | 通码 | 键 | 通码 |
---|---|---|---|---|---|---|---|
Esc | 01 | [ | 1A | \ | 2B | NumLock | 45 |
1~9 | 02~0A | ] | 1B | Z | 2C | ScollLock | 46 |
0 | 0B | Enter | 1C | X | 2D | Home | 47 |
- | 0C | Ctrl | 1D | C | 2E | ↑ | 48 |
= | 0D | A | 1E | V | 2F | PgUp | 49 |
Backspace | 0E | S | 1F | B | 30 | - (小键盘) | 4A |
Tab | 0F | D | 20 | N | 31 | ← | 4B |
Q | 10 | F | 21 | M | 32 | → | 4D |
W | 11 | G | 22 | , | 33 | + | 4E |
E | 12 | H | 23 | . | 34 | End | 4F |
R | 13 | J | 24 | / | 35 | ↓ | 50 |
T | 14 | K | 25 | R-Shift | 36 | PgDn | 51 |
Y | 15 | L | 26 | PrtSc | 37 | Ins | 52 |
U | 16 | ; | 27 | Alt | 38 | Del | 53 |
I | 17 | ‘’ | 28 | Space | 39 | ||
O | 18 | ` | 29 | CapsLock | 3A | ||
P | 19 | L-Shift | 2A | F1~F10 | 3B~44 |
2、引发 9 号中断
键盘的输入到达 60h 端口时,相关的芯片就会向 CPU 发出中断类型码为 9 的可屏蔽中断信息, CPU 检测到该中断信息后,如果 IF = 1,则响应中断,引发中断过程,转去执行 int 9 中断例程
3、执行 int 9 中断例程
BIOS 提供了 int 9 的中断例程,用来进行基本的键盘输入处理,主要的工作如下:
(1)读出 60h 端口中的扫描码
(2)如果是字符键的扫描码,将该扫描码和它所对应的字符码(ASCII 码)送入内存中的 BIOS 键盘缓冲区;如果是控制键(比如 Ctrl )和切换键(比如 CapsLock)的扫描码,则将其转变为状态字节(用二进制位记录控制键和切换键的状态)写入内存中存储状态字节的单元
(3)对键盘系统的相关的控制,比如说,向芯片发出应答信息
BIOS 键盘缓冲区是系统启动后, BIOS 用于存放 int 9 中断例程所接收的键盘输入的内存区,该内存区可以存储15个键盘输入,因为 int 9 中断例除了接收扫描码之外,还要产生和扫描码对应的字符码,所以 BIOS 键盘缓冲区,一个键盘输入用一个字单元存放,高位字节存放扫描码,低位字节存放字符码
键盘状态字节:
0040:17 单元存储键盘状态字节,该字节记录了控制键和切换键的状态
对应键被按下时,对应位置为 1
键盘状态字各位记录的信息如下:
0:右 Shift 状态
1:左 Shift 状态
2:Ctrl 状态
3:Alt 状态
4:Scroll Lock 状态
5:NumLock 状态
6:CapsLock 状态
7:Insert 状态
总结键盘输入的处理过程
① 键盘产生扫描码
② 扫描码进入 60h 端口
③ 引发 9 号中断
④ CPU 执行 int 9 中断例程处理键盘输入
编写 int 9 中断例程
上面的过程中,第1、2、3步都是由硬件系统完成的,我们自己能改变的只有 int 9 中断处理程序
所以我们写新的中断程序处理一些特殊工作时,需要在其中调用 int 9 中断例程来进行一些原本对硬件细节应有的处理
以实现在程序执行期间,按下 Esc 键改变屏幕的背景颜色为例来说明
我们编写的 int 9 中断例程,功能如下:
(1)从 60h 端口读出键盘的输入
(2)调用 BIOS 的 int 9 中断例程,处理其他的硬件细节
(3)判断是否为 Esc 的扫描码,如果是,改变颜色后返回,如果不是,直接返回
功能的实现分析
1、从 60h 端口读出扫描码
in al,60h
2、调用 BIOS 的 int 9 中断例程
主程序必须要将中断向量表中的 int 9 的入口地址改为我们写的新的 int 9 的中断例程的入口地址
但是,要在新的 int 9 中断例程中调用原来的 int 9 中断例程时,现在中断向量表中中的 int 9 的入口地址却不是原先 int 9 中断例程的入口地址,所以不能直接进行 int 调用,需要将原先的 int 9 中断例程的入口地址进行保存,然后用别的指令来对 int 指令进行模拟,从而实现调用
int 过程描述:
(1)标志寄存器入栈
(2)IF = 0,TF = 0
(3)CS、IP 入栈
(4)( IP )= (( ds )* 16 + 0),( CS )= (( ds )* 16 + 2)
注意到:其中第(3)、(4)步和 call dword ptr ds:[ 0 ] 的功能一样
而(1)可以用 pushf 来实现
(2),可以用下面的指令来实现:
pushf
pop ax
and ah, 11111100b
push ax
popf
则,模拟 int 指令的调用功能的实现:
pushf
pushf
pop ax
and ah, 11111100b
push ax
popf
call dword ptr ds:[ 0 ]
3、如果是 Esc 的扫描码,则改变颜返回
cmp al,1(Esc扫描码)
jne iret
inc byte ptr 0b800h:[ bx ]
完整程序如下
为了防止在设置新的 int 9 中断例程的入口地址时发生意外中断,而引起 CPU 去错误的地址去调用中断例程
所以在设置之前,应该将 IF 设置为 0,从而不响应可屏蔽中断
assume cs:code
data segment
db 16 dup(0)
data ends
stack segment
dw 64 dup(0)
stack ends
code segment
start:
mov ax, data
mov ds, ax
mov si, 0
mov ax, stack
mov ss, ax
mov sp, 128
; 将BIOS的int 9中断例程的入口地址保存在ds:[0]的两个字中
mov ax, 0
mov es, ax
mov ax, es:[9*4]
mov [si], ax
mov ax, es:[9*4+2]
mov [si + 2], ax
; 将新的int 9中断例程的入口地址存在中断向量表中
; 为了防止在存储过程中发生键盘中断,从而使CPU转去一个错误的地址去执行
; 应在这段指令前将IF设置为0
pushf
cli
mov ax, offset int9
mov es:[9*4], ax
mov ax, cs
mov es:[9*4+2], ax
popf
mov ax, 0b800h
mov es, ax
mov al, 'a'
s:
mov es:[160*12+40*2], al
inc al
call delay
cmp al, 'z'
jna s
mov ax, 0
mov es, ax
mov ax, ds:[0]
mov es:[9*4], ax
mov ax, ds:[2]
mov es:[9*4+2], ax
mov ax, 4c00h
int 21h
delay:;延时程序
push ax
push dx
mov dx, 10h
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
int9:;新的 int 9 中断例程(按下esc改变显示字符的颜色)
push ax
push bx
push es
in al, 60h
pushf
; 模拟BIOS的int 9调用的实现
pushf
pop bx
and bh, 11111100b
push bx
popf
call dword ptr ds:[0]
cmp al, 1
jne int9final
mov ax, 0b800h
mov es, ax
inc byte ptr es:[160*12+40*2+1]
int9final:
pop es
pop bx
pop ax
iret
code ends
end start
安装新的 int 9 中断例程
安装一个新的 int 9 中断例程
功能:在 DOS 下,按下 A 键,除非不松开,如果松开,就显示满屏幕的 A ;按下 F1 后,改变显示的颜色
完整的程序如下:
assume cs:code
data segment
db 16 dup(0)
data ends
stack segment
dw 64 dup(0)
stack ends
code segment
start:
mov ax, stack
mov es, ax
mov sp, 128
push cs
pop ds
mov ax, 0
mov es, ax
mov si, offset int9
mov di, 200h
mov cx, offset int9end - offset int9
cld
rep movsb
mov ax, es:[9*4]
mov es:[202h], ax
mov ax, es:[9*4+2]
mov es:[202h+2], ax
cli
mov word ptr es:[9*4], 200h
mov word ptr es:[9*4+2], 0
sti
mov ax, 4c00h
int 21h
int9:
jmp short int9start
dd 0
int9start:
push ax
push bx
push cx
push es
push si
in al, 60h
pushf
call dword ptr cs:[202h]
cmp al, 3bh
jne s1
mov bx, 0b800h
mov es, bx
mov si, 1
mov cx, 2000
s:
inc byte ptr es:[si]
add si, 2
loop s
s1:
mov bx, 0b800h
mov es, bx
cmp al, 9eh
jne s2
mov si, 0
mov cx, 4000
s3:
mov byte ptr es:[si], 41h
add si, 2
loop s3
s2:
pop si
pop es
pop cx
pop bx
pop ax
iret
int9end:
code ends
end start
指令系统总结
对 8086CPU 的指令系统进行一下总结
8086 CPU 提供一下几大类指令
1、数据传送指令
mov、push、pop、pushf、popf、xchg 等都是数据传送指令,这些指令实现寄存器和内存、寄存器和寄存器之间的的单个数据传送
2、算数运算指令
add、sub、adc、sbb、inc、dec、cmp、imul、idiv、aaa 等都是算术运算指令,这些指令实现寄存器和内存中的数据算数运算,它们的执行结果影响标志寄存器的 SF、ZF、OF、CF、PF、AF 位
3、逻辑指令
and、or、not、xor、test、shl、shr、sal、sar、rol、ror、rcl、rcr 等都是逻辑指令,除了 not 指令外,它们的执行结果都影响标志寄存器的相关标志位
4、转移指令
可以修改 IP,或同时修改 CS 和 IP 的指令统称为转移指令,转移指令分为以下几类
(1)无条件的转移指令,比如:jmp
(2)条件转移指令,比如:jcxz、je、jb、ja、jnb、jna 等
(3)循环指令,比如:loop
(4)过程,比如:call、ret、retf
(5)中断,比如:int、iret
5、处理机控制指令
cld、std、cli、sti、nop、clc、cmc、stc、hlt、wait、esc、lock 等指令都是处理机控制指令,这些指令对标志寄存器或其他处理机状态进行设置
6、串处理指令
movsb、movsw、cmps、scas、lods、stos 等,这些指令对内存中的批量数据进行处理,若要只用这些指令方便地进行批量数据的处理,则需要和 rep、repe、repne 等前缀指令配合使用
第 16 章 直接定址表
数据标号
assume cs:code
code segment
a db 1,2,3,4,5,6,7,8
b dw 0
start:
...
...
code ends
end start
在 code 段中使用的标号 a,b 后面没有 “ :”,它们是同时描述内存地址和单元长度的标号
标号 a:描述了地址 code:0,和从这个地址开始,以后的内存单元都是字节单元
标号 b:描述了地址 code:8,和从这个地址开始,以后要的内存单元都是字单元
可见,使用这种包含单元长度的标号,可以使我们以简洁的形式访问内存中的数据,以后,我们将这种标号称为数据标号,它标记了存储数据的单元的地址和长度,它不同于仅仅表示地址的地址标号
一般来说,我们不在代码段中定义数据,而是将数据定义到其他段中,在其他段中,我们也可以使用数据标号来描述存储数据的单元的地址和长度
注意:在后面加有 “ :” 的地址标号,只能在代码段中使用,不能在其他段中使用
注意:如果想在代码段中直接使用数据标号访问数据,则需要用伪指令 assume 将标号所在的段和一个段寄存器联系起来,否则编译器在编译的时候,无法确定标号的段地址在哪一个寄存器中
当然,这种联系是编译器需要的,但绝不是说,我们因为编译器的工作需要,用 assume 指令将段寄存器和某个段相联系,段寄存器中,就真的存放该段的地址,我们在程序中还需要用指令对段寄存器进行设置
可以将标号当作数据来定义,编译器将标号所表示的地址当作数据的值
data segment
a db 1, 2, 3, 4, 5, 6, 7, 8
b dw 0
c dw a, b
data ends
数据标号 c 处存储的两个字型数据位标号 a、b 的偏移地址
再比如:
data segment
a db 1, 2, 3, 4, 5, 6, 7, 8
b dw 0
c dd a, b ; 注意这里是 dd
data ends
数据标号 c 处存储的两个双字型数据为标号 a 的偏移地址和段地址,标号 b 的偏移地址和段地址
相当于
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 操作符
seg 操作符的功能是为取的某一标号的段地址
直接定址表
利用表,可以在两个数据集合之间建立一种映射关系,使我们可以用查表的方法根据给出的数据得到其在另一集合中的对应数据,这样做的目的一般来说有三个:
(1)为了算法的清晰和简洁
(2)为了加快运算速度
(3)为了使程序易于扩充
编程的时候要注意程序的容错性,即对于错误的输入要有处理能力
像这种可以通过依据数据,直接计算出所要找的元素的位置的表,我们称为直接定地表
showsin: ; 表中存放着需要计算的 sin 值
jmp short show
table dw ag0, ag30, ag60, ag90, ag120, ag150, ag180
ag0 db '0', 0
ag30 db '0.5', 0
ag60 db '0.866', 0
ag90 db '1', 0
ag120 db '0.866', 0
ag150 db '0.5', 0
ag180 db '0', 0
show:
...