8086汇编
本笔记是笔者观看小甲鱼老师(鱼C论坛)《零基础入门学习汇编语言》系列视频的笔记,在此感谢他和像他一样共享资源、帮助他人的筒子们==本文比较长,由于笔者个人能力有限,错漏在所难免,欢迎读者们批评指正。
一、基础知识
引言
- 基本了解硬件系统的结构;
- 利用硬件系统的编程结构和指令集,有效灵活地控制系统进行工作。
1.1 机器语言
- 机器语言是机器指令的集合。电子计算机的机器指令是一系列二进制数字。计算机将之转换为一系列高低电平脉冲信号来驱动硬件工作的。
1.2 汇编语言的产生
- 由于机器语言指令都是由01组成,难以编写,记忆和维护程序.所以汇编语言为了解决这一问题产生。汇编语言的主体是汇编指令,汇编指令是机器指令的助记符。
- 寄存器: CPU中存储数据的器件,一个CPU中有多个寄存器。
1.3 汇编语言的组成
- 1、汇编指令(机器码的助记符,有对应的机器码);
- 2、伪指令(由编译器执行)和其他符号(由编译器识别)。
1.4 存储器
- CPU工作需要指令和数据,指令和数据存储在存储器中。
1.5 指令和数据
- 在内存或者磁盘中存储的都是为二进制信息,指令和数据由我们设定(走的总线)。
1.6 存储单元
- 存储器被划分为若干个存储单元,每个存储单元从0开始顺序编号。
- B、KB、MB、GB、TB等单位。
1.7 CPU对存储器的读写
-
CPU要对数据进行读写,必须和外部器件进行以下三类信息的交互:
- 1、存储单元的地址(地址信息);
- 2、器件的选择、读或写命令(控制信息);
- 3、读或写的数据(数据信息) 。
-
总线是连接CPU和其他芯片的导线,逻辑上分为地址总线、数据总线、控制总线。
-
CPU从内存单元中读写数据的过程:
- 1、CPU通过地址线将地址信息发出;
- 2、CPU通过控制线发出内存读命令,选中存储器芯片,并通知它将要从中读或写数据;
- 3、存储器将相应的地址单元中的数据通过数据线送入CPU或CPU通过数据线将数据送入相应的内存单元。
1.8 地址总线
- CPU是通过地址总线指定存储单元,地址总线传送的能力决定了CPU对存储单元的寻址能力。(一般32位CPU,寻址能力为2^32=4G)
1.9 数据总线
- CPU通过数据总线来与内存等器件进行数据传送,数据总线的宽度决定了CPU和外界的数据传送速度。
1.10 控制总线
- 控制总线是一些不同控制的集合,CPU通过控制总线对外部器件的控制。控制总线的宽度决定了CPU对外部器件的控制能力。
小结
- 1、汇编指令时机器指令的助记符,与机器指令一一对应。
- 2、每一种CPU都有自己的汇编指令集。
- 3、CPU可以直接使用的信息在存储器中存放。
- 4、在存储器中指令和数据都是二进制信息。
- 5、存储单元从0开始顺序编号。
- 6、一个存储单元可以存储8个bit。
- 7、B、KB、MB、GB等单位之间的转换。
- 8、CPU管脚和总线相连。总线的宽度表示CPU不同方面的性能:
- 地址总线的宽度决定了CPU的寻址能力;
- 数据总线的宽度决定了CPU与其他器件进行一次数据传送的量;
- 控制总线宽度决定了CPU对系统中其他器件的控制。
检测点 1.1
1.11 内存地址空间(概述)
- CPU可寻的内存单元构成这个CPU的内存地址空间。例如一个CPU的地址总线宽度为10,那么可以寻址的1024个内存单元构成了这个CPU的内存空间。
1.12 主板
- 主板主板,主要的电路板 :laughing:
1.13 接口卡
- CPU通过接口卡间接控制外部设备。
1.14 各类存储器
-
随机存储器RAM(主板上的RAM、拓展插槽上的RAM和接口卡上的RAM)和只读存储器器ROM(装有BIOS的ROM)。
1.15 内存地址空间
-
各类存储器在物理上是独立的,但是:
- 1、都和CPU的总线相连;
- 2、 CPU对他们进行读或写的时候都通过控制线发出的内存读写命令。
不同的计算机系统的内存地址空间分配情况是不同的。
二、寄存器(CPU的工作原理)
引言
- CPU由运算器、控制器、寄存器 等器件组成,靠内部总线相连。
- 内部总线实现CPU内部各器件之间的联系;外部总线实现CPU和主板上其他器件的联系。
- 在CPU中:
- 运算器进行信息处理;
- 寄存器进行信息存储;
- 控制器控制各种器件进行工作;
- 内部总线连接各种器件在它们之间进行数据的传送。
2.1 通用寄存器
- 8086有14个寄存器:
- AX、BX、CX、DX、SI、DI、SP、BP、IP、CS、SS、CS、ES、PSW。
- AX、BX、CX、DX通常用来存放一般性数据,被称为通用寄存器。
- 16位寄存器所能存储的数据最大值为216-1 。
- 为保证兼容性,8086 CPU的通用寄存器可以分为两个独立的8位寄存器使用。例: AX可分为AH和AL。
2.2 字在寄存器中的存储
8086 CPU所有的寄存器是16位,可以存放2个字节(一个字)。
一字节由8 bit 组成,可以存在8位寄存器中。
-
字(word)是两字节,16位。
2.3 几条汇编指令
-
汇编指令对大小写不敏感
汇编指令举例
汇编指令 | 控制CPU完成的操作 | 用高级语言的语法描述 |
---|---|---|
mov ax,18 | 将8送入AX | AX=18 |
mov ah,78 | 将78送入AH | AH=78 |
add ax,8 | 将寄存器AX中的数值加上8结果存入AX中 | AX=AX+8 |
mov ax,bx | 将寄存器BX中的数据送入寄存器AX | AX=BX |
add ax,bx | 将AX,BX中的内容相加结果存入AX中 | AX=AX+BX |
检测点 2.1
2.4 物理地址
- 所有的内存单元构成一个一维的线性存储空间。
- CPU访问内存单元时要给出内存单元的唯一地址就是物理地址。
2.5 16位结构的CPU
- 1、运算器一次最多可以处理16位数据。
- 2、 寄存器的最大宽度为16位。
- 3、寄存器和运算器之间的通路是16位。
2.6 8086 CPU给出物理地址的方法
- 8086有20位的地址总线,可以传送20位地址,寻址能力为1M;但8086内部为16位结构,只能传送16位的地址。
- 8086CPU采用一种在内部用两个16位地址合成的方法来形成一个20位的物理地址。
- 8086CPU读写内存的步骤:
- 1、CPU中的相关部件提供段子和偏移地址这两个16位的地址;
- 2、段地址和偏移地址通过内部总线送入到一个称为地址加法器的部件;
- 3、地址加法器将两个16位地址合并成一个20位的地址;
- 4、地址加法器通过内部总线将20位物理地址送送入输入输出地址;
- 5、输入输出控制电路将20位物理地址送上地址总线;
- 6、20位物理地址被地址总线传送到存储器。
- 地址加法器工作原理:<font color="red">物理地址=段地址*16+偏移地址</font>。
- 段地址*16就是数据左移4位(二进制)
移位位数 | 二进制 | 十六进制 | 十进制 |
---|---|---|---|
0 | 10B | 2H | 2 |
1 | 100B | 4H | 4 |
2 | 1000B | 8H | 8 |
3 | 10000B | 10H | 16 |
4 | 100000B | 20H | 32 |
- 一个数据的二进制形式左移N位,相当于该数据乘以2的N次方。一个数据X进制形式左移N位,相当乘以NX。
2.7 段地址*16+偏移地址=物理地址
- CPU可以通过不同的段地址和偏移地址形成一个相同的物理地址。
段地址*16是移位
2.8 段的概念
-
人为定义的,将若干地址连续的内存单元看作一个段。用段地址*16定位段的起始地址(基址),用偏移地址定位段中的内存单元。
一个段的起始地址是16的倍数。偏移地址为16位,寻址能力为64K,所以段的最大长度也是64K。
检测点 2.2
2.9 段寄存器
- 8086 CPU有4个段寄存器:CS(代码段)、DS(数据段)、SS(堆栈段)、ES(附加段),这4个段提供给8086CPU内存单元的段地址。
2.10 CS和IP
- CS(代码段寄存器) 和IP(指令指针寄存器) 是8086CPU中最关键的寄存器,它们指示了CPU当前要读取指令的地址。在任意时刻CPU将CS:IP指向的内容当作指令执行。
- 8086CPU工作过程的简要概述:
-
1、从CS:IP指向内存单元读取指令,读取的指令进入指令缓冲器;
8086PC机刚开始启动时,CPU从内存FFFF0h单元中读取指令执行,FFFF0h单元中的指令时8086PC机开机后执行的第一条指令。
2、 IP=IP+所读取指令的长度,从而正确的指向下一条指令;
3、执行指令。转到步骤1,周而复始。
-
2.11 修改CS、IP的指令
- mov指令(传送指令) 可以改变8086CPU大部分寄存器的值,但不能用于设置CS、IP的值。
- jmp指令(转移指令) 可以用来同时修改CS和IP的值,格式为
jmp 段地址:偏移地址;同时修改CS和IP
jmp 某一合法寄存器;则是仅修改IP
2.12 代码段
- 对于8086PC机,在编程时可以将长度为N(N小于等于64KB)的一组代码存在一组地址连续、起始地址为16的倍数的内存单元中,这段内存是用来存放代码的,从而定义了一个代码段。
- 利用CS:IP来指向内存单元从而让CPU执行其中的内容。
检测点 2.3
使用Debug
<font color="red">windows xp系统自带debug,请使用xp以上系统的读者执行自行下载debug.exe和dosbox,使用方法笔者不再赘述,在dosbox中可以使用debug。</font >
- 可以使用汇编金手指查阅指令。
- R命令查看、改变CPU寄存器的内容;
- D命令查看内存中的内容;
- E命令改写内存中的内容;
- U命令将内存中的机器指令翻译成汇编指令;
- T命令执行一条机器指令;
- G命令跳转到偏移地址;
- P命令结束循环或者是int 21H时是退出程序;
- A命令是以汇编指令的格式在内存中写入一条机器指令。
三、寄存器(内存访问)
3.1 内存中字的存储
- 字是两个字节,要用两个地址连续的内存来存放,字的低位字节存在低地址中,高位字节存放在高地址单元中。
3.2 DS和[address]
- DS通常存放要访问的数据的段地址。
- 8086 CPU由于硬件的设计不支持将数据直接送入段寄存器的操作。
数据 -> 通用寄存器 -> 段寄存器
- [ ]里边的数据代表偏移地址值
-
mov指令:
- 将数据直接送入寄存器;
- 将一个寄存器或内存单元中的内容送入另一个寄存器;
- mov指令格式:
mov 寄存器名,内存单元
3.3 字型的传送
- 高地址单元和高8位寄存器,低地址单元和低8位寄存器相对应。
3.4 mov、add、sub指令
- 有两个操作对象,jmp只有一个操作对象。
- 使用汇编金手指查阅指令
- mov指令的几种形式
mov 寄存器,数据;mov ax,8
mov 寄存器,寄存器;mov ax,bx
mov 寄存器,内存单元;mov ax,[0]
mov 内存单元,寄存器;mov [0],ax
mov 段寄存器,寄存器;mov ds,ax
mov 寄存器,段寄存器;mov ax,ds
……
- add指令的几种形式
add 通用寄存器,数据
add 通用寄存器,通用寄存器
add 通用寄存器,内存单元
add 内存单元,寄存器
- sub指令的几种形式
sub 通用寄存器,数据
sub 通用寄存器,通用寄存器
sub 通用寄存器,内存单元
sub 内存单元,通用寄存器
3.5 数据段
- 对于8086PC机,在编程时可以将长度为N(N小于等于64KB)的一组代码存在一组地址连续、起始地址为16的倍数的内存单元中,这段内存是用来存放数据的,从而定义了一个数据段。
- 可以通过在DS中存放数据段的段地址,用相关的指令访问数据段中的具体单元来访问数据段中的数据。
检测点 3.1
3.6 栈
- 具有特殊的访问方式的存储空间,也是内存空间的一部分,数据先进后出。
- 有两个基本操作:
- 入栈:将一个新的元素放到栈顶;
- 出栈:从栈顶取出一个元素。
- 栈顶元素最后入栈最先出栈。
3.7 8086 CPU提供的栈机制
- 现今的CPU都有栈的设计,基于8086CPU编程可以将一段内存当作栈来使用。
- 8086CPU的入栈(PUSH)和POP(出栈),以字为单位。
- push ax 将寄存器ax中的数据送入栈
- pop ax 从栈顶取出数据送入ax
- 段寄存器SS存放栈顶的段地址,寄存器SP存放栈顶的偏移地址。任意时刻SS:SP指向栈顶元素。push时SP先自减法后写内存,pop先读内存sp后自加。
- pop之后数据还是存在内存中,push时覆盖。
<font color="red">CS和IP存放当前指令的段地址和偏移地址。</font>
3.8 栈顶越界的问题
- 栈是空的,则SP指向栈底+1的内存。
- 8086 CPU只纪录栈顶,栈空间由自己控制。栈顶越界问题导致溢出漏洞。
- 8086CPU只考虑当前的情况:
- 当前栈顶在何处;
- 当前要执行的指令时哪一条。
3.9 push、pop指令
- 可以直接对段寄存器使用。
;push和pop格式
push 寄存器
pop 寄存器
push 段寄存器
pop 段寄存器
push 内存单元
pop 内存单元
- 通用寄存器命名是x结尾的,段寄存器是以s结尾。
- CPU在执行指令时,数据的段地址是从DS中获得,代码是在CS中获得,栈地址是从SS获得。
3.10 栈段
- 对于8086PC机,在编程时可以将长度为N(N小于等于64KB)的一组代码存在一组地址连续、起始地址为16的倍数的内存单元中,这段内存是当作栈来用,从而定义了一个栈段。
- 寄存器清零可用sub ax,ax或者直接赋值0,<font color="red">常见的也有使用xor</font>。
- 当栈空间定义为最大时,栈为空时SP=0。
检测点 3.2
四、第一个程序
引言
编写完成的汇编语言程序,用编译器编译成可执行文件并在操作系统中运行。
4.1 一个源程序从写出到执行的过程
-
编写
- 用编辑器(Sublime Text、Nodepad++、UltraEdit)编写,文件后缀为.asm。
-
编译链接
- 使用MASM.EXE编译生产obj(目标文件)。masm也请读者自行搜索下载。
- LINKE.EXE对目标文件进行连接生产可在操作系统中直接运行的可执行文件。
可执行文件包含程序(机器码)、数据(源程序中定义的数据)和相关的描述信息。
-
执行
- 操作系统中依照可执行文件中的描述信息将可执行文件中的机器码和数据加载入内存并进行相关的初始化,然后CPU执行。
4.2 源程序
- 汇编指令:有对应的机器码的指令,编译为机器码被CPU执行
- 伪指令:没有对应的机器码,不被CPU所执行,由编译器执行来进行相关的编译工作。
- segment和ends是用来定义一个段的,是成对使用的伪指令,再写可被编译器编译的汇编程序是必须要用的。
assume cs:codesg ;假设代码段的名称为codesg
codesg segment ;定义一个codesg段
mov ax,0123H
mov bx,0456H
add ax,bx
add ax,ax
mov ax,4c00h
int 21h
codesg ends ;codesg段结束
end ;是个伪指令,程序的结束标记
assume用来加上某一段寄存器和程序中的某一用segment……ends定义的段相关联。通过assume说明这种关联,在需要的情况下编译程序可以将段寄存器和某一个具体的段相联系。
一个汇编程序是由多个段组成。一个有意义的汇编程序中至少要用一个段来存放代码。
-
程序与源程序
标号:指代地址
程序的结构
小练习:
;编程运算2^3
assume cs:abc ;段与寄存器关联
abc segment ;定义一个段,名称为abc
mov ax,2;写入汇编指令
add ax,ax
add ax,ax
abd ends
end ;程序结束处
- 程序的返回:一个程序结束后将CPU的控制权交还给使它得以运行的程序的过程。应该在程序的末尾添加返回的程序段。
codesg:放在segment前面,作为一个段的名称,这个段的名称最终将被编译、连接程序,称为一个段的段地址 。
mov ax,4c00H
int 21H ;第21号中断
;这两条指令说实现的功能就是程序返回。
- 语法错误和逻辑错误
4.3 编辑源程序
- 使用编辑器编辑,扩展名为.asm
assume cs:ABC
ABC segment
mov ax,2
add ax,ax
add ax,ax
mov ax,4c00H
int 21h
ABC ends
end
4.4 编译
-
masn和 1.asm在同一目录中,dos下使用masm 1.asm命令即可生产1.obj文件。
4.5 连接
-
link 1.obj,生成exe文件,摁enter忽略编译程序提示输入的信息。
当源程序很大时,可以将它分成多个源程序文件编译,每个源程序编译成目标文件后再用连接程序将他们连接到一起,生成一个可执行文件。或者程序中调用了某个库文件中的子程序,需要将这个库文件和该目标文件连接到一起,生成一个可执行文件。或者一个源程序编译后得到存有机器码的目标文件,目标文件中的有些内容还不能直接生成可执行文件,连接程序将此内容处理为最终的可执行文件信息。
4.6 简化编译和连接
- 使用ml命令,ml 1.asm
4.7 exe的执行
- 为兼容16位的程序,使用dosbox运行。
4.8 可执行文件中的程序转入内存并运行的原理
在dos中可执行文件中的程序p1若要运行吗必须有一个正在运行的程序p2将p1从可执行文件中加载如内存,将CPU的控制权交给它,p1才能得以运行;当p1运行完毕后,应该将CPU的控制权交还给使它de'yi 运行的程序p2。
汇编程序从写出到执行的过程:编程 -> 编译 -> 连接 -> 加载 -> 内存中的程序 -> 运行
-
在dos系统中.exe文件中的加载过程
4.9 程序执行过程的跟踪
- 使用debug(xp以上的系统在dosbox中使用)来跟踪一个程序的运行过程。
五、[BX]和loop指令
引言
- 约定符号()来表示一个寄存器或者一个内存单元中的内容。例如(ax)=0010H表示ax中的内容为0010H;(21000H)=0010H,表示2000:1000处的内容为0010H。
- 约定符号idata表示常量。
5.1 [BX]
- inc指令是自增1的意思
- 和[0]有些类似,[0]表示内存单元,它的偏移地址是0。[bx]也是表示一个内存单元,它的内存偏移地址在bx中。
mov bx,0
mov ax,[bx]
mov al,[bx]
- 用以下两种信息描述一个内存单元:
- 1、内存单元的地址;
- 2、内训单元的长度(类型)。
我们用[0]表示一个内训单元时,0表示单元的偏移地址,段地址默认在DS中,单元的长度(类型)可以由具体指令中的其他的操作对象(比如说寄存器)指出。
mov ax,[0];0对应的字单元,主要单位要看操作对象(寄存器)
mov al,[0];字节
5.2 loop指令
- 指令的格式是loop 标号。CUP执行loop指令时要进两步操作:
- CX中存放循环的次数,执行时CX中的内容自减1。相当于C的do while
- 判断CX中的值,不为0则转至标号处执行程序,为0则向下执行。
- 通常loop指令来实现循坏功能CX中存放循环的次数。
assume cs:code
code segment
mov ax,2
add ax,ax
mov ax,4c00H
int 21H
code ends
end
;计算2^3
assume cs:code
code segment
mov ax,2
add ax,ax
add,ax,ax
mov ax,4c00H
int 21h
code ends
end
;计算2^12
assume cs:code
code segment
start: mov ax,2
mov cx,11
p:add,ax,ax
loop p;p是标号
mov ax,4c00H;masm默认数字是十进制
int 21H
code ends
end start
;编程计算123*236,结果放在ax中
assume cs:code
code segment
start:mov ax,0
mov cx,236
an:add ax,123
loop an
mov ax,4c00H
int 21H
code ends
end start
assume cs:code
code segment
start:mov ax,0
mov cx,123
pa:add ax,236
loop pa
mov ax,4c00H
int 21H
code ends
end start
5.3 在Debug中跟踪用loop指令实现的循环程序
- 注意:在汇编源程序中数据不能以字母开头,有字母的在前面加0处理。
- t命令单步执行、G命令和P命令。
- 使用汇编金手指查阅指令。
5.4 Debug和汇编编译器Masm对指令的不同处理
Degug中mov ax,[0],表示将ds:0处的数据存入al中。ah=0,因为一个内存单元是8位的,ax是16位的,同位存储。而编译器[0]会被当作0处理
-
将内存2000:0、2000:1、2000:2、2000:3单元中的数据(字节)送入阿al、bl、cl、dl中。
- debug中:
- 在MASM中:
- 要在编译器中实现用偏移地址[]中的内容传送先bx来代替,mov 偏移地址,bx 再 mov al,[bx]。如要直接使用[ ]则要加上段地址ds:[偏移地址]
在MASM中:
mov al,[0] ;将al赋值0
mov al,ds[0] ;将al赋值段地址为ds,偏移地址为0的内存单元中的内容
mov al,[bx] ;默认段地址为ds,将al赋值偏移地址为bx
mov al,ds:[bx] ;将al赋值段地址为ds,偏移地址为bx
5.5 loop和[BX]的联合应用
- 可以用循环来解决处理地址连续的内存单元中的数据的问题,用变量来给出内存单元的地址。
5.6 段前缀
- 出现在访问内存单元的指令中用显式地指明内存单元的段地址的ds、cs、ss、es称为段前缀。没有显式地给出内存单元的段地址则默认在ds中。
5.7 一段安全的空间
在8086模式中,随意向一段内存空间写入数据是危险的,因为这段空间中可能存放着重要的系统数据或代码。
assume cs:code
code segment
mov ax,0
mov ds,ax
mov ds:[26H],ax
mov ax,4c00H
int 21H
code ends
end
-
但笔者在练习的时候出现dosbox下debug卡死
-
dos下0:200H0:2FFH的256个字节的空间是安全的,dos和其他合法程序一般都不会使用这段空间。内存0000:00000000:03FF大小为1kb的空间是系统存放中断处理程序入口地址的中断向量表。一般情况下0:200H~0:2FFH的256个字节的空间所对应的中断向量表都是空的,操作系统和其他应用程序都不占用。
5.8 段前缀的使用
- 将内存ffff:0~ffff:b段单元中的数据拷贝到0:200 ~ 0:20b单元中
assume cs:code
code segment
mov bx,0 ;(bx)=0,偏移地址从0开始
mov cx,12 ;(cx)=12,循环12次
s: mov ax,offffh
mov ds,ax ;(ds)=0ffffh
mov dl,[bx] ;(ds)=((ds)*16+(bx)),将ffff:bx中的数据送入dl
mov ax,0020h
mov ds,ax ;(ds)=0020h
mov [bx],dl ;((ds)*16+(bx))=dl,将数据送入0020:bx
inc bx ;(bx)=(bx)+1
loop s
mov ax,4c00h
int 21h
code ends
end
- 两个内存单元相差64KB则不再同一个段里,需要设置ds的值两次,效率不高。
- 使用 es(附加段)
;优化后的代码,优化了两次设置ds
assume cs:code
code segment
mov ax,offffh
mov ds,ax ;(ds)=0ffffh
mov ax,0020h
mov es,ax ;(es)=0020H
mov bx,0 ;(bx)=0,此时ds:bx指向ffff:0,es:bx指向0020:0
mov cx,12 ;(cx)=12,循环12次
s: mov dl,[bx] ;(ds)=((ds)*16+(bx)),将ffff:bx中的数据送入dl
mov es:[bx],dl ;((es)*16+(bx))=dl,将数据送入0020:bx
inc bx ;(bx)=(bx)+1
loop s
mov ax,4c00h
int 21h
code ends
end
六、包含多个段的程序
6.1在代码段中使用数据
- 编程计算0123H、0456H,0abxH、0defH、0fesH、0cbaH、0987H这8个数据的和,结果存放在ax中:
assume cs:codesg
codesg segment
dw 0123H,0564H,0789H,0abcH,0defH,0fedH,0cbaH,0987H
;dw,define word,定义字型数据,db定义字节型数据
;由于数据在代码段中,所以段地址是CS
;dw定义的数据在最开始的地方,所以偏移地址是0开始
start:mov bx,0 ;第一条指令
mov ax,0
mov cx,8
s: add ax,cs:[bx]
add bx,2
loop s
mov ax,4c00H
int 21H
codesg ends
end start ;入口找end
- end的作用除了通知编译器结束之外还有告诉编译器程序的入口在什么地方。
- 可执行文件中的程序执行过程
6.2 在代码段中使用栈
- 利用栈编程将定义的数据逆序(联想栈的特性)存放:dw 0123H,0564H,0789H,0abcH,0defH,0fedH,0cbaH,0987H
assume cs:codesg
codesg segment
dw 0123H,0564H,0789H,0abcH,0defH,0fedH,0cbaH,0987H;地址0~15
dw 0,0,0,0,0,0,0,0;定义8个字型空数据,后面当作栈来使用,地址是16~31
start:
mov ax,cs
mov ss,ax
mov sp,32;设置栈底ss:sp指向cs:32,十进制的32
mov bx,0
mov cx,8
s:push cs:[bx]
add bx,2
loop s; 以上代码段0~15个单元中的8个字型数据一次入栈
mov bx,0
mov cx,8
s0:pop cs:[bx]
add bx,2
loop s0;依次出栈8个执行数据到代码段0~15单元中
mov ax,4c00h
int 21h
codesg ends
end start;指明程序入口在start处
- 如果对此程序的栈有疑惑,跳转到 3.6 栈和3.10 栈段
6.3 将数据、代码、栈放入不同的段
- 在8086CPU中数据、栈和代码存储空间不能大于64KB。可以用像定义代码段一样的方法来定义多个段并在其中定义需要的数据,或者通过定义数据来取得栈空间。
assume cs:codesg,ds:data,ss:stack;在源程序中为三个段进行有意义的名称
data segment
dw 0123H,0564H,0789H,0abcH,0defH,0fedH,0cbaH,0987H
data ends
stack segment
dw 0,0,0,0,0,0,0,0;定义8个字型空数据,后面当作栈来使用
stack ends
code segment
start:
mov ax,stack
mov ss,ax
mov sp,16;设置栈底ss:sp指向stack:16,
mov ax,data
mov ds,ax;ds指向data段
mov bx,0;ds:bx指向data段中的第一个单元
s:push cs:[bx]
add bx,2
loop s; 以上代码段0~16个单元中的8个字型数据一次入栈
mov bx,0
mov cx,8
s0:pop cs:[bx]
add bx,2
loop s0;依次出栈8个执行数据到代码段0~16单元中
mov ax,4c00h
int 21h
codesg ends
end start;指明程序入口在start处
- 程序中指令决定了断中的内容是作为数据处理还是作为指令执行还是作为栈空间使用。
检测点 6.1
实验五
assume cs:codesg,ds:data,ss:stack
data segment
dw 0123H,0564H,0789H,0abcH,0defH,0fedH,0cbaH,0987H
data ends
stack segment
dw 0,0,0,0,0,0,0,0
stack ends
codesg segment
start: mov ax,stack
mov ss,ax
mov sp,16
mov ax,data
mov ds,ax
push ds:[0]
push ds:[2]
pop ds:[2]
pop ds:[0]
mov ax,4c00h
int 21h
codesg ends
end start
七、更灵活的定位内存地址的方法
7.1 and和or指令
- and指令:逻辑与指令,按位进行与运算。
and两个同时为真的结果才为真。
mov al,01100011B
and al,00111011B
;执行后 al=00100011B
- 可用and指令将操作对象的相应位设为0,其他位不变
and al,10111111B;将al第六位设为0
and al,01111111B;将al第七位设为0
and al,11111110B;将al第0位设为0
- or指令:逻辑或指令,按位进行或运算。
or两个同时为假的结果才为假
mov al,01100011B
and al,00111011B
;执行后 al=01111011B
- 可用or指令将操作对象的相应位设为1,其他位不变
and al,01000000B;将al第六位设为1
and al,10000000B;将al第七位设为1
and al,00000001B;将al第0位设为1
7.2 关于ASCII码
- 将字符的ascii码写入显存屏幕就显示出相关的字符。
7.3 以字符形式给出数据
- 用‘’的方式指明数据是以字符的形式给出的。例如'A'
assume cs:code,ds:data
data segment
db 'unIx'
db 'foRK'
data ends
code segment
start: mov al,'a'
mov bx,'b'
mov ax,4c00h
int 21h
code ends
end start
7.4 大小写转换的问题
- 大写字母比小写字母ASCII大32(20H)。
大写 | 二进制 | 小写 | 二进制 |
---|---|---|---|
A | 01000001 | a | 01100001 |
B | 01000010 | b | 01100010 |
C | 01000011 | c | 01100011 |
D | 01000100 | d | 01100100 |
- 从第0位开始计算,大写字母ASCII码第五位为0,小写字母ASCII码第五位为1。
;大小写转换
assume cs:codesg,ds:datasg
datasg segment
db'BaSiC'
db'iNfOfMaTiOn'
datasg ends
codesg segment
start: mov ax,datasg
mov ds,ax;设置ds执行datasg段
mov bx,0;设置(bx)=0,ds:bx指向'BaSiC'的第一个字母
mov cx,5;设置循环次数,因为BaSiC有5个字母
s:mov al,[bx];将ASCII码从ds:bx所指向的单元中取出
and al,11011111B;口岸al中ASCII码的第5个位置变为0,变为大写字母
mov [bx],al;转变后将ASCII码写回单元
inc bx;(bx)加1,ds:bx指向下一个字母
loop x
mov bx,5;设置(bx)=5,ds:bx指向'iNfOfMaTiOn'的第一个字母
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
7.5 [bx+idata]
- [bx+idata]表示的是一个内存单元,它的偏移地址为bx+idata
;[bx+idata]可以写成以下格式
mov ax,[200+bx]
mov ax,200[bx]
mov ax,[bx].200
;使用debug查看内存
mov ax,2000H
mov ds:ax
mov bx,1000H
mov ax,[bx]
mov cx,[bx+1]
add cx,[bx+2]
7.6 用[bx+idata]的方式进行数组的处理
- 用[bx+idata]的方式进行数组处理
;改进大小写转换程序
assume cs:codesg,ds:datasg
datasg segment
db'BaSiC'
db'iNfOfMaTiOn'
datasg ends
codesg segment
start: mov ax,datasg
mov ds,ax;设置ds执行datasg段
mov bx,0;设置(bx)=0,ds:bx指向'BaSiC'的第一个字母
mov cx,5;设置循环次数,因为BaSiC有5个字母
s:mov al,[bx+0];将ASCII码从ds:bx所指向的单元中取出
and al,11011111B;口岸al中ASCII码的第5个位置变为0,变为大写字母
mov [bx],al;转变后将ASCII码写回单元
mov [bx+5];定位第二个字符串的字符
or al,00100000B
mov [bx+5],al
inc bx
loop s
mov ax,4c00H
int 21H
codesg ends
end start
- C语言的形式
include<stdio.h>
char a[5]="BaSiC";
char b[11]="iNfOfMaTiOn";
main()
{
int i;
i=0;
do
{
a[i]=a[i]&0xDF;
b[i]=b[i]|0x20;
i++;
}while(i<5);
}
7.7 SI和DI
- SI和DI在8086CPU中和bx功能相近,充当BX的扩充,但是不能分成两个8位寄存器来使用。[SI]段地址默认也是在DS中。
- 下面的指令实现了相同的功能
mov bx,0
mov ax,[bx]
mov si,0
mov ax,[si]
mov di,0
mov ax,[di]
;-------------
;下面的三组指令也实现了另一个组相同的功能
;-------------
mov bx,0
mov ax,[bx+123]
mov si,0
mov ax,[si+123]
mov di,0
mov ax,[di+123]
- 一般ds:si指向要复制的原始空间,ds:di指向复制的目的空间。
;用DI和SI实现复制到它后面的数据区中
assume cs:codesg,ds:datasg
datasg segment
db'welcome to asm!'
db'................'
datasg ends
codesg segment
start :mov ax,datasg
mov ds,ax
mov si,0
mov di,16
mov cx,8
s:mov ax,[si]
mov [di],ax
add si,2
add di,2
loop s
mov ax,4c00h
int 21H
;------
;用数组的思维[bx(si或di)+idata]的方式优化程序
;------
assume cs:codesg,ds:datasg
datasg segment
db'welcome to asm!'
db'................'
datasg ends
codesg segment
start :mov ax,datasg
mov ds,ax
mov si,0
mov cx,8
s:mov ax,[si];第一个字符串的的第一个元素
mov [si+16],ax;目标字符串的第二个元素
add si,2
loop s
mov ax,4c00h
int 21H
codesg ends
end start
7.8 [bx+si]和[bx+di]
mov ax,2000h
mov ds,ax
mov bx,1000h
mov si,0
mov ax,[bx+si]
inc si
mov cx,[bx+si]
inc si
mov di,si
mov ax,[bx+di]
7.9 [bx+si+idata]和[bx+di+idata]
- 常数后要加.例如[bx+si].idata或者[bx].idata[si]
mov ax,2000h
mov ds,ax
mov bx,1000h
mov si,0
mov ax,[bx+2+si]
inc si
mov cx,[bx+si+2]
inc si
mov di,si
mov ax,[bx+di+2]
7.10 不同的寻址方式的灵活应用
- 编程将数据段中每一个单词的头一个字母改为大写字母。
assume cs:codesg,ds:datasg
datasg segment
db'1. file ';长度刚好都是16个字节
db'2. edit '
db'3. search '
db'4. view '
db'5. options '
db'6. help '
datasg ends
codesg segment
start:
mov ax,datasg
mov ds,ax
mov bx,0
mov cx,6
s:
mov al,[bx+3]
and al,11011111B
mov [bx+3],al
add bx,16
loop s
mov ax,4c00h
int 21h
codesg ends
end start
- 编程将数据段中每个单词改为大写字母
;有bug,问题在于cx的使用,进行二重循环,只用一个循环计数器,造成在进行内层的时候覆盖了外层循环的循环计数值。
assume cs:codesg,ds:datasg
datasg segment
db 'ibm '
db 'dec '
db 'dos '
db 'vax '
datasg ends
codesg segment
start:mov ax,datasg
mov ds,ax
mov bx,0;用bx来定位行
mov cx,4
s0:mov si,0;用si来定位列
mov cx,3
s:mov al,[bx+si]
and al,11011111B
mov [bx+si],al
inc si
loop s
add bx,16
loop s0
mov ax,4c00h
int 21h
codesg ends
end start
- 程序没有返回到cmd
loop s;三次循环后cx等于0了
add bx,16
loop s0;先是cx=cx-1再判断时候等于0,此时cx=FFFF不为0再循环,变成死循环了
- 因为loop是和cx一起使用的,不能多用个寄存器来解决loop循环次数的问题。解决的方法是在每次开始内层循环时用dx将外层循环cx的值保存起来,在执行外层循环的loop指令前再回复外层循环的cx的数值。
- 改进后程序
assume cs:codesg,ds:datasg
datasg segment
db 'ibm '
db 'dec '
db 'dos '
db 'vax '
datasg ends
codesg segment
start:
mov ax,datasg
mov ds,ax
mov bx,0;用bx来定会行
mov cx,4
s0:
mov dx,cx;用dx寄存器来临时存放外层cx的值
mov si,0;用si来定位列
mov cx,3
s:
mov al,[bx+si]
and al,11011111B
mov [bx+si],al
inc si
loop s
add bx,16
mov cx,dx;在进行外层循环的时候回复cx的值
loop s0
mov ax,4c00h
int 21h
codesg ends
end start
- 在上面的程序中,8086 CPU si、cx、ax、bx这些寄存器经常要使用到;cs、ip、ds也不能用,因为cs:ip时刻指向当前指令,ds指向datasg段;那么可用的寄存器就只用dx、di、es、ss、sp、bp等寄存器了。内存可以解决经常性的数据暂存问题。为了使程序结构清晰便于阅读,应该使用栈
- 再次被改进的程序
assume cs:codesg,ds:datasg
datasg segment
db 'ibm '
db 'dec '
db 'dos '
db 'vax '
dw 0;定义一个字用来保存cx的值
datasg ends
codesg segment
start:mov ax,datasg
mov ds,ax
mov bx,0;用bx来定位行
mov cx,4
s0:mov ds:[40h],cx;datasg:40h单元存放外层cx的值
mov si,0;用si来定位列
mov cx,3
s:mov al,[bx+si]
and al,11011111B
mov [bx+si],al
inc si
loop s
add bx,16
mov cx,ds:[40h];在进行外层循环的时候回复cx的值
loop s0
mov ax,4c00h
int 21h
codesg ends
end start
- 再次使用栈改进程序
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;定义一个段,用作栈段,容量为16个字节
stacksg ends
codesg segment
start:mov ax,stacksg
mov ss,ax
mov sp,16
mov ax,datasg
mov ds,ax
mov bx,0;用bx来定位行
mov cx,4
s0:push cx;datasg:40h单元存放外层cx的值
mov si,0;用si来定位列
mov cx,3
s:mov al,[bx+si]
and al,11011111B
mov [bx+si],al
inc si
loop s
add bx,16
pop cx;在进行外层循环的时候回复cx的值
loop s0
mov ax,4c00h
int 21h
codesg ends
end start
- 编程将数据段中的每个单词的前四个字母改为大写字母
assume cs:codesg,ds:datasg,ss:stacksg
stacksg segment
stacksg ends
datasg segment
db '1. display '
db '2. brows '
db '3. replace '
db '4. modify '
datasg 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,4
s:mov al,[bx+si+3]
and al,11011111B
mov [bx+si+3],al
inc si
loop s
add bx,16
pop cx
loop s0
mov ax,4c00h
int 21h
codesg ends
end start
八、数据处理的两个基本问题
引言
- 本章是总结性的内容,数据处理的两个基本问题是
- 处理的数据在哪?
- 要处理的数据有多长?
- 自定义得描述符:
- reg寄存器
- ax、bx、cx、dx、ah、al、bh、bl、ch、cl、dh、dl、sp、bp、si、di;
- sreg段寄存器
- ds、ss、cs、es。
- reg寄存器
8.1 bx、si、di、bp
- 在8086 CPU中只有bx、si、di、bp这四个寄存器用在[ ]中进行内存单元寻址。在[]中,组合只能以这四种形式:bx和si、bx和di、bp和si、bp和di
;以下指令是错误的
mov ax,[ax]
mov ax,[cx]
mov ax,[dx]
mov ax,[ds]
mov ax,[bx+bp]
mov ax,[si+di]
- 正确的指令
mov ax,[bx]
mov ax,[si]
mov ax,[di]
mov ax,[bp]
mov ax,[bx+si]
mov ax,[bx+di]
mov ax,[bp+si]
mov ax,[bp+di]
mov ax,[bx+si+idata]
mov ax,[bx+di+idata]
mov ax,[bp+si+idata]
mov ax,[bp+di+idata]
- [bp]的段地址默认在ss中。
8.2 机器指令处理的数据所在的位置
- 绝大部分机器指令时进行数据处理的,大致可以分为3类:读、写、运算。指令在处理前可以在三个地方:CPU内部、内存、端口。
机器码 | 汇编指令 | 指令执行前数据的位置 |
---|---|---|
89C3 | mov bx,[0] | 内存,ds:0单元 |
89C3 | mov bx,ax | CPU内部,ax寄存器 |
BB0100 | mov bx,1 | CPU内部,指令缓冲器 |
8.3 汇编语言中数据位置的表达
- 汇编语言中用三个概念来表达数据的位置。
- 1、立即数(idata)
- 2、寄存器
- 3、段地址(SA)和偏移地址(EA)
8.4 寻址方式总小结
8.5 指令要处理的数据有多长
- 8086 CPU可以处理byte和word两种数据尺寸。
- 通过寄存器指明要处理的数据尺寸;push指令只进行字操作,若没有寄存器名存在的情况下,用操作符word ptr或者byte ptr指明内存单元的长度。 例如
mov word ptr ds:[0],1
inc word ptr [bx]
inc word ptr ds:[0]
add byte ptr [bx],2
;假设内存2000:1000 FF FF FF FF FF FF ……
;如果用以下指令
mov ax,2000H
mov ds,ax
mov byte ptr [1000H],1
;那么内存中的内容变为
;2000:1000 01 FF FF FF FF FF ……
如果是用以下指令
mov ax,2000H
mov ds,ax
mov word ptr [1000H],1
;那么内存中的内容变为
;2000:1000 01 00 FF FF FF ……
8.6 寻址方式的综合应用
- 初步汇编代码
mov ax,seg
mov ds,ax
mov bx,60h;确定记录物理地址:ds:bx
mov word ptr [bx+0ch],38;寄存器相对寻址 排名字段改为38
add word ptr [bx+0eh],70;收入字段增加70
mov si,0;用si来定位产品字符串中的字符
mov byte ptr [bx+10h+si],'V';相对基址变址寻址
inc si
mov byte ptr [bx+10h+si],'A'
inc si
mov byte ptr [bx+10h+si],'X'
- c语言描述
struct company /*定义一个公司记录的结构体*/
{
char cn[3]; /*公司名称*/
char hn[9]; /*总裁姓名*/
int pm; /*排名*/
int sr; /*收入*/
char cp[3]; /*著名产品*/
};
struct compant dec={"DEC","Ken Olsen",137,40,"PDF"};
/*定义一个公司记录的变量,内存中将存有一条公司的记录*/
mian()
{
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;记录首地址送入bx
mov word ptr [bx].och,38;排名字段改为38
add word ptr [bx].0eh,70;收入字段增加70
;产品名字段改为字符串'VAX'
mov si,0
mov byte ptr [bx].10h[si],'V'
inc si
mov byte ptr [bx].10h[si],'A'
inc si
mov byte ptr [bx].10h[si],'X'
- 多种寻址方式为结构化数据的处理提供了方便。
- 一般用[bx+idata+si]的方式来访问结构体,用idata定位结构体中的某一数据项,用si定位数组项中的每个元素。 例如:[bx].idata、[bx].idata[si]。
8.7 div指令
- div(divide)是除法指令,可用乘法模拟,格式为:
div reg(寄存器)
div 内存单元。
- 除数:8位或16位,在寄存器或内存单元中;被除数:默认放在AX或DX和AX中。
div byte ptr ds:[0]
div byte ptr [bx+si+idata]
;al放商,ah放余数
div word ptr es:[0]
div word ptr [bx+si+idata]
;ax放商,dx放余数
除数 | 被除数 |
---|---|
8位 | 16为(AX) |
16位 | 32位(DX高16位+AX低16位) |
- 8位或16位看的是除数。
运算 | 8位 | 16位 |
---|---|---|
商 | AL | AX |
余数 | AH | DX |
- 利用除法指令计算10001/100编程
;被除数1001可用ax寄存器存放,除数100可用8位寄存器存放,要进行8位除法。
mov ax,1001
mov bl,100
div bl
;执行后al的值等于0AH(10),ah的值等于1(余数为1)。
- 利用除法指令计算100001/100编程
;被除数100001大于2^16=65535(FFFF),不能用ax来存放,要用dx和ax两个寄存器联合存放。除数小于255,可用一个8位寄存器存放,但是被除数是32位的,除数应为16位,所以要用一个16位寄存器来存放除数。
;100001的十六进制为186A1H,100001的高16位(1)存放在dx,低16位(86AH)存放在ax中。
mov dx,1
mov ax,86A1H
mov bx,100
div bx
;执行后ax内容等于03E8H(即1000),dx的值等于1(余数)。
8.8 伪指令dd
- db定义字节型数据,dw定于字型数据,dd 定于 dword(double word双字型数据)
data segment
db 1;第一个数据为01h,在data:0处,占1个字节
dw 1;第二个数据为0001h,在data:1处,占1个字
dd 1;第三个数据为00000001h,在data:3处,占2个字
data ends
- 利用除法指令计算 dd 100001H 除以 dw 100,商放在 dw 0中
data segment
dd 100001H;低16位存储在ax中,高16位存储在dx中
dw 100
dw 0
data ends
mov ax,data
mov ds,ax
mov ax,ds:[0];低16位存储在ax中
mov dx,ds:[2];高16位存储在dx中
div word ptr ds:[4]
mov ds:[6],ax
8.9 伪指令dup
- 和db、dw、dd等数据定义伪指令配合使用,用来进行数据的重复。格式 db或者dw或者dd 重复的次数 dup (重复的数据)
- 例如:
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
db 3 dup('abc','ABC')
;定义了18个直接,它们是'abcABCabcABCabcABC'
实验七 寻址方式在结构化数据访问中的应用
- ds已经和data段联系了,数据段不够用时用扩展段ES
;初始化阶段
mov ax,data
mov ds,ax
mov ax,table;data已经被占用
mov es,ax
mov bx,0
mov si,0
mov di,0
mov cx,21
;存放年份,每一个bx就是一个字节
mov al,[bx]
mov es:[di],al
mov al,[bx+1]
mov es:[di+1],al
mov al,[bx+2]
mov es:[di+2],al
mov al,[bx+3]
mov es:[di+3],al
;存放公司的总收入
mov ax,[bx+54H];第一个年收入是dd数据类型,段地址为54H
mov dx,[bx+54H]
mov es:[di+5H],ax
mov es:[di+7H],dx
;存放公司的人数
mov ax,[si+0A8H];第一个人数的数据段地址为0A8H
mov es:[di+0A8H],ax
;计算人均收入并存放
mov ax,[bx+54H]
mov dx,[bx+56H];这两句诗初始化被除数
div word ptr,ds:[si+0A8H];除以人数
mov es:[di+0dH],ax;将商放入指定位置
;为下一次循环时存放数据做准备
add bx,4;bx确定年份和收入
add si,2;si确定人数
add di,16;di确定的是每行的列数
- 完整的程序
assume cs:codesg,ds:data,es:table
data segment
db '1975','1976' '1977' ……
dd 16,22,382 ……
dw 3,7,9 ……
;数据在题目中
data ends
table segment
db 21 dup('year summ ne ?? ')
table ends
start:mov ax,data
mov ds,ax
mov ax,table
mov es,ax
mov bx,0
mov si,0
mov di,0
mov cx,21
s:mov al,[bx]
mov es:[di],al
mov al,[bx+1]
mov es:[di+1],al
mov al,[bx+2]
mov es:[di+2],al
mov al,[bx+3]
mov es:[di+3],al
mov ax,[bx+54H]
mov dx,[bx+56H]
mov es:[di+5H],ax
mov es:[di+7H],dx
mov ax,[si+0A8H]
mov es:[di+0AH],ax
mov ax,[bx+54H]
div word ptr ds:[si+0A8H]
mov es:[di+0dH],ax
add bx,4
add si,2
loop s
mov ax,4c00h
int 21h
codesg ends
end start
九、转移指令的原理
引言
- 可以修改IP,或者同时修改CS和IP的指令统称为转移指令。 简单的来说可以控制CPU执行内存中某处代码的指令就是转移指令。
- 8086
- CPU的转移行为有只修改的段内转移(如jmp ax) 和同时修改该CS和IP的段间转移(如jmp 1000:0)。其中段内转移分为短转移(IP的修改范围为-128~127)和近转移 (IP的修改范围为-32768~32767)。
- 8086 CPU的转移指令分为以下几类:
- 无条件转移指令(如:jmp)
- 条件转移指令
- 循环指令(如:loop)
- 过程
- 中断
9.1 操作符offset
- offset是伪指令,由编译器处理,它的功能是取得标号的偏移地址。
assume cs:codesg
codesg segment
start:mov ax,offset start;相当于 mov ax,偏移地址0,段地址是从0开始
s:mov ax,offset s;相当于 mov ax,3,标记的是代码段中的第二条指令,第一条指令长度为3个字节,则s的偏移地址为3
codesg ends
end start
9.2 jmp指令
- jmp为无条件转移,可以只修改IP,也可以同时修改CS和IP。
- jmp需要两种信息
- 1、转移的目的地址;
- 2、转移的距离(段间转移、段内转移、段内近转移)。
9.3 依据位移进行转移的jmp指令
- 段内短转移,jmp short 标号 ,对IP的修改范围是-128~127,一个字节的空间,即向前转移最多128字节,向后最多127字节。short 表明指令进行的是短转移,标号指明了指令要转移的目的地,转移指令结束后CS:IP指向标号处的指令。
assume cs:codesg
codesg segment
start:mov ax,0
jmp short s
add ax,1
s:inc ax
codesg ends
end start
-
一般汇编指令中的立即数(idata)会出现在对应的机器指令中。而jmp指令的机器指令并不包含目的地址,包含的是相对于当前IP的转移位移,CPU并不需要目的地址就可以实现对IP的修改。
CPU执行指令的过程 在 2.10 CS和IP
-
jmp short s 指令的读取和执行过程:
- 1、CS:IP指向jmp short s 的机器码;
- 2、读取指令码进入指令缓冲器
- 3、 改变IP,(IP)=(IP)+所读取指令的长度,IP指向下一个指令;
- 4、CPU执行指令缓冲器中的指令;
- 5、执行后CS:IP继续指向下一个指令
-
jmp short 标号的功能为(IP)=(IP)+8位位移。
- 1、8位为=标号处的地址-jmp指令后的第一个字节的地址;
- 2、short 指明此处的位移为8位;
- 3、8位位移的范围为-128~127,用补码表示。
- 4、8位位移由编译程序在编译时算出的。
-
jmp near ptr 标号 指令实现段内近转移,功能为(IP)=(IP)+16位位移。
- 1、16位为=标号处的地址-jmp指令后的第一个字节的地址;
- 2、nearptr 指明此处的位移为16位;
- 3、16位位移的范围为-32769~32767,用补码表示。
- 4、16位位移由编译程序在编译时算出的。
9.4 转移的目的地址在指令中的jmp指令
- jmp far ptr 段间转移,又称为远转移
- 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
- 机器码中包含了转移的目的地址。
附注3 汇编编译器(masm.exe)对jmp的相关处理
9.5 转移地址在寄存器中的jmp指令
- jmp 16位寄存器,功能是16位寄存器赋值给IP,实现段内的近(短)转移。
- 参考 2.11 修改CS、IP的指令
9.6 转移地址在内存中的jmp指令
- 转移地址在内存中的jmp指令有两种格式:
- 1、jmp word ptr内存单元地址(16位只能实现段内转移)。 功能是从内存单元地址处开始存放一个字(转移的目的偏移地址),内存单元地址可用寻址方式的格式给出。
mov ax,0123H mov ds:[0],ax jmp word ptr ds:[0] ;相当于 jmp ax,执行后(IP)=0123h mov ax,0123H mov [bx],ax jmp word ptr [bx] ;执行后(IP)=0123h
- 2、jmp dword ptr 内存单元地址(段间转移)。 功能:从内存单元地址处开始存放两个字型数据,高地址是转移的目的段地址,低地址处是转移的目的偏移地址。(CS)=(内存单元地址+2),(IP)=(内存单元地址),内存单元地址可用寻址方式的任一格式给出。
mov ax,0123H mov ds:[0],ax mov word ptr ds:[2],0 jmp dword ptr ds:[0] mov ax,0123H mov [dx],ax mov word ptr [bx+2],0 jmp dword ptr [bx] ;执行后 (CS)=0,(IP)=0123H CS:IP指向0000:0123
检测点 9.1
9.7 jcxz指令
-
指令格式为jcxz 标号,如果cx的值为0,则转移到标号处执行,不为0则向下执行。
- 当cx的值为0时,(IP)=(IP)+8位位移,8位位移=标号处的地址-jcxz指令后的第一个字节的地址。
- 8位位移的范围是-128~127,用补码表。
- 8位位移由编译器在编译时算出。
- jcxz指令是有条件转移指令,所有的条件转移指令都是短指令,在对应的机器码中包含转移的位移而不包含目的地址,对IP的修改范围都为-128-127。
检测点 9.2
9.8 loop指令
-
loop指令为循环指令,所有的循环指令都是短转移,在对应的机器码中包含转移的位移而不包含目的地址。操作i:
- cx先自减1;
- 当cx的值不为0时,(IP)=(IP)+8位位移,8位位移=标号处的地址-loop指令后的第一个字节的地址。
- 8位位移的范围是-128~127,用补码表。
- 8位位移由编译器在编译时算出。
检测点 9.3
9.9 根据位移进行转移的意义
jmp short 标号
jmp near ptr 标号
jcxz 标号
loop 标号
- 它们对IP的修改时根据转移目的地址和转移起始地址自检的位移来进行的。在它们对应的机器码中不包含转移的目的地址,而包含的是目的地址的位移距离。方便了程序段在内存中的浮动分配,没有固定目的地址的限制,更灵活。
9.10 编译器对转移位移超界的检测
- 根据位移进行转移的指令,它们的转移范围受到了转移位移的限制,如果在源程序中出现了转移范围超界的问题,在编译时编译器会报错。
assume cs:code
code segment
start: jmp short s
db 128 dup(0)
s:mov ax,0FFFFH
code ends
end start
实验8
-
实验八可以正常退出
assume cs:codesg
codesg segment
mov ax,4c00h
int 21h
start:mov ax,0
s:nop
nop;nop占用两个字节,不执行任何操作
mov di,offset s
mov si,offset s2
mov ax,cs:[si];jmp short s1的机器码给了ax
mov cs:[di],ax;覆盖到指令 s:nop nop那
s0:jmp short s;s那已经被jmp short s1机器码覆盖
s1:mov ax,0
int 21h
mov ax,0
s2:jmp short s1;jmp -8h,向上跳到s1,s1又向上跳-10字节
nop
codesg ends
end start
实验9
assume cs:code,ds:data,ss:stack
data segment
db'welcome to masm!';定义要显示的字符串(共16字节)
db 02H,24H,71H;定义字符的属性
data ends
stack segment
dw 8 dup(0)
stack ends
code segment
start:
mov ax,data
mov ds,ax
mov ax,stack
mov ss,ax
mov sp,10H
xor bx,bx;bx清零,用来索引颜色
mov ax,0b872H;算出屏幕第12行中间的显存的段起始位置放入ax中
mov cx,3;s3循环控制行数,要显示三个字符串外循环为3次
s3: push cx;三个进栈操作为外循环s3保存相关寄存器的值
push ax;以防止它们的值在内循环中被破坏
push bx
mov es,ax;此时es为屏幕第12行中间的显存的段起始位置
mov si,0;si用来索引代码列的字符
mov di,0;di用来定位目标列
mov cx,10H
;s1循环控制存放的字符,一个字符串中含有10H个字节内循环为10H次
s1: mov al,ds:[si]
mov es:[di],al
inc si
add id,2
loop s1;吃循环实现偶地址中存放字符
mov di,1;设置di的值为1,为在显存奇数地址中存放字符的颜色属性做准备
pop bx
mov al.ds:[bx+10H];取消颜色属性
inc bx
mov cx,10H;第二个内循环也为10H
s2: mov es:[di],al
add di 2
loop s2;此循环实现奇数地址存放字符的颜色属性
;以下4句为下一趟外循环做准备
pop ax
add ax,0AH;将显存的段地址起始地址设置为当前行的下一行
;[在段地址中甲0aH,相当于在偏移地址中加了0a0h(=160d)]
pop cx
loop s3
mov ax,4C00H
int 21H
code ends
end start
- welcome to masm
十、CALL和RET指令
引言
- 回想程序之间的加载返回过程。
- call和ret指令都是转移指令,它们都修改IP或者同时修改CS和IP,经常被共用来实现程序的设计。
- 这一章讲解call和ret指令的原理。
10.1 ret和retf指令
- ret指令用栈中的数据来修改IP的内容,从而实现近转移。
- CPU执行ret指令时:
- 1、(IP)=((SS)*16+(SP)),指向栈顶
- 2、(SP)=(SP)+2
- retf指令用栈中的数据,修改CS和IP的内容,从而实现远转移。
- CPU执行retf指令时,进行下面两步操作:
- 1、(IP)=((SS)*16+(SP))
- 2、(SP)=(SP)+2
- 3、(CS)=((SS)*16+(SP))
- 4、(SP)=(SP)+2
- 用汇编的语法来解释ret和retf指令:
- CPU执行ret指令相当于进行 POP IP
- CPU执行retf指令相当于进行 POP IP和POP CS
assume cs:codesg
stack segment
db 16 dup(0)
stack ends
codesg 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
codesg ends
end start
assume cs:codesg
stack segment
db 16 dup(0)
stack ends
codesg 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
codesg ends
end start
检测点 10.1
10.2 call指令
- call指令经常跟ret指令配合使用,CPU执行call指令时:
- 1、将当前的IP或者CS和IP压入栈;
- 2、转移(jmp)。
- call指令除了不能实现短转移之外,call指令实现转移的方法和jmp指令的原理相同。call指令实现段间的转移(远转移)或近转移。
10.3 依据位移进行转移的call指令
- call标号(将当前的IP压入栈后转到目标处执行指令),执行时进行以下操作:
- 1、(SP)=(SP)-2
((SS)*16+(SP))=(IP) - 2、(IP)=(IP)+16位位移;
- 3、16位位移=标号处的地址减去call指令后的第一个字节的地址。16位位移的范围是-32768~32767,用补码表示。16位位移由编译器编译时算出。
- 1、(SP)=(SP)-2
- 用汇编语法解释call指令:
push IP
jmp near 标号
检测点 10.2
10.4 转移的目的地址在指令中的call指令
- call far ptr 标号 实现的是段间转移,执行时:
- 1、CS先自减2;
- 2、CS的值等于SS的值乘以16加上SP的值,SP自减2,IP的值等于SS的值*16加上SP的值;
- 3、CS的值等于标号所在的段地址,IP的值等于标号所在的偏移地址.
- 用汇编语法解释call指令:
push CS
push IP
jmp far ptr 标号
检测点 10.3
10.5 转移地址在寄存器中的call指令
- 指令格式是:call 16位寄存器,功能是:
- 1、SP的值先自减2;
- 2、IP的值SS的值乘以16再加上SP的值;
- 3、 IP的值等于16位寄存器的内容。
- 用汇编语法解释此种call指令,CPU执行call 16位reg时,相当于:
push IP
jmp 16位寄存器
检测点 10.4
10.6 转移地址在内存中的call指令
- 转移地址在内存中的call指令有两种格式
call word ptr 内存单元地址;段内跳转
call dword ptr 内存单元地址;段间跳转
- 用汇编语法解释call word ptr 内存单元地址
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 内存单元地址
push CS
push IP
jmp word ptr 内存单元地址
- 例子:
mov sp,10h
mov ax,0123H
mov ds:[0],ax
mov word ptr ds:[0],0
call dword ptr ds:[0]
;执行后IP的值等于0123H,SP的值等于0CH,CS的值等于0
检测点 10.5
10.7 call和ret的配合使用
- 下面的程序返回前,bx中的值是多少?
assume cs:code
code segment
start:
mov ax,1
mov cx,3
call s
mov bx,ax
mov ax,4c00h
int 21h
s:
add ax,ax
loop s
ret
code ends
end start
- 具有一定功能的程序段称为子程序,用call转去执行,在子程序后面使用ret实现返回。
- 具有子程序的源程序的框架如下
10.8 mull指令
- mull指令时乘法指令,相乘的两个数要么都是8位的,要么都是16位的
- 8位:在AL中和8位寄存器中或内存字节单元中;
- 16位:在AX中和16位寄存器或内存字单元中。
- 结果
- 8位的存放在AX中;
- 16位:DX(高位)和AX(低位)中。
mull reg
mull 内存单元
mull byte ptr ds:[0]
mull word ptr [bx+si+idata]
;(ax)=(ax)*((ds)*16+(bx)+(si)+idata)
;(dx)=(ax)*((ds)*16+(bx)+(si)+idata)
;计算100*10,两个数都小于255,可以做8位乘法
mov ax,100
mov bx,10
mull bl
;结果(ax)=1000(03E8H)
;计算100*1000,1000都大于255,要做16位乘法
mov ax,100;高位自动补零
mov bx,10000
mull bx
;结果(ax)=4240H,(dx)=000FH,F4240H=1000000
10.9 模块化程序设计
- cal和ret指令共同支持汇编语言编程中的模块化设计。
10.10 参数和结果传递的问题
-
用寄存器来存储参数和结果是最常用的方法。对于存放参数的寄存器和存放结果的寄存器,调用者和子程序的读写操作恰恰相反:
- 调用者将参数送入参数寄存器,从结果寄存器中取到返回值;
- 子程序 从参数寄存器中取到参数,将返回值送入结果寄存器。
编程:根据提供的N来计算N^3
cube:mov ax,bx
mul bx
mul bx
ret
- 编程:计算data段中第一组数据的3次方,结果保存在后面一组dword单元中
assume cs:code
data segment
dw 1,2,3,4,5,6,7,8
dd 8 dup (0)
data ends
code segment
start:
mov ax,data
mov ds,ax
mov si,0;ds:si指向第一组word单元
mov di,16;ds:di指向第二组dword单元
mov cx,8
s: mov bx,[si]
call cube
mov [di],ax
mov [di+2],dx
add si,2;ds:di指向下一个word单元
add di,4;ds:di指向下一个dword单元
loop s
mov ax,4c00h
int 21h
cube:mov ax,bx
mul bx
mul bx
ret
code ends
end start
10.11 批量数据的传递
- 将批量数据放在内存中,然后将他们呢所在内存空间的首地址放在寄存器中,传递给需要的子程序,批量数据的返回结果也是采用同样的方法。除此之外还可以用栈来传递参数。
assume cs:code
data segment
db'conversation'
data ends
start:
mov ax,data
mov ds,ax
mov si,0;ds:si指向字符串(批量数据)所在空间的首地址
mov cx,12;cx存放字符串的长度
call capital
mov ax,4c00h
int 21h
capital:
add byte ptr [si],11011111B
inc si
loop capital
ret
code ends
10.12 寄存器冲突的问题
- 编程:将一个全是字母,以0结尾的字符串转化为大写
capital:
mov cl,[si];低8位
mov ch,0;高8位设置为0
jcxz ok;如果(cx)=0则结束,如果不是0则处理
and byte ptr [si],11011111B
inc si
jmp short capital
ok:
ret
- 编程将data段中的字符串全部转化为大写
assume cs:code
data segment
db'word',0
db'unix',0
db'wind',0
db'good',0
data ends
;此程序有bug,cx有问题
assume cs:code
data segment
db'word',0
db'unix',0
db'wind',0
db'good',0
data ends
code segment
start:
mov ax,data
mov ds,ax
mov bx,0
mov cx,4
s:
mov si,bx
call capital
add bx,5
loop s
mov ax,4c00h
int 21h
capital:
mov cl,[si]
mov ch,0
jcxz ok
and byte ptr [si],11011111b
inc si
jmp short capital
ok:
ret
code ends
end start
实验十
- 实验10.1 显示字符串
assume cs:code
data segment
db 'welcome to masm!',0
data ends
code segment
start:
mov dh,8;行号
mov dl,3;列号
mov cl,2;颜色属性
mov ax,data
mov ds,ax
mov si,0
call show_str
mov ax,4c00h
int 21h
show_str:;子程序
push cx
push si
mov al,0A0h;每行有80*2=160个字节=0a0h
dec dh;行号在显存中下标从0开始,所以减1
mul dh;相当于从第(n-1)*0a0h个byte单元开始
mov bx,ax;定位好的位置偏移地址存放在bx里(行)
mov al,2;每个字符占2个字节
mul dl;定位列,结果ax存放的是定位好的列的位置
sub ax,2;列号在显存中下标从0开始,又因为是偶字节存放字符,所以减2
add bx,ax;此时bx中存放的是行与列的偏移地址
mov ax,0B800h;显存开始的地方
mov es,ax;es中存放的是显存的第0页的起始地段地址
mov di,0;di指向显存的偏移地址,确定指向下一个要处理的字符的位置
mov al,cl;cl存放颜色参数,下边cl要用来临时存放要处理的字符
mov ch,0;下边cx存放的是每次准备处理的字符
s:
mov cl,ds:[si];指向'welcome to masm ',0
jcxz ok;cl为0时跳转
mov es:[bx+di],cl;偶地址存放字符
mov es:[bx+di+1],al;奇地址存放字符的颜色属性
inc si
add di,2;指向了下个字符
jmp short s ;无条件跳转,jcxz是离开的关键跳
ok:
pop si
pop cx
ret;定义结束
code ends
end start
- 实验10.2
assume cs:code,ss:stack
stack segment
dw 8 dup(0)
stack ends
code segment
start:
mov ax,stack
mov ss,ax
mov sp,10h
mov ax,4240h
mov dx,0fh
mov xx,0ah
call divdw
mov ax,4c00h
int 21h
divdw:
push ax;低16位先保存
mov ax,dx;ax这时是高16位了
mov dx,0;为了不影响余数位和高位数
div cx
mov bx,ax
pop ax
div cx
mov cx,dx
mov dx,dx
ret
code ends
end start
- 实验10.3
assume cs:code,ds:data
data segment
db 10 dup(0)
data ends
code segment
start:
mov ax,12666
mov bx,data;指向字符串的首地址
mov ds,bx
mov si,0
call dtoc;实现将word型整数转化为字符串并存储
mov dh,8;打印初始化
mov dl,3
mov cl,0cah
call show_str;开始打印字符串
mov ax,4c00h
int 21h
dtoc:
push dx
push cx
push ax
push si
mov bx,0;bx在子程序中用来存放位数,用栈来临时存放修改后的字符
s1:
mov cx,10d;d表示十进制,cx准备被除,用取余法来取出数字
mov dx,0
div cx;除以十
mov cx,ax;得到的商复制给cx,要利用jcxz
jcxz s2;当商为0则跳到s2
add dx,30h;余数加上30h得到相应的ascii码
push dx
inc bx
jmp short s1
s2:
add ax,30h;当商为0的时候,余数为个位
push dx
inc bx;再进行一次栈操作(补充当商为零而余数不为零时的情况)
mov cx,bx;总共有bx位进栈,所以循环次数为bx
mov si,0
s3:
pop ax;s3实现将栈中的数据依次出栈放到指定的内存中
mov [si],al
inc si
loop s3
okay:
pop bx
pop si
pop ax
pop dx
ret
show_str:;子程序
push bx
push cx
push si
mov al,0A0h;每行有80*2=160个字节=0a0h
dec dh;行号在显存中下标从0开始,所以减1
mul dh;相当于从第(n-1)*0a0h个byte单元开始
mov bx,ax;定位好的位置偏移地址存放在bx里(行)
mov al,2;每个字符占2个字节
mul dl;定位列,结果ax存放的是定位好的列的位置
sub ax,2;列号在显存中下标从0开始,又因为是偶字节存放字符,所以减2
add bx,ax;此时bx中存放的是行与列的偏移地址
mov ax,0B800h;显存开始的地方
mov es,ax;es中存放的是显存的第0页的起始地段地址
mov di,0;di指向显存的偏移地址,确定指向下一个要处理的字符的位置
mov al,cl;cl存放颜色参数,下边cl要用来临时存放要处理的字符
mov ch,0;下边cx存放的是每次准备处理的字符
S:
mov cl,ds:[si]
jcxz ok
mov es:[bx+di],cl
mov es:[bx+di+i],al
inc si
add di,2
jmp short s
ok:
pop si
pop cx
pop bx
ret
code ends
end start
十一、标志寄存器
引言
- CPU内部的寄存器中有一种特殊的寄存器:
- 1、用来存储相关指令的某些执行结果;
- 2、用来为CPU执行相关指令提供行为依据;
- 3、用来控制CPU的相关工作方式。
- 8086 CPU的标志寄存器只有16位,其中存储的信息通常被称为程序状态字(PSW)。
- 本章中的标志寄存器(以下简称为flag)。某些指令将影响标志寄存器中的多个标志位,这些被影响的标记位比较全面地记录ill指令的执行结果,为相关的处理提供了所需的依据。
- flag寄存器是按位起作用的,每一位都有专门的含义,记录特定的信息,与其他寄存器不一样。
- 8086 CPU的flag寄存器的结构:
- flag的1、3、5、12、13、14、15位在8086 CPU中没有使用,而0、2、4、6、7、8、9、10、11位都具有特殊的含义。
11.1 ZF(zero flag)标志
- flag的第6位是ZF,零标志位,它记录相关指令执行后,结果为0,ZF=1(记录下是0这样的肯定信息),结果不为0,ZF=0(表示结果非0)。
mov ax,1
sub ax,1
mov ax,1
and ax,0
;指令执行后,结果为0,则ZF=1
mov ax,2
sub ax,1
mov ax,1
or ax,0
;指令执行后,结果为1,则ZF=0
- 在8086CPU中,add、sub、mul、div、inc、or、and等它们大多都是运算(逻辑运算或是算术运算)指令,是影响标志寄存器的,而mov、push、pop等传送指令对标志寄存器一般没有影响,因为不会产生结果。
11.2 PF标志
- flag的第2位是PF,奇偶标志位,记录指令执行后结果所有的二进制位中1的个数。为偶数,PF=1,为奇数PF=0
mov al,1
add al,10
;执行结果为00001011B,有3个1,则PF=0
mov al,1
or al,10
;执行后结果为00000011B,有2个1,则PF=1
11.3 SF(sign flag)标志
- flag的第7位是SF符号标志位,记录指令执行后结果为负则SF=1,结果为正,SF=0。弱国我们将数据当作无符号数来运算,SF的值没有意义,虽然相关的指令影响了它的值。
- 有符号数与补码
- 计算机默认把负数用补码记录。
- 00000001B,可以看作无符号数1,也可以看作符号数+1;
- 10000001B,可以看作无符号数129,也可以看作有符号数-127。
- 补码
mov al,10000001B
add al,1
;执行指令后al的值是10000010B,无符号数130,有符号数-126
检测点 11.1
11.4 CF(carry flag)标志
-
flag的第0位是CF,进位标志位。一般情况下,在进行无符号运算的时候,它记录了运算结果的最高有效位向更高位的进位值或从更高位的借位值。对于位数为N的无符号数,其对应的二进制信息的最高位为N-1位的最高有效位,假想存在第N位。
- 两个8位的数据运算可能产生进位或者借位,由于这个进位值在8位数中无法保存,8086CPU就用flag的CF位来记录这个进位值。
mov al.98h
add al,al;执行后(al)=30h,cf=1,cf记录了从最高有效位向更高位的进位值
add al,al;执行后(al)=60h,cf=0,cf记录了从更高有效位向更高位的进位值
mov al,97h
sub al,98h;执行后(al)=ffh,cf=1,cf记录了向更高位的借位值
sub al,al;执行后(al)=0,cf=0,cf记录了向更高位的借位值
11.5 OF(overflow flag)标志
- 如果运算结果超出了机器所能表达的范围(对于8位有符号数,机器所能表达的范围是-128~127)将产生溢出,对有符号数而言。
assume cs:code
code segment
start:
mov al,01100010b
add al,01100011b
mov ax,4c00h
int 21h
code ends
end start
assume cs:code
code segment
start:
mov al,10001000b
add al,11110000b
mov ax,4c00h
int 21h
code ends
end start
assume cs:code
code segment
start:
mov al,98h
add al,al
add al,al
mov ax,4c00h
int 21h
code ends
end start
assume cs:code
code segment
start:
mov al,97h
sub al,98h
add al,al
mov ax,4c00h
int 21h
code ends
end start
- CF是对无符号数运算有意义的标志位,而OF是对有符号数运算有意义的标志位; CPU用CF位来记录无符号数运算是否产生了进位,用OF位来记录有符号数是否产生了溢出。用SF位来记录结果的符号
mov al,98d
add al,99d
;对于无符号数运算,98+99没有进位,CF=0
;对于有符号数运算,98+99发生溢出,OF=1
检测点 11.2
11.6 adc指令
- adc是带有进位加法指令,利用了CF位上记录的进位值。格式:adc操作对象1,操作对象2,功能:操作对象1=操作对象1+操作对象2+CF。
mov ax,2
mov bx,1
sub bx,ax
adx ax,1
;执行后 (ax)=4,相当于计算(ax)+1+CF=2+1+1+4
mov ax,1
add ax,ax
adc ax,3
;执行后(ax)=5,相当于执行(ax)+3+CF=2+3+0=5
mov al,98H
add al,al
adx al,3
;执行后 (ax)=34H,相当于执行(ax)+3+CF=30H+3+1=34H
- 由adc指令前面的指令决定在执行adc指令的时候加上的CF的值的含义,关键在于所加上的CF的值是被什么指令设置的。如果CF的值是被sub指令设置的,那么它的含义就是借位值;如果是被add指令设置的,那么它的含义就是进位值。加法运算先是低位相加,再高位相加加上低位相加产生的进位值。
- 编程:计算1EF000H+201000H,结果存放在AX(高16位)和BX(低16位)中。
mov ax,001EH
mov bx,0F000H
add bx,1000H
adc ax,0020H
- 编程:1EF0001000H+2010001EF0H,结果存放在AX(高16位)、BX(次16位)中和cx(低16位)。
mov ax,001EH
mov bx,0F000H
mov cx,1000H
add cx,1EF0H
add bx,1000H
adc ax,0020H
- 编程:对两个128位数据进行相加
assume cs:code,ds:data
data segment
db 16 dup(88H)
db 16 dup(11H)
data ends
code segment
start:
mov ax,data
mov ds,ax
mov si,0
mov di,16
mov cx,8
call add128
mov ax,4C00H
int 21H
add128:
push ax
push cx
push si
push di
sub ax,ax;将CF设置为0
s:
mov ax,[si]
adc ax,[di]
mov [si],ax
inc si;不能用add si,2代替
inc si;因为会影响cf位
inc di;而loop和inc不会影响
inc di
loop s
pop di
pop si
pop cx
pop ax
ret
code ends
end start
11.7 sbb指令
- sbb是带借位减法指令,利用了CF位上记录的借位值。格式:sbb 操作对象1,操作对象2,功能是:操作对象1=操作对象1-操作对象2-CF。
- 利用sbb指令我们可以对任意大的数据进行减法运算。sbb和adc是基于同样的思想设计的两条指令,在应用思路上sbb和adc类似。
- 编程:计算003E1000H-00202000H,结果放在ax、bx中
mov bx,1000H
mov ax,003EH
sbb bx,2000H
sbb ax,0020H
11.8 cmp指令
- cmp是比较指令,功能上相当于减法指令,只是不保存结果。格式:cmp 操作对象1,操作对象2.功能:计算操作对象1-操作对象2但不保存结果,仅仅是根据计算结果对标志寄存器进行设置。
- cmp指令运算执行后通过做减法将对标志寄存器产生影响,其他相关指令通过识别这些被影响的标志寄存器位来得知比较结果。
cmp ax,ax
;执行后结果为0,ZF=1,PF=1,SF=0,CF=0,OF=0
mov ax,8
mov bx,3
cmp ax,bx
;执行后ax、bx的值不变,ZF=0,PF=1,SF=0,CF=0,OF=0
cmp ax,bx
- CPU在执行cmp指令时也包含了对无符号数运算和进行有符号数运算,所以利用cmp指令可以对无符号数进行比较也可以对有符号数进行比较。
-
单纯地考察SF的值不可能知道结果的正负。因为SF记录的只是可以在计算机中存放的相应位数的结果的正负(例如:add ah, al执行后,SF记录的是ah中的8位二进制信息所表示的数据的正负)。如果没有溢出发生的话,实际结果的正负和逻辑上真正结果的正负就一致了。。例如:22H(34)-0A0H(-96)=130=82H(是-126的补码),SF=1。
- 1、如果SF=1或SF=0,OF=0,逻辑上真正结果的正负=实际结果的正负。
- 2、如果SF=1或SF=0,OF=1,逻辑上真正结果的负正=实际结果的正负。
11.9 检测比较结果的条件转移指令
- 与cmp相配使用,根据cmp指令的比较结果(cmp指令执行后相关标志位的值)进行工作的指令。
- cmp指令可以同时进行两种比较,无符号数比较和有符号数比较,所以根据cmp指令的比较结果进行转移的指令也分为两种:
- 根据无符号数的比较结果进行转移的条件转移指令,它们检测ZF、CF的值;
- 根据有符号数的比较结果进行转移的条件转移指令,它们检测SF、OF、ZF的值。
- 它们所检测的标志位都是cmp指令进行无符号数比较时候记录比较结果的标志位。
指令 | 含义 | 检测的相关标志位 |
---|---|---|
je | 等于则转移 | ZF=1 |
jne | 不等于则转移 | ZF=0 |
jb | 低于则转移 | CF=1 |
jnb | 不低于则转移 | CF=0 |
ja | 高于则转移 | CF=0 and ZF=0 |
jna | 不高于则转移 | CF=1 or ZF=1 |
j | e | ne | b | nb | a | na |
---|---|---|---|---|---|---|
jump | equal | not equal | below | not below | above | not above |
- 编程:如果ah的值等于bh则ah的值等于ah的值加ah的值,否则ah的值等于ah的值加上bh的值。
cmp ah,bh
je s;ZF=1则跳转
add ah,bh
jmp short ok
s:
add ah,bh
ok:ret
- je检测的是ZF的位置,不管je前面是什么指令,只要CPU执行je指令时,ZF=1那么就发生转移。
mov ax,0
mov ax,0
je s
inc ax
s:
inc ax
;执行后ax的值等于1,add ax,0使得ZF=1,所以je指令将进行转移。
课堂练习
- 编程:统计data段中数值为8的字节的个数,用ax保存统计结果。
;方案一
assume cs:code
data segment
db 8,11,8,1,8,5,63,38
data ends
code segment
start:
mov ax,data
mov ds,ax
mov bx,0;ds:bx指向第一个字节
mov ax,0;初始化累加器
mov cx,0
s:
cmp byte ptr [bx],8;和8进行比较
jne next;如果不相等转到next,继续循环
inc ax;如果相等就计数值加1
next:
inc bx
loop s;执行后:(ax)=3
mov ax,4c00h
int 21h
code ends
end segment
</br>
;方案二
assume cs:code
data segment
db 8,11,8,1,8,5,63,38
data ends
code segment
start:
mov ax,data
mov ds,ax
mov bx,0;ds:bx指向第一个字节
mov ax,0;初始化累加器
mov cx,0
s:
cmp byte ptr [bx],8;和8进行比较
je ok;如果不相等转到ok,继续循环
jmp short next;如果不想等就转到next,继续循环
ok:
inc ax;如果相等就计数值加1
next:
inc bx
loop s;执行后:(ax)=3
mov ax,4c00h
int 21h
code ends
end segment
- 编程:统计data段中数值大于8的字节的个数,用ax保存统计结果。
assume cs:code
data segment
db 8,11,8,1,8,5,63,38
data ends
code segment
start:
mov ax,data
mov ds,ax
mov bx,0;ds:bx指向第一个字节
mov ax,0;初始化累加器
mov cx,0
s:
cmp byte ptr [bx],8;和8进行比较
jna next;如果大于8转到next,继续循环
inc ax;如果大于就计数值加1
next:
inc bx
loop s;执行后:(ax)=3
mov ax,4c00h
int 21h
code ends
end segment
检测点 11.3
11.10 DF(direction flag)标志和串传送指令
-
flag的第10位是DF,方向标志位,在串处理指令中,控制每次操作后si(一般指向原始偏移地址)、di(一般指向目标偏移地址)的增减。
- DF=0:每次操作后si、di递增;
- DF=1,每次操作后so、di递减。
- movsb(mov string byte)串传送指令,以字节为单位传送。将ds:si指向的内存单元中的字节送入es:di中,然后根据标志寄存器DF位的值将si和di递增1或递减1。movsw,以字为单位传送。将ds:si指向的内存单元中的字送入es:di中,然后根据标志寄存器DF位的值将si和di递增2或递减2。
-
movsb和movsw进行的是串传送操作中的一个步骤,一般和rep配合使用,格式:rep movsb,rep的作用是根据cx 的值,重复执行后面的串传送指令。由于每次执行一次movsb指令si和di都会递增或递减指向后一个单元或前个单元,则rep movsb就可以循环实现(cx)个字符的传送。
- 1、传送的原始位置;
- 2、传送的目的位置;
- 3、传送的长度;
- 4、传送的方向。
- movsb功能:((es)16+(di))=((ds)16+(si)),如果DF=0,则(si)=(si)+1,(di)=(di)+1;如果DF=1,则(si)=(si)-1,(di)=(di)-1。
- 由于flag的DF位决定着串传送指令执行后,si和di改变的方向,8086CPU提供两条指令对DF位进行设置:
- cld指令:将标志寄存器的DF位设置为0;
- std指令:将标志寄存器的DF位设置为1。
11.11 pushf和popf
- pushf的功能 是件标志寄存器的值压栈,popf是从栈中弹出数据m,送入标志寄存器中。pushf和popf为直接访问标志寄存器提供了一种方法。
;下面的程序执行后ax的值是多少?
mov ax,0
push ax
popf
mov ax,0fff0h
add ax,0010h
pushf
pop ax
and al,11000101b
and ah 00001000b
- 编程:用串传送指令将data段总的第一个字符串复制到它后面的空间中。
assume cs:code
data segment
db'welcome to masm!'
db 16 dup(0)
data ends
code segment
start:
mov ax,data
mov ds,ax
mov si,0;指向data:0
mov es,ax
mov di,16;指向data:16
mov cx,16;rep循环16次
cld;设置DF=0,正向传送
rep movsb
mov ax,4c00h
int 21h
code ends
end start
- 用串传送指令将F00H段中的最后16个字符复制到data段中
assume cs:code
data segment
db 16 dup(0)
data ends
code segment
start:
mov ax,0f00h
mov ds,ax
mov si,0ffffh;指向f0000:ffff
mov ax,data
mov es,ax
mov di,16;指向data:15
mov cx,16;rep循环16次
std;设置DF=1,逆向传送
rep movsb
mov ax,4c00h
int 21h
code ends
end start
检测点 11.4
11.12 标志寄存器在Debug中的表示
标志 | 值为1的标记 | 值为0的标记 |
---|---|---|
OF | OV | NV |
SF | NG | PL |
ZF | ZR | NZ |
PF | PE | PO |
CF | CY | NC |
DF | DN | UP |
十二、内中断
引言
- 中断时CPU处理外部突发事件的一个重要技术。它能使CPU在运行过程中对外部事件发出的中断请求几时进行处理,处理完成后又立即返回断电,基础进行CPU原来的工作。引起中断的原因或是说发出中断请求的来源叫做中断源。根据中断源的不同,可以把中断分为硬件中断和软件中断两大类,而硬件中断又可以分为外部中断和内部中断两类。
- 外部中断一般是指由计算器外部设备发出的中断请求。如:键盘中断、打印机中断、定时器中断等。外部中断时可以屏蔽的中断,业绩是说利用中断控制器可以屏蔽这些外部设备的中断请求。
- 内部中断是指因硬件出错(如突然掉电)或运算出错(如除数为0、单步中断)所引起的中断。内部中断是不可屏蔽的。
- 软件中断其实并不是真正的中断,它们只是可被调用执行的一般程序以及DOS的系统功能调用(int 21)等都是软件中断。
- 中断的优先权:
- 1、除法出错、溢出中断、软件中断;
- 2、不可屏蔽中断;
- 3、可屏蔽中断;
- 4、单步中断。
- 中断信息中包含有标识中断源的类型码。根据CPU的设计,中断源类型码的作用就是用来定位中断处理程序。
12.1 内中断的产生
- 8086CPU内部有以下情况发生时将产生相应的中断信息:
- 1、除法错误;
- 2、单步执行;
- 3、执行into指令;
- 4、执行int指令。
- 8086CPU中的中断类型码如下:
- 1、除法错误:0
- 2、单步执行:1
- 3、执行into指令:4
- 4、执行int指令,该指令格式为int n,n为立即数是提供给CPU的中断类型码。
12.2 中断处理程序
- CPU在收到中断信息后立即去执行该中断信息的处理程序。
12.3 中断向量表
- 中断向量列表就是中断向量(中断处理程序的入口地址)的列表,其在内存中保存,存放着256个中断源说对应的中断处理程序的入口。8086PC机中断向量表放在内存地址0处。从内存0000:0000到0000:03FF的1024(一个物理地址是由段地址和偏移地址构成,要用4个字节来存放)个单元中存放着中断向量表。
- CPU用8位的中断类型码通过中断向量表找到相应的中断处理程序的入口地址。中断向量表中存放的就是各个类型的处理程序的地址,8位的类型码是个索引。
12.4 中断过程
- 用中断码在中断向量表中找到中断处理程序的入口地址,用它来设置CS和IP,使CPU执行中断程序。用中断类型码找到中断向量并用它设置CS和IP,这个工作室由CPU的硬件自动完成的,这个工作的过程被称为中断过程。
- 8086CPU的中断过程:
- 1、从中断信息中取得中断类型码;
- 2、标志寄存器的值入栈,以保护标志位;
- 3、设置标志寄存器的第8位TF和第9位IF的值为0;
- 4、CS的内容入栈,IP的内容入栈;
- 5、从内存地址为中断类型码* 4和中断类型码 *4+2的两个字单元中读取中断处理程序的入口地址设置IP和CS。
- 在最后一步完成后,CPU开始执行由程序员编写的中断处理程序。
12.5 中断处理程序和iret指令
- 常规的步骤
- 1、保存用到的寄存器;
- 2、处理中断;
- 3、 恢复用到的寄存器;
- 4、 用iret指令返回。
- iret指令的功能为相应的按顺序恢复之前保存起来的IP、CS地址和标志位寄存器。用汇编语法描述为:
pop IP
pop CS
popf
12.6 除法错误中断的处理
- 当CPU执行dvi等处罚指令的时候,如果发生了除法溢出错误,将产生中断类型码为0的中断信息,CPU将检测到这个信息然后引发中断过程,转去执行0号中断所对应的中断处理程序。
assume cs:codesg
codesg segment
start:
mov ax,1000h
mov bh,1
div bh
codesg ends
end start
12.7 编程处理0号中断
- 改变0号中断处理程序的功能,在屏幕中间显示字然后再返回操作系统。
- 当发生除法溢出时产生0号中断信息,引发中断过程。
- 此时CPU将进行以下工作(中断过程)
- 当中断0发生时,CPU将转去执行中断处理程序。
- 先进行相关处理,然后向显示缓冲区送字符串,最后返回。
- 改变后的中断处理程序应该放在内存中,因为除法溢出随时可能发生,CPU随时都可能将CS:IP指向改变后的中断处理程序的入口执行程序。
- 把程序存入内存,修改向量表(即将内存地址登记在中断向量表的对应表项中),中断时调用这个内存。
- 当发生除法溢出时产生0号中断信息,引发中断过程。
除法溢出对应的中断类型码为0,它的中断处理程序的入口地址应该从0* 4+2地址单元开始存放,段地址存放在0* 4+2字单元中,偏移地址存放在0*4字单元中。也就是改变后的中断处理程序的段地址0存放在0000:0002字单元中,偏移地址200H存放在0000:0000字单元中。如果要显示的字符串在程序的data段中,那么程序执行完成后返回,它所占用的内存空间被系统释放,在其中存放的信息也可能被别的信息覆盖。
assume cs:code
code segment
start:
mov ax,cs
mov ds,ax
mov si,offset do0;设置ds:di指向源地址
mov ax,0
mov es,ax
mov di,200h;设置es:si指向目的地址
mov cx,offset do0end - offset do0;设置cx为传输长度,编译器可以识别加减乘除运算符
cld;设置传输方向为正
rep movsb
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"welcome to masm!";在代码段中存储数据
do0start:
mov ax,cs
mov ds,ax
mov si,202h;jmp short do0start这条指令栈两个字节
;显示字符串,设置es:di指向字符串
mov ax,0b800h;显存空间,直接显示在显示器上
mov es,ax
mov di,12*160+36*2;这只es:di指向显存空间的中间位置
mov cx,16;设置cx为字符串(welcome to masm!)长度
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
12.8 单步中断
- CPU执行完一条指令之后,如果检测到标志寄存器的TF位为1,则产生单步中断引发中断过程。单步中断的中断类型码为1,它所引发的中断过程如下:
- 1、取得中断类型码;
- 2、标志寄存器入栈,TF、IF设置为0;
- 3、CS、IP入栈;
- 4、指向指定类型码的中断向量表。
12.9 响应中断的特殊情况
- 在有些情况下CPU在执行完当前指令后,即便是发生了中断也不会响应。
在执行完向ss寄存器传送数据的指令后,即便检测到了中断信号CPU也不会响应。因为ss:sp指向栈顶,对他们的设置应该连续完成。如果在执行完设置ss指令后mCPU响应中断引发中断过程,要在栈中压入标志寄存器、CS和IP的值。而ss改变,sp并未改变则ss:sp指向不是正确的栈顶将引发错误。
- 我们要将栈顶设置为1000:0,不应该隔开
应该 | 不应该 |
---|---|
mov ax,1000h | mov ax,1000h |
mov ss,ax | mov ss,ax |
mov sp,0 | mov ax,0 |
mov ax,0 | mov sp,0 |
十三、int 指令
引言
- 在第12章中了解中断过程和除法错误中断和单步中断的处理,这章了解int指令。
13.1 int 指令
- int格式:int n,n为中断类型码,它的功能是引发中断过程。CPU执行int n之力量能够,相当引发一个n号的中断过程,可以在程序中使用int指令调用任何一个中断的中断处理程序。执行过程如下:
- 中断过程从,此处去执行n号中断的中断处理程序。
assume cs:code code segment start: mov ax,0b800h mov es,ax mov byte ptr es:[12*160+40*2],'!' int 0;执行int 0指令,引发中断过程,执行0号中断处理程序 code ends end start
- int指令的最终功能和call类似,都是调用一段程序。一般情况下系统将一些具有一定功能的子程序以中断处理程序的方式提供给应用程序调用,也可以自己编写一些中断处理程序供别人使用。
13.2 编写供应用程序调用的中断例程
- 中断处理程序简称为中断例程。
- 实例1:编写、安装中断7ch的中断例程实现求一word型数据的平方。
- 1、编程实现求平方功能的程序;
- 2、安装程序在0:200处;
- 3、设置中断向量表将程序的入口地址保存在7ch表项中,使其成为中断7ch的中断例程。
;计算
ssume cs:code
code segment
start:
mov ax,3456
int 7ch
add ax,ax
adc ax,dx
mov ax,4c00h
int 21h
code ends
end start
;安装程序
assume cs:code
code segment
start:
mov ax,cs
mov ds,ax
mov si offset sqr;设置ds:si指向源地址
mov ax,0
mov es,ax
mov di,200h;设置es:di指向目的地址
mov cx,offset sqrend- offset sqr;设置cx为传输长度
cld;设置传输方向为正
rep movsb
mov ax,0
mov es,ax
mov word ptr es:[7ch*4],200h
mov word ptr ws:[7ch*4+2],0
mov ax,4c00h
int 21h
sqr:
mul ax
iret
sqrend:
nop
code ends
end start
- CPU执行int 7ch指令进入中断例程之前,标志寄存器、当前的CS和IP都被压入栈中,在执行完中断例程后,用iret指令恢复int 7ch执行前的标志寄存器和CS和IP的值,从而接着执行应用程序。
- int指令和iret指令配合使用与call指令和ret指令配合使用具有相似的思路。
- 实例2:编写、安装中断7ch的中断例程,实现将一个全是字母,以0为结尾的字符串转化为大写。
assume cs:code
data segment
db'conversation',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
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
and byte ptr [si],11011111b
inc si
jmp short change
ok:
pop si
pop cx
iret
capitalend:
nop
code ends
end start
- 要注意用到的寄存器冲突。
13.3 对int、iret和栈的深入理解
- 中断处理程序和iret指令
- 编程:用7ch中断例程完成loop指令的功能,在屏幕中间显示80个"!".
loop指令需要循环次数和到标号的位移。为了模拟loop指令7ch中断例程应具备下面dec cx和如果cx的值不等于0则转移到标号s处。
- int 7ch引发中断过程后,进入7ch中断例程在中断过程中当前的标志寄存器、CS和IP都要压栈。此时压入的CS和IP中的内容分别是调用程序的段地址(可以认为是标号s的段地址)和int 7ch后一条指令的偏移地址(即标号se的偏移地址)。使用iret指令用栈中的内容设置CS、IP,从而实现转移到标号s处。
```asm
assume cs:code
code segment
start:
mov ax,0b800h;显存地址
mov es,ax
mov di,160*12
mov bx,offset s- offset se;设置从标号s的转移位移
mov cx,80
s:
mov byte ptr es:[di],'!'
add di,2
int 7ch;如果cx的值不等于0则转移到标号s处
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
```
13.4 BIOD和DOS所提供的中断例程
- bios中主要包含以下几部分内容:
- 1、硬件操作系统的检测和初始化程序;
- 2、外部中断和内部中断的中断例程;
- 3、用于对硬件设备进行I\O操作的中断例程;
- 4、其他和硬件系统相关的中断例程。
- bios和dos在所提供的中断例程中包含了许多子程序,可以用int指令直接调用。和硬件设备相关的dos中断例程中一般都调用了bios的中断例程
13.5 bios和dos中断例程的安装过程
- 1、开机后8086CPU一加电初始化CS和IP,自动执行FFFF:0处指令,转去执行bios中的硬件系统检测和初始化程序。
- 2、初始化程序将建立bios所支持的中断向量,即将biso提供的中断例程的入口地址登记在中断向量表中。
- 3、硬件系统检测和初始化完成后,调用int 19h进行操作系统的引导。从此将计算机交由操作系统控制。
- 4、dos启动后除完成其他工作外,还将它所提供的中断例程装入内存并建立相应的中断向量。
13.6 bios中断例程应用
- bios和dos提供的中断例程douyongah来传递内部子程序的编号。
- int 10h中断例程是bios提供的中断例程,其中包含了多个和屏幕输出相关的子程序。int 10h中断例程的设置光标位置功能:
mov ah,2;表示调用10h号中断例程的2号子程序,功能为设置光标位置
mov bh,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
```
- 编程:在屏幕的第5行12列显示3个红底高亮闪烁绿色的'a'
```asm
assume cs:code
code segment
mov ah,2;设置光标
mov bh,0;第0页
mov dh,5;dh中放行号
mov dl,12;dl中放列号
int 10
mov ah,9;设置光标
mov al,'a';字符
mov bl,11001010b;颜色属性
mov bh,0;第0页
mov cx,3;字符重复个数
int 10h
mov ax,4c00h
int 21h
code ends
end
13.7 dos中断例程应用
- int 21h 中断例程
mov ah,4ch;程序返回
mov al,0;返回值0是正常返回
;合起来写就是 mov ax,4c00h
int 21h
- int 2h中断例程还具有在光标位置显示字符串的功能、
ds:dx;要显示的字符串需要用 $ 作为结束符 mov ah,9;功能号9,表示在光标位置显示字符串 int 21h
十四、端口
引言
- CPU可以直接读写3个地方的数据:
- 1、CPU内部的寄存器;
- 2、内存单元;
- 3、 端口。
14.1 端口的读写
-
mov、push、pop等死内存读写指令。in和out是端口读指令写指令时in是从端口读取数据,out是往端口写入数据。in和out指令只能用ax或al来存放从端口中读入的数据或要发送到端口中的数据,访问8位短空时用al,访问16位端口时用ax。
- 访问内存:
- mov ax,ds:[8+0];假设(ds)=0
- 执行时,与总线相关的操作:
- 1、CPU通过地址线信息8发出;
- 2、CPU通过控制线发出内存读命令,选中存储器芯片并通知它将要从中读取数据;
- 3、 存储器将8号单元中的数据通过数据线送入CPU。
- 访问端口:
- in al,60h;从60h号端口读入一个字节。
- 执行时与总线相关的操作:
- 1、CPU通过地址线将地址信息60h发出;
- 2、CPU通过控制线发出端口读命令,选中端口所在的芯片,并通知它,将要从中读取数据;
- 3、端口所在的芯片将60h端口中的数据通过数据线送入CPU。
- 访问内存:
;对0~255以内的端口进行读写
in al,20h;从20h端口读入一个字节
out 20h,al;往20h端口写入一个字节
;对256~65535的端口进行读写时,端口放在dx中
mov dx,3f8h;将端口号3f8h送入dx
in al,dx;从3f8端口读入一个字节
out 3f8h,al;往3f8h端口写入一个字节
14.2 CMOS RAM芯片
- CMOA RAM特征:
1、包含一个实时钟和一个有128个存储单元的RAM存储器。(早期的计算机位64个字节)。
2、该芯片靠电池供电。因此关机后其内部的实时钟仍可正常工作,RAM中的信息不会丢失。
3、128个字节的RAM,内部实时钟占用0~0dh单元来保存时间信息默契与大部分单元用于保存系统配置信息,供系统启动时bios程序读取。
bios也提供了相关的程序使用户在开机时配置CMOS RAM中的系统信息。
4、该芯片内部有两个端口,端口地址为70h和71
h。CPU通过这两个端口读写CMOS RAM。5、70h为地址端口,存放要访问的CMOS RAM单元的地址;71h为数据端口,存放从选定的CMOS RAM单元中读取的数据,或要写入到其中的数据。
- CPU对CMOS RAM的读写分两步进行,以读2号单元为例:
- 1、将2送入端口70h;
- 2、从71h读出2号单元的内容。
14.3 shl和shr指令
- shl为逻辑左移指令功能为:
- 1、将一个寄存器或内存单元中的数据向左移位;
- 2、将最后移出的一位写入CF中;
- 3、最低位用0补充。
mov al,01001000b
shl al,1;将al中的数据左移一位
;执行后al的值是10010000b,CF=0
- 如果移动位数大于1时,必须将移动位数放在cl中。
mov al,01010001b
mov cl,3
shl al,cl
;执行后al的值为10001000b,cf=0
- 二进制逻辑左移一位,相当于执行x=x*2(2是进制位)
mov al,00000001b | 执行后al的值等于00000001b=1 |
---|---|
shl al,1 | 执行后al的值等于00000010b=2 |
shl al,1 | 执行后al的值等于00000100b=4 |
shl al,1 | 执行后al的值等于00001000b=8 |
mov cl,3 | |
shl al,cl | 执行后al的值等于01000000b=64 |
- shr为逻辑左移指令功能为:
- 1、将一个寄存器或内存单元中的数据向右移位;
- 2、将最后移出的一位写入CF中;
- 3、最高位用0补充。
- 二进制逻辑右移一位,相当于执行x=x/2(2是进制位)
14.4 CMOS RAM中存储的时间信息
- 在CMOS RAM中以每个信息一字节存放着当前的时间信息:年09h,月08h,日07h,时04h,分02h,秒00h。这些数据以BCD码的方式存放,BCD码以4位为一位。
- 数值26BCD码表示为0010 0110,用两个BCD码表示两位十进制,高4位表示十位,低4位表示各位。
- 编程:在屏幕中间显示当前的月份。
assume cs:code
code segment
start:
;向地址端口70h写入要访问的单元地址,读取CMOS RAM的信息
mov al,8
out 70h,al
in al,71h;从数据端口中取得指定单元中的数据
mov ah,al;al中为从CMOS RAM的8号端口读出数据
mov cl,4
shr ah,cl;ah中为月份的十位数码值
and al,00001111b;ah中为月份的个位数值码
add ah,30h;BCD码值+30h(字符'0')=十进制对应的ASCII码
add al,30h
;用BCD码表示的月份以十进制的形式显示到屏幕上。
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
十五、外中断
15.1接口芯片和端口
- CPU通过端口和外设进行联系。在PC系统的接口卡和主板上,装有各种接口芯片。这些外设接口芯片的内部有若干寄存器,CCPU将这些寄存器当作端口来访问。外设的输入不直接送入内存和CPU,而是送入相关的接口芯片的端口中;CPU向外设的输出也不是直接送入到外设而是先送入端口再由相关的芯片送到外设。
15.2外中断信息
- 外中断源有两类:
- 1、可屏蔽中断;
- 可屏蔽中断时CPU可以不响应的外中断。CPU是否响应可屏蔽中断要看标志寄存器的IF位的设置。
- 2、不可屏蔽中断
- 1、可屏蔽中断;
- 当CPU检测到可屏蔽中断信息时:
- 如果IF=1,则CPU在执行完当前指令后响应中断引发中断过程。
- 如果IF=0,着不响应可屏蔽中断。
- 内中断过程
可屏蔽中断所引发的中断过程,除在第一步的实现上有所不同外,基本上和内中断的中断过程相同。因为可屏蔽中断信息来自于CPU外部,中断类型码是通过数据总线送入CPU的;而内中断的中断类型码是在CPU内部产生的。在中断过程中将IF置0的原因是在进入中断处理程序后禁止其他的可屏蔽中断。
- 8086CPU提供的设置IF的指令如下:
- sti,设置IF=1;
- cli,设置if=0.
- 不可屏蔽中断是CPU必须响应的外中断。当CPU检测到不可屏蔽中断信息时,则在执行完当前指令后立即响应引发中断过程。对于8086CPU不可屏蔽的中断类型码固定为2。所以中断过程中不需要取中断类型码。几乎所有外设引发的外中断都是可屏蔽中断。
- 不可屏蔽中断过程:
- 1、标志寄存器入栈,IF=0,TF=0’
- 2、CS和IP入栈;
- 3、(IP)=(8),(CS)=(0AH)
15.3PC机及键盘的处理过程
- 键盘输入的处理过程:
- 1、键盘输入产生扫描码;
- 2、扫描码送入60h端口;
- 3、引发9号中断;
- 4、执行int 9中断例程。
前三步由硬件系统自动完成,第四步用户可以修改int 9中断程序。
- 按下一个键产生的扫描码称为通码,松开一个键产生的扫描码称为断码。扫描码被送入主板上的相关接口芯片端口地址为60h的寄存器中。
- 扫描码长度为一个字节,通码的第7位为0,断码的第7位为1。即断码=通码+80h。
- bios提供了int 9中断例程,用来进行基本键盘输入处理,主要的工作如下:
- 1、读出60h端口中的扫描码;
- 2、如果是字符键的扫描码就将它和它所对应的字符码(ASCII码)送入内存中的bios键盘缓冲区;
- 键盘的输入到达60h端口时相关的芯片就会向CPU发出中断类型码为9的可屏蔽中断信息。
- CPU检测到该中断信息后,如果IF=1,则相应中断,引发中断过程,转去执行int 9中断例程。
- 如果是控制键(如ctrl)和切换键(如capslock)的扫描码,则将其转变为状态字节(用为进制位记录控制键和切换键状态的字节)写入内存中存储状态字节的单元
- 3、键盘系统进行相关的控制。如向相关芯片发出应答信息。
- bios键盘缓冲区是系统启动后mbios用于存放int 9中断例程所接收的键盘输入的内存区。该内存可以存储15个键盘输入,在bios键盘缓冲区中一个键盘输入用一个字单元存放,高位字节存放扫描码,低位字节存放字符码。0040:17单元存储键盘状态字节该字节记录了控制键和切换键的状态
- 0:置1表表示按下右shift键
- 1:置1表表示按下左shift键
- 2:置1表表示按下ctrl
- 3:置1表表示按下alt
- 4:置1表表示按下scroll指示灯亮
- 5:置1表表示按下numlock,小键盘输入的是数字
- 6:置1表表示按下capslock,输入大写字母
- 7:置1表表示按下insert。处于删除状态
15.4编写int 9中断
- 键盘输入的处理过程
- 编程:在屏幕中间依次显示让人看清的a~z,按下esc键后改变显示的颜色。
;显示字符
code segment
start:
mov ax,0b800h
mov es,ax
mov ah,'a'
s:
mov es:[160*12+40*2],ah
inc ax
cmp ah,'z'
jna s
mov ax,4c00h
int 21h
code ends
end start
</br>
;延迟显示字符
assume cs:code
stack segment
db 128 dup(0)
stack ends
code segment
start:
mov ax,stack
mov ss,ax
mov sp,128
mov ax,0b800h
mov es,ax
mov ah,'a'
s:
mov es:[160*12+40*2],ah
call delay
inc ah
cmp ah,'z'
jna s
mov ax,4c00h
int 21h
delay:
push ax
push dx
mov dx,10h;循环100次,延迟的时间和CPU的计算能力成反比
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
code ends
end start
</br>
;实现IF=0,TF=0步骤
pushf
pop ax
and ah,11111100b
push ax
popf
- int指令在执行时CPU进行的工作
- 完整程序
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
push es:[9*4]
pop ds:[0]
push es:[9*4+2]
pop ds:[2];将原来的int9中断例程的入口地址保存
mov word ptr es:[9*4+2],offset int9
mov es:[9*4+2],cs;在中断向量表中设置新的int 9中断例程的入口地址
mov ax,0b800h
mov es,ax
mov ah,'a'
s:
mov es:[160*12+40*2],ah
call delay
inc ah
cmp ah,'z'
jna s
mov ax,0
mov es,ax
push ds:[0]
pop es:[9*4]
push ds:[2]
pop es:[9*4+2];将中断向量表中int9中断例程的入口恢复为原来的地址
mov ax,4c00h
int 21h
delay:
push ax
push dx
mov dx,10h;循环100次,延迟的时间和CPU的计算能力成反比
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
in al,60h
pushf
pushf
pop bx
and bh,11111100b
push bx
popf
call dword ptr ds:[0];对int指令进行模拟,调用原来的int9中断例程
cmp al,1;esc键盘扫描码
jne int9ret
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
15.5安装新的int 9中断例程
- 小甲鱼版(笔者未成功运行)
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
push es:[9*4]
pop ds:[0]
push es:[9*4+2]
pop ds:[2];将原来的int9中断例程的入口地址保存
mov word ptr es:[9*4+2],offset int9
mov es:[9*4+2],cs;在中断向量表中设置新的int 9中断例程的入口地址
mov ax,0b800h
mov es,ax
mov ah,'a'
s:
mov es:[160*12+40*2],ah
call delay
inc ah
cmp ah,'z'
jna s
mov ax,0
mov es,ax
push ds:[0]
pop es:[9*4]
push ds:[2]
pop es:[9*4+2];将中断向量表中int9中断例程的入口恢复为原来的地址
mov ax,4c00h
int 21h
delay:
push ax
push dx
mov dx,10000h;循环100次,延迟的时间和CPU的计算能力成反比
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
in al,60h
pushf
pushf
pop bx
and bh,11111100b
push bx
popf
call dword ptr ds:[0];对int指令进行模拟,调用原来的int9中断例程
cmp al,1;esc键盘扫描码
jne int9ret
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
- 王爽原版(笔者未成功运行)
assume cs:code
stack segment
db 128 dup(0)
stack ends
code segment
start:
mov ax,stack
mov ss,ax
mov sp,128
push cs
pop ds
mov ax,0
mov es,ax
mov si,offset int9;设置ds:si指向源地址
mov di,204h;设置es:di指向目的地址
mov cx,offset int9end - offset int9;设置cx为传输长度
cld;设置传输方向
rep movsb
push es:[9*4]
pop es:[200h]
push es:[9*4+2]
pop es:[202h]
cli
mov word ptr es:[9*4],204h
mov word ptr es:[9*4+2],0
sti
mov ax,4c00h
int 21h
int9:
push ax
push bx
push cx
push es
in al,60h
pushf
call dword ptr cs:[200h];当此中断例程执行时(CS)=0
cmp al,3bh;f1的扫描码
jne int9ret
mov ax,0b800h
mov es,ax
mov bx,1
mov cx,2000
s:
inc byte ptr es:[bx]
add bx,2
loop s
int9ret:
pop es
pop cx
pop bx
pop ax
iret
int9end:
nop
code ends
end start
第16章 直接定址表
16.1 描述单元长度的标号
assume cs:code
cod segment
a:db 1,2,3,4,5,6,7,8
b:dw 0
start:
mov si,offset a
mov bx,offset b
mov cx,8
s:
mov al,cs:[si]
mov ah,0
add cs:[bx],ax
inc si
loop s
mov ax,4c00h
int 21h
code ends
end start
;代码中的 s、start等都是标号,表示了内存的地址
在code段中使用的标号a,b后面没有:,因此他们可以同时描述内存地址和单元长度的标号
assume cs:code
cod segment
a db 1,2,3,4,5,6,7,8 ;描述了地址code:0,和从这个地址开始以后的内存单元都是直接单元
b dw 0 ;则b是code[8]
start:
mov si,0
mov cx,8
s:
mov al,a[si] ;相当于mov al,cs:0[si]
mov ah,0
add b,ax
inc si
loop s
mov ax,4c00h
int 21h
code ends
end start
检测点 16.1
16.2 在其他段中使用数据标号
- 注意:在后面加有:的地址标号只能在代码段中使用,不能在其他段中使用。
assume cs:code,ds:data
cod segment
a:db 1,2,3,4,5,6,7,8
b:dw 0
data ends
start:
mov ax,data
mov ds,ax
mov si,0
s:
mov al,a[si]
mov ah,0
add b,ax
inc si
loop s
mov ax,4c00h
int 21h
code ends
end start
- 如果现在代码段中直接用数据标号访问数据,则需要用伪指令assume将标号所在的段和一个段寄存器联系起来。 我们可以将标号当作数据来定义,此时编译器将标号所表示的地址当作数据的值。
data segment
a db 1,2,3,4,5,6,7,8
b dw 0
c dw a,b
;相当于 c dw offset a,offset b
data ends
data segment
a db 1,2,3,4,5,6,7,8
b dw 0
c dd a,b
;相当于 c dw offset a,seg a,offset b,seg b
;seg操作符,功能是取得某一标号的段地址
data ends
16.3 直接定址表
- 利用表,在两个数据集合之间建立一种映射关系,使我们可以利用查表的方法根据给出的数据得到其在另一集合中对应数据
- 目的:
- 为了算法的清晰和简洁
- 为了加快运算速度
- 为了使程序易于扩充
- 目的:
- 小练习,编写子程序,以十六进制的形式在屏幕中间显示给定的byte型数据。小技巧,利用映射关系,0-9数值+30h=对应字符的ascii值,10-15和A到F之间的银色关系是:数值+37h=对应字符的ascii的值
assume cs:code
code:segment
mov al,0eh
call showbyte
mov ax,4c00h
int 21
;子程序,用al传送要显示的数据
showbyte:
jmp short show
table db '1023456789ABCDEF';字符表
show:
push bx
push es
mov ah,al
shr ah,1
shr ah,1
shr ah,1
shr ah,1;右移4位,ah中得到高4位的值
and al,00001111b;al中为低4位
mov bl,ah
mov bh,0
mov ah,table[bx];用高4位的值作为相对于table的便宜,取得对应的字符
mov bx,0b800h
mov es,bx
mov es:[160*12+40*2],ah
mov bl,al
mov bh,0
mov al,table[bx];用低4位的值作为相对于table的偏移,取得对应的字符
mov es:[160*12+40*2+2],al
pop es
pop bx
ret
code ends
end start
16.4 程序入口地址的直接定址表
- 小练习
- 清屏:将显存中当前屏幕中的支付设为空格;
- 设置前景色:设置显存中当前屏幕中处于奇地址的属性字节的第0、1、2位;
- 设置背景色:设置显存中当前屏幕中处于奇地址的属性字节的第4、5、6位;
- 向上滚动一行:依次将第n+行的内容复制到第n行处,最后一行为空。
;================================入口函数1=====================================
;入口函数说明;
;用ah传递功能号,0是清屏,1是设置前景色,2是设置背景色,3是向上滚动一行
setscreen:
jmp short set
table dw sub1,sub2,sub3,sub4
set:
push bx
cmp ah,3;判断传递的功能号是否大于3
ja sret
mov bl,ah
mov bh,0
add bx,bx;根据ah中的功能号计算对应子程序的地址在table表中的偏移
call word ptr table[bx];调用对应的子程序
sret;
pop bx
iret
;================================入口函数2=====================================
;入口函数说明;
;用ah传递功能号,0是清屏,1是设置前景色,2是设置背景色,3是向上滚动一行
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
jmp short sret
;子功能==========================================================================
;清屏
sub1:
push bx
push cx
push es
mov bx,0b800h
mov es,bx
mov bx,0
mov cx,2000
sub1s:
mov byte ptr es:[bx],''
add bx,2
loop sub1s
pop es
pop cx
pop bx
ret
;设置前景色
sub2:
push bx
push cx
push es
mov bx,0b800h
mov es,bx
mov bx,1
mov cx,2000
sub2s:
mov byte ptr es:[bx],11111000b
or es:[bx],al
add bx,2
loop sub2s
pop es
pop cx
pop bx
ret
;设置背景色
sub3:
push bx
push cx
push es
mov cl,4
shl al,cl
mov bx,0b800h
mov es,bx
mov bx,1
mov cx,2000
sub3s:
mov byte ptr es:[bx],10001111b
or es:[bx],al
add bx,2
loop sub3s
pop es
pop cx
pop bx
ret
;向上滚动一行
sub4:
push cx
push si
push di
push es
push ds
mov si,0b800h
mov es,si
mov ds,si
mov si,160;ds:si指向第n+行
mov di,0;es:di指向第n行
cld
mov cx,24;共复制24行
sub4s:
push cx
mov cx,160
rep movsb;复制
pop cx
loop sub4s
mov cx,80
mov si,0
sub4s1:
mov byte ptr es:[160*24+si],'';最后一行清空
add si,2
loop sub4s1
pop ds
pop es
pop di
pop si
pop cx
ret;结束
第十七章 使用BIOS进行键盘输入和磁盘读写
略。。。。。。
笔者看不下去了。。。。有兴趣的读者可以继续找相关的资料看。。。