重温汇编语言(16位CPU)
书籍推荐与习题
- 汇编语言(第3版)王爽
- 链接:https://pan.baidu.com/s/1uR_XNaPXZWt3mm2ttzheNA
- 提取码:z2o7
- 练习
- 链接:https://pan.baidu.com/s/1F_X-sfQ9Gjf3mSZg4hH4jw
- 提取码:glzl
- 环境配置
- 链接:https://pan.baidu.com/s/1xn7bob0_dXGjqVVBebxmag
- 提取码:kwfi
- 参考博客
基础知识
- 汇编语言的组成
- 汇编指令。计算机唯一能识别的只有机器码,汇编指令与机器码一一对应。
- 伪指令。没有对应的机器码,由编译器执行。
- 其他符号,如+,-,*,/。没有对应机器码,由编译器识别。
- CPU与内存如何交互?
注
:
总
线
的
宽
度
就
是
总
线
的
个
数
\color{red}{注:总线的宽度就是总线的个数}
注:总线的宽度就是总线的个数
- 地址总线:CPU通过地址总线告诉内存自己要访问哪个存储单元。
- 地址总线的宽度决定了CPU的寻址能力
- 控制总线:CPU通过控制总线告诉内存自己要读还是要写。
- 控制总线的宽度决定了CPU对其他器件的控制能力
- 数据总线:传送数据。
- 数据总线决定了CPU与外界的数据传送能力
- 地址总线:CPU通过地址总线告诉内存自己要访问哪个存储单元。
- CPU如何控制外设?
- CPU通过总线向接口卡发送命令,接口卡根据CPU的命令控制外设工作
寄存器
8086CPU简介
-
CPU的组成
- 运算器
- 寄存器
- 控制器
- 内部总线
-
寄存器
- 8086CPU的所有寄存器都是16位的。
-
16位结构CPU(8086CPU)的特性
- 运算器一次最多可以处理16位数据
- 寄存器的最大宽度为16位
- 寄存器和运算器之间的通路为16位
-
16位结构的等价说法
- 16位机
- 字长为16位
-
8086CPU有20根地址总线,而8086CPU又是16位结构,那么如何给出内存单元的物理地址呢?
- 8086CPU采用在内部用两个16位地址合成的方法来形成一个20位的物理地址
- 具体讲,就是地址加法器采用物理地址=段地址*16+偏移地址
- 假设8086CPU要访问123C8h的内存单元,那么在CPU内部就会进行此运算1230h*16+00c8h=123c8h
-
内存真的被分段了吗?
- 内存并没有分段
- 段的划分来自于CPU
-
段的概念
- 段的起始地址=段地址*16
- 段地址*16必然是16的倍数,所以段的起始地址必然是16的倍数
- 偏移地址为16位,16位地址寻址能力为64KB,所以段的最大长度为64KB
- CPU可以用不同的段地址和偏移地址形成同一个物理地址
CS和IP
-
段寄存器
- 段寄存器用于存放段地址
- 8086CPU有4个段寄存器
- CS
- DS
- SS
- ES
-
CS和IP
- CS为代码段寄存器,存放指令的段地址。
- IP为指令指针寄存器,存放指令的偏移地址。即CPU使用(CS)*16+(IP)合成指令的物理地址。
- 也就是说,CS和IP共同指示了CPU要读取指令的地址。
-
8086CPU指令执行的过程总结
- 1 从CS:IP指向的内存单元读取指令,读取的指令进入指令缓冲器;
- 2 IP=IP+所读取指令的长度,从而指向下一条指令;
- 3 执行指令。转到步骤1,重复这个过程。
-
那么,CPU启动后执行的第一条指令会是什么呢?
- 8086CPU中,CPU刚启动时,CS被设置为FFFFh,IP被设置为0000h,因此FFFF0h单元中的指令是开机后执行的第一条指令。
-
如何修改CS和IP的值呢?
- mov指令不能实现修改
- jmp指令可以实现修改
- jmp 2AE3:3,执行后,CS=2AE3h,IP=0003h,CPU从2AE33h处读取指令
- jmp ax,可以想象为mov IP,ax
-
再问自己一次,CPU如何知道当前要执行的指令所在的位置?
- 任意时刻,CS:IP指向当前要执行指令的地址。CS存放指令的段地址,IP存放指令的偏移地址。
DS和[address]
- 字如何存储呢?
- 在CPU中,16位寄存器存储一个字。高8位存储高位字节,低8位存储低位字节。
- 在内存中,字的低位字节存放在低地址单元,高位字节存储在高地址单元
- DS和[address]
- DS为数据段寄存器,存放数据的段地址。
mov bx,100h
mov ds,bx
//8086CPU自动取ds中的数据为内存单元的段地址,从而构造内存单元的物理地址
mov al,[0]
SS和SP
- 栈
- 在8086CPU中,可以将一段内存当做栈来使用。
- 在8086CPU中,入栈和出栈都是以字为单位进行的。
- SS和SP
- SS为栈段寄存器,存放栈顶的段地址。SP存放栈顶的偏移地址。
- CPU如何知道栈顶的位置呢?
- 任意时刻,SS:SP指向栈顶元素。SS存放栈顶的段地址,SP存放栈顶的偏移地址。
- push与pop的使用
//入栈的执行流程
push ax
1 SP=SP-2
2 向SS:SP指向的字单元中送入数据
//出栈的执行流程
pop ax
1 从SS:SP指向的字单元中读取数据
2 SP=SP+2
- 当栈为空的时候,没有栈顶元素,SS:SP指向哪里?
- SS:SP只能指向栈最底部字单元的偏移地址+2处的单元
- 栈顶超界问题
- 我们希望CPU有记录栈顶上限和栈底的寄存器,在我们push和pop自动检测,防止超界,然而并没有。
- 必须自己关注栈顶超界问题
- push和pop与mov的区别
- push和pop都需要进行两步操作
- push:改变SP,传送数据到SS:SP
- pop:读取SS:SP处数据,改变SP
- push和pop修改的只是SP,栈顶变化范围0~FFFFh
- mov只需要一步操作
- 传送数据
- 当然数据的传送就是在寄存器与内存单元之间进行,即CPU和内存之间
- push和pop都需要进行两步操作
- 一段内存,可以既是代码段,又是数据段,还是栈段,也可以什么都不是,关键在于CPU中寄存器的设置,即CS,IP,SS,SP,DS的指向。
第一个汇编程序
- 第一个汇编程序
/*伪指令由编译器执行*/
assume cs:codesg //assume伪指令,将代码段寄存器cs与段codesg关联起来,表示codesg是一个代码段
codesg segment //segment...ends伪指令,定义了一个段
//codesg,段名,也称作标号,此处指代一个段的段地址。
mov ax, 0123H
mov bx, 0456H
add ax, bx
add ax, ax
mov ax, 4c00H
int 21H //程序返回
codesg ends
end //end伪指令,表示汇编程序结束
-
程序的生命周期
- masm 1.asm编译
- link 1.obj链接
- 1.exe执行
-
DOS系统中.exe文件中程序被加载到内存的过程
-
程序的运行
- 在DOS中运行程序时,是command将程序加载到内存,程序运行结束返回到command中。
- 在debug中将程序加载入内存,程序运行结束返回debug中。
- 使用Q命令退出debug,将返回到command中。因为debug是command加载运行的。
- 在debug的时候,要使用P命令执行int 21,执行结束后,显示“Program terminated normally”,程序返回到debug中,表示程序正常结束。
[BX]和loop指令
- [BX]和loop指令
- [BX]表示什么?
- 表示一个内存单元,段地址在DS中,偏移地址在寄存器BX中
- loop指令的格式
- loop 标号
- 使用loop实现循环,cx存放循环次数
- CPU执行loop执行时,进行两步操作
- (cx)=(cx)-1
- 判断cx中的值,不为0则转至标号处执行,如果为0则向下执行
- [BX]表示什么?
- 计算2^12
assume cs:code
code segment
mov ax, 2
mov cx, 11 //cx控制循环次数
s: add ax, ax //s为标号,标号代表一个地址
loop s //可以理解为C语言中的do...while
mov ax,4c00h
int 21h
code ends
end
- 在汇编程序中,数据不能以字母开头
- debug和汇编源程序中的两种写法(段前缀)
//debug中
mov ax,0
mov bx,100h
mov ds,bx
mov al,[0]
mov al,[ax]
//汇编源程序中
mov ax,0
mov bx,100h
mov ds,bx
mov al,ds:[0] //如果是常量,就必须显式指明段地址所在段寄存器,ds:或者cs:等也叫段前缀
mov al,ds:[ax] //如果是寄存器,就不需要显式指明了。不过,建议还是加上段前缀,这样可以使得代码可读性强。
- 计算ffff:0 ~ ffff:b单元中的数据的和,结果存储在dx中
- 方案一:(dx)=(dx)+内存中的8位数据
- 显然8位和16位不能相加
- 可以采取,将8位转化为16位,再与16位相加
- 方案二:(dl)=(dl)+内存中的8位数据
- 显然不成立,可能超界
- 没有办法采取
- 方案一:(dx)=(dx)+内存中的8位数据
assume cs:code
code segment
mov ax, 0ffffh
mov ds, ax
mov bx, 0
mov dx, 0
mov cx, 12
s: mov al, [bx]
mov ah, 0
add dx, ax
inc bx
loop s
mov ax, 4c00h
int 21h
code ends
end
- 将内存ffff:0~ffff:b单元中的数据复制到0:200 ~ 0:20b单元中
- 0:2000:20b和0020:00020:b对应同一段内存空间
- 这样一转换,代码量一下子就少了很多
- 只需要控制两个段地址就行了
- 0:2000:20b和0020:00020:b对应同一段内存空间
assume cs:code
code segment
mov ax, 0ffffh
mov ds, ax
mov ax, 0020h
mov es, ax
mov bx, 0
mov dx, 0
mov cx, 12
s: mov dl, ds:[bx]
mov es:[bx], dl
inc bx
loop s
mov ax, 4c00h
int 21h
code ends
end
一个包含多个段的汇编程序
/*实现的功能:数据逆序存放*/
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,0,0,0,0,0,0,0,0
stack ends
//代码段
code segment
start: mov ax, stack //start,标号,为程序的入口地址
mov ss, ax
mov sp, 20h
mov ax, data
mov ds, ax
mov bx, 0
mov cx, 8
s: push [bx]
add bx, 2
loop s
mov bx, 0
mov cx, 8
s0: pop [bx]
add bx, 2
loop s0
mov ax, 4c00h
int 21h
code ends
end start
/*
end的两大关键作用
1 通知编译器程序结束
2 通知编译器程序的入口在什么地方(隐含着在程序编译、链接之后,CS默认被设置为start)
*/
更灵活的定位内存地址的方法
字符串如何存储以及大小写转换
- 字符串如何存储?
- 'f’的ASCII码值为66H,同理’o’为6FH,'R’为52H,'K’为4BH
db 'foRK'
等价于db '66H,6FH,52H,4BH'
,故内存地址由低到高存储为66H 6FH 52H 4BH
- 将字符串中小写字母转为大写字母
/*
如何区分大写字母与小写字母?
-- 小写字母的ASCII码值比大写字母大20H
-- 大写字母ASCII码值的二进制第5位为0(从右往左数),小写字母为一
*/
/*
如何实现小写字母转大写字母呢?
-- 只需要将小写字母ASCII码值第5位由1变0,大写字母由0变0即可
-- 使用and,or可以实现二进制的与,或。当然这里我们是用and al,11011111B
*/
assume cs:codesg,ds:datasg
datasg segment
db 'BaSiC'
db 'iNfOrMaTion'
datasg end
codesg segment
start: mov ax, datasg
mov ds, ax
mov bx, 0
mov cx, 5
s: mov al, [bx]
and al, 11011111B
mov [bx], al
inc bx
loop s
mov bx, 5
mov cx, 11
s0: mov al, [bx]
or al, 00100000B
mov [bx], al
inc bx
loop s0
mov ax, 4c00h
int 21h
codesg ends
end start
不同寻址方式
- [idata]
- 使用一个常量表示地址,直接定位一个内存单元
- [bx]
- 使用一个变量表示地址,间接定位一个内存单元
- [bx+idata]
- 使用一个变量和一个常量表示地址
- 可以应用到一维数组
/*将第一个字符串转化为大写,第二个字符串转化为小写*/
/*
-- 在C语言中我们定义数组:char a[]="hello";
a存放的是字符h的地址,可以使用a[0]来访问字符h
-- 在汇编语言中我们定义数组:db 'BaSiC'
0存放的就是B的地址,可以使用0[0]来访问字符B
所以下面代码中的[bx]实际就是0[bx],[bx+5]实际就是5[bx]
*/
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
s: mov al, [bx]
and al, 11011111b
mov [bx], al
mov al, [bx+5]
or al, 00100000b
mov [bx+5], al
inc bx
loop s
mov ax, 4c00h
int 21h
codesg ends
end start
- [bx+si]
- 使用两个变量表示地址
- si和di不能被分为两个8位寄存器使用
- si和di与bx功能相近
- [bx+si+idata]
- 使用两个变量一个常量表示地址
二重循环问题的处理
/*将每个单词改为大写字母*/
/*
方案一
--四个一维数组,四次循环即可。
--显然这样代码量太大
方案二
--看成4*16的二维数组
--可行
*/
/*
实现二维数组的方法
--外层循环次数压栈
--进行内层循环
--内层循环结束
--外层循环次数出栈
*/
assume cs:codesg,ds:datasg,ss:stacksg
datasg segment
db 'ibm '
db 'dec '
db 'dos '
db 'vax '
datasg ends
stacksg segment
dw 0, 0, 0, 0, 0, 0, 0, 0
stacksg ends
codesg segment
start: mov ax, stacksg
mov ss, ax
mov sp, 16
mov ax, datasg
mov ds, ax
mov bx, 0
mov cx, 4 //外层循环控制数组个数
s0: push cx //外层循环次数压栈
mov si, 0
mov cx, 3 //内层循环控制每个数组的元素个数
s: mov al, [bx+si]
and al, 11011111b
mov [bx+si], al
inc si
loop s
add bx, 16 //切换到第二个一维数组的起始地址
pop cx //外层循环次数出栈
loop s0
mov ax,4c00H
int 21H
codesg ends
end start
数据处理的两个基本问题
处理的数据在什么地方?
-
内存寻址
- 只有bx,si,di,bp这四个寄存器可以用于内存寻址,也就是说可以写在[]中
- 可以单独出现,也可以以以下四种组合出现,bx和si,bx和di,bp和si,bp和di,当然可以有idata加持
- 只要[]中出现bp,段地址默认在ss中
-
机器指令处理的数据在什么地方呢?
- 可以在CPU内部,内存,端口
- 可以在CPU内部,内存,端口
-
汇编语言中数据位置的表达
- 立即数idata
mov ax,1
- 1执行前在指令缓冲器里
- 寄存器
mov bx,ax
- 段地址和偏移地址
mov ax,[bx]
- 段地址默认在ds中
- 立即数idata
寻址方式
- 什么是寻址方式?
- 定位内存单元的方式
- 定位内存单元的方式
要处理的数据有多长?
- 通过寄存器名指明要处理的数据长度
mov al,[0]
处理一个字节数据mov ax,[0]
处理一个字数据
- 通过操作符指定
mov byte ptr [0],1
处理一个字节数据mov word ptr [0],1
处理一个字数据
- 默认处理长度
- push和pop指令只进行字操作
汇编语言与C语言的对比
-----------------------修改结构体中的值------------------------
/*C语言*/
struct company
{
char cn[3];
char hn[9];
int pm;
int sr;
char cp[3];
};
int main()
{
struct company dec = {"DEC", "Ken Olsen", 137, 40, "PDP"};
int i;
dec.pm = 38;
dec.sr = dec.sr + 70;
i = 0;
dec.cp[i] = 'V';
i++;
dec.cp[i] = 'A';
i++;
dec.cp[i] = 'X';
return 0;
}
/*汇编语言*/
//建议参考上面的图片阅读,自己对比,理解C语言的底层实现
mov ax, seg
mov ds, ax
mov bx, 60h
mov word ptr [bx+0ch], 38
//mov word ptr [bx].0ch, 38 //dec.pm = 38
add word ptr [bx+0eh], 70
mov si, 0
mov byte ptr [bx+10h+si], 'V'
//mov byte ptr [bx].10h[si], 'V' //dec.cp[i] = 'V';
inc si
mov byte ptr [bx+10h+si], 'A'
inc si
mov byte ptr [bx+10h+si], 'X'
div指令和mul指令
- div指令
/*利用除法指令计算100001/100*/
//10001>65535,16位除法
mov dx, 1
mov ax, 86A1H
mov bx, 100
div bx
/*利用除法指令计算1001/100*/
//1001<65535,8位除法
mov ax, 1001
mov bl, 100
div b1
- mul指令
- 被乘数与乘数
- 要么都是8位,要么都是16位
- 如果是8位乘法的话,一个默认放在AL中
- 如果是16位乘法的话,一个默认放在AX中
- 乘积
- 如果是8位乘法,默认存放在AX中
- 如果是16位乘法,高位默认存放在DX中,低位默认存放在AX中
- 被乘数与乘数
/*计算100*10*/
//100和10小于255,8位乘法
mov al,100
mov bl,10
mul bl
/*计算100*10000*/
//100小于255,10000大于255,16位乘法
mov ax,100
mov bx,10000
mul bx
- 伪指令dd
- db定义字节型数据,8位
- dw定义字型数据,16位
- dd定义双字型数据,32位
- dup操作符
- 由编译器识别
- 进行数据的重复
db 3 dup (0, 1, 2)
- 定义了9个字节
- 相当于db 0,1,2,0,1,2,0,1,2
转移指令
- 转移行为的分类
- 段内转移。只修改IP。jmp ax。
- 短转移。IP修改范围-128~127
- 近转移。IP修改范围-32768~32767
- 段间转移。同时修改CS和IP。jmp 1000:0。
- 段内转移。只修改IP。jmp ax。
- 转移指令的分类
- 无条件转移jmp
- 条件转移
- 循环指令loop
- 过程
- 中断
- offset操作符与nop指令
- offset由编译器识别,主要功能是取得标号的偏移地址
- nop指令,也叫空指令,什么都不做,nop的机器码占一个字节
- jmp指令要给出的两种信息
- 转移的目的地址
- 转移的距离
依据位移进行转移的jmp指令
- CPU依据位移进行跳转
/*汇编语言*/
assume cs:codesg
codesg segment
start:mov ax,0
jmp short s
add ax,1
s:inc ax
codesg ends
end start
/*机器码*/
0BBD:0000 B80000 MOV AX,0000
0BBD:0003 EB03 JMP 0008
0BBD:0005 050100 ADD AX,0001
0BBD:0008 40 INC AX
/*
可以看出,不论idata是一个数据还是一个内存单元偏移地址,它都会对 应机器指令中给出。
但是,jmp 0008中的idata却没有在机器码中给出。
那CPU根据什么进行转移的呢?根据机器码,可以看出03其实是位移,CPU 是依据位移进行转移的。
*/
-
CPU根据位移进行转移有什么意义呢?
- 程序装在内存中的不同位置都可正确执行
-
段内短转移
jmp short 标号
- 功能
(IP)=(IP)+8位位移
- 8位位移=标号处的偏移地址-jmp指令下一条指令的偏移地址
- 注意
- short指明此处的位移是8位位移
- 8位位移-128~127,用补码表示
- 8位位移由编译程序在编译时算出
- 功能
-
段内近转移
jmp near ptr 标号
- 功能
(IP)=(IP)+16位位移
- 16位位移=标号处的偏移地址-jmp指令下一条指令的偏移地址
- 注意
- near ptr指明此处的位移是16位位移
- 16位位移-32768~32767,用补码表示
- 16位位移由编译程序在编译时算出
- 功能
转移的目的地址在指令中的jmp指令
- CPU依据目的地址进行跳转
/*汇编语言*/
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
/*机器码*/
0BBD:0000 B80000 MOV AX,0000
0BBD:0003 B80000 MOV BX,0000
0BBD:0006 EA 0B01BD0B JMP 0BBD:010B
/*
可以看出,jmp far ptr s对应的机器码,包含转移的目的地址。
高地址是段地址,低地址是偏移地址
*/
- 段间转移,也叫远转移
jmp far ptr 标号
- 功能
- (CS)=标号所在段的段地址,(IP)=标号所在段的偏移地址
- far ptr指明了指令用标号的段地址和偏移地址修改CS和IP
- 功能
转移地址在寄存器中的jmp指令
jmp ax
- 功能
(IP)=ax
- 功能
转移地址在内存中的jmp指令
- 段内转移
jmp word ptr 内存单元地址
- 内存单元存放着一个字,是转移的目的偏移地址
- 段间转移
jmp dword ptr 内存单元地址
- 内存单元存放着两个字,高地址是转移的目的段地址,低地址是转换的目的偏移地址
条件转移指令jcxz
- 所有的条件转移指令都是短转移
jcxz 标号
- 功能
- 如果cx等于0,
jmp short 标号
- 如果cx不等于0,什么也不做,程序继续向下执行
- 如果cx等于0,
- 功能
循环指令loop
- 所有的循环指令都是短转移
loop 标号
- 功能
(cx)=(cx)-1
- 如果cx不等于0,
jmp short 标号
;如果cx等于0,什么也不做,程序继续向下执行
- 功能
过程(程序的模块化开发)
-
ret与retf指令
- ret指令用栈中数据,修改IP,实现近转移
- 相当于
pop IP
- 相当于
- retf指令用栈中数据,修改CS和IP,实现远转移
- 相当于先
pop IP
然后pop CS
- 相当于先
- ret指令用栈中数据,修改IP,实现近转移
-
call指令
- CPU执行call指令时,进行两步操作
- 将当前IP或CS和IP压入栈
- 转移
- call指令不能实现短转移
- 依据位移进行转移的call指令
call 标号
- 功能相当于
push IP
jmp near ptr 标号
- 功能相当于
- 转移的目的地址在指令中的call指令
call far ptr 标号
- 功能相当于
push CS
push IP
jmp far ptr 标号
- 功能相当于
- 转移地址在寄存器中的call指令
call ax
- 功能相当于
push IP
jmp ax
- 功能相当于
- 转移地址在内存中的call指令
call word ptr 内存单元地址
- 功能相当于
push IP
jmp word ptr 内存单元地址
- 功能相当于
call dword ptr 内存单元地址
- 功能相当于
push CS
push IP
jmp dword ptr 内存单元地址
- 功能相当于
- CPU执行call指令时,进行两步操作
-
call和ret的简单配合:(bx)=?
//当然是(bx)=8
assume cs:code
code segment
start: mov ax,1
mov cx,3
call s //CPU缓冲寄存器存放call指令,IP指向mov bx,ax指令,执行call指令,IP压栈,然后将IP的值改变为标号s的偏移地址
mov bx,ax
mov ax,4c00h
int 21h
s: add ax,ax
loop s //loop循环结束,(ax)=8
ret //(IP)等于栈中元素,即语句mov bx,ax的偏移地址
code ends
end start
- call和ret配合实现高级语言中的函数调用案例
/*将字符串转化为大写*/
//参数(返回值)可以使用寄存器传递,也可以使用栈来传递
assume cs:code
data segment
db 'conversation'
data ends
code segment
main:mov ax,data //主函数
mov ds,ax
mov si,0 //参数一,字符串首地址存放在ds:[0]中
mov cx,12 //参数二,字符串长度存放在cx中
call capital //函数调用
mov ax,4c00h
int 21h
capital:and byte ptr [si],11011111b //子函数
inc si
loop capital
ret //子函数返回
code ends
end main
标志寄存器
-
标志寄存器是按位起作用的
-
零标志位ZF
- 记录相关指令执行后结果是否为0。为0,zf=1;非0,zf=0;
-
奇偶标志位PF
- 记录相关指令执行后结果的所有bit位中1的个数为奇还是偶。为奇,pf=0;为偶,pf=1;
-
符号标志位SF
- 记录有符号数运算结果是否为负(无符号数运算的时候SF的值没有意义)。为负,sf=1;非负,sf=0;
-
进位标志位CF
- 记录了无符号数运算结果的最高有效位向更高位的进位值(加法)或从更高位的借位值(减法)。
-
溢出标志位OF
- 记录了有符号数运算结果是否发生了溢出。有,of=1;无,of=0;
-
adc指令
adc ax,bx
- 带进位加法指令
- 实现的功能是
(ax)=(ax)+(bx)+cf
- 如何设置cf的值为0呢?
sub ax,ax
- 对任意大的数进行加法运算
/*计算1EF000H+201000H*/
//将计算分两步进行,先将低16位相加,然后将高16位和进位值相加。
mov ax, 001EH
mov bx, 0F000H
add bx, 1000H
adc ax, 0020H
- sbb指令
sbb ax,bx
- 带借位减法指令
- 实现的功能是
(ax)=(ax)-(bx)-cf
- 对任意大的数进行减法计算
/*计算003E1000H-00202000H*/
mov bx, 1000H
mov ax, 003EH
sub bx, 2000H
sbb ax, 0020H
- cmp指令
-
cmp ax,bx
- 通过做减法运算,影响标志寄存器,从而得知比较结果
-
对于无符号数运算,关注zf和cf
-
对于有符号数运算,关注zf和sf和of。当然=和≠判断与无符号数相同。
-
cmp指令和条件转移指令搭配使用(相当于C语言的if判断)
- jcxz检测cx的值
- 其余大多数条件转移指令检测cmp影响的标志位的值
-
- 方向标志位DF
- df=0,每次操作后di,si递增
- df=1,每次操作后di,si递减
- 串传送指令
- movsb
- 将ds:si指向的内存单元中的字节送入es:di中,然后根据标志寄存器df位的值,将si和di递增或递减
- movsw
- 将ds:si指向的内存字单元中的字送入es:di中,然后根据标志寄存器df位的值,将si和di递增2或递减2
- rep movsb
- rep的作用是根据cx的值,重复执行后面的串传送指令
- 可以循环实现(cx)个字符的发送
- 如何设置df位的值呢?
- cld
- df=0
- std
- df=1
- cld
- movsb
/*将data段中的第一个字符串复制到它后面的空间中*/
data segment
db 'Welcome to masm!'
db 16 dup (0)
data ends
mov ax, data
mov ds, ax
mov si, 0
mov es, ax
mov di, 16
mov cx, 16
cld
rep movsb
/*
rep movsb相当于
s:movsb
loop s
*/
- pushf和popf指令
pushf
是将标志寄存器的值压栈popf
是从栈中弹出数据,送入标志寄存器- pushf和popf是直接访问标志寄存器的一种方法
内中断
- CPU内部发生什么情况时,会产生内中断?中断源
- 除法错误,比如溢出【0】
- 单步执行 【1】
- 执行into指令 【4】
- 执行int指令 【n】
- CPU如何识别中断信息的来源呢?中断类型码
- 中断类型码是一个字节型数据,可以表示256种中断信息的来源
- CPU如何找到中断处理程序的入口地址呢?
- CPU用8位中断类型码通过中断向量表找到对应的中断处理程序的入口地址
- 中断向量表就是中断处理程序入口地址的列表,保存在内存指定位置处。中断处理程序也一直保存在内存中。
- 在8086CPU中,内存0000:0000-0000:03FF的1024个单元存放着中断向量表。入口地址包括段地址和偏移地址,所以一个表项占两个字。高地址字段存放段地址,低地址字段存放偏移地址,共4*256=1024个单元。
- CPU的中断过程
- 取得中断类型码N
- pushf
- TF=0,IF=0
- push CS
- push IP
- (IP)=(N4),(CS)=(N4+2)
- 中断处理程序编写步骤
- 保存用到的寄存器
- 处理中断
- 恢复用到的寄存器
- 用iret指令返回
- iret指令的功能是什么?
pop IP;pop CS;popf;
- 也就是恢复现场
- iret指令的功能是什么?
- 除法溢出
/*编程处理0号中断:当发生除法溢出时,在屏幕中间显示"overflow!",返回DOS系统*/
/*
1 编写可以显示"overflow!"的中断处理程序:do0
2 将do0送入内存0000:0200处
3 将do0的入口地址0000:0200存储在中断向量表0号表项中
*/
assume cs:code
code segment
start:
//将do0送入内存0000:0200处
mov ax, cs
mov ds, ax
mov si, offset do0
mov ax, 0
mov es, ax
mov di, 200h
mov cx, offset do0end - offset do0
cld
rep movsb
//将do0的入口地址0000:0200存储在中断向量表0号表项中
mov ax, 0
mov es, ax
mov word ptr es:[0*4], 200h
mov word ptr es:[0*4+2], 0
mov ax,4c00h
int 21h
do0: jmp short do0start
db "overflow!"
do0start:
//编写可以显示"overflow!"的中断处理程序:do0
mov ax, cs
mov ds, ax
mov si, 202h
mov ax, 0b800h
mov es, ax
mov di, 12*160+36*2
mov cx, 9
s: mov al, [si]
mov es:[di], al
inc si
add di, 1
mov al, 02h
mov es:[di], al
add di, 1
loop s
mov ax, 4c00h
int 21h
do0end: nop
code ends
end start
- 单步中断
- 什么时候会发生单步中断?
- CPU在执行完一条指令后,如果检测到TF位为1,则会产生单步中断。
- debug中的t命令是如何实现的呢?
- debug提供了单步中断的处理程序,功能为显示所有寄存器的内容后等待输入命令。
- 在使用t命令执行命令时,debug将TF设置为1,使得CPU工作在单步中断方式下,则在CPU执行完这条指令后就引发单步中断,执行单步中断的处理程序,所有寄存器内容显示在屏幕上,并且等待输入命令。
- 那么在CPU执行中断过程的时候为什么将TF设置为0呢?
- 避免在执行中断处理程序时发生单步中断
- CPU为什么要提供单步中断功能?
- 为单步跟踪程序执行过程,提供了实现机制
- 为什么要将设置ss和设置sp的指令连续存放?
- 因为在设置ss以后,CPU不会响应中断。只有设置完sp,CPU才会响应中断。
- 什么时候会发生单步中断?
- int 指令
- 格式:
int n
- 功能:引发中断过程。
- 格式:
/*求2*3456^2*/
assume cs:code
code segment
start:
mov ax, 3456
int 7ch //计算(ax)的平方
add ax, ax
adc dx, dx
mov ax,4c00h
int 21h
code ends
end start
/*安装中断7ch的中断例程*/
assume cs:code
code segment
start:
//将程序安装在0:200处
mov ax,cs
mov ds,ax
mov si,offset sqr
mov ax,0
mov es,ax
mov di,200h
mov cx,offset sqrend - offset sqr
cld
rep movsb
//将程序入口地址保存在7ch表项中
mov ax,0
mov es,ax
mov word ptr es:[7ch*4], 200h
mov word ptr es:[7ch*4+2], 0
mov ax,4c00h
int 21h
//计算平方
sqr:
mul ax //如果是16位乘法,高位默认存放在DX中,低位默认存放在AX中
iret //恢复现场
sqrend:nop
code ends
end start
/*将一个全是字母,以0结尾的字符串,转化为大写*/
assume cs:code
data segment
db 'conversation',0 //0标记着字符串的结束
data ends
code segment
start: mov ax, data
mov ds, ax
mov si, 0
int 7ch //转化为大写
mov ax,4c00h
int 21h
code ends
end start
/*安装中断7ch的中断例程*/
assume cs:code
code segment
start:
mov ax,cs
mov ds,ax
mov si,offset capital
mov ax,0
mov es,ax
mov di,200h
mov cx,offset capitalend - offset capital
cld
rep movsb
mov ax,0
mov es,ax
mov word ptr es:[7ch*4],200h
mov word ptr es:[7ch*4+2],0
mov ax,4c00h
int 21h
capital:
push cx
push si
change:
mov cl,[si]
mov ch,0
jcxz ok //判断是否为0
and byte ptr [si],11011111b
inc si
jmp short change
ok:
pop si
pop cx
iret
capitalend:nop
code ends
end start
- 如何实现loop指令的功能?
/*在屏幕中间显示80个'!'*/
assume cs:code
code segment
start:mov ax,0b800h
mov es,ax
mov di,160*12
mov bx,offset s-offset se
mov cx,80
s:mov byte ptr es:[di],'!'
add di,2 //一个字符在缓冲区占两个字节,分别存放字符的ASCII和属性
int 7ch
se:nop
mov ax,4c00h
int 21h
code ends
end start
/*安装中断7ch的中断例程*/
lp:push bp
mov bp,sp
dec cx
jcxz lpret
add [bp+2],bx
lpret:pop bp
iret
- BIOS和DOS提供的中断处理程序,都用ah来传递内部子程序的编号
- BIOS的int 10H中断处理程序
/*设置光标位置功能*/
mov ah,2 //表示调用第10h号中断例程的2号子程序
mov bh,0 //第0页
mov dh,5 //行号
mov dl,12 //列号
int 10h
/*在光标位置显示字符功能*/
mov ah,9
mov al,'a' //字符
mov bl,7 //颜色属性
mov bh,0 //第0页
mov cx,3 //字符重复个数
int 10h
- 显示缓冲区的结构
/*bh中的页号介绍*/或/*显示缓冲区的结构*/
--内存地址空间中,B8000H-BFFFFH共32KB的空间,为80*25彩色字符模式的显示缓冲区,向这个地址空间写入数据,写入的内容会立即出现在显示器上。
--在80*25彩色字符模式下,显示器可以显示25行,每行80个字符,每个字符可以由256种属性。
--一个字符在缓冲区占两个字节,分别存放字符的ASCII和属性(低位ASCII,高位属性)。一屏的内容在显示缓冲区共占4000个字节。
--显示缓冲区分为8页,每页4KB,约4000字节,显示器默认显示第0页。也就是B8000H-B8F9FH中的4000个字节。
/*bl中的颜色属性介绍*/或/*属性字节的格式*/
7 6 5 4 3 2 1 0
BL R G B I R G B
7为闪烁,4、5、6为背景,3为高亮,0、1 、2为前景
例如:红底高亮闪烁绿色的属性字节为11001010b
闪烁的效果必须在全屏DOS方式下才能看到。
- DOS的int 21H中断处理程序
mov ax,4c00h
int 21h
/*真实含义*/
mov ah,4ch //表示调用第21h号中断例程的4ch号子程序,功能为程序返回
mov al,0 //返回值
int 21h
/*在屏幕的5行12列显示字符串"Welcome to masm!"*/
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
mov ah, 9
int 21h
//程序返回功能
mov ax, 4c00h
int 21h
code ends
end start
端口
- 和CPU通过总线相连的芯片
- 各种存储器
- 各种接口卡(网卡、显卡)上的接口芯片,它们控制接口卡进行工作
- 主板上的接口芯片,CPU通过它们对部分外设进行访问
- 其他芯片,用来存储相关的系统信息或进行相关的输入输出处理
- 端口
- 在这些芯片中,都有一组可以由CPU读写的寄存器
- CPU对这些寄存器进行读写时都是通过控制线向它们所在的芯片发出端口读写命令
- 所以,CPU其实将这些寄存器当做端口,对它们进行统一编址,从而建立一个统一的端口地址空间,每个端口在地址空间有一个地址
- CPU可以直接读写以下三个地方的数据
- CPU内部寄存器
- 内存单元
- 端口
- 端口的读写
//在in和out指令中,只能使用ax,al存放从端口读出或写入端口的数据,访问8位端口用al,访问16位端口用ax
/*对0~255以内的端口进行读写*/
in al, 20h //从20h端口读入一个字节
out 20h, al //往20h端口写入一个字节
/*对256~65535的端口进行读写,端口号放在dx中*/
mov dx, 3f8h //将端口号3f8h送入dx
in al, dx //从3f8h端口读入一个字节
out dx, al //向3f8h端口写入一个字节
- CMOS RAM芯片
- 芯片有两个端口
- 70H为地址端口,存放要访问的CMOS RAM单元的地址
- 71H为数据端口,存放读取或写入的数据
- 芯片有两个端口
/*在屏幕中间显示当前的月份*/
assume cs:code
code segment
start: mov al,8 //访问8号单元
out 70h,al
in al, 71h //取出8号单元数据
mov ah, al //00010100b表示14,4为个位,1为十位
mov cl, 4
shr ah, cl //ah右移4位,ah为月份的十位数码值
and al, 00001111b //al为月份的个位数码值
add ah, 30h //BCD码值+30h=十进制数对应的ASCII
add al, 30h
//屏幕显示
mov bx, 0b800h
mov es, bx
mov byte ptr es:[160*12+40*2], ah
mov byte ptr es:[160*12+40*2+2], al
mov ax,4c00h
int 21h
code ends
end start
- shl指令和shr指令
mov al, 01001000b
shl al, 1 //将a1中的数据左移一位执行后(al)=10010000b,CF=0
mov al, 01010001b
mov cl, 3 //如果移动位数大于1时,必须将移动位数放在cl中
shl al, c1
mov al, 10000001b
shr al, 1 //将al中的数据右移一位执行后(al)=01000000b,CF=1
外中断
- CPU通过端口与外设进行联系
- 外设的输入不直接送入内存和CPU,而是送入相关的接口芯片的端口中
- CPU向外设的输出也不是直接送入外设,而是先送入端口,再由相关芯片送到外设
- CPU发送控制命令给相关芯片的端口,相关芯片再根据命令对外设进行控制
- 外中断源分为两类
- 可屏蔽中断
- CPU可以不响应的外中断
- 当CPU检测到可屏蔽中断信息后,如果IF=1,则CPU执行完当前指令后响应中断;如果IF=0,则不响应中断。
- 可屏蔽中断属于外部中断,中断类型码由数据总线送入CPU;而内中断的中断类型码是在CPU内部产生的。
- 为什么CPU中断过程中将IF置为0?
- 在进入中断处理程序后,禁止其他的可屏蔽中断。
- 如何设置IF的值?
- sti设置IF=1
- cli设置IF=0
- 几乎所有由外设引发的外中断都是可屏蔽中断
- 不可屏蔽中断
- CPU必须响应的中断
- 当CPU检测到不可屏蔽中断信息时,则在执行完当前指令后,立即响应。
- 中断类型码固定为2
- 可屏蔽中断
- 键盘的处理过程
- 键盘输入
- 按下一个键,键盘芯片产生一个扫描码,送入主板上相关接口芯片寄存器中,该寄存器端口地址60H
- 扫描码说明了按键在键盘中的位置
- 按键的扫描码叫通码;松键的扫描码叫断码
- 扫描码长度为一个字节,通码第7位为0,断码第7位为1
- 断码=通码+80H
- 松下一个键,键盘芯片产生一个扫描码,送入主板上相关接口芯片寄存器中,该寄存器端口地址60H
- 按下一个键,键盘芯片产生一个扫描码,送入主板上相关接口芯片寄存器中,该寄存器端口地址60H
- 引发9号中断
- 输入到达60H,相关芯片向CPU发出中断类型码为9的可屏蔽中断。CPU检测到后,如果IF=1,则响应中断,执行int9中断例程(BIOS提供的)
- 执行int9中断例程
- 读出60H端口扫描码
- 如果是字符键的,将该码和它对应字符码送入内存中的BIOS键盘缓冲区。如果是控制键和切换键的,将其转变为状态字节写入内存中的存储状态字节的单元。
- 对键盘系统进行相关控制,比如向相关芯片发出应答信息
- 键盘输入
/*在屏幕中间依次显示“a”~“z”,并可以让人看清。在显示的过程中,按下'Esc'键后,改变显示的颜色*/
/*实则为编写int9中断例程*/
assume cs:code
stack segment
db 128 dup (0)
stack ends
data segment
dw 0,0
data ends
code segment
start:
mov ax,stack
mov ss,ax
mov sp,128
mov ax,data
mov ds,ax
mov ax,0
mov es,ax
//将原来的int 9中断例程入口地址保存在ds:[0]和ds[2]里
push es:[9*4]
pop ds:[0]
push es:[9*4+2]
pop ds:[2]
//在中断向量表中设置新的int 9中断例程入口地址
mov word ptr es:[9*4], offset int9
mov es:[9*4+2], cs
//屏幕中间依次显示字符a-z
mov ax, 0b800h
mov es, ax
mov ah, 'a'
s:
mov es:[160*12+40*2], ah
call delay //调用delay函数进行延时,使得每显示一个字母,可以让人看清
inc ah
cmp ah, 'z'
jna s
//在中断向量表中设置为原来的int 9的中断例程入口地址(恢复操作)
mov ax,0
mov es,ax
push ds:[0]
pop es:[9*4]
push ds;[2]
pop es;[9*4+2]
//程序返回
mov ax,4c00h
int 21h
//延时函数
delay:
push ax
push dx
mov dx, 2000h
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
/*新的int 9中断例程*/
int9:
//将需要改变的寄存器压栈
push ax
push bx
push es
//从端口60H读入用户的键盘输入
in al, 60h
/*模拟执行原int 9中断例程*/
//标志寄存器入栈
pushf
//IF=0,TF=0
pushf
pop bx
and bh,11111100b
push bx
popf
//(IP)=((ds)*16+0),(CS)=((ds)*16+2)
call dword ptr ds:[0]
//看用户输入是否为esc
cmp al,1
jne int9ret
//如果是esc的话,属性值+1,从而改变颜色
mov ax,0b800h
mov es,ax
inc byte ptr es:[160*12+40*2+1]
//寄存器出栈
int9ret:
pop es
pop bx
pop ax
iret
code ends
end start