前段时间搞微信余额修改遇到瓶颈,想着还是学学汇编吧,工作之余看了几个月的书,收获还是不少,把重点记录在此,方便查阅。看的书是王爽老师的《汇编语言》,写的真是非常的清晰易懂,推荐想学汇编的先看这本书。
基础知识
汇编指令
首先汇编是不区分大小写的。 汇编有3类指令
1.汇编指令:和机器码一一对应
2.伪指令:没有对应的机器码,由编译器执行,计算机不执行,有点像define
3.其他符号:如+、-、*、/
等,由编译器识别,没有对应的机器码
存储器
存储器被划分成若干个存储单元,每个存储单元从0开始编号,例如一个存储器有128个存储单元,编号从0~127。
每个存储单元能放一个Byte(字节)
,1Byte=8bit
。也就是说每个存储单元是8位的。
CPU数据读写
cpu对存储器的读写需要3个信息交互
1.存储单元的地址(地址信息)
2.器件的选择、读或写的命令(控制信息)
3.读或写的数据(数据信息)
用生活中的例子来表示就像快递,首先要知道快递网点的地址,然后要知道你是取还是寄包裹,最后要把包裹带上。
信息信号物理上是通过总线
来传输的,逻辑上又分为了地址总线
控制总线
数据总线
要让CPU进行数据读写,只要输入驱动它工作的电平信息(机器码)就行了。
CPU通过地址总线来指定存储单元,CPU有N根地址线就能寻找2的N次方个内存单元。(用2进制传输一个数字,表示取第几个单元)。
CPU通过数据总线来进行数据传输,数据总线的宽度决定了CPU和外接的数据传输速度。一根线一次传输一个bit。8086CPU总线宽度为16,一次能传输2个字节的数据。
CPU通过控制总线来对外部器件进行控制,控制总线是一些不同控制线的集合。控制总线的数量决定了CPU对外部器件有多少种控制。对于内存读取来说,有一根“读信号输出”
的控制线复制CPU向外传送读信号。当CPU向控制线输出低电平表示读取数据;还有一根“写信号输出”
的控制线负责传送写信号。
总的来说
1.地址总线的宽度决定了CPU的寻找能力。
2.数据总线的宽度决定了CPU与其他器件进行数据传送时的单次数据量,进而决定了传送次数。
3.控制总线的宽度决定了CPU对系统中其他器件的控制能力。
寄存器
CPU由运算器、控制器、寄存器等器件构成,这些器件通过内部总线相连。
- 运算器进行信息处理;
- 寄存器进行信息存储;
- 控制器控制各种器件进行工作;
- 内部总线连接各种器件,在器件之间进行数据的传送;
汇编其实就是操作寄存器,程序员通过指令读写寄存器来实现对CPU的控制。不同的CPU寄存器的个数、结构是不同的。
8086的14个寄存器分别为AX、BX、CX、DX、SI、DI、SP、BP、IP、CS、SS、DS、ES、PSW。
8086的所有寄存器都是16位的。AX、BX、CX、DX、这4个通常用来存放一般性的数据,被称为通用寄存器。为了兼容老CPU的8位寄存器,这4个寄存器都可以分为两个8位的独立寄存器使用。
- AX分为AH和AL
- BX分为BH和BL
- CX分为CH和CL
- DX分为DH和DL
8086可以一次性处理以下两种尺寸的数据
- 字节:记为byte,一个字节由8个bit组成,可以存放在8位寄存器中。
- 字:记为Word,一个字有两个字节组成,这两个字节分别称为这个字的高位字节和低位字节。
一个内存单元可存放8位数据,CPU寄存器可存放n个8位的数据,也就是说,计算机中的数据大多是有1~N个8位数据构成。为了直观显示,一般用16进制来显示。比如20000写成4E20就能直观的看出,这个数据是由4E和20这两个8位数据构成。
内存地址
CPU访问内存单元时,需要给出内存单元的地址,所有的内存单元构成的存储空间是一个一维的线性空间,每个内存单元在这个空间中都有唯一的地址,我们将这个唯一的地址称为物理地址。
CPU通过地址总线送入存储器的是一个内存单元的物理地址,这个物理地址是在CPU内部形成的,不同的CPU可以有不同的形成方式。
8086形成物理地址的方法 8086CPU用一个20位的地址总线,寻址能力1MB。但是又是16位的结构,内部存储、传输、暂时存储的地址为16位。这样如果只是直接发出,那么只能送出16位的地址,寻址能力就只有64KB。比较浪费资源。 所以8086采用了一种在内部用两个16位地址合成的方法来形成一个20位的物理地址。流程如下
1.CPU中的相关部件提供两个16位的地址,一个称为段地址,另一个称为偏移地址。
2.段地址和偏移地址通过内部总线送入地址加法器。
3.地址加法器将两个16位的地址合成位一个20位的物理地址。
4.地址加法器通过内部总线将20位物理地址送入输入输出控制电路。
5.输入输出控制电路将20位物理地址送上地址总线。
6.20位物理地址被地址总线传送到存储器。
地址加法器采用物理地址=段地址x16+偏移地址
的方法用段地址和偏移地址合成物理地址。
传入段地址1230和00C8两个地址,把段地址x16即左移一位(二进制的左移4位),变成12300。然后和偏移地址相加得到123CB。
段地址在8086的段寄存器中存放,8086有4个段寄存器CS、DS、SS、ES
。
指令寄存器CS,IP
CS和IP是8086中最关键的寄存器,它们只是了CPU当前要读取指令的地址。CS为代码段寄存器,IP为指令指针寄存器,从名称上我们可以看出它们和指令的关系。
在8086中,任意时刻,设CS中的内容为M
,IP中的内容为N
,8086将从Mx16+N
单元开始,读取一条指令并执行。
也就是说任意时刻CPU将CS:IP指向的内容当做命令执行。读取一条指令后,IP中的值自动增加,让CPU读取下一条指令,当前指令长度决定了IP的增加值。
具体流程如下
1.从CS:IP指向的内存单元读取指令,读取的指令进入指令缓冲器;
2.IP=IP+所读取指令的长度,从而指向下一条指令;
3.执行指令。转到步骤1,重复。
在8086中,加电启动或复位后,CS=FFFFH,IP=0000。即FFFF0H是开机后执行的第一条指令。
CS和IP不能通过MOV来赋值,能够修改CS、IP内容的指令被统称为转移指令
。最简单的就是jmp
。
当我们想修改CS、IP的内容可以通过jmp 段地址:偏移地址
这样的指令来完成。如
jmp 2AE3:3 -> CS=2AE3H IP=0003H -> 2AE33H
jmp 3:0B16 -> CS=0003H IP=0B16H -> 00B46H
若我们只想修改IP的内容,需要用一个寄存器做中转。如
ax = 1000H;
jmp ax,
IP就变为了1000H,CS不变。含义上和MOV IP,AX 类似。
内存中字的存储
CPU中,用16位寄存器来存储一个字,在内存中一个字要用两个地址连续的字节单元来存放,这个字的低位字节存放在低地址单元中,高位字节存放在高地址单元中。
DS段地址寄存器与[address]偏移地址
8086中内存地址由段地址和偏移地址组成,DS寄存器通常用来存放将要访问数据的段地址。
使用mov ax,[偏移地址]
就能把DSx16+偏移地址
的值取出来放到ax中。
给DS寄存器赋值需要通过一个寄存器来中转。
mov bx,1000H
mov ds,bx
常用指令
指令形式 | 例子 |
---|---|
mov 寄存器,数据 | mov ax,8 |
mov 寄存器,寄存器 | mov ax,bx |
mov 寄存器,内存单元 | mov ax,[0] |
mov 内存单元,寄存器 | mov [0],ax |
mov 段寄存器,寄存器 | mov ds,ax |
指令形式 | 例子 |
---|---|
add 寄存器,数据 | add ax,8 |
add 寄存器,寄存器 | add ax,bx |
add 寄存器,内存单元 | add ax,[0] |
add 内存单元,寄存器 | add [0],ax |
指令形式 | 例子 |
---|---|
sub 寄存器,数据 | sub ax,8 |
sub 寄存器,寄存器 | sub ax,bx |
sub 寄存器,内存单元 | sub ax,[0] |
sub 内存单元,寄存器 | sub [0],ax |
栈和SS,SP寄存器
栈和收纳箱有点像,最先放进去的东西,最后才能拿出来,先进后出,后进先出。
8086入栈和出栈指令为PUSH
和POP
。8086的入栈和出栈都是以字节为单位。
push ax表示将寄存器ax中的数据送入栈中。
pop ax表示从栈顶取出数据送入ax。
8086中有两个寄存器,段寄存器SS和寄存器SP,栈顶的段地址存放在SS中,偏移地址存放在SP中。任意时刻,SS:SP指向栈顶元素。push指令和pop指令执行时,CPU通过SS和SP找到栈顶的地址。 push指令的执行过程如下
1.SP向上移动2位。SP=SP-2。
2.将数据放入SS:SP指向的内存单元。
可以看出入栈时,栈顶从高地址向低地址方向移动。
pop和push的执行过程相反 1.将SS:SP指向内存单元处的数据送入ax中。 2.SP向下移动2位。SP=SP+2。
Debug使用
Debug常用功能
column | column |
---|---|
R | 查看、改变CPU寄存器的内容 |
D | 查看内存中的内容 |
E | 改写内存中的内容 |
U | 将内存中的机器指令翻译成汇编指令 |
T | 执行一条机器指令 |
A | 以汇编指令的格式在内存中写入一条机器指令 |
R命令
R命令直接输入是查看寄存器内容,R 寄存器
表示要修改寄存器的值,按Enter
键后,出现;
输入你想改的值。
D命令
为设置地址的D
命令会列出Debug预设的地址的内容。D传入一个地址,D 段地址:偏移地址
,会显示从指定内存单元开始的128个内存单元的内容。当传入过一次地址后,直接输入D
会列出后续的内容。
E命令
E命令通过E 段地址:偏移地址
选定要修改的起始单元,会让你一次输入要为的值。
直接输入16精致数据
-e 1000:0 b8 01 00 b9 02 00 01 c8
可以直接用E命令写入字符。
-e 1000:0 'a' 'b' 'c' 1 2 3 4
e命令也可以直接输入字符串
-e 1000:0 "helloworld"
可以通过e命令输入16精制的命令的机器码,然后用U命令查看啊,机器指令和对应的汇编指令。
U命令
U命令可以查看内存里的机器指令和对应的汇编指令。
U 1000:0
A命令
T命令可以执行内存里的指令。 先用R命令修改CS和IP为1000:0,然后直接输入T即可执行一条指令。执行后IP会对应自增,要执行下一条指令直接再次输入T即可。
A命令可以通过汇编指令的形式写入指令。
-a 1000:0
1000:0000 mov ax,1
.
.
.
1000:000D add ax,ax
依次输入指令。两次回车结束
伪指令
segment和ends
segment
和ends
是一对成对使用的伪指令,作用是定义一个段,segment
表示段开始,ends
表示段结束。
用法:
段名 segment
代码在此
段名 ends
end
end
是一个汇编程序的结束标记,编译器遇到end
就会结束后面的编译。
assume
assume
表示“假设”,它假设某一寄存器和程序中的某一个用segment...ends
定义的段相关联。
assume cs:codesg //因为段的内容是代码段,所以把codesg代码段和cs寄存器联系起来
codesg segment //codesg代码段开始
mov ax,0H
mov ax,4c00H //这两句代码表示程序返回
int 21H
codesg ends //codesg代码段结束
end //程序结束
[BX]和loop指令
[BX]
[BX]和[0]类似,需要配合ds段寄存器使用
mov ax,[bx] //[bx]是偏移地址,ds是段地址,即
inc
inc bx 表示bx中内容+1
mov bx,1
inc bx //bx=2
[loop]循环
loop循环的次数是cx寄存器控制的。
loop执行步骤:
1.cx-1
2.判断cx的值,不为0就跳转至标号处执行,为0就向下执行。
mov ax,2
mov cx,10 //设置执行的次数
s: add ax,ax //设置要重复执行的标号
loop s
包含多个段的程序
在代码段中使用数据
代码里面有数据的存在,需要致命程序的入口,不然不知道哪里开始是代码。标号会转化为一个入口地址,存储在可执行文件的描述信息中。当程序被加载入内存之后,加载者从程序的可执行文件的描述信息中读取到程序的入口地址,设置CS:IP。CPU就能从我们希望的地址处开始执行。
assume cs:code
code segment
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h//声明一些数据
start: mov bx,0//添加一个标号start
mov ax,0
mov cx,8
s: add ax,cs:[bx]
add bx,2
loop s
mov ax,4c00h
int 21h
code ends
end start//这里指明了程序的入口标号。
数据、代码、栈放入不同的段
assyme cs:code,ds:data,ss:stack //把code和cs寄存器,data和ds寄存器,stack和ss寄存器关联起来
data segment //数据段开始,data已经和ds寄存器相关联了
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
data ends //数据段结束
stack segment //栈段开始
dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
stack ends //栈段结束
code segment //代码段开始
start: mov ax,stack//栈顶地址初始化,段地址为stack的起始段地址
mov ss,ax
mov sp,20h //ss:sp指向stack:20
mov ax,data //数据地址初始化到data的起始段地址
mov ds,ax
mov bx,0 //指向数据的第一个单元
mov cx,8 //设置循环次数
s: push [bx] //把ds:bx数据入栈
add bx,2 //指向下一个数据
loop s //循环
mov bx,0
mov cx,8
s0: pop [bx] //出栈到ds:bx,上一个循环最后入栈的现在在最上面,第一个出栈
add bx,2 //偏移地址指向下一个位置
loop s0
mov ax,4c00h//中断程序
int 21h
code ends //代码段结束
end start //指明程序执行入口
上面代码中的code,data,stack没有实际意义,只是方便我们辨识,段中的内容不会被自动识别为数据还是栈还是指令,这些都是由CS:IP,SS:SP,DS的设置来决定的, CS:IP指向就当指令来执行。 SS:SP指向就当栈来执行。 DS指向就当数据来执行
更灵活的定位内存地址的方法
and和or指令
and
and是逻辑与指令,按位比较,相同就为1,不同就为0
mov al,01100011B
and al,00111011B
得al = 00100011B
利用and指令能快速的把指定位设为0,其他位不变
and al,11111110B //将第0位设为0,其他位不变
and al,01111111B //将第7位设为0,其他位不变
or
or是逻辑或指令,按位比较,都为0才是0。
mov al,01100011B
or al,00111011B
得 al=01111011B
利用or能快速的将指定为设为1,其他位不变。
or al,10000000B //将第7位设为1,其他位不变
or al,00000001B //将第0位设为1,其他位不变
字符形式给出数据
在汇编中,可以用'......'
的方式给出数据,编译器会转化为对应的ASCII
码。
assume cs:code,ds:data
data segment
db 'unIX' //相当于db 75H,6EH,49H,58H
db 'foRK' //相当于db 66H,6FH,52H,4BH
data ends
code segment
start: mov al,'a' //相当于mov al,61H
mov bl,'b' //相当于mov al,62H
mov ax,4c00h
int 21h
code ends
end start
大小写转换
在ASCII
中,同一个字母的大小写ASCII
码相差20H
,即十进制的32
。
字母 | 16进制 | 二进制 |
---|---|---|
A | 41 | 01000001 |
a | 61 | 01100001 |
从2进制可以看出,第五位正好控制大小写,大写为0,小写为1。应为二进制的第五位刚好等于10进制的32,16进制的20H。
这样,通过前面的and
和or
就能轻松控制大小写了。
and al,11011111B //将al中的ASCII码第五位置为0,变成大写字母
or al,00100000B //将al中的ASCII码第五位置为1,变成小写字母
[bx+idata]方式进行数组的处理
bx可以加一个起始偏移地址,不改变bx,可以在一个循环里,很方便的对多个偏移地址进行操作。
[bx+0]//起始偏移地址不变
[bx+5]//从第5个开始加偏移地址
//上面的代码可以简写成下面的样式,和C语言的数组很像。
0[bx]
5[bx]
SI和DI
si和di是8086中和bx功能相近的寄存器,不能分成2个8位寄存器来使用。
//这3段代码功能相同
mov bx,0
mov ax,[bx]
mov si,0
mov ax,[si]
mov di,0
mov ax,[di]
//si和di一样能使用[si+idata]的形式
mov ax,[si+100]
[bx+si]和[bx+di]
bx,si,di不仅能加一个数,还能用[bx+si]和[bx+di]的形式来指明一个内存单元
也可以写成
mov ax,[bx][si]//常用写法
[bx+si+idata]和[bx+di+idata]
除了bx和si,di的相加,还能再多加一个数。
有下面及种写法
mov ax,[bx+200+si]
mov ax,[200+bx+si]
mov ax,200[bx][si]
mov ax,[bx].200[si]
mov ax,[bx][si].200
bp
在[…]的用法中,只有4个寄存器能这样写,bx,si,di和bp
前面讲到bx可以和si,di采用组合的形式来表示一个地址mov ax,[bx+200+si]
。
bp一样也可以mov ax,[bp+200+si]
,但是有一点bp和bx不能组合像mov ax,[bx+bp]
是错误的。
在[…]中使用寄存器bp,如果没有显性的给出段地址,段地址默认是在ss
中。
mov ax,[bp] //mov ax,ss:bp
数据处理的两个基本问题
指令执行前,所要处理的数据可以在3个地方:CPU内部、内存、端口。
汇编中数据位置的表达
汇编语言中用3个概念来表达数据的位置。
1.立即数(idata) mov ax,1
2.寄存器 mov ax,bx
3.段地址+偏移地址 mov ax,[bx+si+8]
寻址方式
寻址方式 | 名称 | 用法 |
---|---|---|
[idata] | 直接寻址 | [idata] |
[bx] | 寄存器间接寻址 | [bx] |
[bx+idata] | 寄存器相对寻址 | 用于结构体[bx].idata、用于数组idata[si]、用于二维数组[bx][idata] |
[bx+si] | 基址变址寻址 | 用于二维数组[bx][si] |
[bx+si+idata] | 相对基址变址寻址 | 用于表格(结构)中的数组项[bx].idata[si]、用于二维数组idata[bx][si] |
指令要处理的数据长度
通过寄存器名指定
8086的指令可以处理2中尺寸的数据,byte
和word
,汇编语言通过指明寄存器名(ax还是al)指明要处理的数据的尺寸。
mov ax,1 //指明了指令是字操作
mov bx,ds[0]
mov ds,ax
mov al,1 //指明了指令是字节操作
mov al,bl
通过操作符指定
在没有寄存器名的情况下,用操作符X ptr
指明内存单元的长度,X可以为word
或byte
。
1.指明为字操作
mov word ptr ds:[0],1
inc word otr [bx]
2.指明为字节操作
mov byte ptr ds:[0],1
inc byte ptr [bx]
其他方法
有一些指令默认了访问的是字单元还是字节单元,如push [1000H]
,就不用指明访问的是字单元还是字节单元,因为push
指令只进行字操作。
div指令
//被除数默认是在AX或(DX和AX)中
div 除数的内存单元
//al=ax/ds:0的商 ah=ax/ds:0的余 因为是除数是8位AX存放商和余就够了
div byte ptr ds:[0]
div是除法指令,使用时应注意以下问题。 1.除数:有8位和16位两种 2.被除数:默认放在AX或DX和AX中,如果除数为8位,被除数则为16位,默认在AX中存放。如果除数为16位,被除数则为32位,在DX和AX中存放,DX存放高16位,AX存放低16位。 3.结果:如果除数为8位,则AL存储除法操作的商,AH存储除法操作的余数。如果除数为16位,则AX存储除法操作的商,DX存储除法操作的余数。
做成表格看起来比较直观
除数 | 除数8位 | 除数16位 |
---|---|---|
被除数 | 被除数为16位,默认在AX中存放 | 被除数为32位,在DX和AX中存放,DX存放高16位,AX存放低16位 |
结果 | AL存储除法操作的商,AH存储除法操作的余数 | AX存储除法操作的商,DX存储除法操作的余数 |
计算100001/100
//因为100001大于65535是一个32位的,被除数应为16位
//先把100001拆成高16位和低16位,高16位放到dx,低16位放到ax中
mov dx,1
mov ax,86A1H //1*10000H+86A1H=100001
mov bx,100
div bx
//结果ax=03E8(即商为1000),dx=1(余数为1)。
伪指令dd
前面我们用db和dw定义字节型数据和字型数据,dd是用来定义doubleword,双字型数据。
data segment
db 1 //01H 在data:0处,占1个字节
dw 1 //0001H 在data:1处,占1个字
dd 1 //00000001H 在data:3处,占2个字
dup
dup是一个操作符,在汇编语言中和db、dw、dd等一样,有编译器识别处理。它是和db、dw、dd等数据定义伪指令配合使用的,用来进行数据的重复。
//定义了3个字节,值都是0,相当于db 0,0,0
db 3 dup (0)
//定义了9个字节,分别是0,1,2,0,1,2,0,1,2相当于db 0,1,2,0,1,2,0,1,2
db 3 dup (0,1,2)
定义了18个字节,值是'abcABCabcABCabcABC'相当于db 'abcABCabcABCabcABC'
db 3 dup ('abc',"ABC")
可见dup的使用格式如下
数据类型 重复的次数 dup (重复的数据)
转移指令的原理
可以修改IP,或同时修改CS和IP的指令统称为转移指令。
转移指令的分类
8086CPU的转移行为有一下几类。
转移类型 | 示例 |
---|---|
只修改IP,称为段内转移 | jmp ax |
同时修改CS和IP,称为段间转移 | jmp 1000:0 |
对于转移指令对IP的修改范围不同,段内转移又分为:短转移和近转移。
- 短转移IP的修改范围为-128~127
- 近转移IP的修改范围为-32768~32767
8086CPU的转移指令分为以下几类
- 无条件转移指令(jmp)
- 条件转移指令
- 循环指令(loop)
- 过程
- 中断
操作符offset
offset
是由编译器处理的符号,它的功能是取得标号的偏移地址。
assume cs:codesg
codesg segment
start:mov ax,offset start //相当于mov ax,0
s:mov ax,offset s //相当于mov ax,3
codesg ends
end start
offset
取得了标号的start
和s
的偏移地址。
jmp指令
jmp
是无条件转移指令,可以只修改IP
,也可以同时修改CS
和IP
jmp指令要给出两种信息:
1.转移的目的地址
2.转移的距离
jmp short 标号
表示转移到标号处执行指令
这种格式的jmp指令实现的是段内短转移,它对IP的修改范围为-128~127。指令中的short
说明指令进行的是短转移。指令中的标号指明了要转移的目的地,转移指令结束后,CS:IP应该指向标号处的指令。
assume cs:codesg
codesg segment
start:mov ax,0
jmp short s //跳转到s标号处继续执行
add ax,1 //这句被跳过了
s:inc ax
codesg ends
end start
jmp short 标号
jmp short
指令对应的机器码中不包含转移的目的地址,而是转移的位移。
jmp short
的实际功能为IP=IP+(8位的位移)。
1.8位位移=标号处的地址-jmp指令后的第一个字节的地址。
2.short指明了此处的位移为8位。
3.8位位移范围为-128~127,用补码表示。
4.8位位移由编译器在编译时算出。
jmp near ptr 标号
还有一种和jmp short 标号
功能相近的指令格式,jmp near ptr 标号
,它实现的是段内近转移。
jmp near ptr 标号
的功能为IP=IP+(16位位移)。
1.16位位移=标号处的地址-jmp指令后的第一个字节的地址。
2.near ptr指明此处的位移为16位位移,进行的是段内近转移。
3.16位位移的范围为-32768~32767,用补码表示。
4.16位位移由编译程序在编译时算出。
jmp far ptr 标号
前面的jmp指令,对应的机器指令中没有目的地址,而是相对于当前IP的偏移地址。
jmp far ptr 标号
实现的是段间转移,又称为远转移。
CS=标号所在段的段地址;IP=标号在段中的偏移地址。
far ptr 指明了指令用标号的段地址和偏移地址修改CS和IP。
assume cs:codesg
codesg segment
start:mov ax,0
mov bx,0
jmp far ptr s //机器码中包含了目的地址
db 256 dup (0) //被跳过
s:add ax,1
inc ax
codesg ends
end start
转移地址在寄存器中的jmp指令
指令格式:jmp 16位寄存器 功能:IP = 16位寄存器中的转移地址
转移地址在内存中的jmp指令
转移地址在内存中的jmp指令有两种格式:
1.jmp word ptr 内存单元地址
(段内转移)
功能:从内存单元地址处取出转移目的的偏移地址(一个字)。
内存单元地址可用任意寻址方式给出
mov ax,0123H
mov ds:[0],ax
jmp word ptr ds:[0]
//IP=0123H
2.jmp dword ptr 内存单元地址
(段间转移)
功能:从内存单元地址处取出转移目的地址(两个字),高地址的字是转移目的的段地址,低地址的字是转移目的的偏移地址。
CS = 内存单元地址+2
IP = 内存单元地址
mov ax,0123H
mov ds:[0],ax
mov word ptr ds:[2],0 //指明存的是字,占2个位置
jmp dword ptr ds:[0]
//CS=0,IP=0123,CS:IP指向0000:0123
jcxz指令
jcxz指令为有条件转移指令,所有的有条件转移指令都是短转移,在对应的机器码中包含转移的位移,而不是目标地址,对IP的修改范围都是:-128~127。由cx控制。
指令格式:jcxz 标号(如果cx = 0 ,转移到标号处执行)。 当cx = 0时,IP = IP+8位的位移。 8位位移 = 标号处的地址-jcxz指令后的第一个字节的地址。 8位位移的范围为-128~127,用补码表示。 8位位移由编译程序在编译时算出。
cx≠0时什么也不做
loop指令
loop指令为循环指令,所有循环指令都是短转移。
指令格式:loop 标号(cx = cx-1,如果cx ≠ 0,转移到标号处执行。等于0就继续向下执行) cx = cx-1 如果cx ≠ 0,IP = IP+8位位移。 8位位移 = 标号处的地址 - loop指令后的第一个字节的地址。 8位位移的范围为 - 128~127,用补码表示。 8位位移由编译程序在编译时算出。
CALL和RET指令
ret和retf
ret指令用栈中的数据,修改IP的内容,从而实现近转移。 retf指令中栈中的数据,修改CS和IP的内容,从而实现远转移。
CPU执行ret指令时,进行下面两步操作:
1.IP=ss*16+SP
2.sp=sp+2
相当于进行了 pop IP 操作
CPU指令retf指令时,进行下面4步操作:
1.IP = ss*16+sp
2.sp = sp+2
3.CS = ss*16+sp
4.sp = sp+2
相当于进行了: pop IP pop CS 操作
例:ret指令执行后,IP = 0,CS:IP指向代码段的第一条指令。
assume cs:code
stack segment
db 16 dup(0)
stack ends
code segment
mov ax,4c00h
int 21h
start: mov ax,stack
mov ss,ax
mov sp,16
mov ax.0
push ax
mov bx,0
ret
code ends
end start
例:retf指令执行后,CS:IP指向代码段的第一条指令
assume cs:code
stack segment
db 16 dup(0)
stack ends
code segment
mov ax,4c00h
int 21h
start: mov ax,stack
mov ss,ax
mov sp,16
mov ax.0
push cs
push ax
mov bx,0
retf
code ends
end start
call指令
CPU执行call指令时,进行两步操作:
1.将当前的IP或CS和IP压入栈中。
2.转移
call指令不能实现短转移,实现方法和jmp指令的原理相同。
相对地址的call 指令格式:call 标号(将当前的IP压栈后,转移到标号处执行指令)
CPU执行这种格式的call指令时,进行如下的操作:
1.sp = sp - 2
ss*16 + sp = IP
2.IP=IP+16位位移
16位位移 = 标号处的地址 - call指令后的第一个字节的地址。 16位位移的范围为-32768~32767,用补码表示。 16位位移由编译程序在编译时算出。
“call 标号” 相当于
push IP
jmp near ptr 标号
目的地址的call 指令格式:call far ptr 标号 这种格式的call指令实现的是段间转移。
CPU执行此种格式的call指令是,进行如下的操作。
1.sp = sp-2
ss*16+sp = CS
sp = sp-2
ss*16+sp = IP
2.CS = 标号所在段的段地址
IP = 标号在段的偏移地址
“call far ptr 标号” 相当于
push CS
push IP
jmp far ptr 标号
转移地址在寄存器中的call指令
指令格式:call 16位 reg
sp = sp-2
ss*16+sp = IP
IP = 16位reg
相当于
push IP
jmp 16位reg
转移地址在内存中的call指令
转移地址在内存中的call指令有两种格式。
- call word ptr 内存单元地址 (仅修改IP)
相当于
push IP jmp word ptr 内存单元地址 例: mov sp,10h mov ax,0123h mov ds:[0],ax call word ptr ds:[0] //执行后IP=0123H,sp=0EH
- call dword ptr 内存单元地址 (同时修改CS和IP)
相当于
push CS push IP jmp dword ptr 内存单元地址 例: mov sp,10h mov ax,0123h mov ds:[0],ax mov word ptr ds:[2],0 call dword ptr ds:[0] //执行后CS=0,IP=0123H,sp=0CH
call和ret的配合使用
assume cs:code
code segment
start: mov ax,1
mov cx,3
call s //将当前IP的值压栈,然后跳转到s标号执行
mov bx,ax
mov ax,4c00h
int 21h
s: add ax,ax
loop s
ret //从栈中取一个值送入IP中,转移到IP处执行,
//即call s的下一句代码
code ends
end start
由此可以看出call和ret可以实现子程序的效果,s标号的代码段执行完后又回到了前面的代码继续执行。
子程序框架
assume cs:code
code segment
main: //主程序
...
call sub1 //调用子程序1
...
mov ax,4c00h
int 21h
sub1: //子程序1
...
call sub2 //调用子程序2
...
ret //子程序1的返回到主程序
sub2: //子程序2
...
ret //子程序2返回到子程序1
code ends
end main
mul指令
mul是乘法指令。 两个相乘的数:必须同时为8位或者16位。如果是8位,一个默认放在AL中,另一个放在8位寄存器或内存字节单元中。如果是16位乘法,一个默认在AX中,另一个放在16位reg或内存字单元中。 结果:如果是8位乘法,结果默认放在AX中,如果是16位乘法,结果高位默认放在DX中,低位放在AX中。
指令格式:mul reg
或者 mul 内存单元
内存单元可以用不同的寻址方式给出,如
mul byte ptr ds:[0]
//含义为ax=al*(ds*16+0);
mul word ptr [bx+si+8]
//含义为ax=ax*(ds*16+bx+si+8)结果的低16位
// dx=ax*(ds*16+bx+si+8)结果为高16位
例如:
//计算100*10
mov al,100
mov bl,10
mul bl
//结果ax=1000
标识寄存器
CPU内部的寄存器中,有一种特殊的寄存器,有一下作用
1.用来存储相关指令的某些执行结果。
2.用来为CPU执行相关指令提供行为依据。
3.用来控制CPU的相关工作方式。
这种特殊的寄存器在8086中,被称为标识寄存器。8086的寄存器有16位,其中存储的信息通常被称为程序状态字(PSW)。
有的指令影响标志寄存器,如add、sub、mul、div、inc、or、and等,大多是运算指令。 有的指令不影响标识寄存器,如mov、push、pop等,大多是传送指令。
flag寄存器每一位都有专门的含义,记录特定的信息。
flag寄存器的1、3、5、12、13、14、15位在8086CPU中没有使用,没有含义,而0、2、4、6、7、8、9、10、11位都具有特殊的含义。
15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
OF | DF | IF | TF | SF | ZF | AF | PF | CF |
ZF标志
flag的第6位是ZF,零标志位,它记录相关指令执行后,其结果是否为0。如果结果为0,那么zf=1;如果结果不为0,那么zf=0。
如:
mov ax,1
sub ax,1
//执行后,结果为0,则zf=1
mov ax,2
sub ax,1
//执行后,结果不为0,则zf=0
PF标志
flag的第2位是PF,奇偶标志位。它记录相关指令执行后,其结果的所有bit位中1的个数是否为偶数,如果1的个数为偶数,pf=1,如果为奇数,pf=0。
如:
mov al,1
add al,10
//执行后,结果为00001011B,其中有3个1,则pf=0
mov al,1
or al,2
//执行后,结果为00000011B,其中有2个1,则pf=1
SF标志
flag的第7位是SF,符号标志位。它记录相关指令执行后,其结果是否为负。如果结果为负,sf=1;如果非负,sf=0。
计算机中通常用补码来表示有符号数据。计算机中的一个数据可以看作是有符号数,也可以看成是无符号数。比如:
00000001B,可以看作无符号数1,或有符号数+1;
10000001B,可以看作无符号数129,也可以看作有符号数-127
也就是说对于同一个二进制数据,计算机可以将它当作无符号数据来运算,也可以当做有符号数据来运算 如:
mov al,10000001B
add al,1
//结果 al=10000010B
//可以将add指令进行的运算当做无符号数的运算,也可以当做有符号数计算(-127+1),结果为-126(10000010B)。
SF标识,就是CPU对符号数运算结果的一种记录,它记录数据的正负,在我们将数据当作有符号数来运算的时候,可以通过它来得知结果的正负。如果我们将数据当作无符号数来运算,SF的值则没有意义。
CF标志
flag的第0位是CF,进位标志位。一般情况下,在进行无符号数运算的时候,它记录了运算结果的最高有效位向更高位的进位值,或从更高为的借位值。
对于位数为N的无符号数来说,其对应的二进制信息的最高位,即第N-1位,就是它的最高有效位。而假象存在的第N位,就是相对于最高有效位的更高位
7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | |
---|---|---|---|---|---|---|---|---|
假想的更高位 | 0(最高有效位) | 0 | 0 | 1 | 1 | 1 | 0 | 0 |
当两个数据相加的时候,有可能产生从最高有效位向更高位的进位。这个进位值就用CF来保存 如:
mov al,98H
add al,al //执行后al=30,CF=1,CF记录了从最高有效位向更高位的进位值
add al,al //执行后al=60,CF=0,CF记录了从最高有效位向更高位的进位值
而当两个数据做减法的时候,有可能向更高位借位。比如,两个8位数据:97H-98H,将产生借位,借位后相当于197H-98H,而flag的CF位也可以用来记录这个借位值。 如:
mov al,97H
sub al,98H //执行后al=FFH,CF=1,CF记录了向更高位的借位值
sub al,al //执行后al=0,CF=0,CF记录了向更高位的借位值
OF标志
在进行有符号运算的时候,如果结果超过了机器所能表示的范围称为溢出。
8位的寄存器有效范围是-128~127,16位寄存器的有效范围是-32768~32767。如果运算结果超出了机器所能表达的范围,将产生溢出。 如:
mov al,98
add al,99
//执行后将产生溢出,al=al+99=98+99=197
//197超过了8位有符号数的范围:-128~127
//得到的结果是al=0C5H,因为是有符号数的运算,所以al存的是补码,C5H是有符号数的-59,98-99=-59显然是不对的。
面对这种情况,flag的第11位OF(溢出标志位)就起作用了。 如果发生了溢出OF=1;如果没有OF=0。
- 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
adx 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
mov al,98H
add al,al
adc al,3
//执行后al=34,adc执行时相当于al+3+CF=30H+3+1=34H
可以看出adc
指令比add
指令多加了一个CF位的值。CF的值有adc指令前面的指令决定的。
adc
指令是用来进行加法的第二步运算的。add和adc指令的配合能对更大的数据进行加法运算。
例0198H和0183H相加,可以分为两步来进行
1.低位相加 add al,bl
2.高位相加再加上低位相加产生的进位值。adc ah,bh
adc就是用来进行第二步的,会自动帮我们加上进位值CF。(add指令执行后,CF表示低位相加的进位置)
sbb指令
和adc指令类似,通过CF位来记录借位值。
指令格式:sbb 操作对象1,操作对象2
功能:操作对象1 = 操作对象1-操作对象2-CF
sbb ax,bx
实现的功能是ax=ax-bx-CF
利用sbb指令可以对任意大数据进行减法运算 如:
//计算003E1000H-00202000H,结果放在ax,bx中
mov bx,1000H
mov ax,003EH
sub bx,2000H//如果这里借位了,CF为1
sbb ax,0020H//减去低位的借位值
cmp指令
cmp是比较指令,功能相当于减法指令,只是不保存结果。cmp比较指令执行后,将对标志寄存器产生影响,其他指令通过标识位来得知比较结果。
指令格式:cmp 操作对象1,操作对象2 功能:计算操作对象1-操作对象2 当不保存结果,仅设置标识寄存器。
cmp ax,bx
结果 | 减法运算 | 标志状态 |
---|---|---|
ax=bx | ax-bx=0 | zf=1 |
ax≠bx | ax-bx≠0 | zf=0 |
ax<bx | ax-bx将借位 | cf=1 |
ax≥bx | ax-bx不借位 | cf=0 |
ax>bx | ax-bx不借位,结果也不为0 | cf=0;zf=0 |
ax≤bx | ax-bx即可能借位,结果可能为0 | cf=1;zf=1 |
这个比较的设计思路是通过减法运算,影响标识寄存器,来得到大小。
标志状态 | 结果 |
---|---|
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 |
前面的是无符号数的规则,下面看看有符号数
sf=1;of=0 没有溢出,逻辑正负=实际正负,ab<bh。
sf=1;of=1 有溢出,逻辑正负≠实际正负,因sf=1,实际结果为负,又有溢出,表示是溢出导致的结果为负。逻辑上必然为正,ah>bh。
sf=0;of=1 有溢出,逻辑正负≠实际正负,实际结果为正,又有溢出,表示是溢出导致的结果为正,逻辑上必然为负,ah<bh。
sf=0,of=0 没有溢出,逻辑正负=实际正负,sf=0实际非负,逻辑也非负,ah≥bh
检测比较结果的条件转移指令
jcxz是一个条件转移指令,它可以检测cx中的数值,如果cx=0,就修改IP,否则什么也不做。所有条件转移指令的转移位移都是-128~127。
除了jcxz之外,CPU还提供了其他条件转移指令,大多数条件转移指令都检测标志寄存器的相关标志位来决定是否修改IP。 通常检测被cmp指令影响的那些标志位,所以这些条件指令通常和cmp配合使用。
指令 | 含义 | 检测的标志位 |
---|---|---|
je | 等于则转移 | zf=1 |
jne | 不等于则转移 | zf=0 |
jb | 低于则转移 | cf=1 |
jnb | 不低于则转移 | cf=0 |
ja | 高于则转移 | cf=0且zf=0 |
jna | 不高于则转移 | cf=1或zf=1 |
e:表示equal ne:表示not equal b:表示below nb:表示not below a:表示above na:表示not above
例:
//实现ah=bh则ah=ah+ah,否则ah=ah+bh
cmp ah,bh //比较ah和bh改变对应的标志位
je s //标志位符合等于跳转到s标号执行
add ah,bh //标志位不符合等于执行这里
jmp short ok //跳过s标号的语句
s:add ah,ah
ok:....
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
3.如果df=1则 si=si-1,di=di-1
也可以按字来传送 格式:movsw 将ds:si指向的内存单元中的字送入es:di中,然后根据标识寄存器df位的值,将si和di加2或减2。
相当于
mov es:[di],word ptr ds:[si]
如果df=0
add si,2
add di,2
如果df=1
sub si,2
sub di,2
一般情况下movsb和movsw进行的是串传送操作中的一个步骤,需要和rep配合使用。
rep movsb//循环调用movsb,循环cx次
//和下面代码功能一样
s:movsb
loop s
由于flag的df位决定这串传送指令执行后,si和di的改变方向,我们需要提前设置好df位的值。 8086提供了2条指令来对df位进行设置。
cld指令:将标志寄存器的df位置0
std指令:将标志寄存器的df位置1
例:
//将data段的第一个字符串复制到它后面的空间
data segment
db 'Welcome to masm!'
db 16 dup (0)
data ends
code segment
mov ax,data //设置数据的起始位置
mov ds,ax
mov si,0
mov es,ax //设置数据的目标位置
mov di,16
mov cx,16 //设置循环次数
cld //设置方向,si和di递增
rep movsb //开始循环传送
code ends
pushf和popf
pushf的功能是将标志寄存器的值压栈,而popf是从栈中取出数据送入标志寄存器。
标志寄存器在Debug中的表示
标志 | 值为1的标记 | 值为0的标记 |
---|---|---|
of | OV | NV |
sf | NG | PL |
zf | ZR | NZ |
pf | PE | PO |
cf | CY | NC |
df | DN | UP |
内中断
当CPU内部发生以下事情的时候,将产生相应的中断信息。
- 除法错误,如执行div指令产生的除法移除。中断码:0
- 单步执行。中断码:1
- 执行into指令。中断码:4
- 执行int指令。中断码为指令携带,
int n
,终端吗即为n
中断向量表中存储了中断码对应的处理程序的入口地址。 中断向量表在内存中保存,其中存放了256个中断源所对应的中断处理程序的入口。CPU通过中断码就能找到对应的处理程序。 在8086中,中断向量表指定放在内存地址0处,从0000:0000到0000:03FF的1024个单元中。不能放在别处。
8086收到中断信息后,中断过程
- 从中断信息中取得中断类型码。
- 标志寄存器的值入栈(因为中断过程中要改变标志寄存器的值,所有先将其保存在栈中)。
- 设置标识寄存器的第8位TF和第9位IF的值为0
- CS的内容入栈
- IP的内容入栈
- 从内存地址为(中断类型码4)和(中断类型码4+2)的两个字单元中读取中断处理程序的入口,设置IP和CS。
中断处理程序和iret指令
中断处理程序的编写方法和子程序比较相似
- 保存用的的寄存器
- 处理中断
- 恢复用到的寄存器
- 用iret指令返回
iret
实现了下面的功能
pop IP
pop CS
popf
单步中断
CPU执行完一条指令后,如果检测到标志寄存器的TF位为1,则产生单步中断,引发中断过程。单步中断的类型码为1.则中断过程如下。
- 取得中断类型码1
- 标志寄存器入栈,TF、IF设置为0
- CS、IP入栈
- IP=14,CS=14+2
Debug程序的T命令就是通过这个实现的。使用t命令执行指令时,Debug将TF设置为1。这条指令执行完后,引发单步中断,执行单步中断处理程序。
CPU提供单步中断功能的原因就是,为单步跟踪程序的执行过程,提供了实现机制。
一些情况下即使发生中断,也不会响应。 例如当执行完想ss寄存器传送数据的指令后,即便是发生中断,CPU也不会响应。这样做的主要原因是,ss:sp联合指向栈顶,而对它们的设置应该连续完成。如果在执行完设置ss的指令后,CPU响应中断,引发中断过程,要在栈中压入标志寄存器、CS和IP的值。而ss改变,sp并未改变,ss:sp指向的不是正确的栈顶,将引起错误。 我们应该利用这个特性,将设置ss和sp的指令连续存放,使得设置sp的指令紧接着设置ss的指令执行,而在此之间CPU不会引发中断过程。
如:
//将栈顶设置为1000:0
mov ax,1000h
mov ss,ax
mov sp,0
而不应该
mov ax,1000h
mov ss,ax
mov ax,0
mov sp,0
int指令
格式:int n
。n为中断类型码
可以在程序中使用int 指令调用任何一个中断的中断处理程序。
int
配合iret
配合使用与call
指令和ret
指令的配合使用有相似的思路。
BIOS和DOS中断例程的安装过程
- 开机后,CPU加电,初始化CS=0FFFFH,IP=0,自动从FFFF:0单元开始执行程序。FFFF:0处有一条跳转指令,CPU执行该指令后,转去执行BIOS中的硬件系统检测和初始化程序。
- 初始化程序将建立BIOS所支持的中断向量,即将BIOS提供的中断例程的入口地址登记在中断向量表中。
- 硬件系统检测和初始化完成后,调用int 19h进行操作系统的引导,从此将计算机交由操作系统控制。
- DOS启动后,除完成其他工作外,还将它所提供的中断例程转入内存,并建立相应的中断向量。
BIOS中断例程的应用
int 10h 中断例程是BIOS提供的中断例程,其中包含了多个和屏幕输出相关的程序。
设置光标位置
mov ah,2 //设置用int 10h中断例程里面的2号子程序
mov bh,0 //第0页
mov dh,5 //行号
mov dl,12 //列号
int 10h
在光标位置显示字符
mov ah,9 //9号子程序,在光标位置显示字符
mov al,'a' //要显示的字符
mov bl,7 //颜色
mov bh,0 //第0页
mov cx,3 //字符重复个数
int 10h //
bl中颜色的格式如下
7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|
BL | R | G | B | I | R | G | B |
闪烁 | 背景 | 背景 | 背景 | 高亮 | 前景 | 前景 | 前景 |
若想显示红的高亮将bl设置为11001010b
就可以了。
DOS中断例程应用
int 21h就是DOS提供的中断例程。
mov ah,4ch //表示调用4ch号子程序
mov al,0 //返回值
int 21h
int 21h在光标位置显示字符串的功能
ds:dx 指向字符串 //要显示的字符串用$作为结束符
mov ah,9 //使用第9号子程序
int 21h
在屏幕5行12列显示字符串
assume cs:code
data segment
db 'Welcome to masm','$'
data ends
code segment
start:mov ah,2 //选择设置光标功能
mov bh,0 //设置页数
mov dh,5 //行数
mov dl,12 //列数
int 10h //执行,移动光标
mov ax,data //设置数据
mov ds,ax
mov dx,0 //ds:dx指向字符串的首地址data:0
mov ah,9 //选择9号功能,显示字符串
int 21h //执行
mov ax,4c00h//相当于设置mov ah,4ch
int 21h
code ends
end start
端口
在PC机系统中,和CPU通过总线相连的芯片除了各种存储器外,还有以下3种芯片
- 各种借口卡(网卡,显卡)上的接口芯片,它们控制接口卡进行工作。
- 主板上的接口芯片,CPU通过它们对部分外设进行访问。
- 其他芯片,用来存储相关的系统信息,或进行相关的输入输出处理。
这些芯片中,都有一组可以有CPU读写的寄存器。这些寄存器,它们在物理上可能处于不同的芯片中,但它们在一下两点上相同。
- 都和CPU的总线相连,当然这种连接是通过它们所在的芯片进行的。
- CPU对它们进行读或写的时候都通过控制线向它们所在的芯片发出端口读写命令。
可见,从CPU的角度,将这些寄存器都当做端口,对它们进行统一编址,从而建立了一个统一的端口地址空间。每一个端口在地址空间中都有一个地址。
CPU可以直接读写一下3个地方的数据
- CPU内部寄存器
- 内存单元
- 端口
端口的读写
CPU通过端口地址来定位端口,因为端口所在的芯片和CPU通过总线相连,所以,端口地址和内存地址一样,通过地址总线来传送。在PC系统中,CPU最多可以定位64KB个不同的端口。则端口的地址范围为0~65535.
对端口的读写不能用mov、push、pop
等内存读写指令。
端口的读写指令只有两条in
和out
,分别用于从端口读取数据和往端口写入数据。
访问端口:
in al,60h //从60h号端口读入一个字节
CPU通过地址线将地址信息60h发出; CPU通过控制线发出端口读命令,选中端口所在的芯片,并通知它,将要从中读取数据; 端口所在芯片将60h端口中的数据通过数据线送入CPU
在in
和out
指令中,只能使用ax
或al
来存放从端口中读取或要发送到端口中的数据。
访问8位端口时用al,访问16位端口用ax。
CMOS RAM芯片
PC机中,有一个CMOS RAM
芯片,一般简称CMOS
。芯片特性如下
- 包含一个实时钟和一个有128个存储单元的RAM存储器。
- 该芯片靠电池供电,关机后内部实时钟仍可正常工作,RAM中的信息不丢失。
- 128个字节的RAM中,内部实时钟占用0~0dh单元来保存时间信息,其余大部分单元用于保存系统配置信息,供系统启动时BIOS程序读取。BIOS页也提供了相关的程序,使我们可以在开始的时候配置
CMOS RAM
中的系统信息。 - 该芯片内部有两个端口,端口地址为70h和71h。CPU通过这两个端口来读写
CMOS RAM
。 - 70h为地址端口,存放要访问的
CMOS RAM
单元的地址;71h位数据端口,存放从选定的CMOS RAM
单元读取的数据,或要写入到其中的数据.CPU对CMOS RAM
的读写分两步进行。1.将2送入端口70h; 2.从端口71h读出2号单元的内容
。
shl和shr指令
shl和shr是逻辑位移指令。
shl:
- 将一个寄存器或内存单元中的数据向左移位
- 将最后移出的移位写入CF中
- 最低位补0
mov al,01001000b
shl al,1 //将al中的数据左移一位
//执行后al=10010000b,CF=0
CMOS RAM中存储的时间信息
在CMOS RAM中,存放着当前的时间:年、月、日、时、分、秒。这6个信息长度都为1字节,存放单元为: 秒0 分2 时4 日7 月8 年9
这些数据以BCD码方式存放。一个字节可以表示两个BCD码,高4位BCD码表示十位,低4位BCD码表示各位。 例:00010100b表示14
//读取月份
mov al,8 //设置要读取的单元
out 70h,al //要读取的单元地址传给CMOS
in al,71h //从数据端口71h中取得指定单元的数据
外中断
CPU除了能执行指令,进行运算外,还应该能够对外部设备进行控制,接受它们的输入,向它们输出。I/O能力。
外设的输入不直接送入内存和CPU,而是送入相关的接口芯片的端口中。CPU向外设的输出也不是直接送入外设。 CPU还可以向外设输出控制命令,也都是先送到相关芯片的端口中,在由相关芯片根据命令对外设实施控制。
CPU通过端口和外部设备进行联系
当外设的输入到达,相关芯片将向CPU发出相应的中断信息。CPU执行完当前指令后,检测到发送过来的中断信息,引发中断过程,处理外设的输入。
在PC系统中,外中断源一共有一下两类:
1.可屏蔽中断 可屏蔽中断是CPU可以不响应的外中断。CPU是否响应可屏蔽中断,要看标志寄存器的IF位的设置,IF=1,CPU执行完当前指令后响应中断,IF=0,不响应。 前面讲到的内中断中有一步是IF=0,TF=0,这里就能开出作用了,内中断时不响应可屏蔽中断。
8086提供了设置IF的指令 sti,设置IF=1。cli,设置IF=0。
2.不可屏蔽中断 不可屏蔽中断是CPU必须响应的外中断。当CPU检测到后,当前指令执行完,立即响应。 8086的不可屏蔽中断的中断类型码固定为2,所以中断过程中,不需要取中断类型码。中断过程如下:
- 标志寄存器入栈,设置IF=0,TF=0
- CS、IP入栈
- IP=0,CS=0AH
几乎所有由外设引发的外中断,都是可屏蔽中断。 不可屏蔽中断是系统中有必须处理的紧急情况发生时用来通知CPU的中断信息。
PC机键盘的处理过程
键盘输入
键盘上的每一个键相当于一个开关,键盘中有一个芯片对键盘上的每一个键的开关状态进行扫描。按下一个键时,开关接通,该芯片就产生一个扫描码,扫描码说明了键在键盘的位置,扫描码被送入主板上的相关接口芯片的寄存器中,端口为60h。 松开按键时,也产生一个扫描码。说明了松开按键的位置,页送入60h端口中。
一般讲按下产生的扫描码称为通码,松开时产生的称为断码。扫描码长度为一个字节,通码的第7位为0,断码第7位为1。 断码=通码+80h
引发9号中断
当键盘的输入到达60h端口时,相关芯片就会向CPU发出中断类型码为9的可屏蔽中断信息。CPU检测到该中断信息后,如果IF=1,则响应中断。执行int 9的中断例程。
执行9号中断
BIOS提供了int 9中断例程,用来进行基本的键盘输入处理。
1.读出60h端口中的扫描码
2.如果是字符键的扫描码,将该扫描码和它所对应的字符码(ASCII码)送入内存中的BIOS键盘缓存区;如果是控制键(如Ctrl)和切换键的扫描码,则将其转变为状态字节,写入内存中存储状态的字节单元。
3.对键盘系统进行相关的控制,比如说,向相关芯片发出应答信息。
BIOS键盘缓冲区是系统启动后,BIOS用于存放int 9中断例程所接收的键盘输入的内存区。该内存区可以储存15个键盘输入。因为int 9中断例程除了接收扫描码外,还要产生和扫描码对应的字符码,所以在BIOS的键盘缓冲区中,一个键盘输入用一个字单元存放,高位字节存放扫描码,低位字节存放字符码。
0040:17单元存储键盘状态字节,该字节记录了控制键和切换键的状态。键盘状态字节各位记录的信息如下。
第几字节 | 置1表示 |
---|---|
0 | 右shift |
1 | 左shift |
2 | Ctrl |
3 | Alt |
4 | Scroll灯亮 |
5 | 小键盘输入的是数字 |
6 | 输入大写字母 |
7 | 删除态 |
直接定址表
前面有用标号来标记指令、数据、段的起始地址的用法。这些标号仅仅表示了内存单元的地址。
还要一种标号,不但表示内存单元的地址,还表示内存单元的长度,即表示在此标号处的单元,是一个字节单元,还是字单元,还是双字单元。
如:
assume cs:code
code segment
//这里的标号后面没有":",它们是同时描述内存地址和单元长度的标号
a db 1,2,3,4,5,6,7,8 //a描述了地址code:0,以后的内存单元都是字节单元
b dw 0 //b描述了地址code:8,以后的内存单元都是字单元
start:mov si,0
mov cx,8
s:mov al,a[si]
mov ah,0
add b,ax
inc si
loop s
mov ax,4c00h
int 21h
code ends
end start
这样指定之后,下面的指令中,标号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]
在其他段中使用数据标号
一般来说,我们不在代码段中定义数据,而是将数据定义到其他段中,在其他段中,我们也可以使用数据标号来描述储存数据的单元的地址和长度。 在后面加油”:”的地址标号,只能在代码段中使用,不能在其他段中使用。
assume cs:code,ds:data //把data和ds寄存器联系起来
data segment
a db 1,2,3,4,5,6,7,8
b dw 0
c dw a,b //可以将标号当作数据来定义,标号所表示的地址当作数据的值。
//相当于c dw offset a,seg a,offset b,seg b。seg操作符能取得标号的段地址
data ends
code segment
start: mov ax,data //先要把ds设置为data段的段地址
mov ds,ax
mov si,0
mov cx,8
s: mov al,a[si] //相当于mov al,[si+0]
mov ah,0
add b,ax //相当于add [8],ax
inc si
loop s
mov ax,4c00h
int 21h
code ends
end start
程序的入口地址也能用直接定址表存放
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 //计算子程序的便宜地址
call word ptr table[bx] //调用对应的子程序
sret: pop bx
ret
运用前面的知识还能改成下面这样
setscreen: cmp ah,0
je do1
cmp ah,1
je do2
cmp ah,2
je do3
cmp ah,3
je do4
jmp short sret
do1: call sub1
jmp short sret
do2: call sub2
jmp short sret
do3: call sub3
jmp short sret
do4: call sub4
sret: ret