《汇编语言》第二版,王爽著,汇编语言学习笔记。
一、 Introduction
汇编语言,assembly language,是一种用于电子计算机、微处理器、微控制器或其他可编程器件的低级语言,也叫符号语言。不同的设备中,汇编语言对应不同的机器语言指令集,通过汇编过程转换成机器指令。特定的汇编语言和特定的机器语言指令集是对应的,不同平台之间不可直接移植。汇编语言通常用在底层,硬件操作和高要求的程序优化的场合。驱动程序、嵌入式操作和实时运行程序都需要汇编语言。
机器语言是机器指令的集合。机器指令是一台机器可以正确执行的命令。电子计算机的机器指令是一列二进制数字。计算机将之转变为一列高低电平,以使计算机的电子器件受到驱动,进行运算。计算机只能直接读懂机器语言。通过编译器,将汇编语言转换成机器指令,由计算机执行。汇编指令是机器指令的助记符。
汇编语言是直接面向处理器的程序设计语言,就是面向机器的语言。汇编语言操作的对象不是数据,而是寄存器或者存储器。这使得汇编语言比其他语言要快,但是也更加复杂。
二、 汇编语言的机器环境
汇编语言由3类组成。1,汇编语言;2,伪指令;3,其他符号。汇编语言的核心是汇编指令,它决定了汇编语言的特性。
Cpu是计算机的核心部件,它控制整个计算机的运行并进行运算,让一个cpu工作,就必须向它提供指令和数据。指令和数据在存储器中存放,也就是在内存中存放。Cpu工作依赖域内存,磁盘的数据被cpu使用也依赖于内存。在内存或者磁盘上,指令和数据没有任何区别,都是二进制信息。比如:1000 1001 1101 1000,表示一个数据,89D8H(H表示是16进制,即十六进制的89D8);1000 1001 1101 1000,表示一条指令,MOV AX,BX。
存储器被划分为若干个存储单元,每个存储单元从0开始顺序编号。例如:一个存储器有128个存储单元,编号从0—127。
Cpu进行数据的读写,必须和外部器件进行信息的交互。这些信息有:存储单元的地址(地址信息),器件的选择,读或写命令(控制信息),读或者写的数据(数据信息)。电子计算机能处理和传输的信息都是电信号,电信号需要导线传送。cpu和存储芯片之间的导线实现着这些信息的传递。这个导线被称为总线。Cpu和存储器之间的总线,根据作用分为3类,地址总线,数据总线和控制总线。
Cpu通过地址总线来指定存储单元。地址总线上能传送多少个不同的信息,cpu就可以多少个存储单元进行寻址。一个cpu最多有n根地址总线,可以认为这个cpu的地址总线的宽度为n。这样的cpu最多可以寻找2的n次方个内存单元。
Cpu域内存或其他器件之间的数据传送通过数据总线来进行的。数据总线的宽度决定了cpu和外界的数据传送速度。比如8086cpu的数据总线是16位,8088cpu的数据总线是8位。
Cpu对外部器件的控制通过控制总线进行。控制总线是不同控制线的集合。有多少根控制总线,就意味着cpu提供了对外部器件的多少种控制。控制总线的宽度决定了cpu对外部器件的控制能力。
每一台计算机中,都有一个主板,主板上有核心器件和一些主要器件。这些器件通过总线相连。计算机系统中,所有可用程序都收到cpu的控制。Cpu通过插在扩展槽上的接口卡,控制外部设备,比如显示器、音箱和打印机等。
一个pc机中,装有多个存储器芯片。从读写性质上,分为随机存储器(RAM)和只读存储器(ROM)。从功能和连接上分类,包括随机存储器RAM,装有bios的rom,接口卡上的ram。
装有BIOS的ROM,bios是basic input/output system,基本输入输出系统。Bios是由主板和各类接口卡厂商提供的软件系统,可以通过它利用该硬件设备进行最基本的输入输出。在主板和某些接口卡上插有存储相应bios的rom。
上述的存储器都是独立的器件。它们和cpu的总线相连,cpu对它们读写的时候通过控制线发出内存读写命令。
对cpu来讲,系统中的所有存储器中的存储单元都处于一个统一的逻辑存储器中,它的容量受cpu寻址能力的限制。这个逻辑存储器就是内存地址空间。不同的计算机系统的内存地址空间分配情况是不同的。
三、 寄存器
汇编语言主要就是和cpu交互。一个典型的cpu有运算器、控制器、寄存器等器件构成,这些器件靠内部总线相连。这个总线相对于cpu与其他器件间的总线来说,称为内部总线。在cpu中,运算器进行信息处理,寄存器进行信息存储,控制器控制各种器件进行工作。对于编程人员来说,cpu中的主要部件是寄存器。寄存器是可以用指令读写的部件。程序员通过改变各种寄存器中的内容来实现对cpu的控制。控制了cpu就可以进一步控制pc。不同的cpu,寄存器的个数和结构都不同。8086cpu有14个寄存器,名称分别是:AX、BX、CX、DX、SI、DI、SP、BP、IP、CS、SS、DS、ES、PSW。
根据用途,可将14个寄存器分为通用寄存器、指针指令、标志寄存器和段寄存器,见下表:
通用寄存器 |
数据寄存器 |
AH&AL=AX(accumulator),累加寄存器。常用语运算,所有的I/O指令都使用这个寄存器与外界设备传送数据。 |
BH&BL=BX(base),基址寄存器,常用于地址索引 |
||
CH&CL=CX(count),计数寄存器,用于计数,比如在移位指令,循环和串处理指令中用作隐含的计数器。 |
||
DH&DL=DX(data),数据寄存器,常用于数据传递。 |
||
指针寄存器、变址寄存器 |
SP(Stack Pointer),堆栈指针,与ss配合使用,指向目前的堆栈位置。 |
|
BP(Base Pointer),基址指针寄存器,可用作ss的一个相对基址位置。 |
||
SI(Source Index),源变址寄存器,可用来存放相对于ds段的源变址指针。 |
||
DI(Destination Index),目的变址寄存器,用来存放相对于es段的目的变址指针。 |
||
指令指针 |
IP(Instruction point),指向当前需要取出的指令字节,当biu从内存中取出一个指令字节后,ip就自动加指向下一个指令字节。 |
|
标志寄存器 |
FR(Flag Register),又称状态字寄存器,是一个存放条件标志、控制标志寄存器,主要用于反映处理器的状态和运算结果的某些特征及控制指令的执行。 |
|
段寄存器 |
CS(Code segment),代码段寄存器 |
|
DS(Data segment),数据段寄存器 |
||
SS(Stack segment),堆栈段寄存器 |
||
ES(Extra segment),附加段寄存器 |
1. 通用寄存器
8086cpu的所有寄存器都是16位的,可以存放两个字节。AX、BX、CX、DX通常用来存放一般性的数据,称为通用寄存器。8086cpu上一代的cpu中的寄存器都是8位的,为了保证兼容,8086中的这4个寄存器可以分为两个独立使用的8位寄存器来用。分化名称见表。H表示高位,L表示低位。
AX |
AH |
AL |
BX |
BH |
BL |
CX |
CH |
CL |
DX |
DH |
DL |
2. 字在寄存器中的存储
一个字由两个字节组成,这个两个字节分别称为字的高位字节和低位字节。一个字可以存在一个16位寄存器中。这个字的高位字节和低位字节就存在这个寄存器的高8位寄存器和低8位寄存器中。
3. 汇编指令的例子
汇编指令控制cpu进行工作。比如如下汇编指令:
汇编指令 |
实现的操作 |
用高级语言表示 |
mov ax,18 |
将18送入寄存器AX |
AX=18 |
mov ah,88 |
将88送入寄存器AH |
AH=88 |
add ax,5 |
将寄存器AX中的数值加上5 |
AX=AX+5 |
mov ax,bx |
将寄存器BX中的数据送入寄存器AX |
AX=BX |
add ax,bx |
将AX和BX中的数值相加,结果存在AX中 |
AX=AX+BX |
汇编指令中的寄存器的名称不区分大小写,如mov ax,2和MOV AX,2的含义相同。
4. 物理地址
Cpu访问内存单元,都给出内存单元的地址。所有的内存单元构成的存储空间是一个一维的线性空间,每一个内存单元在这个空间中都有唯一的地址,这个就是物理地址。不同的cpu形成物理地址的方式不同。8086是16为cpu。16位cpu是运算一次最多可以处理16位的数据,寄存器的最大宽度是16位,寄存器和运算器之间的通路是16位。8086的地址总线是20位,达到1mb寻址能力。8086又是16位结构的,在cpu内部是处理、传输和暂时存储的地址是16位,那么单从内部结构来看,从内部只能发送16位的地址,寻址能力是64kb。为了内部和外部协调,8086采用内部用两个16位地址合成的方法来形成一个20位的物理地址。
比如当8086读写内存时:cpu中的相关部件提供两个16位的地址,一个是段地址,一个是偏移地址;段地址和偏移地址通过内部总线送入地址加法器;地址加法器将两个16位地址合成一个20位的物理地址;地址加法器通过内部总线将20位地址送入输入输出控制电路;输入输出控制电路将20位物理地址送上地址总线,地址总线将物理地址送到存储器。
地址加法器是协调的关键,采用物理地址=段地址*16+偏移地址的方法。这个公式的本质含义是:cpu在访问内存时,用一个基础地址(段地址*16)和一个相对于基础地址的偏移地址相加,给出内存单元的物理地址。
注意:段的划分并不是来自内存,而是来自cpu,cpu使用的物理地址的计算方式使得可以用分段的方式来管理内存。
5. 段寄存器、CS和IP
段地址在段寄存器中存放。8086cpu有4个段寄存器,CS、DS、SS、ES。当cpu要访问内存时,这4个段寄存器会提供内存单元的段地址。CS和IP是8086中最关键的两个寄存器。Cs为代码段寄存器,ip为指令指针寄存器。在8086中,任意时刻,如果cs中的内容为M,ip中的内容为N,8086将从内存M*16+N单元开始,读取一条指令并执行。
程序员通过改变寄存器中的内容实现对cpu的控制。Cpu从何处执行指令有cs和ip中的内容决定,程序员通过改变cs和ip中的内容来控制cpu执行目标指令。使用mov可以改变大部分寄存器中的值,mov指令被称为传送指令。但,mov不能用于设置cs和ip中的值。一般,通过转移指令改变cs和ip的内容,关键字是jmp。语法:jmp 段地址: 偏移地址。比如:jmp 2AE4:4,执行后,cs=2AE4,ip=0004H,cpu将从2AE33H处读指令。
如果只修改ip的内容,使用“jmp 某个合法寄存器”的指令完成,表示用寄存器中的值修改ip,比如,jmp ax,如果指令执行前,ax=1000H,cs=2000H,ip=0003H;那么指令执行后,ax=1000H,cs=2000H,ip=1000H。
6. 代码段
在8086中,可以根据需要,将一组内存单元定义为一个段。比如,将长度N(N<64kb)的一组代码,存在一组地址连续、起始地址为16的倍数的内存单元中,可以将这段内存看作是用来存放代码的,这就定义了一个代码段。比如将一个长度为10个字节的命令存放在123A0H~123A9H的一组内存单元中,可以认为123A0H~123A9H这段内存是用来存放代码的,是一个代码段,它的段地址为123AH,长度为10个字节。Cpu不会自动执行我们认为定义的代码段,cpu只认cs:ip指向的内存单元中的内容。必须将cs:ip指向所定义的代码段中的第一条指令的首地址。比如上面的例子,可以设cs=123AH,ip=0000H。
7. 使用debug
Bebug是dos、windows提供的实模式(8086方式)程序的调试工具。可以查看cpu各种寄存器中的内容、内存的情况和在机器码级跟踪程序的运行。
常用的debug功能命令
R |
查看、改变cpu寄存器的内容 |
D |
查看内存中的内容 |
E |
改写内存中的内容 |
U |
将内存中的机器指令翻译成汇编指令 |
T |
执行一条机器指令 |
A |
以汇编指令的格式在内存中写入一条机器指令 |
进入debug,开始》运行》cmd》debug。Win7系统需要下载dosBox和debug.exe。安装后打开debug.exe,进入dedug。
1) r
输入r可以查看寄存器中的内容。修改一个寄存器中的值,比如:r ax然后enter,然后输入要写入的数据。修改后使用r可以查看。
2) d
d 段地址:偏移地址。查看内存中的内容。使用d 段地址:偏移地址后debug将列出从指定内存单元开始的128个内存单元的内容。进入dedug直接使用d,将列出debug预设的地址处的内容。使用d 段地址:偏移地址后,再使用d,将列出后续的内容。
d 段地址:起始偏移地址 结尾偏移地址。查看指定范围的内容。比如:d 0343:0 14。d 0343:1 1。
3) e
e 起始地址 数据 数据 数据 数据 …。改写内存中的内容。比如:e 1000:0 1 1 1 2 2 3 4 6 6。
e 起始地址。采用提问式修改内容单元的内容。
也可以向内存中写入字符,比如:e 1000:0 1 ‘a’ 2 ‘b’。也可以向内存中写入字符串,比如:e 1000:0 1 “a+b” “c++” “hah”。
4) u
u查看内存中机器码的含义。比如:-u 0070:1 15。使用e可以将机器码写入内存,比如:e 1000:0 b8 01 00。
5) t
t执行cs:ip指向的指令。
6) a
使用汇编语言在内存中写入命令。比如:a 1000:0,从1000段地址和0偏移地址开始写入命令。
四、 内存访问
1. 内存中字的存储
内存中存储时,内存单元是字节单元(一个单元存放一个字节),那么一个字要用两个地址连续的内存单元来存放。字单元是存放一个字型数据的内存单元,由两个地址连续的内存单元组成。将起始地址为N的字单元简称为N地址字单元。
2. DS和[address]
内存地址由段地址和偏移地址组成。DS寄存器用来存放要访问的数据的段地址。对于指令mov bx,1000H mov ds,bx mov al,[0],是将10000H(1000:0)中的数据读到al中。Mov指令可是将数据送入寄存器,将一个寄存器的内容送入另一个寄存器,也可以将一个内存单元中的内容送入一个寄存器。当执行到mov al,[0],[0]中的0表示内存单元的偏移地址,至于段地址,当指令执行时会自动取ds中的数据作为段地址。mov bx,1000H mov ds,bx,将1000H通过中转寄存器bx送入ds。那么mov al,[0]就是从1000:0单元到al传送数据。
3. 字的传送
8086cpu是16位结构的,一次性可以传送16位数据,正好是一个字。比如:
mov bx,1000H
mov ds,bx
mov ax,[0] ;1000:0处的字型数据传入ax
mov [0],cx ;cx中的16位数据送到1000:0处
4. mov、add、sub
mov指令常用的形式
mov 寄存器,数据 |
mov ax,6 |
mov 寄存器,寄存器 |
mov ax,bx |
mov 寄存器,内存单元 |
mov ax,[0] |
mov 内存单元,寄存器 |
mov [0],ax |
mov 段寄存器,寄存器 |
mov ds,ax |
mov 寄存器,段寄存器 |
mov ax,ds |
mov 内存单元,段寄存器 |
mov [0],cs |
mov 段寄存器,内存单元 |
mov ds,[0] |
add和sub拥有同mov一样的操作形式。
5. 数据段
在编程时,可以将一组内存单元定义为一个段。比如可以将一组长度为N(N<=64kb)、地址连续、起始地址为16的倍数的内存单元当作存储数据的内存空间,这就是一个数据段。比如用134A0H~123A9H这段内存空间来存放数据,这段内存就是一个数据段,它的段地址为123AH,长度为10个字节。
6. 栈和cpu提供的栈机制
为了便于理解,暂且认为栈是具有特殊的访问方式的存储空间,栈的特殊性在于最后进入栈的数据,最先出去。栈有两个基本操作,入栈和出栈。入栈就是将一个新的元素放到栈顶,出栈就是从栈顶取出一个元素。栈顶的元素总是最后入栈,出栈时,又最先从栈中取出。这种操作规则称为,LIFO(last in first out,后进先出)。
Cpu提供相关的指令来以栈的方式访问内存空间。在cpu编程中,可以将一段内存当作栈来使用。8086cpu提供的入栈和出栈指令,是PUSH(出栈)和POP(入栈)。比如,push ax表示将ax中的数据送入栈,pop ax表示从栈顶取出数据放入ax。8086cpu的入栈和出栈操作以字为单位。
Cpu如果将一段内存当作栈使用,需要明确这个栈的栈顶的位置。在cpu中,段寄存器SS和寄存器SP,来存放栈顶的信息。栈顶的段地址存放在SS中,偏移地址存放在SP中。任意时刻,SS:SP指向栈顶元素。当执行push或者pop指令时,cpu从ss和sp中得到栈顶的地址。8086cpu执行入栈时,栈顶从高地址向低地址方向增长。
当栈满的时候,再使用push指令入栈,或者在栈空的时候使用pop指令出栈,都将发生栈顶越界问题。栈顶越界是危险的。因为栈顶越界可能会无意中修改栈之外存放的重要的数据,从而可能引发一连串的额错误。Cpu并不能保证和防止栈顶越界的问题出现。只能靠编程者编程的时候要小心谨慎注意防止栈顶越界。
7. push、pop指令
push和pop指令的常用形式:
Push 寄存器 |
将一个寄存器中的数据入栈 |
Pop 寄存器 |
用一个寄存器接收出栈的数据 |
Push 段寄存器 |
将一个段寄存器中的数据入栈 |
Pop 段寄存器 |
用一个段寄存器接收出栈的数据 |
Push 内存单元 |
将一个内存单元处的字入栈,栈操作以字为单位 |
Pop 内存单元 |
用一个内存单元接收出栈的数据 |
比如:
mov ax,1000H
mov ds,ax
push [0]
pop [2]
与数据段类似,可以将长度为N(N<=64kb)的一组地址连续、起始地址为16的倍数的内存单元,当作栈空间来用,这就定义了一个栈段。比如,将10010H~1001FH这段长度为16字节的内存空间当作栈来用,以栈的方式进行访问。这段空间就称为栈段,段地址为1001H,大小为16字节。如果要对这个栈段进行push和pop操作,需要将ss:sp指向这个栈段。
五、 汇编程序的编写
编写一个完整的汇编语言程序,用编译和连接程序将它们编译连接为可执行文件(如.exe文件),在操作系统中运行。首先需要了解,源程序从写出到执行的过程。
1. 源程序从写出到执行的过程
基本步骤是:
第一步,编写汇编源程序,使用文本编辑器,用汇编语言编写源程序。
第二步,对源程序进行编译连接。使用汇编语言编译程序对源程序文件中的源程序进行编译,产生目标文件,再用连接程序对目标文件进行连接,生成可以在操作系统中直接运行的可执行文件。
第三步,在操作系统中执行可执行文件中的程序。
2. 源程序的编写
源程序的组成与编辑
完整的源程序由指令和标号组成。指令包含二种,汇编指令和伪指令。
汇编指令有对应的机器码的指令,可以被编译为机器指令,由cpu执行。
伪指令,没有对应的机器指令,最终不能被cpu执行。伪指令由编译器来执行的指令。编译器根据伪指令进行编译。常用的伪指令,如表,
Xxx segment … … … Xxx ends |
Segment和ends成对使用,功能是定义一个段。Segment声明一个段的开始,ends标明结束。比如: codesg segment …… codesg ends 表示定义一个叫做codesg的段,这个段的开始和结束位置。 |
end |
汇编程序的结束标记。编译器在编译过程中,如果碰到了end就结束编译。如果程序写完,要在结尾加上end,否则,编译器不知道在哪里结束。 |
assume |
意为假设。假设某一个段寄存器和程序中的某一个用segment…ends定义的段相关联。 |
源程序中的标号就是一个标记的代号,比如codesg segment … codesg ends。Codesg就是一个标号,指代一个地址。
总体上,源程序由若干段构成。段中存放代码、数据、或将某个段当作栈空间。比如一段简单的源程序,
assume cs:aaa
aaa segment
mov ax,22
add ax,ax
mov ax,bx
aaa ends
end
一个程序结束后,将cpu的控制权交还给使它得以运行的程序,这个过程为程序返回。声明程序返回的指令是:
mov ax,4c00H
int 21H
一个完整的源程序,需要在段中最后声明程序返回。
比如:
assume cs:aaa
aaa segment
mov ax,22
add ax,ax
mov ax,bx
mov ax,4c00H
int 21H
aaa ends
end
在文本编辑器中编辑好源程序,将其存储为纯文本文件,就完成了源程序的编辑。比如,将一段源程序存储为new.asm。
3. 源程序的编译
使用编译器对源程序文件进行编译,可以生成包含机器代码的目标文件。汇编编译器有多种。可以使用微软的masm系列汇编编译器。下载,放到d:\assembly\目录下,进入dos,然后进入d:\assembly\,使用指令masm运行masm.exe。
运行masm后显示一些版本信息,然后提示输入要编译的源程序文件的名称。Source filename[.asm]提示默认的文件扩展名为asm。比如要编译的文件名为new.asm,只要输入aa即可。如果文件不是以asm为扩展名,那么要输入全名,比如,aa.txt。输入文件名的时候要输入文件的路径,比如:d:\assembly\test\aa.txt。如果文件在当前路径下,可以省略文件路径,直接输入文件名就可以。如果在编译过程中,使用的是dosbox软件,如果要使用某路径,请将这个路径挂载到dosbox的某个虚拟盘。比如:mount d d:\aseembly\test,否则找到这个路径。
输入文件全路径和名后,enter。提示Object filename [1.OBJ],表示默认输出的目标文件名为:1.obj。也可以指定生成的目标文件的目录。然后,继续enter。显示Source listing [NUL.LST],提示输入列表文件的名称,这个文件是编译器将源程序编译为目标文件的过程中产生的中间结果。直接enter,可以让编译器不生成这个文件。然后显示Cross-reference [NUL.CRF],提示输入交叉引用文件的名称,这个也是编译器将源程序文件编译为目标文件过程中产生的中间结果。可以不生成,直接enter。然后,显示编译结束的信息。编译完成。
4. 编译后目标文件的连接
在对源程序进行编译得到目标文件后,对目标文件进行连接后就可得到执行文件。使用的是微软的overlay linker3.60连接器。
进入dos,进入link.exe的文件目录,运行link.exe。显示一些版本信息。Object Modules [.OBJ]。提示输入要被连接的文件的名称,默认扩展名是obj。比如文件名为new.obj,输入new即可。如果不是obj的文件,输入全名,如new.bin。输入目标文件名的时候,只要指明其路径,比如d:\assembly\test
\new.obj。
输入目标文件后,enter。提示Run File [1.EXE],表示要生成的可执行文件的名称,可执行文件是对一个程序连接要得到的最终结果。可以指定生成的可执行文件的目录,比如:c:\xxx\xxx。也可以直接生成到当前文件下。
继续enter,生成可执行文件。然后提示List File [NUL.MAP],输入映像文件的名称,这个文件是连接程序将目标文件连接为可执行文件过程中产生的中间结果,可以让连接程序不生成这个文件,直接enter。
然后,提示Libraries [.LIB],输入库文件的名称。库文件里面包含了一些可以调用的子程序,如果程序中调用了某一个库文件中的子程序,就需要在连接的时候,将这个库文件和目标文件连接在一起,生成可执行文件。直接enter,忽略库文件的输入。
然后,提示LINK:warning L4021: no stack segment。不用管这个错误提示。
连接完成后,在默认的目录下出现一个新的文件,1.exe。
那么,连接的作用,1),当源程序很大时,可以将它分为多个源程序文件来编译,每个源程序编译成为目标文件后,再用连接程序将它们连接,生成一个可执行文件;2),程序中调用了某个库文件中的子程序,需要将这个库文件和该程序生成的目标文件连接到一起,生成一个可执行文件;3),一个源程序编译后,得到存有机器码的目标文件,目标文件中的有些内容不能直接生成可执行文件,使用连接程序将这些内容处理为最终的可执行文件。
5. 使用简化的方式进行编译和连接
进入dos,在编译软件的目录下,直接masm c:\xx\1;注意加分号,然后enter,编辑器就对c:\1.asm进行编译,在当前路径下生成目标文件1.obj。
进入dos,在连接软件的目录下,直接link c:\xx\1.obj;注意加分号,然后enter,连接器就在指定路径下1.obj文件进行处理,在当前路径下生成可执行文件1.exe。
6. xxx.exe文件的执行
在编译器和连接器的软件目录下,直接输入可执行文件的路径和文件名,如果在软件目录下可以省略路径,比如1,或者c:\xx\1。
执行后界面上看不到任何过程的提示和结果的提示,是在后台执行的。
任何通用的操作系统,都要提供一个为shell(外壳)的程序,使用这个程序来操作计算机系统进行工作。Dos中的cmd命令行,在dos中称为命令解释器,就是dos的shell。比如,在dos中执行得到的可执行文件1.exe时,cmd将1.exe中的程序加载入内存。然后cmd设置cpu的cs:ip指向程序的第一条指令,使得程序得以运行,程序运行结束,返回cmd。
7. 跟踪程序执行过程
使用debug可以跟踪一个程序的运行过程。使用debug跟踪程序,便于发现程序中的难以发现的错误。比如对于可执行文件1.exe的执行进行跟踪。
进入dos,进入masm的软件包目录,输入debug 1.exe,然后enter,debug将1.exe中的程序加载如内存,进行初始化后设置cs:ip指向程序的入口。
然后r,可以查看寄存器的情况。Cx中存放程序的长度。
程序加载后,ds中存放着程序所在内存区的段地址,如果这个内存区的偏移地址为0,那么程序所在的内存区的地址为:ds:0。内存的前256字节存放的是psp,作用是dos和程序进行通信。256字节后存放的是程序。根据ds和psp的字节总数256可以计算出程序的物理地址为:SA+10H:0(SA为psp的段地址,也就是ds的值)。
得到程序的内存地址后,使用-u,可以查看程序的指令码。
然后,使用t可以查看每条指令的执行结果。当执行到int21,使用p命令执行。然后显示“Program terminated normally”,返回到debug,表示程序正常结束。
在debug中,使用q命令可以退出debug,返回cmd。
六、 [BX]和loop指令
1. [BX]
[bx],一般用于数据的传输,比如mov ax,[bx],表示将bx的数据作为偏移地址EA,以ds中的数据为段地址SA,将SA:EA处的数据放入ax中。
2. Loop
Loop是循环指令。功能是循环执行一个程序段。Loop指令的格式是:loop标号,标号是一个段的标记。Cpu执行loop指令时,分两步,首先(cx)=(cx)-1;然后判断cx中的值,如果不为0则转至标号处执行程序,如果为0则向下执行。Cx中的值影响loop的次数。Cx中存放的是loop的次数。
比如使用loop计算2的10次方的程序,
assume cs:mode
code segment
mov ax,2
mov cx,9
s: add ax,ax
loop s
mov ax,4c00h
int 21h
code ends
end
上述代码中s就是一个标号,实际上代表一个地址,这个地址处有一条指令:add ax,ax。
使用cx和loop配合实现循环功能的框架为:
mov cx,循环次数
s:
循环执行的程序段
loop s
比如用加法计算123*236,结果存在ax中:
assume cs:plus
plus segment
mov ax,0
mov cx,123
s: add ax,236
loop s
mov ax,4c00h
int 21h
plus ends
end
注意:在汇编程序中,数据不能以字母开头,比如写A000H时,要写为0A000H,mov ax,0A0000H。
在使用debug跟踪循环的时候,如果不需要跟踪循环段之前的程序,可以使用g命令,比如循环段从cs:0012开始,使用g 0012,可以直接执行到循环段的位置。如果循环的次数很多,不方便一次一次的跟踪,可以使用p命令,当执行到loop指令时,输入p,那么就自动重复完循环中的指令,直到(cx)=0为止。也可以使用g命令执行完循环,比如循环结束后的位置为0016,那么g 0016也可以一次执行完循环。
3. Debug和masm对指令的不同处理
对于mov ax,[0]。在debug中执行,表示将ds:0处的数据传入ax中。在masm编译这段时,会当作mov ax,0处理。那么在源程序中将内存中的数据,比如2000:0单元中的数据送入al的方式是,向将偏移地址送入bx,再使用[bx]的方式访问内存单元。比如:
mov ax,2000h
mov ds,ax
mov bx,0
mov al,[bx]
也可以不使用[bx]的方式,而是使用ds:[0]的方式,比如,mov al,ds:[0]
4. loop和[bx]的联合使用
需求,计算ffff:0~ffff:b单元中的数据的和,结果存储在dx中。
assume cs:lianhe
lianhe 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
lianhe ends
end
inc,指向的寄存器中的内容加1。
5. 段前缀
一般用于访问内存单元的指令中,显示地指明内存单元的段地址的寄存器名字,称为段前缀。比如,
mov ax,ds:[bx]
段地址在ds中,偏移地址在bx中。
mov ax,ss:[bx]
段地址在ss中,偏移地址在bx中。
mov ax,cs:[bx]
段地址在cs中,偏移地址在bx中。
mov ax,es:[bx]
段地址在es中,偏移地址在bx中。
mov ax,ss:[0]
段地址在ss中,偏移地址为0。
mov ax,ds:[0]
段地址在ds中,偏移地址为0。
mov ax,cs:[1]
段地址在cs中,偏移地址为1。
使用段前缀,将ffff:0~ffff:b单元中的数据复制到0:200~0:20b单元中。
assume cs:duanpre
duanpre segment
mov ax,0ffffh
mov ds,ax
mov ax,0020h
mov es,ax
mov bx,0
mov cx,12
s: mov dl,[bx]
mov es:[bx],dl
int bx
loop s
mov ax,4c00h
inc 21h
duanpre ends
end
6. 一段内存空间的安全性
在8086模式中,随意向一段内存中写入内容是不安全的,这段空间可能存着重要的系统数据或代码。
为了安全起见,一般的pc的dos下,dos和其他合法的程序一般都不会使用0:200~0:2ff的256个字节的空间。这段空间是相对安全的,如果不确定,可以使用debug查一个这段空间的内容是否都是0,都是0说明没有被使用,安全。
7. 练习
向内存0:200~0:23F依次传送数据0~63(3FH)
assume cs:shisi
shisi segment
mov ax,0020h
mov ds,ax
mov bx,0
mov cx,64
s: mov ds:[bx],bl
inc bl
loop s
mov ax,4c00h
int 21h
shisi ends
end
七、 包含多个段的程序
内存中的0:200~0:2ff空间只是相对安全的,而且容量有限,不能要求大的需求。为了解决安全和容量,在操作系统中,合法地通过操作系统取得空间是安全的,而且在系统允许的情况下,可以取得任意容量的空间。有两种方法,在加载程序的时候为程序分配,在执行的过程中向系统申请。如果要在加载的时候取得所需的空间,必须在源程序中做出说明。一般,通过源程序中的定义段来获取需要的内存空间。
除了获取内存空间要使用定义段外,从程序设计的角度,为了是程序结构清晰,一般通过不同的段来存放要处理的数据,运行的代码以及对于栈空间的使用。
1. 在代码段中使用数据
比如,编一个程序,计算这8个数据的和,让结果存在ax寄存器中。
0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
当程序中定义了这些数据后,这些数据会被编译、连接程序作为程序的一部分写到可执行文件。当可执行文件中的程序加载如内存时,这些数据也同时载入内存。比如:
assume cs:code
code segment
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
mov bx,0
mov ax,0
mov cx,8
s: add ax,cs:[bx]
add bx,2
loop s
mov ax,4c00h
int 21h
code ends
end
上面的dw(define word)表示定义字型数据,也可以使用db(define byte)表示定义字节型数据。因为程序执行时是加载到cs段指向的内存空间的,一般从0000开始。所以,程序的开头,也就是数据部分就加载到了cs段指向的,偏移地址为0的的内存空间,也就是从cs:0处载入这些数据。所以执行,add ax,cs:[bx],可以从内存中取得对应的载入的数据。
一般,一段程序执行加载到内存后,是从程序的第一行开始执行的,当向程序开头的地方写入了数据后,程序的第一行就是数据,不是要执行的代码,这样会出错。所以,需要指明程序的入口,比如:
assume cs:code
code segment
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
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
code ends
end start
在程序的第一条指令前加上一个标号start,将这个标号写到end后面,end指令就可以指明程序的入口。
2. 在代码段中使用栈
根据程序中写入数据,然后在程序运行时,系统给这些数据分配内存空间的特点,可以在程序中写入栈需要的容量的数据以便在程序执行时可以得到一段空间。然后将这段空间作为栈使用,只需要将这部分数据的段地址赋值给栈的段地址,设置适当的栈指针,就相当于将这段空间作为栈。比如,利用栈,将程序中定义的数据逆序存放:
assume cs:code
code segment
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
dw 0,0,0,0,0,0,0,0,0,0
start: mov ax,cs
mov ss,ax
mov sp,30h
mov bx,0
mov cx,8
s: push cs:[bx]
add bx,2
loop s
mov bx,0
mov cx,8
s1: pop cs:[bx]
add bx,2
loop s1
mov ax,4c00h
int 21h
code ends
end start
3. 将数据、代码、栈放入不同的段
为了是程序看起来结构清晰,以及8086段的最大长度为64kb的制约,需要采用多个段来存放数据、代码和段。使用和定义代码段一样的方式可以定义存放数据和取得栈空间的段。
比如:
assume cs:code
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
stack ends
code segment
start: mov ax,stack
mov ss,ax
mov sp,30h
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
s1: pop [bx]
add bx,2
loop s1
mov ax,4c00h
int 21h
code ends
end start
所有定义在段中的内容,在载入内存后,都有内存地址,而这个段的标号也是指向这个段的内存的段地址,使用mov ax,data或者mov ax,stack,可以将这个段的内存段地址放入累加寄存器,以便放入ds或者ss。
八、 更灵活的定位内存地址的方法
使用[0]、[1]、[bx]的方法可以访问内存,除此之外还有更灵活的定位内存地址的编程方法。
1. and和or指令
and,逻辑与指令,按位进行与运算。比如,
mov al,01100011B
and al,00111011B
执行后,al=00100011B。
通过该指令可以将操作对象的相应位设为0,其他位不变。因为与运算全真才为真,有假就是假。比如如果al的第6位不是0,那么执行,add al,10111111B后第6位为0。
or,逻辑或指令,按位进行或运算。比如:
mov al,01100011B
or al,00111011B
执行后,al=01111011B。
通过该指令可以将操作对象的相应位设为1,其他位不变。因为或运算全假才为假,有真就是真。比如如果al的第6位不是1,那么执行,or,01000000B后第6位为1。
2. 字符形式的数据以及字符的大小写转换
1) 字符形式给出数据
在汇编程序中,以’……’的方式指明数据是以字符的形式给出的,编译器会把它们转化为对应的ascii码。比如,
assume cs:code,ds:data
data segment
db ‘unHa’
db ‘fasS;
data ends
code segment
start: mov al,’a’
mov bl,’b’
mov ax,4c00h
int 21h
code ends
end start
db ‘unHa’,就相当于定义了字节型,即,db 75h,6Eh,48h,61h。
mov al,’a’,就相当于放入字节型,即,mov al,61h。
2) 字符的大小写转换
一般字符的大小写转换其实就是改变大小写字符对应的ascii码的值。而在汇编中习惯使用16进制。小写字母的16进制ascii码值比大写字母的16进制ascii码值大20。
比如,将数据段中的两串数据,
data segment
db ’asdCsDg’
db ‘nsdSEFSEfs’
data ends
第一串转换为大写,第二串转换为小写。如果使用十六进制码值,那么在转换时就要判断一个字符的值是大于还是小于61H,对于第一串,如果是大于61H,则sub 20H,如果不是则不改变。但是,在没有了解汇编中使用判断的语法前,这样做是行不通的。通过观察大小写字符的2进制码值,发现规律,同一个大小写字符,比如A和a的二进制码值中除了第5位,其他位都相同,大写的是0,小写的是1。那么要转为小写,只需将这个字符的第5位设置为1,其他位不变,这可以使用逻辑或。要转为大写,只需将这个字符的第5位转为0,其他位不变,这可以使用逻辑与。
那么,将上述需求编码
assume cs:code,ds:data
data segment
db ’asdCsDg’
db ‘nsdSEFSEfs’
data ends
code segment
start: mov ax,data
mov ds,ax
mov bx,0
mov cx,7
s: mov al,[bx]
and al,11011111B
mov [bx],al
loop s
inc bx
mov cx,10
s1: mov al,[bx]
or al,00100000B
mov [bx],al
inc bx
loop s1
mov ax,4c00h
int 21h
code ends
end start
3. 使用[bx+idata]
使用[bx]可以指明一个内存单元。使用[bx+idata]可以更灵活的指明内存单元,它的偏移地址为bx中的值加上idata。比如,
mov ax,[bx+200]
表示将偏移地址为bx+200,段地址为ds中的值,处的数据放入ax中。该指令还可以书写为:
mov ax,[200+bx]
mov ax,200[bx]
mov ax,[bx].200
合理使用[bx+idata],可以实现以类似数组的方式,处理数据段中的数据。比如,对于将两串字节数一样的字符串,第一串转为大写,第二串转为小写,使用[bx+idata]编程为:
assume cs:code,ds:data
data segment
db ‘adbEd’
db ‘ESEDa’
data ends
code segment
start: mov ax,data
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
code ends
end start
4. SI和DI
si和di的功能和bx相似,bx是基址寄存器,si是存放相对于ds段的源变址寄存器,di是存放相对于es段的目的变址寄存器,这两个不能分成两个8位寄存器使用。实际上,这两个地址寄存器,是为了操作的方便而多提供的存放偏移地址的寄存器,它们和bx有几乎相似的功能。比如:
mov bx,0
mov ax,[bx]
mov si,0
mov ax,[si]
mov di,0
mov ax,[di]
上述3组指令的功能是相同的。同样的,
mov bx,0
mov ax,[bx+12]
mov si,0
mov ax,[si+12]
mov di,0
mov ax,[di+12]
这3组指令的功能也是相同的。
使用si和di实现将字符串hello world haha复制到它后面的数据区中。数据段如下:
assume cs:code,ds:data
data segment
db ‘hello world haha’
db ‘…………….’
data ends
这两串数据中,第一串存放的地址,也就是源地址为:data:0,长度为16,这串数据后面的数据区的偏移地址为16,也就是目的地址为:data:16。所以,使用ds:si指向数据源,ds:di指向要复制的目的空间。编码如下:
assume cs:code,ds:data
data segment
db ‘hello worldhaha’
db ‘…………….’
data ends
code segment
start: mov ax,data
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
code ends
end start
使用[bx(si或di)+idata]的方式,简化上述代码
assume cs:code,ds:data
data segment
db ‘hello worldhaha’
db ‘…………….’
data ends
code segment
start: mov ax,data
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
code ends
end start
5. [bx+si]和[bx+di]
使用[bx+si]和[bx+di]可以更灵活的指明一个内存单元。[bx+si]和[bx+di]的含义相似。
使用[bx+si]表示一个内存单元,它的偏移地址为(bx)+(si),bx的值加上si的值。比如:
mov ax,[bx+si],表示将段地址为ds中的值,偏移地址为bx+si的值,处的2字节的数据放入ax。该指令也可以写成:
mov ax,[bx][si]
6. [bx+si+idata]和[bx+di+idata]
[bx+si+idata]和[bx+di+idata]的含义相似。[bx+si+idata]表示一个内存单元,它的偏移地址为(bx)+(si)+idata,就是bx中的值加上si中的值加上idata中的值。比如:
mov ax,[bx+si+20],表示将段地址为ds中的值,偏移地址为bx+si+200的值,处的2字节的数据放入ax。该指令也可以写成:
mov ax,[bx+20+si]
mov ax,[20+bx+si]
mov ax,200[bx][si]
mov ax,[bx].200[si]
mov ax,[bx][si].200
7. 不同的寻址方式的应用
总结几种定位内存地址的方法的特点:
[idata] |
用一个常量来表示地址,可用于直接定位一个内存单元 |
[bx] |
用一个变量来表示内存地址,用于间接定位一个内存单元 |
[bx+idata] |
用一个变量和常量表示地址,可在一个起始地址的基础上用变量间接定位一个内存单元 |
[bx+si]和[bx+di] |
用两个变量表示地址 |
[bx+si+idata]和[bx+di+idata] |
用两个变量和一个常量表示地址 |
例子1,将data段中每个单词的头一个字母改为大写字母。
assume cs:code,ds:data
data segment
db '1, file '
db '1, edie '
db '1, search '
db '1, view '
db '1, options '
db '1, help '
data ends
code segment
start: mov ax,data
mov ds,ax
mov bx,3
mov cx,6
s: mov al,[bx]
and al,11011111b
mov [bx],al
add bx,16
loop s
mov ax,4c00h
int 21h
code ends
end start
例子2,将data段中每个单词改为大写字母。
使用嵌套循环实现。
assume cs:code,ds:data
data segment
db ‘len ‘
db ‘hah ‘
db ‘aad ‘
db ‘yxs ‘
data ends
code segment
start: mov ax,data
mov ds,ax
mov bx,0
mov cx,4
s1: mov si,0
mov cx,3
s2: mov al,[bx+si]
and al,11011111b
mov [bx+si],al
inc si
loop s2
add bx,16
loop s1
mov ax,4c00h
int 21h
code ends
end start
上面的程序是有问题的,因为内层循环将外层循环的计数器cx中的值清零了,导致外层循环无法按预计需求执行。可以采用dx,数据寄存器,在内存循环开始前保存cx的值,等外层循环结束后,再将dx中保存的值恢复给cx。改写后代码如下:
assume cs:code,ds:data
data segment
db 'len '
db 'hah '
db 'aad '
db 'yxs '
data ends
code segment
start: mov ax,data
mov ds,ax
mov bx,0
mov cx,4
s1: mov dx,cx
mov si,0
mov cx,3
s2: mov al,[bx+si]
and al,11011111b
mov [bx+si],al
inc si
loop s2
add bx,16
mov cx,dx
loop s1
mov ax,4c00h
int 21h
code ends
end start
虽然,上述方法中采用dx作为暂存cx数据的中介是可行的。但是由于cpu中的寄存器的数量是有限的,而且在复杂的运算中,可能出现作为中介的寄存器不够用的情况,那么,这就需要采用更具有广泛性的通用性的办法作为暂时存放数据的中介。除了cpu的寄存器,内存也是保存数据的好地方。可以在程序中开辟一段内存空间,来作为暂时存放数据的中介。比如:
assume cs:code,ds:data
data segment
db 'len '
db 'hah '
db 'aad '
db 'yxs '
dw 0
data ends
code segment
start: mov ax,data
mov ds,ax
mov bx,0
mov cx,4
s1: mov ds:[40h],cx
mov si,0
mov cx,3
s2: mov al,[bx+si]
and al,11011111b
mov [bx+si],al
inc si
loop s2
add bx,16
mov cx,ds:[40h]
loop s1
mov ax,4c00h
int 21h
code ends
end start
虽然使用内存作为中介保存数据,增强了通用性,但是操作起来并不方便,因为要记住数据放在哪个内存单元,不够简单明了。一般来说,在需要暂存数据的时候,应该使用栈,栈的操作只需要push、pop等,就可以方便的放入、取出。如果使用栈作为中介,程序如下:
assume cs:code,ds:data,ss:stack
data segment
db 'len '
db 'hah '
db 'aad '
db 'yxs '
data ends
stack segment
dw 0,0,0,0,0,0,0,0
stack ends
code segment
start: mov ax,data
mov ds,ax
mov bx,0
mov ax,stack
mov ss,ax
mov sp,16
mov cx,4
s1: push cx
mov si,0
mov cx,3
s2: mov al,[bx+si]
and al,11011111b
mov [bx+si],al
inc si
loop s2
add bx,16
pop cx
loop s1
mov ax,4c00h
int 21h
code ends
end start
例子3,将data段中每个单词的前4个字母改为大写字母。
assume cs:code,ds:data,ss:stack
data segment
db '1. lendiss '
db '2. hsdah '
db '3. aasdfd '
db '4. yxsdfs '
data ends
stack segment
dw 0,0,0,0,0,0,0,0
stack ends
code segment
start: mov ax,data
mov ds,ax
mov bx,0
mov ax,stack
mov ss,ax
mov sp,16
mov cx,4
s1: push cx
mov si,0
mov cx,4
s2: mov al,[bx+si+3]
and al,11011111b
mov [bx+si+3],al
inc si
loop s2
add bx,16
pop cx
loop s1
mov ax,4c00h
int 21h
code ends
end start
九、 数据处理的两个基本问题
数据处理的基本问题是:处理的数据具体在哪里?处理的数据容量有多长?
1. 数据在哪里?
首先数据在内存里,cpu中的段寄存器ds、ss、ds、es和地址寄存器bx、si、di、bp可以用来指向详细到单元字节的内存地址。
在8086中,只有bx\si\di\bp这四个寄存器可以用在[…]中进行内存单元的寻址,比如:
mov ax,[bx]
mov ax,[bx+si]
mov ax,[bx+di]
mov ax,[bp]
mov ax,[bp+si]
mov ax,[bp+di]
注意:其他的寄存器不能用在[…]中做内存寻址,比如mov ax,[cx]是错误的。
在[…]中,这4个寄存器可以单独使用,如果组合使用只能是bx+si、bx+di、bp+si、bp+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]
注意:其他组合是错误的,比如[bx+bp],[si+di]。
只要在[…]中使用了寄存器bp,如果指令中没有显性给出段地址,段地址默认在ss中。比如,
mov ax,[bp]
指向的内存空间是ss中的值*16+bp中的值。
2. 机器指令处理的数据在哪里
一般,机器指令对数据的处理,可以分为3类,读取、写入、运算。对于机器指令来将,要执行前必须得到要处理的数据所在的位置。一般,所要处理的数据所在的位置,见表:
机器码 |
汇编指令 |
执行前数据位置 |
8E1E0000 |
mov bx,[0] |
内存中,ds:0单元 |
89C3 |
mov bx,ax |
CPU内部,ax寄存器 |
BB0100 |
mov ax,1 |
CPU内部,指令寄存器 |
3. 汇编中数据位置的表达方式
立即数 |
直接包含在机器指令中的数据,执行前在cpu的指令缓存中。立即数不可用于直接向段寄存器中存放数据。 |
mov ax,1 add bx,2000h or bx,00011000b mov al,’b’ |
寄存器 |
指令要处理的数据在寄存器中,给出相应的寄存器名,表示这个数据的位置 |
mov ax,bx mov ds,ax push bx mov ss,ax |
段地址(SA,segment address)和偏移地址(EA, excursion address) |
指令要处理的数据在内存中,可用[xx]的格式给出EA,SA在某个段寄存器中。段寄存器默认为ds或者ss,如果使用bp则默认为ss,否则为ds。也可以使用段前缀显性的给出段寄存器。 |
mov ax,[0] mov ax,[di] mov ax,[bx+si] 等,段寄存器默认为ds。 mov ax,[bp] mov ax,[bp+8] 等,段寄存器默认为ss。 mov ax,es:[bx] mov ax,ds:[bp] mov ax,ss:[bx] mov ax,cs:[bx] 等,是显性指明段寄存器。 |
4. 关于8086cpu寻址方式的总结
见表:
寻址方式 |
名称 |
[idata] |
直接寻址 |
[bx] |
寄存器间接寻址 |
[si] |
|
[di] |
|
[bp] |
|
[bx+idata] |
寄存器相对寻址 |
[si+idata] |
|
[di+idata] |
|
[bp+idata] |
|
[bx+si] |
基址变址寻址 |
[bx+di] |
|
[bp+si] |
|
[bp+di] |
|
[bx+si+idata] |
相对基址变址寻址 |
[bx+di+idata] |
|
[bp+si+idata] |
|
[bp+di+idata] |
5. 指令要处理的数据的长度
8086cpu可以处理两种尺寸的数据,byte和word。在指令中要指明处理的是字还是字节。常见的情况有3种。
1) 通过寄存器指明要处理的数据的尺寸
如果源或者目的寄存器为8位,则处理的数据为byte,如果源或目的寄存器为16位,则处理的数据为word。
比如,
mov ax,1
mov bx,ds:[0]
mov ds,ax
mov ds:[0],ax
inc ax
add ax,1000
进行的是字操作。
mov al,1
mov al,bl
mov bl,ds:[0]
mov ds:[0],al
inc al
add al,100
进行的是字节操作。
2) 使用x ptr指明内存单元的长度
在没有寄存器名存在的情况下,用操作符x ptr指明内存单元的长度,x可以为word或byte。
用word ptr指明指令访问的内存单元是一个字单元的例子,
mov word ptr ds:[0],1
inc word ptr [bx]
inc word ptr ds:[0]
add word ptr [bx],2
用byte ptr指明指令访问的内存单元是一个字节单元的例子,
mov byte ptr ds:[0],1
inc byte ptr [bx]
inc byte ptr ds:[0]
add byte ptr [bx],2
对于
2000:1000内存单元中数据为:aa aa aa aa …
执行
mov ax,2000h
mov ds,ax
mov byte ptr [1000h],1
的结果为,2000:1000 01 aa aa aa…
执行
mov ax,2000h
mov ds,ax
mov word ptr [1000h],1
的结果为,2000:1000 01 00 aa aa …
3) 其他方式
有的指令默认了访问的是子单元还是字节单元,比如,push [1000h]访问的是字单元。因为push只进行字操作。
6. 寻址方式的例子
有一条公司的记录如下,
公司名称:HAHA
总裁姓名:heihei
排名:123
收入:20
著名产品:HEHE
假设数据存放在data段,偏移地址从60开始的内存中。数据的存储图示为:
+00 |
‘HAHA’ |
+04 |
heihei |
+0c |
123 |
+0e |
20 |
+10 |
HEHE |
后来,公司的情况发生了变化,公司排名上升至15位,收入增加了40,著名产品变为XIXI。请汇编编程修改公司数据为新的数据。
假设data段是已知的,那么关键代码为:
mov ax,data
mov ds,ax
mov bx,60h
mov word ptr [bx+0ch],15
add word ptr [60+0eh],40
mov si,0
mov byte ptr [bx+10h+si],’H’
inc si
mov byte ptr [bx+10h+si],’E’
inc si
mov byte ptr [bx+10h+si],’H’
inc si
mov byte ptr [bx+10h+si],’E’
使用bx,指定公司信息的基值,使用地址大小,指定要访问的数据在公司信息中的位置,使用源变址,指定要访问的数据中的某个字节在这个数据的位置。
如果使用c语言描述这个程序,大致为:
struct compamy{
char cn[4];
char hn[6];
int pm;
int st;
char cp[4];
};
struct company dec={“HAHA”,”heihei”,123,20,”HEHE”};
main(){
int i;
dec.pm = 15;
dec.sr = dec.sr + 40;
i = 0;
dec.cp[i] = ‘H’;
i++;
dec.cp[i] = ‘E’;
i++;
dec.cp[i] = ‘H’;
i++;
dec.cp[i] = ‘E’;
return 0;
}
仿照c语言的语法格式,可将上述汇编语言修改为:
mov ax,data
mov ds,ax
mov bx,60h
mov word ptr [bx].0ch,15
add word ptr [60].0eh,40
mov si,0
mov byte ptr [bx].10h[si],’H’
inc si
mov byte ptr [bx].10h[si],’E’
inc si
mov byte ptr [bx].10h[si],’H’
inc si
mov byte ptr [bx].10h[si],’E’
7. div指令
div是除法指令,使用div做除法的时候应注意的问题。
1) 除数
除数有8位和16位两种,在一个寄存器或内存单元中。
2) 被除数
默认放在ax或者ax和dx中。如果除数为8位,被除数则为16位,默认在ax中存放;如果除数为16位,被除数则为32位,在dx和ax中存放,dx中存放高16位,ax存放低16位。
3) 结果
如果除数为8位,则al存储除法的商,ah存储除法的余数;如果除数为16位,则ax存储除法的商,dx存储除法的余数。
Div的使用形式为:
Div 寄存器
或者
Div x ptr 内存单元
比如,
div ax
div byte ptr ds:[0]
关于使用内存单元时,比如,当除数为8位时,也就是是一个字节时,
div byte ptr ds:[0]
表示:(al)=(ax)/((ds)*16+0)的商;(ah)=(ax)/((ds)*16+0)的余数。
比如,当除数为16位时,也就是一个字时,
div word ptr es:[0]
表示:(ax)=[(dx)*10000h+(ax)]/((es)*16+0)的商;(dx)=[(dx)*10000h+(ax)]/((es)*16+0)的余数。
案例1,编程,利用除法指令计算100001/100
分析下,被除数100001大于65535,不是16位的,所以被除数为32位,需要使用dx和ax来存放100001,100001的16进制为186A1h,那么dx中存放1h,ax中存放86A1h。除数100小于255,可以放在一个8位寄存器中,但是因为被除数是32位的,除数应为16位,所以要使用16位寄存器来存放除数。
关键代码为:
mov dx,1h
mov ax,86a1h
mov bx,100
div bx
案例2,编程,利用除法指令计算1001/100
1001小于16位,可以用ax寄存器存放,被除数为16位,除数100小于255,可用8位寄存器存放。
mov ax,1001
mov bl,100
div bl
8. 伪指令dd
dd是用来定义dword(double word,双字)型数据的,占据的字节为32位。比如:
data segment
db 1
dw 1
dd 1
data ends
在data中定义了3个数据,第一个数据为01H,在data:0处,占一个字节;第二个数据为0001H,在data:1处,占一个字;第三个数据为00000001H,在data:3处,占2个字。
案例,用下面data段中第一个数据除以第二个数据后的结果,商存在第三个数据的存储单元中。
data segment
dd 100001
dw 100
dw 0
data ends
第一个为双字型数据,长度为4个字节,32位,位置为data:0,作为被除数应该将低16位存在ax中,高16位存在dx中;
第二个字型数据,长度为2个字节,位置为:data:4,作为除数,应该存放在16位寄存器中;
第三个字型数据,长度为2个字节,位置为data:6。
关键程序为:
mov ax,data
mov ds,ax
mov ax,[0]
mov dx,[2]
mov bx,[4]
div bx
mov [6],ax
也可以直接除即
div word ptr [4]
9. dup指令
dup和db、dw、dd等数据定义伪指令配合使用,用来进行数据的重复。比如:
db 3 dup (0)
表示定义了3个字节,值都是0,相当于db 0,0,0。
db 3 dup (1,2,3)
定义了9个字节,值是1,2,3,1,2,3,1,2,3,相当于db 1,2,3,1,2,3,1,2,3。
db 3 dup (‘abc’,’edf’)
定义了18个字节,值是adcdefabcdefabcdef,相当于db ‘adcdefabcdefabcdef’。
dup的使用格式为:
db 重复的次数 dup (重复的字节型数据)
dw 重复的次数 dup (重复的字型数据)
dd 重复的次数 dup (重复的双字型数据)
dup指令符的重要意义有简化定义数据的操作,比如定义一个容量为1000个字节的栈段,即使使用dd,也要打很多0,使用dup可以大大简化编程,比如:
stack segment
db 200 dup (0)
stack ends
10. 综合使用寻址方式处理结构化数据访问的例子
某公司从1975年成立一直到1995年的基本情况如下,
年份 |
收入 |
雇员 |
人均收入 |
1975 |
16 |
3 |
? |
1976 |
22 |
7 |
? |
1977 |
356 |
9 |
? |
给出数据材料如下:
data segment
db '1975','1976','1977','1978','1979','1980','1981','1982','1983'
db '1984','1985','1986','1987','1988','1989','1990','1991','1992'
db '1993','1994','1995'
dd 16,22,382,1356,2390,8000,16000,24486,50065,97479,140417,197514
dd 345980,590827,803530,1183000,1843000,2795000,3753000,4649000,5937000
dw 3,7,9,12,28,38,130,220,476,778,1001,1442,2258,2793,4072,5646,8334
dw 11542,14430,15257,17800
data ends
table segment
db 21 dup ('year summ ne ?? ')
table ends
编程,将data段中的数据,按如下格式写入到table段中,并计算每年的人均收入(取整),结果也按照下面格式保存在table段中。
年份(4字节) |
空格 |
收入(4字节) |
空格 |
人数(2字节) |
空格 |
平均(2) |
空格 |
|||||||||
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
a |
b |
c |
d |
e |
f |
|
1975 |
|
16 |
|
3 |
|
? |
|
|||||||||
1976 |
|
22 |
|
7 |
|
? |
|
编程
assume cs:code
data segment
db '1975','1976','1977','1978','1979','1980','1981','1982','1983'
db '1984','1985','1986','1987','1988','1989','1990','1991','1992'
db '1993','1994','1995'
dd 16,22,382,1356,2390,8000,16000,24486,50065,97479,140417,197514
dd 345980,590827,803530,1183000,1843000,2795000,3753000,4649000,5937000
dw 3,7,9,12,28,38,130,220,476,778,1001,1442,2258,2793,4072,5646,8334
dw 11542,14430,15257,17800
data ends
table segment
db 21 dup ('year summ ne ?? ')
table ends
code segment
start: mov ax,data
mov ds,ax
mov ax,table
mov es,ax
mov bx,0h
mov si,0h
mov di,0h
mov cx,21h
s: mov ax,ds:[0h+si]
mov es:[bx+0h],ax
mov ax,ds:[2h+si]
mov es:[bx+2h],ax
mov ax,ds:[54h+si]
mov es:[bx+5h],ax
mov dx,ds:[56h+si]
mov es:[bx+7h],dx
mov ax,ds:[0a8h+di]
mov es:[bx+0ah],ax
push cx
mov cx,ds:[0a8h+di]
div cx
mov es:[bx+0dh],ax
add si,4h
add di,2h
pop cx
add bx,10h
loop s
mov ax,4c00h
int 21h
code ends
end start
也可以不使用栈作为暂存,而是直接使用内存单元做除法,即div word ptr ds:[0a8h+di]。
十、 转移指令的原理
可以修改ip,或者同时修改cs和ip的指令统称为转移指令。转移指令就是可以控制cpu执行内存中某处代码的指令。
8086cpu的转移行为按照修改对象,分为:只修改ip的段内转移,比如,jmp ax;同时修改cs和ip的段间转移,比如,jmp 1000:0。
针对ip的修改范围不同,段内转移又分为:段转移,ip的修改范围为-128~127;近转移,ip的修改范围为-32768~32767。
按照转移的功能,8080cpu的转移指令分为:无条件转移指令,如jmp;条件转移指令;循环指令,如loop;过程;中断。
1. 操作符offset
操作符offset在汇编语言中是由编译器处理的符号,它的功能是取得符号的偏移地址。比如程序:
assume cs:code
code segment
start: mov ax,offset start
s: mov ax,offset s
code ends
end start
offset start,取得了start的偏移地址,start是程序最开始处,它的偏移地址为0。所以,mov ax,offset start,相当于mov ax,0。
因为,mov ax,0占了3个字节。所以,标号s处的偏移地址为3,mov ax,offset s,相当于mov ax,3。
比如,写一段程序,将s处的一条指令复制到s0处。
assume cs:code
code segment
s: mov ax,bx
mov si,offset s
mov di,offset s0
mov ax,cs:[si]
mov cs:[di],ax
s0: nop
nop
code ends
end s
由于当程序执行时,程序被加载在内存中的位置的段地址在cs中,所以使用cs,在结合偏移地址,可以获得当前程序在内存中的详细地址。
2. jmp指令
jmp为无条件转移指令,可以只修改ip,也可以同时修改cs和ip。
使用jmp指令需要给出两种信息:转移的目的地址;转移的距离(段间转移,段内短转移,段内近转移)。不同的给出目的地址的方法,和不同的转移位置,对应不同格式的jmp指令。
1) 依据位移进行转移的jmp指令
jmp short 标号
转到标号处执行指令。这种格式的jmp指令实现的是段内短转移,对ip的修改范围为-128~127,向前转移时最多越过128个字节,向后转移时最多越过127个字节。Short表示进行短转移。标号是代码段中的标号,指明转移的目的地,转移结束后,cs:ip指向标号出的指令。
比如,程序
assume cs:code
code segment
start: mov ax,0
jmp short s
add ax,1
s: inc ax
code ends
end start
程序执行后,ax中的值为1,因为jmp short s,使得程序越过了add ax,1,ip指向了标号s处的inc ax。
原理是:Cpu在执行jmp指令时不需要知道转移的目的地址,执行过jmp指令,ip就指向了将要跳过的指令的起始位置(也就是执行过jmp后,指令缓冲器中ip指向的地址),然后根据转移目的地址(也就是标号处的地址)减去jmp紧跟着的指令的起始地址(也就是执行过jmp后,ip在缓存中指向的地址)得到一个差值,然后,新的ip就是ip在缓冲器中的地址加上这个差值。
比如上例中,假设偏移地址与汇编程序的对应如下:
assume cs:code
code segment
start:0000h mov ax,0
0003h jmp short s
0005h add ax,1
s: 0008h inc ax
code ends
end start
当执行jmp时,ip指向了jmp后的指令,也就是0005,当执行jmp short时,cpu根据标号处的偏移地址0008h减去ip当前的位置0005h得到一个差值03(因为是短转移,所以小于100h,也就是128),当执行过jmp后,ip得到了要跳转的命令,新的ip就是ip在缓冲器中的值0005h加上这个差值03h,即0008h,那么ip就指向了inc ax这条指令,然后执行之。
归纳起来,jmp short 标号的功能是:ip=ip+8位位移。基本过程是:
8位位移=标号处的地址-jmp指令后的第一个字节的地址;
Short表明此处的位移为8位位移;
8位位移的范围为-128~127,用补码表示;
8位位移由编译程序在编译时算出。
与这个相似的段内近转移,jmp near ptr 标号,实现的原理相似,即ip=ip+16位位移,基本过程是:
16位位移=标号处的地址-jmp指令后的第一个字节的地址;
near表明此处的位移为16位位移;
16位位移的范围为-32768~32767,用补码表示;
16位位移由编译程序在编译时算出。
3. 转移的目的地址在指令中的jmp指令
jmp far ptr 标号,实现的是段间转移,又称为远转移。功能是:
将cs指向标号所在段的段地址;ip指向标号所在段中的偏移地址。实际上是修改了cs和ip的值。
比如:
assume cs:code
code segment
start: mov ax,0
mov bx,0
jmp far ptr s
db 256 dup(0)
s: add ax,1
inc ax
code ends
end start
在debug中将这段程序翻译为机器码,可以看到jmp far ptr s,对应的机器码为EA0B016A07,076A标明了转移目的地址的段地址,010B标明了转移目的地址的偏移地址。
4. 转移地址在寄存器中的jmp指令
jmp 16位的寄存器,功能是:(ip)=(16位寄存器)。比如,jmp ax。将ip的值改为ax中的值。
5. 转移地址在内存中的jmp指令
转移地址在内存中的jmp指令有两种格式。
1) jmp word ptr 内存单元地址(段内转移)
功能是从内存单元地址处开始的一个字,作为转移目的偏移地址。内存单元地址可用任意合法格式的寻址方式给出。
比如1,
mov ax,0133h
mov ds:[0],ax
jmp word ptr ds:[0]
比如2,
mov ax,0134h
mov [bx],ax
jmp word ptr [bx]
2) jmp dword ptr 内存单元地址(段间转移)
功能是从内存单元地址处开始存放的两个字,高地址处的字是转移目的段地址,低地址处是转移目的偏移地址。即,CS=内存单元地址+2,IP=内存单元地址。内存单元地址可以使用任意合法格式的寻址方式。
比如1,
mov ax,0123h
mov ds:[0],ax
mov word ptr ds:[2],0
jmp dword ptr ds:[0]
执行上述程序后,cs中的值=0,ip中的值=0123h。cs:ip指向0000:0123。
比如2,
mov ax,0123h
mov [bx],ax
mov word ptr [bx+2],0
jmp dword ptr [bx]
执行后,cs中的值=0,ip中的值=0123h。cs:ip指向0000:0123。
6. jcxz指令
jcxz指令为有条件转移指令,所有的有条件转移指令都是短转移,在对应的机器码中包含转移的位移,而不是目的地址。对ip的修改范围为:-128~127。
指令格式:jcxz 标号(如果(cx)=0,转移到标号出执行。)
操作:当(cx)=0时,(ip)=(ip)+8位位移。
8位位移=标号处的地址-jcxz指令后的第一个字节的地址。
8位位移的范围是-128~127,用补码表示。
8位位移由编译程序在编译时算出。
当cx中的值不等于0是,jcxz指令什么也不做。
实际上,jcxz的功能相当于,if((cx)==0) jmp short 标号。
比如,编程实现,利用jcxz指令,在内存2000h段中查找第一个值为0的字节,找到后,将它的偏移地址存储在dx中。
assume cs:code
code segment
start: mov ax,2000h
mov ds,ax
mov bx,0
s: mov cx,0
mov cl,[bx]
jmp ok
inc bx
jmp short s
ok: mov dx,bx
mov ax,4c00h
int 21h
code ends
end start
7. loop指令
loop指令是循环指令,所有的循环指令都是短转移,在对应的机器码中标明转移的位移,而不是目的地址。对ip的修改范围为:-128~127。
指令格式:loop标号((cx)=(cx)-1,如果(cx)不等于0,转移到标号出执行。)。
操作过程关键部分:
(cx)=(cx)-1;
如果(cx)不等于0,(ip)=(ip)+8位位移。
8位位移=标号处的地址-loop指令后的第一个字节的地址。
8位位移的范围是-128~127,用补码表示。
8位位移由编译程序在编译时算出。
loop转移是有条件转移。
如果(cx)=0,什么都不做,程序向下执行。
类比c语言,loop的功能相当于:(cx)--;if((cx)!=0) jmp short 标号;
比如,编程实现,利用loop指令,实现在内存2000h段中查找第一个值为0的字节,找到后,将它的偏移地址存储在dx中。
assume cs:code
code segment
start: mov ax,2000h
mov ds,ax
mov bx,0
s: mov cl,[bx]
mov ch,0
inc cx
inc bx
loop s
ok: dec bx
mov dx,bx
mov ax,4c00h
int 21h
code ends
end start
8. 根据位移转移的意义和对转移位移超界的检测
1) 根据位移转移的意义
jmp short 标号
jmp near ptr 标号
jcxz 标号
loop 标号
等指令,都是根据转移目的地址和转移起始地址之间的位移来修改ip的值。这样有利于实现程序段在内存中的浮动装配。使用位移来得到目的地址,只涉及ip要做的位移,而不是固定住ip的地址。如果执行转移指令的机器码是固定的目的地址,就是对内存中的偏移地址做了严格的限制,如果这个固定的目的地址上没有要转移到的指令,那么程序的执行就会出错。如果使用位移就可以避免这个问题,因为位移是根据指令的实际情况计算得出的。即使发生了意外,仍然根据实际情况计算得出意外后的位移数,增强了程序的健壮性。
2) 编译器对转移位移超界的检测
根据位移进行转移的指令,它们的转移范围受到转移位移的限制,如果在源程序中出现了转移范围超界的问题,在编译的时候,编译器将报错。
比如,
assume cs:code
code segment
start: jmp short s
db 128 dup (0)
s: mov ax,0ffffh
code ends
end start
9. 综合练习
在屏幕中间分别显示绿色、绿底红色、白底蓝色的字符串“hello assembly !”。
编程前,需要先了解80*25彩色字符模式显示缓冲区的结构。
内存地址空间中,在b8000H~bffffH共32kb的空间中,为80*25彩色字符模式的显示缓冲区。向这个地址空间写入数据,写入的内容将立即出现在显示器上。在80*25彩色字符模式下,显示器可以显示25行,每行80个字符,每个字符可以有256种属性(背景、前景色、闪烁、高亮等组合信息,也就是2的8次方种组合)。一个字符在显示缓冲区中占两个字节,分别存放字符的ascii码和属性。80*25模式下,一屏的内容在显示缓冲区中占4000个字节。显示缓冲区分为8页,每页4kb,显示器可以显示任意一页的内容。一般情况下,显示第0页的内容,通常是,b8000H~b8f9fH中的4000个字节的内容出现在显示器上。在一行中,一个字符占两个字节的存储空间,低位字节存储字符的ascii码,高位字节存储字符的属性。比如,在显示器的0行0列显示黑低绿色的字符串’ABC’。A的ascii码值为41H,02H表示黑底绿色。在显示缓冲区中,偶地址存放字符,奇地址存放字符的颜色属性。属性字节的格式为:
7 |
6 |
5 |
4 |
3 |
2 |
1 |
0 |
BL |
R |
G |
B |
I |
R |
G |
B |
闪烁与否 |
背景 |
高亮与否 |
前景 |
属性用8位的2进制数表示,比如,红底闪烁绿字,11000010B,16进制为caH,缓冲区中存放的是16进制的数,如果将红底闪烁绿字的A放入缓冲区的0行0列,则显示字符内容为:41 CA
闪烁的效果必须在全屏dos方式下才能看到。Alt+enter进入全屏。
编程:
assume cs:code,ds:data
data segment
db 'hello assembly !'
data ends
code segment
start: mov ax,data
mov ds,ax
mov bx,0
mov ax,0b800h
mov es,ax
mov si,64
mov cx,16
s: mov al,[bx]
mov ah,01000010b
mov es:[si],ax
inc bx
add si,2
loop s
mov bx,0
mov si,1*160+64
mov cx,16
s1: mov al,[bx]
mov ah,01100010b
mov es:[si],ax
inc bx
add si,2
loop s1
mov bx,0
mov si,2*160+64
mov cx,16
s1: mov al,[bx]
mov ah,01100010b
mov es:[si],ax
inc bx
add si,2
loop s1
mov ax,4c00h
int 21h
code ends
end start
十一、 CALL和RET指令
call和ret指令都是转移指令,它们都修改ip,或同时修改cs和ip。经常被共同用来实现子程序的设计。
1. ret和retf
ret指令用栈中的数据,修改ip的内容,从而实现近转移。
retf指令用栈中的数据,修改cs和ip的内容,从而实现远转移。
Cpu执行ret指令时,进行的操作是:(ip)=((ss)*16+(sp));(sp)=(sp)+2。相当于出栈操作,将出栈的字作为ip的值。
Cpu执行retf指令时,进行的操作是:(ip)=((ss)*16+(sp));(sp)=(sp)+2;(cs)=((ss)*16+(sp));(sp)=(sp)+2。相当于两次出栈操作,第一次出栈赋值给ip,第二次出栈赋值给cs。
比如,编程实现从内存1000:0000处开始执行指令。
assume cs:code
stack segment
db 16 dup (0)
stack ends
code segment
start: mov ax,stack
mov ss,ax
mov sp,16
mov ax,1000h
push ax
mov ax,0
push ax
retf
code ends
end start
2. call指令
cpu执行call指令时,进行两步操作,将当前的ip或cs和ip压入栈中,然后转移。Call指令不能实现短转移,call指令实现转移的方法和jmp指令的原理相同。
1) 依据位移进行转移的call指令
call 标号,将当前的ip压栈后,转到标号处执行指令。
Cpu执行这个call指令时,进行的操作是:
(sp)=(sp)-2
((ss)*16+(sp))=(ip)
(ip)=(ip)+16位位移
16位位移=标号处的地址-call指令后
的第一个字节的地址;
16位位移的范围为-32768-32767,用补码表示;
16位位移由编译程序在编译时算出。
这条指令相当于,执行了push ip和jmp near ptr 标号。
比如,下面的程序执行后,计算ax中的值
1000:0 |
b8 00 00 |
mov ax,0 |
1000:3 |
e8 01 00 |
call s |
1000:6 |
40 |
inc ax |
1000:7 |
58 |
s:pop ax |
ax中的值为0006。
2) 转移目的地址在指令中的call指令
call far ptr 标号
是段间转移。Cpu执行这种格式的call指令是,操作过程是:
(sp)=(sp)-2
((ss)*16+(sp))=(cs)
(sp)=(sp)-2
((ss)*16+(sp))=(ip)
(cs)=标号所在段的段地址
(ip)=标号所在段中的偏移地址
这种call指令相当于,push cs和push ip和jmp far ptr 标号。
比如,下面的程序执行后,计算ax中的值
1000:0 |
b8 00 00 |
mov ax,0 |
1000:3 |
9A 09 00 00 10 |
call far ptr s |
1000:8 |
40 |
inc ax |
1000:9 |
58 |
s:pop ax |
|
|
add ax,ax |
|
|
pop bx |
|
|
add ax,bx |
ax中的值为1010h。
3) 转移地址在寄存器中的call指令
Call 16位寄存器
Cpu的操作过程是:
(sp)=(sp)-2
((ss)*16+(sp))=(ip)
(ip)=(16位寄存器)
相当于执行了push ip和jmp 16位寄存器。
比如,下面的程序执行后,计算ax中的值
1000:0 |
b8 06 00 |
mov ax,6 |
1000:2 |
ff d0 |
call ax |
1000:5 |
40 |
inc ax |
1000:6 |
|
mov bp,sp |
|
|
add ax,[bp] |
ax中的值为000bh。
4) 转移地址在内存中的call指令
转移地址在内存中的call指令有两种格式。
第一种,call word ptr 内存单元地址
Cpu执行这种指令时,相当于进行,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 内存单元地址
Cpu执行这种指令相当于执行了,push cs和push ip和jmp dword ptr 内存单元地址。
比如,
mov sp,10h
mov ax,0123h
mov ds:[0],ax
mov word ptr ds:[2],0
call word ptr ds:[0]
执行后,(cs)=0,(ip)=0123h。
3. call和ret的配合使用
配合使用ret和call可以实现子程序。
1) 案例1,程序返回前,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
程序返回前,bx中的值是8。
根据上面程序的特点,可以写一个具有一定功能的程序段,作为子程序,在需要的时候,用call指令转去执行这个子程序。Call指令转去执行子程序之前,call指令后面的指令的地址将存储在栈中,所以可以在子程序的后面使用ret指令,用栈中的数据设置ip的值,从而转到call指令后面的代码继续执行。
使用call和ret来实现的子程序的框架如下,
assume cs:code
code segment
main: …
…
call sub1
…
…
mov ax,4c00h
int 21h
sub1: …
…
call sub2
…
…
ret
sub2: …
…
ret
code ends
end main
4. mul指令
mul是乘法指令,使用mul指令需要注意的两点。
两个相乘的数,要么都是8位,要么都是16位。如果是8位,一个默认放在al中,另一个放在8为寄存器或内存单元中;如果是16位,一个默认在ax中,另一个放在16位寄存器或内存单元中。
相乘的结果,如果是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位。
案例1,计算100*12。
100和10都小于255,都是8位,做8位乘法。程序片段,
mov al,100
mov bl,10
mul bl
案例2,计算99*1000。
100小于255,但是1000大于255,所以必须做16位乘法,程序片段,
mov ax,100
mov bx,1000
mul bx
5. 模块化程序设计
使用call和ret指令可以实现汇编语言编程中的模块化设计。在实际编程中,程序的模块化是必不可少的。现实问题复杂,把复杂的问题转化成为相互联系、不同层次的子问题,是必须的解决方法。利用call和ret,可以用简洁的方法,实现多个相互联系、功能独立的子程序来解决一个复杂的问题。
有关子程序设计中的相关问题和解决方法。
1) 参数和结果传递的问题
通常,子程序要根据提供的参数处理一定的事务,处理后,将结果提供给调用者。这就是参数和结果传递的问题。比如,设计一个子程序,根据提供的n,计算n的3次方。
这个程序涉及到参数n的存储地方和计算得到的值存储在什么地方的问题。实际上,可以使用寄存器来存储。比如,
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 0,0,0,0,0,0,0,0
data ends
code segment
start: mov ax,data
mov ds,ax
mov si,0
mov di,16
mov cx,8
s: mov bx,ds:[si]
call cube
mov ds:[di],ax
mov ds:[di+2],dx
add si,2
add di,4
loop s
mov ax,4c00h
int 21h
cube: mov ax,bx
mul bx
mul bx
ret
code ends
end start
2) 批量数据的传递
如果子程序只有一个参数,可以放在bx中,如果有更多个时,就不适合放在寄存器中了,因为寄存器的数量是有限的。对于返回值,也有同样的问题。
解决的方式可以是,将批量数据放到内存中,然后将它们所在内存空间的首地址存放在寄存器中,传递给需要这些数据的子程序。对于具有批量数据的返回结果,同此。
比如,设计一个子程序,将一个全是字母的字符串转化为大写形式。
因为字符串可能很长,不方便将整个字符串中的所有字母全部直接传递给子程序。但是,可以将字符串在内存中的首地址放在寄存器中传递给子程序。然后在子程序中做循环,循环的长度就是字符串的长度。
assume cs:code
data segment
db ‘sdfbsodjfleojfljsldjf’
data ends
code segment
start: mov ax,data
mov ds,ax
mov bx,0
mov cx,21
call cap
mov ax,4c00h
int 21h
cap: and byte ptr [bx],11011111b
inc bx
loop cap
ret
code ends
end start
注意,除了使用寄存器传递参数外,还有一种通用的方法是用栈来传递参数。
3) 寄存器冲突的问题
案例1
设计一个子程序,将一个全是字母,以0结尾的字符串,转化为大写。
由于字符串后面有一个0标记字符串的结束。所以,在转化的时候,要先判断,如果不是0就转化,如果是0就结束。可以用jcxz来检测0,做判断结束处理。
assume cs:code
data segment
db ‘sdfbsodjfleojfljsldjf’,0
data ends
code segment
start: mov ax,data
mov ds,ax
mov bx,0
call cap
mov ax,4c00h
int 21h
cap: mov cl,[bx]
mov ch,0
jcxz ok
add byte ptr [bx],11011111b
inc bx
jmp short cap
ok: ret
code ends
end start
案例2,将如下data段中的字符串全部转化为大写
assume cs:code
data segment
db ‘haha’,0
db ‘hehe’,0
db ‘heih’,0
db ‘hell’,0
data ends
code segment
start: mov ax,data
mov ds,ax
mov bx,0
mov cx,4
s: call cap
inc bx
loop s
mov ax,4c00h
int 21h
change: mov cl,[bx]
mov ch,0
jcxz ok
add byte ptr [bx],11011111b
inc bx
jmp short change
ok: ret
code ends
end start
上述程序中,出现了主程序和子程序寄存器冲突的问题。一般解决思路是,编写子程序的时候主要看看是否使用了冲突的寄存器,如果有,调用者使用别的寄存器或者子程序中不要使用产生冲突的寄存器。然而,这样的解决思路是行不通的,因为子程序很多时候是具有广泛使用性的,有时候编写的子程序到底会被哪个程序调用具有可变性的。这个问题的关键在于是调用者和子程序相互不用关心对方使用了哪些寄存器。通用的解决思路是,在调用子程序的时候,将所有用到的寄存器中的内容保存起来,到栈中,在子程序返回前再恢复这些寄存器中内容。可以很好的实现相互的隔离性。
使用栈保存子程序开始时用到的寄存器中的内容
assume cs:code
data segment
db ‘haha’,0
db ‘hehe’,0
db ‘heih’,0
db ‘hell’,0
data ends
stack segment
dw 0,0,0,0
stack ends
code segment
start: mov ax,stack
mov ss,ax
mov sp,4
mov ax,data
mov ds,ax
mov bx,0
mov cx,4
s: call cap
add bx,5
loop s
mov ax,4c00h
int 21h
cap: push cx
push bx
change: mov cl,[bx]
mov ch,0
jcxz ok
and byte ptr [bx],11011111b
inc bx
jmp short change
ok: pop bx
pop cx
ret
code ends
end start
注意出入栈数据的先后顺序。
引入栈作为隔离机制后,子程序的标准框架为:
子程序开始: 子程序中使用的寄存器入栈
子程序内容
子程序中使用的寄存器出栈
返回(ret、retf)
6. 子程序综合案例
1) 按行列和颜色要求显示字符串
功能需求,在指定的位置,用指定的颜色,显示一个用0结束的字符串。
参数,(dh)=行号(取值范围0~24),(dl)=列号(取值范围0~79),(cl)=颜色,ds:si指向字符串的首地址。
返回值,无。
比如,在屏幕的8行3列,用绿色显示data段中的字符串。
assume cs:code
data segment
db ‘hello assembly!’,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 dx
push cx
mov ax,0b800h
mov es,ax
mov al,160
dec dh
mul dh
mov dh,0
mov bx,ax
mov al,2
dec dl
mul dl
mov dl,al
add bx,dx
mov di,bx
change: mov cl,[si]
mov bl,cl
mov ch,0
jcxz ok
pop cx
mov es:[di],bl
mov es:[di+1],cl
inc si
add di,2
push cx
jmp short change
ok: pop cx
pop dx
ret
code ends
end start
2) 解决除法溢出的问题
用div做除法的时候,是可能发生溢出的。8位除法,al存储商;16位除法,ax存储商。如果商大于al或ax所能存储的最大值,就会溢出。比如
mov bh,1
mov ax,1000
div bh
结果商为1000,大于al所能存放的最大值255。
比如,
mov ax,1000h
mov dx,1
mov bx,1
div bx
结果的商为11000h,大于ax所能存放的最大数ffffh。
这类情况,将导致cpu发生一个内部错误,就是除法溢出。
使用子程序解决除法溢出的问题
功能,进行不会产生溢出的除法运算,被除数为dword型,除数为word型,结果为dword型。
参数,(ax)=dword型数据的低16位,(dx)=dword型数据的高16位,(cx)=除数。
返回值,(dx)=结果的高16位,(ax)=结果的低16位,(cx)=余数。
比如,计算1000000/10(f4240h/0ah)。
编写这个子程序,需要用到一个公式:
X:被除数,范围[0,ffffffff]
N:除数,范围[0,ffff]
H:X高16位,范围[0,ffff]
L:X低16位,范围[0,ffff]
int():描述性运算符,取商,比如,int(45/10)=4
rem():描述性运算符,取余数,比如,rem(45/10)=5
公式:X/N=int(H/N)*65536+[rem(H/N)*65536+L]/N
公式将可能产生溢出的除法运算:X/N,转变为多个不会产生溢出的除法运算。等号右边的所有除法运算都可以用div指令来做,不会导致除法溢出。
assume cs:code
stack segment
dw 0,0,0,0,0,0,0,0
stack ends
code segment
start: mov ax,stack
mov ss,ax
mov sp,16
mov ax,4240h
mov dx,000fh
mov cx,0ah
call divdw
mov ax,4c00h
int 21h
divdw: push bx
push ax
mov ax,dx
mov dx,0
div cx
mov bx,ax
pop ax
div cx
mov cx,dx
mov dx,bx
pop bx
ret
code ends
end start
3) 数值显示
关于数字的数值,有多种表示方式,数据在机器中存储为二进制形式的,显卡遵循的是ascii编码,外界在显示器上看到的字符在内存中的形式为16进制的,为了能够以常用10进制的形式在显存中看到这个数值,需要编写程序实现数据以十进制形式显示。这个程序分为两步,将使用二进制存储的数据转变为十进制形式的字符串,显示十进制形式的字符串。关于显示字符串,上述已经用子程序实现。所以,还需要编写一个将二进制数据转为十进制形式的字符串的子程序。
功能,将word型数据转变为表示十进制数的字符串,字符串以0为结尾符。
参数,(ax)=word型数据,ds:si指向字符串的首地址。
返回,无。
比如,编程,将数据12345以十进制的形式在屏幕的8行3列,用绿色显示。
assume cs:code
data segment
db 10 dup (0)
data ends
code segment
start: mov ax,12345
mov bx,data
mov ds,bx
mov si,0
mov di,0
call dtoc
mov dh,8
mov dl,3
mov cl,2
call show_str
mov ax,4c00h
int 21h
show_str: push dx
push cx
mov ax,0b800h
mov es,ax
mov al,160
dec dh
mul dh
mov dh,0
mov bx,ax
mov al,2
dec dl
mul dl
mov dl,al
add bx,dx
mov di,bx
change: mov cl,[si]
mov bl,cl
mov ch,0
jcxz ok
pop cx
mov es:[di],bl
mov es:[di+1],cl
inc si
add di,2
push cx
jmp short change
ok: pop cx
pop dx
ret
dtoc: push ax
push cx
push bx
push si
push dx
mov bx,0
divdw: mov dx,0
mov cx,10
div cx
add dx,'0'
push dx
mov cx,ax
inc bx
jcxz ok2
jmp short divdw
ok2: mov cx,bx
x1: pop ds:[si]
inc si
loop x1
pop dx
pop si
pop bx
pop cx
pop ax
ret
code ends
end start
7. 综合设计
任务,将前面做过的某公司的数据按照图中所示的格式在屏幕上显示出来。注意除法的溢出问题和字符串的长度问题。
1975 |
16 |
3 |
5 |
1976 |
22 |
7 |
3 |
1977 |
343 |
9 |
43 |
1978 |
2345 |
13 |
134 |
…… …… |
|
|
|
1994 |
46750000 |
15467 |
304 |
1995 |
59780000 |
17809 |
356 |
编程
assume cs:code
data segment
db '1975','1976','1977','1978','1979','1980','1981','1982','1983'
db '1984','1985','1986','1987','1988','1989','1990','1991','1992'
db '1993','1994','1995'
dd 16,22,382,1356,2390,8000,16000,24486,50065,97479,140417,197514
dd 345980,590827,803530,1183000,1843000,2795000,3753000,4649000,5937000
dw 3,7,9,12,28,38,130,220,476,778,1001,1442,2258,2793,4072,5646,8334
dw 11542,14430,15257,17800
data ends
agency segment
db 8 dup (0)
agency ends
code segment
start: mov ax,0b800h
mov es,ax
mov di,0
mov cx,80*24
cls: mov byte ptr es:[di],‘ ’;将屏幕清空
mov byte ptr es:[di+1],0
inc di
inc di
loop cls
mov ax,data
mov es,ax
mov di,0
mov bx,0
mov ax,agency
mov ds,ax
mov si,0
mov dh,4
mov cx,21
x1: push cx
mov ax,es:[di]
mov [si],ax
mov ax,es:[di+2]
mov [si+2],ax
mov byte ptr [si+4],0 ;显示年份
mov dl,0
mov cl,2
call show_str
mov ax,es:[84+di]
push dx
mov dx,es:[84+di+2]
call dtoc_dword ;显示收入
pop dx
mov dl,20
mov cl,2
call show_str
mov ax,es:[84+84+bx]
call dtoc_word ;显示雇员数
mov dl,40
mov cl,2
call show_str
mov ax,es:[84+di]
push dx
mov dx,es:[84+di+2]
div word ptr es:[84+84+bx] ;显示人均收入
call dtoc_word
pop dx
mov dl,60
mov cl,2
call show_str
add di,4
add bx,2
add dh,1
pop cx
loop x1
mov ax,4c00h
int 21h
;show_str
;功能,在屏幕的指定位置,用指定颜色,显示一个用0结尾的字符串
;参数,(dh)=行号,(dl)=列号,(cl)=颜色,ds:si,该字符串的首地址
;将指定内存中的字符串显示在屏幕上
show_str:
push ax
push cx
push dx
push di
push si
push es
mov ax,0b800h
mov es,ax
mov al,160
mul dh
add bl,bl
mov dh,0
add ax,bx
mov di,ax
mov ah,cl
show_str_x:
mov cl,[si]
mov ch,0
jcxz show_str_f
mov al,cl
mov se:[di],ax
inc si
add di,2
jmp show_str_x
show_str_f:
pop di
pop si
pop dx
pop cx
pop ax
ret
;dtoc_dword
;功能,将32位的二进制转化为对应的十进制字符串
;参数,(ax)=32位的低16位,(dx)=32位的高16位
;将转化的字符串放入ds:si指向的内存单元,以0结尾
dtoc_dword:
push ax
push bx
push cx
push dx
push si
mov bx,0
dtoc_dword_x:
mov cx,10
call divdw
push cx
inc bx
cmp ax,0
jne dtoc_dword_x
cmp dx,0
jne dtoc_dword_x
mov cx,bx
dtoc_dword_x1:
pop ds:[si]
add byte ptr ds:[si],’0’
inc si
loop dtoc_dword_x1
pop si
pop dx
pop cx
pop bx
pop ax
ret
;dtoc_word
;功能,将一个word型数转化为字符串
;参数,(ax)=word型的数据,ds:si指向字符串的首地址
;返回,ds:[si]存放转化后的字符串,以0结尾
dtoc_word:
push ax
push bx
push cx
push dx
push si
mov bx,0
dtoc_word_x:
mov dx,0
mov cx,10
div cx
mov cx,ax
add dx,’0’
push dx
inc bx
jcxz dtoc_word_f
jmp dtoc_word_x
dtoc_word_f:
mov cx,bx
dtoc_word_x1:
pop ds:[si]
inc si
loop dtoc_word_x1
pop si
pop dx
pop cx
pop bx
pop ax
ret
;divdw
;功能,除法,被除数32位,除数16位,商32位,余数16位,不会溢出
;参数,(ax)=被除数低16位,(dx)=被除数高16位,(cx)=除数
;返回,(dx)=商高16位,(ax)=商低16位,(cx)=余数
divdw:
push bx
push ax
mov ax,dx
mov dx,0
div cx
mov bx,ax
pop ax
div cx
mov cx,dx
mov dx,bx
pop bx
ret
code ends
end start
十二、 标志寄存器
Cpu内部中有一种特殊的寄存器,标志寄存器,flag register。具有3种作用,用来存储相关指令的某些执行结果,用来为cpu执行相关指令提供行为依据,用来控制cpu的相关工作方式。标志寄存器有16位,其中存储的信息通常被称为程序状态字(PSW)。标志寄存器按位起作用,每一位都有专门的含义,记录特定的信息。标志寄存器的结构示意图,
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 |
在8086cpu中,标志寄存器的1,2,5,12,13,14,15位没有使用,不具有任何含义。其他位都具有特殊的含义。
1. ZF标志
标志寄存器的第6位,零标志位。记录相关指令执行后,结果是否为0,如果结果为0,那么zf=1;如果结果不为0,那么zf=0。
比如,指令mov ax,1 sub ax,1执行后,结果为0,则zf=1。指令mov ax,1 sub ax,0执行后,结果不为0,则zf=0。
需要注意,在8086cpu的指令集中,有的指令的执行是影响标志寄存器的,比如,add、sub、mul、div、inc、or、and等,这些大都是运算指令;有的指令的执行对标志寄存器没有影响,比如,mov、push、pop等,大都是传送指令。使用指令的时候,主要指令的全部功能,包括执行结果对标志寄存器的哪些标志位造成影响。
2. PF标志
标志寄存器的第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。
3. SF标志
标志寄存器的第7位是sf,符号标志位。它记录相关指令执行后,其结果是否为负。如果为负,sf=1;如果非负,sf=0。计算机中通常用补码表示有符号数据。计算机中的一个数据可以看作是有符号数,也可以看成是无符号数。比如,00000001b,可以看作为无符号数1,或有符号数+1;10000001b,可以看作无符号数129,也可以看作有符号数-127。
也就是说,同一个二进制数据,计算机可以将它当作无符号数据来运算,也可以当作有符号数据来运算,比如,mov al,10000001b add al,1,结果为(al)=10000010b,将add指令当作无符号运算,相当于计算129+1,结果为130(10000010b);也可以将add指令进行的运算当作有符号数的运算,那么add指令相当于计算-127+1,结果为-126(10000010b)。
Sf标志,就是cpu对有符号数运算结果的一种记录,它记录数据的正负。如果我们需要将数据当作有符号数来运算,可以通过sf得知结果的正负;如果我们将数据当作无符号数来运算,sf的值没有意义,虽然相关指令影响了它的值。也就是说,cpu执行add指令时,必然要影响到sf标志位的值。
比如,mov al,10000001b add al,1执行后,结果为10000010b,sf=1,表示如果指令进行的是有符号数运算,那么结果为负。比如,mov al,10000001b add al,01111111b执行后,结果为0,sf=0,表示如果指令进行的是有符号数运算,那么结果为非负。
某些指令影响标志寄存器的多个标记位,这些被影响的标记位比较全面地记录了指令的执行结果,为相关的处理提供了所需的依据。比如指令sub al,al执行后,zf、pf、sf等标志位都受到了影响,它们分别是:1、1、0。
特别注意,只有那些会影响标志寄存器的指令执行后,标志寄存器的值才会变化,而不是某个非标志寄存器的值变化了就一定变化。
4. CF标志
标志寄存器的第0位是cf,进位标志位。一般情况下,在进行无符号数运算的时候,它记录了运算结果的最高有效位向更高位的进位值,或从更高位的借位值。
当两个数相加的时候,有可能从最高有效位向更高位的进位。比如,有8个数据,98H+98H,将产生进位。这个位值在8位数中无法保存,cpu在运算的时候,并不丢弃这个进位值,而是记录在一个特殊的寄存器的某一位,就是cf位来记录这个进位值。比如,mov al,98h add al,al执行后,(al)=30h,cf=1,cf记录了从最高位有效位向更高位的进位值。
当两个数做减法的时候,有可能向更高位借位。比如,两个8位数据,97H-98H,将产生借位,借位后,相当于计算197H-98H,cf位会记录这个借位值。比如,mov al,97h sub al,98h执行后,(al)=ffh,cf=1。
5. OF标志
在进行有符号数运算的时候,如果结果超过了机器所能表示的范围称为溢出。对于8位的有符号数据,机器所能表示的范围是-128~127,对于16位有符号数,机器所能表示的范围是-32768~32767。
比如,对于有符号数运算,mov al,98 add al,99执行后将产生溢出。比如,mov al,0f0h;f0h,为有符号数-16的补码add al,088h ;88h,为有符号数-120的补码。执行后,将产生溢出。
标志寄存器的第11位of,溢出标志位。一般情况下,of记录了有符号数运算的结果是否发生了溢出。如果发生了溢出,of=1;如果没有,of=0。
Cf和of的区别,cf是对无符号数运算有意义的标志位,而of是对有符号数运算有意义的标志位。比如,mov al,98 add al,99执行后,cf=0,of=1。
比如,mov al,0f0h add al,88h指令执行后,cf=1,of=1。对于无符号数运算,0f0h+88h有进位,cf=1;对于有符号数运算,0f0h+88h发生溢出,of=1。
比如 mov al,0f0h add al,78h指令执行后,cf=1,of=0。对于无符号运算,有进位,cf=1;对于有符号运算,不发生溢出,of=0。
Cf和of所表示的进位和溢出,是分别对无符号数和有符号数运算而言的,它们之间没有任何关系。
6. abc指令
abc是带进位加法指令,利用了cf位上记录的进位值。
格式,abc操作对象1,操作对象2
功能,操作对象1=操作对象1+操作对象2+cf
比如,abc ax,bx实现的功能是,(ax)=(ax)+(bx)+cf。
比如,
mov ax,2
mov bx,1
sub bx,ax
adc ax,1
执行后,cf=1,(ax)=2+1+1=4。
mov ax,1
add ax,ax
adc ax,3
执行后,cf=0,(ax)=2+3+0=5。
adc指令比add指令多加了一个cf位的值。
加法可以分为两步来进行,低位相加,高位相加再加上低位相加产生的进位值。比如指令add ax,bx可以通过add al,bl和adc ah,bh实现。Adc的意义是进行加法的第二步运算。Adc指令和add指令相配合可以对更大的数据进行加法运算。
案例,编程计算1fe000h+201000h,结果放在ax(高16位)和bx(低16位)中。
两个数据的位数都大于16,用add指令无法进行计算。将计算分两步进行,先将低16位相加,然后将高16位和进位值相加。
mov ax,001fh
mov bx,1000h
add bx,e000h
adc ax,0020h
adc指令执行后,也可能产生进位值,也会对cf进行设置。这样,使得可以对任意大的数据进行加法运算。比如,编程,计算1ef0001000h+2010001ef0h。结果放在ax(最高16位),bx(次高16位),cx(低16位)中。
计算分3步,先计算低16位,再使用adc计算次高16位加上cf中的值,再使用adc计算最高16位加上cf中的值。
mov cx,1000h
mov bx,f000h
mov ax,1eh
adc cx,1ef0h
adc bx,1000h
adc ax,20h
利用adc,编写一个子程序,对两个128位数据进行相加。
;名称:add128
;功能,两个128位数据进行相加
;参数,ds:si指向存储第一个数的内存空间,因数据为128位,需要8个字单元,由低地址单元向高地址单元依次存放这128位数据由低到高的各个字。ds:di指向存储第二个数的内存空间,运算结果存储在第一个数的存储空间中。
add128: push ax
push cx
push si
push di
mov ax,data
mov ds,ax
mov si,0
mov di,128
sub ax,ax ;将cf值为0
mov cx,8
ladc: mov ax,[si]
adc ax,[di]
mov [si],ax
inc si
inc si
inc di
inc di
loop ladc
pop di
pop si
pop cx
pop ax
ret
注意,inc和loop指令不影响cf的值。由于add指令会影响cf的值,如果上面的inc指令换成add,在上题的数据环境中,会将cf置为0,从而可能导致错误的结果。
7. sbb指令
sbb是带借位减法指令,利用了cf上记录的借位值。
指令格式:sbb操作对象1,操作对象2
功能,操作对象1=操作对象1-操作对象2-cf。
比如,sbb ax,bx,实现的功能是:(ax)=(ax)-(bx)-cf。
sbb指令执行后,将对cf进行设置。利用sbb指令可以对任意大的数据进行减法运算。比如,计算003e1000h-00202000h,结果放在ax,bx中。
mov bx,1000h
mov ax,003eh
sub bx,2000h
sbb ax,0020h
8. cmp指令
cmp是比较指令,cmp的功能相当于减法指令,只是不保存结果。Cmp指令执行后,将对标志寄存器产生影响。其他相关指令通过识别这些影响的标志寄存器位来得知比较结果。
Cmp指令格式:cmp 操作对象1,操作对象2。
功能,计算操作对象1-操作对象2,但不保存结果,仅仅根据计算结果对标志寄存器进行设置。
比如,指令cmp ax,ax,会做(ax)-(ax)的运算,结果为0,当不会在ax中保存,仅影响相关的标志位。执行后,zf=1,pf=1,sf=0,cf=0,of=0。
比如,指令mov ax,8 mov bx,3 cmp ax,bx执行后,(ax)=8,zf=0,pf=1,sf=0,cf=0,of=0。
通过cmp执行后,相关标志位的值就可以看出比较的结果。
比如,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,也不借位,zf=0且cf=0;
如果(ax)<=(bx),则(ax)-(bx)=0,也会借位,zf=1且cf=0;
根据标志位的结果和两个数大小关系的对应,可以利用标志位判断两个数的大小关系。即,
zf=1 |
(ax)=(bx) |
zf=0 |
(ax)!=(bx) |
cf=1 |
(ax)<(bx) |
cf=0 |
(ax)>=(bx) |
zf=0且cf=0 |
(ax)>(bx) |
zf=1且cf=0 |
(ax)<=(bx) |
Cpu执行cmp时,分为无符号数运算和有符号数运算。利用cmp可以对无符号数进行比较,也可以对有符号数进行比较。上述为cmp进行无符号比较时的情况。当cmp进行有符号数比较时,比如cmp ah,bh。
如果(ah)=(bh),则(ah)-(bh)=0,zf=1;
如果(ah)!=(bh),则(ah)-(bh)!=0,zf=0;
在cmp比较有符号数ah和bh中的数的大小的时候,应该看sf和of。
如果sf=1,of=0,说明没有没有溢出,实际结果为负,(ah)<(bh)。
如果sf=1,of=1,说明有溢出,虽然实际结果为负,那么逻辑结果必然为正,所以,(ah)>(bh)。
如果sf=0,of=1,说明有溢出,虽然实际结果为正,但逻辑结果为负,所以,(ah)<(bh)。
如果sf=0,of=0,说明没有溢出,实际结果的正负等于逻辑结果的正负,实际结果为非负,所以,(ah)>=(bh)。
9. 检测比较结果的条件转移指令
转移,表示能够修改ip,条件指根据某种条件,决定是否修改ip。比如,jcxz就是一个条件转移指令,它可以检测cx中的值,如果为0,就修改ip,否则什么也不做。而且所有的条件转移指令的转移位移都是[-128,127]。
Cpu提供的其他条件转移指令,大多数都是检测标志寄存器的相关标志位,根据检测的结果决定是否修改ip。检测的标志位就是被cmp指令影响的哪些。这些条件转移指令通常都和cmp配合使用,就像call和ret指令的配合一样。
因为cmp指令可以同时进行无符号数比较和有符号数比较,所以根据cmp指令的比较结果进行转移的指令也分为两种,即根据无符号数的比较结果进行转移的条件转移指令(它们检测zf、cf的值)和根据有符号数的比较结果进行转移的条件转移指令(检测sf、of和zf的值)。
常用的根据无符号数的比较结果进行转移的条件转移指令,如表,
指令 |
含义 |
检测的相关标志位 |
je |
等于则转移 |
zf=1 |
jne |
不等于则转移 |
zf=0 |
jb |
低于则转移 |
cf=1 |
jnb |
不低于则转移 |
cf=0 |
ja |
高于则转移 |
cf=0且zf=0 |
jna |
不高于则转移 |
cf=1或zf=1 |
j表示jump,e表示equal,ne表示not equal,b表示below,a表示above。
比如,编程实现如果(ah)=(bh)则(ah)=(ah)+(ah),否则(ah)=(ah)+(bh)。
cmp ah,bh
je s1
add ah,bh
jmp short ok
s1:
add ah,ah
ok:
……
当然,上述的条件转移指令,不一定要用在比较条件中,只要检测到相关的标志位符合转移发生的值就转移。比如,
mov ax,0
add ax,0
je s
inc ax
s:
inc ax
虽然上述程序没有进行比较,但是zf=1,je指令发生转移。
综合使用cmp和条件转移指令的例子,
比如,data段中的8个字节如下:
data segment
db 5,11,2,1,5,5,65,49
data ends
1) 统计data段中数据为5的字节的个数,用ax保存统计结果。
assume cs:code
data segment
db 5,11,2,1,5,5,65,49
data ends
code segment
start: mov ax,data
mov ds,ax
mov si,0
mov ax,0
mov bl,5
mov cx,8
s0:
cmp bl,[si]
je s1
goon:
inc si
loop s0
jmp short ok
s1:
inc ax
jmp short goon
ok:
mov ax,4c00h
int 21h
code ends
end start
或者
assume cs:code
data segment
db 5,11,2,1,5,5,65,49
data ends
code segment
start: mov ax,data
mov ds,ax
mov si,0
mov ax,0
mov bl,5
mov cx,8
s0:
cmp bl,[si]
jne s1
inc ax
s1:
inc si
loop s0
mov ax,4c00h
int 21h
code ends
end start
2) 统计data段中数值大于5的字节的个数,用ax保存统计结果
assume cs:code
data segment
db 5,11,2,1,5,5,65,49
data ends
code segment
start: mov ax,data
mov ds,ax
mov si,0
mov ax,0
mov bl,5
mov cx,8
s0:
cmp bl,[si]
jna s1
inc ax
s1:
inc si
loop s0
mov ax,4c00h
int 21h
code ends
end start
3) 统计data段中数值小于5的字节的个数,用ax保存统计结果
assume cs:code
data segment
db 5,11,2,1,5,5,65,49
data ends
code segment
start: mov ax,data
mov ds,ax
mov si,0
mov ax,0
mov bl,5
mov cx,8
s0:
cmp bl,[si]
jnb s1
inc ax
s1:
inc si
loop s0
mov ax,4c00h
int 21h
code ends
end start
10. DF标志和串传送指令
标志寄存器的第10位是DE,方向寄存器。在串处理指令中,控制每次操作后si、di的增减。
df=0 每次操作后si、di递增;df=1 每次操作后si、di递减。
1) 以字节为单位的串传送指令
格式:movsb
功能,执行movsb指令相当于进行下面操作。
((es)*16+(di))=((ds)*16+(si))
如果df=0,则(si)=(si)+1 (di)=(di)+1
如果df=1,则(si)=(si)-1 (di)=(di)-1
movsb的功能相当于将ds:si指向内存单元中的字节送入es:di中,然后根据寄存器df位的值,将si和di递增或递减。
2) 以字为单位的串传送指令
格式:movsw
movsw的功能相当于将ds:si指向内存单元中的字节送入es:di中,然后根据寄存器df位的值,将si和di递增2或递减2。
3) rep
一般来说,movsb和movsw都和rep配合使用,格式如下:
rep movsb 相当于s:movsb loop s
rep movsw 相当于s:movsw loop s
rep的作用是根据cx的值,重复执行后面的串传送指令。
4) cld和std
df位决定串传送指令执行后,si和di改变的方向。cld指令,将df位置为0;std指令,将df位置为1。
5) 案例1,编程,用串传送指令,将data段中的第一个字符串复制到它后面的空间中。
分析:
指令中,传送的起始位置ds:si,传送的目的位置es:di,传送的长度cx,传送的方向df。
需求中,传送的起始位置data:0000,传送的目的位置data:0010,传送的长度16,传送的方向df=0。
编程:
assume cs:code,ds:data
data segment
db ‘hello assembly!‘
db 16 dup (0)
data ends
code segment
start: mov ax,data
mov ds,ax
mov es,ax
mov si,0
mov di,16
mov cx,16
cld
rep movsb
mov ax,4c00h
int 21h
code ends
end start
6) 案例2,编程,用串传送指令,将f000h段中的最后16个字符复制到data段中。
要传送的字符串位于f000h段的最后16个单元中,它的最后一个字符的位置,f000:ffff,将ds:si指向f000h段的最后一个单元,将es:si指向data中的最后一个单元,然后逆向传送16个字节。
assume cs:code,ds:data
data segment
db 16 dup (0)
data ends
code segment
start: mov ax,0f000h
mov ds,ax
mov si,0ffffh
mov ax,data
mov es,ax
mov di,0fh
mov cx,16
std
rep movsb
mov ax,4c00h
int 21h
code ends
end start
11. pushf和popf
pushf的功能是将标志寄存器的值压栈,而popf是从栈中弹出数据,送入标志寄存器中。pushf和popf,为直接访问标志寄存器提供了一种方法。
栈一次性操作的对象是字,标志寄存器刚好16位,就是一个字。pushf就是将标志寄存器的16位二进制数压入栈,popf是从栈中弹出一个数据,转化16位二进制数,对应标志寄存器的各个位。
12. 标志寄存器在debug中的表示
在debug中,标志寄存器是按照有意义的各个标志位单独表示的。Debug中,使用r命令,在显示的数据的最后8个大写字符就是标志寄存器中位的值。比如:
AX=0000 |
BX=0000 |
CX=0000 |
DX=0000 |
SP=0000 |
BP=0000 |
SI=0000 |
DI=0000 |
|||||
DS=**** |
ES=**** |
SS=**** |
CS=**** |
IP=**** |
NV |
UP |
EI |
PL |
NZ |
NA |
PO |
NC |
|
|
|
|
|
OF |
DF |
|
SF |
ZF |
|
PF |
CF |
部分标志位和值的对应关系如表,
标志 |
值为1的标记 |
值为0的标记 |
OF |
OV |
NV |
SF |
NG |
PL |
ZF |
ZR |
NZ |
PF |
PE |
PO |
CF |
CY |
NC |
DF |
DN |
UP |
13. 综合案例
编写一个子程序,将包含任意字符,以0结尾的字符串中的小写字母转变成大写字母。
子程序描述
名称:uppercase
功能:将以0结尾的字符串中的小写字母转变成大写字母
参数:ds:si指向字符串首地址
assume cs:code
data segment
db “beginner’sall-puipose haha asembly 1234 inc >>0++coder world.”,0
data ends
code segment
start: mov ax,data
mov ds,ax
mov si,0
call uppercase
mov ax,4c00h
int 21h
uppercase: push cx
push si
s0: mov cl,ds:[si]
cmp cl,0
jne s1
pop si
pop cx
ret
s1: cmp cl,61h
jb s2
cmp cl,7ah
ja s2
and cl,11011111b
mov ds:[si],cl
s2: inc si
jmp short s0
code ends
end start
十三、 内中断
任何通用的cpu,都具备一种能力,可以在执行当前正在执行的指令之后,检测到从cpu外部发送过来的或内部产生的一种特殊信息,并且可以立即对所接收到的信息进行处理。这种特殊的信息,称为中断信息。中断的意思是,cpu不再接着向下执行,而是转去处理这个特殊信息。
1. 内中断的产生
对于8086cpu,当内部发生如下情况,将产生中断信息。
除法错误,比如,执行div指令产生的除法溢出;
单步执行;
执行into指令;
执行int指令。
8086cpu采用称为中断类型码的数据来标志中断信息的来源。中断类型码为一个字节型数据,可以表示256种中断信息的来源。中断信息的来源,简称中断源。这4种中断源,在8086cpu中的中断类型码如下,
除法错误,0
单步执行,1
执行into指令,4
执行int指令,该指令的格式为int n,指令中的n为字节型立即数,是提供给cpu的中断类型码。
2. 中断处理程序
Cpu收到中断信息后,需要对中断信息进行处理。可以编程来对中断程序处理,这种程序称为中断处理程序。对于不同的中断信息,需要编写不同的处理程序。Cpu收到中断信息后,要将cs:ip指向中断信息处理程序的入口。中断类型码的作用就是来定位中断处理程序。比如cpu根据中断类型码4,可以找到4号中断的处理程序。
Cpu用8位的中断类型码通过中断向量表找到相应的中断处理程序的入口地址。中断向量表是中断向量的列表。中断向量就是中断处理程序的入口地址。中断向量表在内存中保存,其中存放着256个中断源所对应的中断处理程序的入口。Cpu只要知道了中断类型码,就可以将中断类型码作为中断向量表的表项号,定位相应的表项,从而得到中断处理程序的入口地址。
8086cpu中,中断向量表指定存放在内存地址0处。从内存0000:0000到0000:03ff的1024个单元中存放着中断向量表。一个表项存放一个中断向量,即一个中断处理程序的入口地址,这个入口地址包括段地址和偏移地址,一个表项占两个字,高地址字存放段地址,低地址字存放偏移地址。
3. 中断过程
用中断类型码找到中断向量,并用它设置cs和ip,这个由cpu硬件自动完成。Cpu硬件完成这个工作的过程被称为中断过程。Cpu执行完中断处理程序后,应该返回原来的执行点继续执行下面的指令。在执行中断过程中,设置cs:ip之前,还要将原来的cs和ip的值保存起来。
中断过程的执行过程是:
从中断信息中取得中断类型码;
标志寄存器的值入栈,因为中断过程中要改变标志寄存器的值;
设置标志寄存器的第8位TF和第9位IF的值为0;
Cs的内容入栈;
Ip的内容入栈;
从内存地址为中断类型码*4和中断类型码*4+2的两个字单元中读取中断处理程序的入口地址设置ip和cs。
简洁描述中断过程是:
取得中断类型码N;
pushf
TF=0,IF=0
push cs
push ip
(IP)=(N*4),(CS)=(N*4+2)
4. 中断处理程序的编写和iret指令
中断处理程序的编写方法和子程序的编写类似,一般步骤:
保存用到的寄存器;
处理中断;
恢复用到的寄存器;
用iret指令返回。
iret指令的功能是:pop ip pop cs popf
iret通常和硬件自动完成的中断过程配合使用。在中断过程中,寄存器入栈的顺序是标志寄存器、CS、IP,iret的出栈顺序是IP、CS、标志寄存器,刚好对应入栈,实现执行中断处理程序前的cpu恢复标志寄存器和CS、IP的工作。iret指令执行后,cpu回到执行中断处理程序前的执行点继续执行程序。
5. 除法错误中断的处理
除法错误中断,即0号中断。当cpu执行div等除法指令的时候,如果发生了除法溢出错误,将产生中断类型码为0的中断信息,cpu检测到这个信息,然后引发中断过程,执行0号中断所对应的中断处理程序。比如:
mov ax,1000h
mov bh,1
div bh
cpu执行0号中断处理程序,显示提示信息后,然后返回到操作系统。
可以自定义编写一个0号中断处理程序,它的功能是在屏幕中间显示“overflow!”后,返回到操作系统。
经过分析,自定义中断处理程序包括以下工作:
编写可以显示”overflow!”的中断处理程序do0;
将do0送入内存0000:0200处;
将do0的入口地址0000:0200存储在中断向量表0号表项中。
程序的框架为:
assume cs:code
code segment
start: do0安装程序
设置中断向量表
mov ax,4c00h
int 21h
do: 显示字符串”overflow!”
mov ax,4c00h
int 21h
code ends
end start
这个程序可以分为两部分:安装do0,设置中间向量的程序;do0。
6. 安装do0
安装do0就是使用movsb将do0的代码送入0:200处。程序如下:
assume cs:code
code segment
start: mov ax,cx
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
设置中断向量表
mov ax,4c00h
int 21h
do0: 显示字符串”overflow!”
mov ax,4c00h
int 21h
do0end: nop
code ends
end start
-符号是编译器可以识别的运算符号,编译器可以用它来进行两个常数的减法。
7. do0
do0程序的作用是显示字符串,这就涉及到字符串存放位置的问题。这个字符串不能放到程序段中,比如
data segment
db “overflow!”
data ends
这样就是在do0程序中设置ds指向当前程序所在的内存段的起始位置,然后将使用这段内存。然后,当执行完这个安装do0的过程后,这个安装程序占用的内存空间将释放,data段中的字符串也就被覆盖。那么当发生除法溢出时,难以保证cs仍然指向这个安装程序当初的段,即使恰好是指向这个段,也难以保证这个段中的字符串仍然存在。所以,应该将字符串存放在不会覆盖,并且相对于安装的do0的内存位置相对固定的内存空间中。
assume cs:code
code segment
start: 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
设置中断向量表
mov ax,4c00h
int 21h
do0: jmp short do0start
db “overflow!”
do0start: 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,2
loop s
mov ax,4c00h
int 21h
do0end: nop
code ends
end start
8. 设置中断向量
将do0的入口地址0:200,写入中断向量表的0号表项中,使do0成为0号中断的处理程序。
mov ax,0
mov ds,ax
mov si,0
mov word ptr ds:[si],200h
mov word ptr ds:[si+2],0h
9. 关于单步中断
Cpu在执行完一条指令后,如果检测到标志寄存器的TF位为1,则产生单步中断,引发中断过程。单步中断的中断类型码为1,引发的中断过程为:
取得中断类型码1;
标志寄存器入栈,tf、if设置为0;
Cs、ip入栈;
(ip)=(1*4),(cs)=(1*4+2)。
使用debug的t命令的时候,将tf设置为1,使得cpu工作于单步中断方式下。为了防止由于tf=1,使得cpu无线执行单步中断,在进入中断处理程序之前,设置tf=0。
Cpu提供单步中断功能,为单步跟踪程序的执行过程,提供了实现机制。
10. 关于响应中断的特殊情况
有些情况下,cpu在执行完当前指令后,即便是发生中断,也不会响应。比如,在执行完ss寄存器传送数据的指令后,即便发生中断,cpu也不会响应。因为ss:sp联合指向栈顶,对它们的设置要连续完成。如果在执行完设置ss的指令后,cpu响应中断,要在栈中压入标志寄存器、cs和ip的值。这将导致执行到sp后,ss改变,ss:sp指向不正确的栈顶,引起错误。比如设置栈顶为1000:0应该:
mov ax,1000h
mov ss,ax
mov sp,0
而不是
mov ax,1000
mov ss,ax
mov ax,0
mov sp,0
11. 自定义0号中断的完整代码
assume cs:code
code segment
start: mov ax,cs
mov ds,ax
mov si,offset do0 ;安装do0
mov ax,0
mov es,ax
mov di,200h
mov cx,offset do0end-offset do0
cld
rep movsb
mov ax,0
mov ds,ax ;设置中断向量表
mov si,0
mov word ptr ds:[si],200h
mov word ptr ds:[si+2],0h
mov ax,4c00h
int 21h
do0: jmp short do0start ;do0程序
db “overflow!”
do0start: 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,2
loop s
mov ax,4c00h
int 21h
do0end: nop
code ends
end start
十四、 int指令
中断信息可以来自cpu的内部和外部,当cpu的内部有需要处理的事情发生的时候,将产生需要马上处理的中断信息,引发中断过程。除了由除法错误、单步执行、执行into等引发的内中断,还有一种是由int指令引发的中断。
1. int指令
格式:int n。n为中断类型码,它的功能是引发中断过程。
Cpu执行int n指令,相当于引发一个n号中断的中断过程。执行过程为:
取得中断类型码n;
标志寄存器入栈,tf、if设置为0;
Cs、ip入栈;
(ip)=(1*4),(cs)=(1*4+2)。
然后去执行n号中断的中断处理程序。
可以在int指令调用任何一个中断处理程序。比如,
assume cs:code
code segment
start: mov ax,0b800h
mov es,ax
mov byte ptr es:[12*160+40*2],’!’
int 0
code ends
end start
一般,系统将一些具有一定功能的子程序,以中断程序的方式提供给应用程序调用。编程的时候,可以用int指令调用这些子程序。也可以自己编写一些中断处理程序供别人使用。将中断处理程序简称为中断例程。
2. 编程供应用程序调用的中断例程
1) 编写安装中断7ch的中断例程
功能,求一个word型数据的平方。
参数,(ax)=要计算的数据。
返回值,dx、ax中存放结果的高16位和低16位。
比如,求2*34562的程序,其中int调用7ch计算ax中数的平方。
assume cs:code
code segment
start: mov ax,3456
int 7ch
add ax,ax
adc dx,dx
mov ax,4c00h
int 21h
code ends
end start
编写7ch的中断例程,包括3部分内容,
编写实现求平方功能的程序;
安装程序,将其安装在0:200处;
设置中断向量表,将程序的入口地址保存在7ch表项中,使其称为中断7ch的中断例程。
安装7ch程序的代码:
assume cs:code
code segment
start: 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
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
iret
sqrend: nop
code ends
end start
2) 编写、安装中断7ch的中断例程,实现字母转换
功能,将一个全是字母,以0结尾的字符串,转化为大写。
参数,ds:si指向字符串的首地址。
比如,将data段中的字符串转化为大写。
assume cs:code
data segment
db ‘conhahvassembly’,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 uppercase
mov ax,0
mov es,ax
mov di,200h
mov cx,offset uppercaseend-offset uppercase
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
uppercase: push cx
push si
change: mov cl,ds:[si]
mov ch,0
jcxz ok
and byte ptr ds:[si],11011111b
inc si
jmp short change
ok: pop si
pop cx
iret
uppercaseend:
nop
code ends
end start
3. 对int、iret和栈的深入理解
用7ch中断例程完成loop指令的功能。
loop s的执行需要两个信息,循环次数和到s的位移。如果7ch中断例程要完成loop指令的功能,也需要这两个信息作为参数。用cx存放循环次数,用bx存放位移。
比如,编程,在屏幕中间显示80个‘!’。
assume cs:code
code segment
start: mov ax,0b800h
mov es,ax
mov di,12*160
mov bx,offset s-offset se
mov cx,80
s: mov byte ptr es:[di],’!’
add di,2
int 7ch
se: nop
mov ax,4c00h
int 21h
code ends
end start
在程序中,使用int 7ch调用7ch中断例程进行转移,用bx传递转移的位移。那么7ch中断例程应该具备的功能,dex cx和如果(cx)!=0,转到标号s处执行,否则向下执行。
int 7ch引发中断例程后,当前的标志寄存器、cs和ip都要压栈。Cs是调用程序的段地址,ip是int 7ch后一条指令的偏移地址。
只需要使用栈中ip的值加上保存在bx中的位移数,就得到要转移的目的的偏移地址,将这个新的偏移地址设置为栈中压入的ip的位置的值,然后使用iret指令,用栈中的内容设置cs和ip,从而实现转移到标号s处。
7ch中断例程为:
因为要访问栈,修改栈中特定位置的值,使用bp。
lp: push bp
mov bp,sp
dec cx
jcxz lpret
add [bp+2],bx
lpret: pop bp
iret
也可以不使用bp访问栈,而是直接出栈,修改后入栈。
lp: pop ax
dec cx
jcxz lpret
add ax,bx
lpret: push ax
iret
案例,用7ch中断例程完成jmp near ptr s指令的功能,用bx向中断例程转移位移。
比如,在屏幕的第15行,显示data段中以0结尾的字符串。
assume cs:code
data segment
db ‘conversation’,0
data ends
code segment
start: mov ax,data
mov ds,ax
mov si,0
mov ax,0b800h
mov es,ax
mov di,15*160
mov bx,offset s-offset s1
s: cmp byte ptr [si],0
je s1
mov al,ds:[si]
mov es:[di],al
inc si
add di,2
int 7ch
s1: mov ax,4c00h
int 21h
code ends
end start
7ch中断例程要做的事情就是将执行中断,压入栈中的ip的值加上bx的值就可以。然后使用iret返回段cx和修改后的ip。
jmpto: pop ax
add ax,bx
push ax
iret
或者使用bp
jmpto: push bp
mov bp,sp
add [bp+2],bx
pop bp
iret
4. BIOS和DOS所提供的中断例程
在系统的rom中存放着一套程序,称为BIOS(基本输入输出系统),BIOS主要包含以下内容:
硬件系统的检测和初始化程序;
外部中断和内部中断的中断例程;
用于硬件设备进行I/O操作的中断例程;
其他和硬件系统相关的中断例程。
操作系统DOS也提供了中断例程,从操作系统的角度看,dos的中断例程就是操作系统向程序员提供的编程资源。它们提供的中断程序中包含了许多子程序,这些子程序实现了很多功能。可以用int指令直接调用它们提供的中断例程,来完成某些工作。
和硬件相关的dos中断例程中,一般都是调用了bios的中断例程。
5. BIOS和DOS中断例程的安装过程
BIOS和DOS提供的中断例程安装到内存中的过程:
开机后,cpu一入电,就初始化(cs)=0ffffh,(ip)=0,自动从ffff:0单元开始执行程序。ffff:0处有一条跳转指令,cpu执行这指令后,转去执行BIOS中的硬件系统检测和初始化程序。
初始化程序将建立BIOS所支持的中断向量,即将BIOS提供的中断例程,只需将入口地址登记在中断向量表中即可。它们是固化到rom中的程序,一直在内存中存在。
硬件系统检测和初始化完成后,调用int 19h进行操作系统的引导。从此将计算机交由操作系统控制。
Dos启动后,除完成其他工作外,还将它所提供的中断例程装入内存,并建立相应的中断向量。
6. BIOS中断例程应用
int 10h中断例程是BIOS提供的中断例程,其中包含了多个和屏幕输出相关的子程序。一般,一个可以使用的中断例程包含多个子程序,中断例程内部用传递进来的参数来决定执行哪一个子程序。BIOS和DOS提供的中断例程,都用ah来传递内部子程序的编号。
比如,
mov ah,2 ;置光标
mov bh,0 ;第0页
mov dh,5 ;dh中放行号
mov dl,12 ;dl中放列号
int 21h
(ah)=2表示调用第10h号中断例程的2号子程序,功能是设置光标位置。光标的位置参数为80*25彩色字符模式的显示缓冲区中的位置。
比如,
mov ah,9 ;在光标位置显示字符
mov al,’a’ ;要显示的字符
mov bl,7 ;颜色属性
mov bh,0 ;第0页
mov cx,3 ;字符重复个数
int 10h
(ah)=9表示调用第10h号中断例程的9号子程序,功能是在光标位置显示字符。
比如,使用BIOS的int 10h,实现在屏幕的8行12列显示4个红底高亮闪烁绿色的’c’。
assume cs:code
code segment
start: mov ah,2
mov bh,0
mov dh,8
mov dl,12
int 10h
mov ah,9
mov al,’c’
mov bl,11001010b
mov bh,0
mov cx,3
int 10h
mov ax,4c00h
int 21h
code ends
end start
7. DOS中断例程应用
int 21h中断例程就是DOS提供的中断例程,其中包含了DOS提供给程序员在编程时调用的子程序。前述一直使用的是int21h中断例程的4ch号功能,如下,
mov ah,4ch ;程序返回
mov al,0 ;返回值
int 21h
(ah)=4ch表示调用第21h号中断例程的4ch号子程序,功能为程序返回,可以提供返回值作为参数。
为了简便,上述程序常写为:mov ax,4c00h int 21h。
int 21h中断例程在光标位置显示字符串的功能:
ds:dx指向字符串 ;要显示的字符串需用“$”作为结束符
mov ah,9 ;功能号9,表示在光标位置显示字符串
int 21h
(ah)=9表示调用21号中断例程的9号子程序,功能为在光标位置显示字符串,可以提供显示字符串的地址作为参数。
比如,编程在屏幕的第8行第12列显示字符串“hello assembly !”。
assume cs:code
data segment
db ‘hello assembly!’,’$’
data ends
code segment
start: mov ah,2
mov bh,0
mov dh,8
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
8. 案例
编写并安装int 7ch中断例程,功能为显示一个用0结束的字符串,中断例程安装在0:200处。
参数,(dh)=行号,(dl)=列号,(cl)=颜色,ds:si指向字符串首地址。
assume cs:code
code segment
start: mov ax,cs
mov ds,ax
mov si,offset string
mov ax,0
mov es,ax
mov di,200h
mov cx,offset stringend-offset string
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
string: mov ax,0b800h
mov es,ax
mov ax,160
mul dh
add dl,dl
mov dh,0
add ax,dx
mov di,ax
s: mov al,ds:[si]
cmp al,0
je ok
mov ah,cl
mov es:[di],ax
inc si
add di,2
jmp short s
ok: iret
stringend: nop
code ends
end start
测试安装的7ch
assume cs:code
data segment
db “hello assembly!”,0
data ends
code segment
start: mov dh,10
mov dl,10
mov cl,2
mov ax,data
mov ds,ax
mov si,0
int 7ch
mov ax,4c00h
int 21h
code ends
end start
十五、 端口
各种存储器都和cpu的地址线、数据线、控制线相连。Cpu在操控它们的时候,把它们都当作内存来对待。Cpu把它们当作一个有若干个存储单元组成的逻辑存储器,这个逻辑存储器为内存地址空间。
在pu机中,和cpu通过总线相连的芯片除各种存储器外,还有以下3种芯片。
各种接口卡(网卡、显卡等)上的接口芯片,它们控制接口卡进行工作;
主板上的接口芯片,cpu通过它们对部分外设进行访问;
其他芯片,用来存储相关的系统信息,或进行相关的输入输出处理。
在这些芯片中,都有一组可以由cpu读写的寄存器。这些寄存器,都和cpu总线相连,cpu对它们进行读或写的时候通过控制线向它们所在的芯片发出端口读写命令。从cpu角度,是将这些寄存器当作端口,对它们统一编址,从而建立一个统一的端口地址空间。每一个端口地址空间都有一个地址。
综合来看,cpu可以读写以下3个地方的数据,cpu内部的寄存器;内存单元;端口。
1. 端口的读写
访问端口时,cpu通过端口地址来定位端口。因为端口所在的芯片和cpu通过总线相连,所以,在端口地址和内存地址一样,通过地址总线来传送。在pc系统中,cpu最多可以定位64kb个不同的端口,所以,端口地址的范围是0~65535。
对端口的读写不能使用mov、push、pop等内存读写指令。端口的读写指令只有两条,in和out,分别用于从端口读取数据和往端口写入数据。
Cpu执行内存访问命令和端口访问指令时,总线上的信息:
1) 访问内存
比如,mov ax,ds:[8]
执行时与总线的操作如下所示。
Cpu通过地址线将地址信息8发出;
Cpu通过控制线发出内存读命令,选中存储器芯片,并通知它,将要从中读取数据;
存储器将8号单元中的数据通过数据线送入cpu。
2) 访问端口
比如 in al,60h ;从60h端口读入一个字节
执行时与总线相关的操作如下。
Cpu通过地址线将地址信息60h发出;
Cpu通过控制线发出端口读命令,选中端口所在的芯片,并通知它,将要从中读取数据;
端口所在的芯片将60h端口中的数据通过数据线送入cpu。
在in和out指令中,只能使用ax或al来存放从端口中读入的数据或要发送到端口中的数据。访问8位端口时用al,访问16位端口时用ax。
对0~255以内的端口进行读写时:
int al,20h ;从20h端口读入一个字节
out 20h,al ;向20h端口写入一个字节
对256~65535以内的端口进行读写时,端口号放在dx中:
mov dx,3f8h ;将端口号3f8h送入dx
int ax,dx ;从3f8h端口读入一个字节
out dx,ax ;向3f8h端口写入一个字节
2. CMOS RAM芯片
Pc机中,有一个CMOS RAM芯片,简称CMOS。它的特征为:
包含一个实时钟和一个有128个存储单元的RAM存储器(早期的计算机为64个字节)。
该芯片靠电池供电。所以,关机后内部的实时钟仍可正常工作,ram中的信息不丢失。
128个字节的ram中,内部实时钟占用0~0dh单元来保存时间信息,其余大部分单元用于保存系统配置信息,供系统启动时BIOS程序读取。BIOS也提供了相关的程序,是我们可以在开机的时候配置CMOS RAM中的系统信息。
该芯片内部有两个端口,端口地址为70h和71h。cpu通过这两个端口来读写CMOS RAM。
70h为端口地址,存放要访问的CMOS RAM单元的地址;71h为数据端口,存放从选定的CMOS RAM单元中读取的数据,或要写入到其中的数据。
Cpu对CMOS RAM的读写分两步进行,比如,读CMOS RAM的2号单元。将2送入端口70h;从端口71h读出2号单元的数据。
案例,编程读取CMOS RAM的2号单元的内容
assume cs:code
code segment
start: mov al,2
out 70h,al
in al,71h
mov ax,4c00h
int 21h
code ends
end start
编程,向CMOS RAM的2号单元写入0。
assume cs:code
code segment
start: mov al,2
out 70h,al
mov al,0
out 71h,al
mov ax,4c00h
int 21h
code ends
end start
3. shl和shr指令
shl和shr是逻辑位移指令。
1) shl
shl是逻辑左移指令,它的功能是:将一个寄存器或内存单元中的数据向左移位;将最后移出的一位写入cf中;最低位用0补充。
比如,指令mov al,01001000b shl al,1执行后,(al)=10010000b,cf=0。如果接着继续执行一条shl al,1,则执行后,(al)=00100000b,cf=1。
如果移动位数大于1时,必须将移动位数放在cl中。比如,指令:
mov al,01010001b
mov cl,3
shl al,cl
执行后,(al)=10001000b,因为最后移出的一位是0,所以cf=0。
将x逻辑左移一位,相当于执行x=x*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
2) shr
shr是逻辑右移指令,和shl所进行的操作相反。
将一个寄存器或内存单元中的数据向右移位;将最后移出的一位写入cf中;最高位用0补充。
比如,指令,mov al,10000001b shr al,1执行后(al)=01000000b,cf=1。
如果接着上面,继续执行一条shr al,1,则执行后,(al)=00100000b,cf=0。
如果移动位数大于1时,必须将移动位数放在cl中。
比如,指令:
mov al,01010001b
mov cl,3
shr al,cl
执行后(al)=00001010b,因为最后移出的一位是0,所以cf=0。
将x逻辑右移一位,相当于执行x=x/2。
3) 案例,编程用加法和移位指令计算(ax)=(ax)*10。
assume cs:code
code segment
start: mov bx,ax
shl bx,1
mov cl,3
shl ax,cl
add ax,bx
mov dx,0
adc dx,0
mov ax,4c00h
int 21h
code ends
end start
或者
assume cs:code
code segment
start: shl ax,1
mov bx,ax
mov cl,2
shl ax,cl
add ax,bx
mov dx,0
adc dx,0
mov ax,4c00h
int 21h
code ends
end start
4. CMOS RAM中存储的时间信息
在CMOS RAM中,存放着当前的时间:年、月、日、时、分、秒。这6个信息的长度为1个字节,存放单元为:
秒:0 分:2 时:4 日:7 月:8 年:9
这些数据以BCD码的方式存放。BCD码是以4位二进制数表示十进制数码的编码方法,如下表所示,
十进制数码 |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
对应的BCD码 |
0000 |
0001 |
0010 |
0011 |
0100 |
0101 |
0110 |
0111 |
1000 |
1001 |
比如,数值27,用BCD码表示就是:0010 0111。
一个字节表示两个BCD码,在CMOS RAM存储时间信息的单元中,存储了用两个BCD码表示的两位十进制数,高4位的BCD码表示十位数,低4位的BCD码表示个位数。比如,00010010表示12。
案例,编程,在屏幕中间显示当前的月份。
这个程序包括两部分工作。
从CMOS RAM的8号单元读出当前月份的BCD码。将用BCD码表示的月份以十进制的形式显示在屏幕上。获取BCD码表示的十进制数的十位和个位,然后将获取的两个数加上30h就是对应的ascii码值。
获取BCD表示的数的两个位数的方法
mov ah,al ;al中是从cmos ram的8号单元中读出的数据
mov cl,4
shr ah,cl ;ah中为月份的十位数码值
and al,00001111b ;al中为月份的个位数码值
完整代码:
assume cs:code
code segment
start: mov ax,0b800h
mov ds,ax
mov si,12*160+4
mov al,8
out 70h,al
in al,71h
mov ah,al
mov cl,4
shr ah,cl
add ah,30h
and al,00001111b
add al,30h
mov ch,2
mov cl,ah
mov [si],cx
mov cl,al
mov [si+2],cx
mov ax,4c00h
int 21h
code ends
end start
5. 综合案例
编程,以“年/月/日 时:分:秒”的格式,显示当前的日期、时间。
assume cs:code
data segment
db 9,8,7,4,2,0
db " / / : : "
data ends
code segment
start: mov ax,0b800h
mov es,ax
mov di,12*160+4
mov ax,data
mov ds,ax
mov si,0
mov bx,6
mov cx,6
s: push cx
mov al,[si]
out 70h,al ;从CMOS RAM中取出时间
in al,71h
mov ah,al
mov cl,4
shr ah,cl
add ah,30h
and al,00001111b
add al,30h
mov dh,al
mov dl,ah
mov [bx],dx
inc si
add bx,3
pop cx
loop s
mov cx,17
mov si,6
s1: mov al,[si]
mov ah,02h
mov es:[di],ax
inc si
add di,2
loop s1
mov ax,4c00h
int 21h
code ends
end start
也可以使用,int 21h显示字符串,要以$结尾。
assume cs:code
data segment
table db 9,8,7,4,2,0
time db " / / : : $"
data ends
code segment
start: mov ax,0b800h
mov es,ax
mov di,12*160+4
mov ax,data
mov ds,ax
mov si,offset table
mov bx,offset time
mov cx,6
s: push cx
mov al,[si]
out 70h,al ;从CMOS RAM中取出时间
in al,71h
mov ah,al
mov cl,4
shr ah,cl
add ah,30h
and al,00001111b
add al,30h
mov es:[bx],ah
mov es:[bx+1],al
inc si
add bx,3
pop cx
loop s
mov ah,0
mov bh,0
mov dh,10 ;将光标置于10行4列
mov dl,4
int 10h
mov dx,offset time
mov ah,9 ;显示字符串
int 21h
mov ax,4c00h
int 21h
code ends
end start
十六、 外中断
Cpu在计算机系统中,除了能够执行指令,进行运算以外,还应该能够对外部设备进行控制,接收它们的输入,向它们进行输出。Cpu除了有运算能力外,还要有I/O(input/output)能力。比如,按下键盘上的一个键,cpu最终要能够处理这个键。在使用文本编辑器时,按下b,屏幕上出现b。
Cpu要处理外设的输入,要处理两个问题,外设的输入随时可能发生,cpu怎么知道发生了;cpu从哪里得到外设的输入。
1. 接口芯片和端口
外设的输入不直接送入内存和cpu,而是送入相关的接口芯片的端口中,cpu向外设的输出也不是直接送入外设,而是先送入端口中,再由相关的芯片送到外设。Cpu还可以向外设输出控制命令,而这些控制命令也是先送到相关芯片的端口中,然后再由相关的芯片根据命令对外设进行控制。
Cpu通过端口和外部设备进行联系。
2. 外中断信息
Cpu提供中断机制得知外设随时可能发生的需要cpu及时处理的事件。不同与内中断,当cpu的内部有需要处理的事情发生的时候,将产生中断信息,引发中断例程。外中断来自于cpu外部,当cpu外部有需要处理的事情发生的时候,比如,外设的输入到达,相关芯片将向cpu发出相应的中断信息。Cpu在执行完当前指令后,可以检测到发送过来的中断信息,引发中断过程,处理外设的输入。Pc系统中,外中断源有两类,可屏蔽中断和不可屏蔽中断。
1) 可屏蔽中断
可屏蔽中断是cpu可以不响应的外中断。Cpu是否响应可屏蔽中断,要看标志寄存器的if位的设置。当cpu检测到可屏蔽中断信息时,如果if=0,则cpu在执行完当前指令后响应中断,引发中断过程;如果if=0,则不响应可屏蔽中断。
比较内中断引发的中断过程:
取中断类型码n;
标志寄存器入栈,if=0,tf=0;
Cs、ip入栈;
(ip)=(n*4),(cs)=(n*4+2)。
可屏蔽中断引发的中断过程,除第1步的实现上有所不同外,基本上和内中断的中断过程相同。因为可屏蔽中断信息来自cpu外部,中断类型码是通过数据总线送入cpu的;而内中断的中断类型码是在cpu内部产生的。
中断发生时,将if设为0的原因是,在进入中断处理程序后,禁止其他的可屏蔽中断。如果在中断处理程序中需要处理可屏蔽中断,可以用指令将if置为1。8086cpu提供的设置if的指令,sti,设置if=1;cli,设置if=0。
2) 不可屏蔽中断
不可屏蔽中断是cpu必须响应的外中断。当cpu检测到不可屏蔽中断信息时,则在执行完当前指令后,立即响应,引发中断过程。
对于8086cpu,不可屏蔽中断的中断类型码固定为2,所以中断过程中,不需要取中断类型码。不可屏蔽中断的过程为:
标志寄存器入栈,if=0,tf=0;
Cs、ip入栈;
(ip)=(8),(cs)=(0ah)。
几乎所有由外设引发的外中断,都是可屏蔽中断。当外设有需要处理的事件(比如,键盘输入)发生时,相关芯片向cpu发出可屏蔽中断信息。不可屏蔽中断是在系统中有必须处理的紧急情况发生时用来通知cpu的中断信息。
3. Pc机键盘的处理过程
1) 键盘输入
键盘上的每一个键相当于一个开关,键盘中有一个芯片对键盘上的每一个键的开关状态进行扫描。按下一个键时,开关接通,该芯片就产生一个扫描码,扫描码说明了按下的键在键盘上的位置。扫描码被送入主板上的相关接口芯片的寄存器中,寄存器的端口地址为60h。
一般将按下一个键时产生的扫描码称为通码,松开一个键产生的扫描码称为断码。扫描码长度为一个字节,通码的第7位为0,断码的第7位为1,即,断码=通码+80h。比如,g键的通码为22h,断码为a2h。
键盘上部分键的扫描码,如表:
键 |
扫描码 |
键 |
扫描码 |
键 |
扫描码 |
键 |
扫描码 |
Esc |
01 |
Enter |
1C |
B |
30 |
→ |
4D |
1~9 |
02~0A |
Ctrl |
1D |
N |
31 |
+ |
4E |
0 |
0B |
A |
1E |
M |
32 |
End |
4F |
- |
0C |
S |
1F |
, |
33 |
↓ |
50 |
= |
0D |
D |
20 |
. |
34 |
PgDn |
51 |
Backspace |
0E |
F |
21 |
/ |
35 |
Ins |
52 |
Tab |
0F |
G |
22 |
Shift(右) |
36 |
Del |
53 |
Q |
10 |
H |
23 |
Prtsc |
37 |
|
|
W |
11 |
J |
24 |
Alt |
38 |
|
|
E |
12 |
K |
25 |
Space |
39 |
|
|
R |
13 |
L |
26 |
Capslock |
3A |
|
|
T |
14 |
; |
27 |
F1-F10 |
3B~44 |
|
|
Y |
15 |
‘ |
29 |
NumLock |
45 |
|
|
U |
16 |
Shift(左) |
2A |
ScrollLock |
46 |
|
|
I |
17 |
\ |
2B |
Home |
47 |
|
|
O |
18 |
Z |
2C |
↑ |
48 |
|
|
P |
19 |
X |
2D |
PgUp |
49 |
|
|
[ |
1A |
C |
2E |
- |
4A |
|
|
] |
1B |
V |
2F |
← |
4B |
|
|
2) 引发9号中断及其执行
键盘的输入到达60h端口时,相关的芯片就会向cpu发出中断类型码为9的可屏蔽中断信息。Cpu检测到该中断信息后,如果if=1,则响应中断,引发中断过程,转去执行int9中断例程。
BIOS提供了int9中断例程,用来进行基本的键盘输入处理,主要的工作如下:
读出60h端口中的扫描码;
如果是字符键的扫描码,将该扫描码和它所对应的字符码(即ascii码)送入内存中的BIOS键盘缓冲区;如果是控制键(比如ctrl)和切换键(比如cpaslock)的扫描码,则将其转换为状态字节(用二进制记录控制键和切换键状态的字节)写入内存中存储状态字节的单元;
对键盘系统进行相关的控制,比如说,向相关芯片发出应答信息。
BIOS键盘缓冲区是系统启动后,BIOS用于存放int 9中断例程所接收的键盘输入的内存区。该内存区可以存储15个键盘输入,因为int 9中断例程除了接收扫描码外,还要产生和扫描码对应的字符码,所以在BIOS键盘缓冲区中,一个键盘输入用一个字单元存放,高位字节存放扫描码,低位字节存放字符码。
0040:17单元存储键盘状态字节,该字节记录了控制键和切换键的状态。键盘状态字节各位记录的信息如下。
0 |
右shift状态,置1表示按下右shift键; |
1 |
左shift状态,置1表示按下左shift键; |
2 |
Ctrl状态,置1表示按下ctrl键; |
3 |
Alt状态,置1表示按下alt键; |
4 |
ScrollLock状态,置1表示scroll指示灯亮; |
5 |
NumLock状态,置1表示小键盘输入的是数字; |
6 |
CapsLock状态,置1表示输入大写字母; |
7 |
Insert状态,置1表示处于删除态; |
4. 编写int 9中断例程
键盘输入的处理过程:键盘产生扫描码;扫描码送入60h端口;引发9号中断;cpu执行int 9中断例程处理键盘输入。
这个过程中,第1、2、3步都是由硬件系统完成的。能改的只有int 9中断处理程序。可以重新编写int 9中断例程,按照自己的意图来处理键盘的输入。
案例,在屏幕中间一次显示“a”~“z”,并可以让人看清。在显示的过程中,按下esc键,改变显示的颜色。
首先,依次显示“a”~“z”的程序。
assume cs:code
code segment
start: mov ax,0b800h
mov es,ax
mov ah,’a’
s: mov es:[160*12+40*2],ah
inc ah
cmp ah,’z’
jna s
mov ax,4c00h
int 21h
code ends
end start
这个程序的执行过程中,人们是无法看全屏幕上的显示的,因为一个字母刚显示到屏幕上,cpu执行后,就编程另一个字母,字母间切换太快,无法看清。应该在每显示一个字母后,延时一段时间,看清后,再显示下一个字母。可以让cpu执行一段时间的空循环。循环的次数一定要大,用两个16位寄存器来存放32位的循环次数。如,
mov dx,10h
mov ax,0
s:
sub ax,1
sbb bx,0
cmp ax,0
jne s
cmp dx,0
jne s
这个循环了100000h次。
也可以这样设计一个循环。
mov dx,1000h ;循环2000000h次
s2:mov ax,200h
s1:
sub ax,1
cmp ax,0
jne s1
sub dx,1
cmp dx,0
jne s2
我们可以将循环延时的程序段写为一个子程序。
将上述程序改进如下,
assume cs:code
code segment
start: 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,1000h ;循环10000000h次,可以根据实际调节
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
然后需要实现按下esc,改变显示的颜色。
键盘输入到达60h端口后,就会引发9号中断,cpu则转去执行int 9中断例程。可以编写int 9中断例程的功能如下:
从60h端口读出键盘的输入;
调用BIOS的int 9中断例程,处理其他硬件细节;
判断是否为esc的扫描码,如果是,改变显示的颜色后返回;如果不是直接返回。
实现从端口60h读出键盘的输入int al,60h
调用BIOS的int 0中断例程。自定义的改变颜色的中断处理程序要成为新的int 9中断例程,主程序必须将中断向量表中的int 9中断例程的入口地址改为新的中断处理的入口地址。那么在新的中断例程中调用原来的int 9例程时,由于中断向量表中的int 9中断例程的入口地址不是原来的int 9中断例程的地址,所以不能使用int指令直接调用。如果要在新中断例程中调用原来的中断例程,必须在将中断向量表的中断例程的入口地址改为新地址之前,将原来的入口地址保存起来。可以将其保存在一段内存中。
有了原来的中断例程的入口地址后,虽然不能使用int 9调用,但是可以用别的指令来模拟int指令,从而实现对中断例程的调用。
int指令执行时,cpu进行以下的工作。
取中断类型码n;
标志寄存器入栈;
if=0,tf=0;
cs,ip入栈;
(ip)=(n*4),(cs)=(n*4+2)。
取中断类型码是为了定位中断例程的入口,这里已经直到入口地址,假设保存在ds:0和ds:2单元中。那么,模拟的程序代码:
pushf ;将标志寄存器入栈
pushf
pop ax
and ah,11111100b ;if和tf为标志寄存器的第9位和第8位
push ax
popf ;设置if=0,tf=0
call dword ptr ds:[0] ;cs,ip入栈,并设置新的cs和ip,(ip)=((ds)*16+0),(cs)=((ds)*16+2)
如果是esc的扫描码,改变显示的颜色后返回。比如显示的位置在屏幕的中间,第15行40列,显存中的偏移地址为:160*12+40*2。偏移地址:160*12+40*2+1处是字符的属性,只要改变此处的数据就可以改变在段地址段地址b800h,偏移地址160*12+40*2出显示的字符的颜色了。
最后,要在程序返回前,将中断向量表中的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] ;将原来的int 9中断例程的入口地址保存在ds:0、ds:2单元中
mov word ptr es:[9*4],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] ;将中断向量表中int 9中断例程的入口恢复为原来的地址
mov ax,4c00h
int 21h
delay: push ax
push dx
mov dx,1000h
s2: mov ax,200h
s3: dec ax
cmp ax,0
jne s3
dec dx
cmp dx,0
jne s2
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 9调用原来的int 9中断例程
cmp al,1
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
5. 安装新的int 9中断例程
安装一个新的int 9中断例程。功能是在dos下,按下F1键后改变当前屏幕的显示颜色,其他键照常处理。
这个程序主要包括以下工作:
改变屏幕颜色,就是改变从b800h开始的4000个字节中所有奇地址单元中的内容,当前屏幕的显示颜色即发生改变。
其他键照常处理,可以调用原来int 9中断例程,来处理。
原int 9中断例程入口地址的保存。不能保存在安装程序中,因为安装程序返回后地址将丢失,可以保存在0:200单元处。
新的int 9中断例程的安装,可以将新的int 9中断例程安装在0:204处。
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
mov di,204h
mov cx,offset int9end-offset int9
cld
rep movsb
push es:[9*4]
pop es:[200h]
push es:[9*4+2]
pop es:[202h]
mov word ptr es:[9*4],204h
mov word ptr es:[9*4+2],0
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
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
6. 综合案例
安装一个新的int 9中断例程,功能,在dos下,按下“A”键后,除非不再松开,如果松开,就显示满屏幕的“A”;其他的键照常处理。
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
mov di,204h
mov cx,offset int9end-offset int9
cld
rep movsb
push es:[9*4]
pop es:[200h]
push es:[9*4+2]
pop es:[202h]
mov word ptr es:[9*4],204h
mov word ptr es:[9*4+2],0
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,9eh
jne int9ret
mov ax,0b800h
mov es,ax
mov bx,0
mov cx,2000
s: mov byte ptr es:[bx],”A”
add bx,2
loop s
int9ret: pop es
pop cx
pop bx
pop ax
iret
int9end: nop
code ends
end start
十七、 直接定址表
1. 描述单元长度的标号
一般,在代码段中,标号用来标记指令、数据、段的起始地址。比如,
assume cs:code
code 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
程序中,code、a、b、start、s都是标号。这些标号仅仅表示了内存单元的地址。还有一种标号,不但可以表示内存单元的地址,还表示了内存单元的长度,也就是表示在此处标号处的单元,是一个字节单元,还是字单元,还是双字单元。比如,
assume cs:code
code segment
a db 1,2,3,4,5,6,7,8
b dw 0
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
程序中,在code段中使用的标号a、b后没有“:”,它们同时描述了内存地址和单元长度的标号。标号a,描述了地址code:0,和从这个地址开始以后的内存单元都是字节单元;而标号b描述了地址code:8,和从这个地址开始以后的内存单元都是字单元。
这种标号包含了对单元长度的描述,在指令中,它可以代表一个段中的内存单元。比如,对于程序中的b dw 0。
指令,mov ax,b相当于,mov ax,cs:[8]。指令,mov b,2相当于mov word ptr cs:[8],2。指令,inc b相当于inc word ptr cs:[8]。
指令中的b代表了一个内存单元,地址为code:8,长度为两个字节。
如下指令会引起编译错误,mov al,b add b,al。
对于程序中的a db 1,2,3,4,5,6,7,8。
指令,mov al,a[si]相当于mov al,cs:0[si]。指令mov al,a[3]相当于mov al,cs:0[3]。
这种标号一般称为数据标号,它标记了存储数据的单元的地址和长度。
2. 在其他段中使用数据标号
一般,不在代码段中定义数据,而将数据定义到其他段中。也可以使用数据标号来描述存储数据的单元的地址和长度。
在后面加有“:”的地址标号,只能在代码段中使用,不能在其他段中使用。
如下的程序将data段中a标号处的8个数据累加,结果存储到b标号处的字中。
assume cs:code,ds:data
data segment
a db 1,2,3,4,5,6,7,8
b dw 0
data ends
code segment
start: mov ax,data
mov ds,ax
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
如果在代码段中直接使用数据标号访问数据,需要用伪指令assume将标号所在的段和一个段寄存器联系起来。在程序中还要使用指令对段寄存器进行设置。
比如,指令mov al,a[si]编译为mov al,[si+0]。指令add b,ax编译为add [8],ax。
还可以将标号当作数据来定义,此时,编译器将标号所表示的地址当作数据的值。
比如,
data segment
a db 1,2,3,4,5,6,7,8
b dw 0
c dw a,b
data ends
数据标号c处存储的两个字型数据为标号a、b的偏移地址,相当于:
data segment
a db 1,2,3,4,5,6,7,8
b dw 0
c 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
data ends
数据标号c处存储的两个双字型数据为标号a的偏移地址和段地址、标号b的偏移地址和段地址,相当于:
data segment
a db 1,2,3,4,5,6,7,8
b dw 0
c dw offset a,seg a,offset b,seg b
data ends
seg的功能是取得某一个标号的段地址。
3. 直接定址表
要求,编写子程序,以十六进制的形式在屏幕中间显示给定的字节型数据。
一个字节需要用两个十六进制数码来表示,子程序要在屏幕上显示两个ascii字符。用0-f这个16个字符显示十六进制数码。可以将一个字节的高4位和低4位分开,分别用它们的值对应数码字符。比如,2bh,高4位是2,低4位是11,可以将每个位的数分别与0-15比较,等于0,则显示‘0’,依次类推,这样就要使用很多条比较、转移指令,程序长且混乱。如果在数值0-15和字符‘0’-‘f’建立映射关系,可以使程序简化。
数值0-9和字符0-9之间的映射关系是,数值+30h=对应字符的ascii值。数值10-15和字符a-f之间的映射关系是,数值+37h=对应字符的ascii码值。根据这个思路设计,虽然简化了程序,但是还有更简化的思路。抛开这些数值和字符间原有的关系,人为建立它们之间新的映射关系。方式是建立一张表,表中一次存储字符0-f,通过数值0-15可直接查找到对应的字符。子程序如下:
;名称,showbyte
;功能,将一个字节数据用十六进制显示在屏幕中间
;参数,(al)=要显示的数据
;返回,将十六进制显示在屏幕中间
showbyte: jmp short show
table db ‘0123456789abcdef’
show: push bx
push es
mov ah,al
mov cl,4
shr ah,cl
and al,00001111b ;右移4位,ah中得到高4位的值,al中逻辑与高4位为0,得到低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
利用表,在两个数据集合之间建立一种映射关系,使得可以用查表的方法根据给出的数据得到其在另一集合中的对应数据。目的有以下3个,
为了算法的清晰和简洁;为了加快运算速度;为了使程序易于扩充。
比如,为了加快运算速度的例子,编写一个子程序,计算sin(x),x∈{0°,30°,60°,90°,120°,150°,180°},并在屏幕中间显示计算结果,比如sin(30)的结果显示为“0.5”。
可以使用麦克劳林公式来计算sin(x),但是其中涉及多次乘法和除法,乘除法是费时的,执行时间大约是加法、比较指令的5倍。如果不使用乘除法,通过观察x等于集合中角度时的sin的值,可以建立一张角度值和sin值的表的子程序。如下:
;名称,showsin
;功能,根据由ax传送来的角度值,在屏幕显示对应sin值
;参数,(ax)=角度
;返回,无
showsin: jmp short show
table dw ag0,ag30,ag60,ag90,ag120,ag150,ag180
ag0 db ‘0’,0
ag30 db ‘0.5’,0
ag60 db ‘0.866’,0
ag90 db ‘1’,0
ag120 db ‘0.866’,0
ag150 db ‘0.5’,0
ag180 db ‘0’,0
show: push bx
push es
push si
mov bx,0b800h
mov es,bx
mov ah,0
mov bl,30
div bl
mov bl,al
mov bh,0
add bx,bx ;用dw存放偏移地址,所以bx要乘以2
mov bx,table[bx]
mov si,160*12+40*2
shows: mov ah,cs:[bx]
cmp ah,0
je showret
mov es:[si],ah
inc bx
add si,2
jmp short shows
showret: pop si
pop es
pop bx
ret
上述通过依据数据,直接计算出所要找的元素的位置的表,称为直接定址表。
4. 程序入口地址的直接定址表
可以在直接定址表中存储子程序的地址,从而方便地实现不同子程序的调用。比如,实现一个子程序setscreen,为显示输出提供如下功能。
(1) 清屏;(2)设置前景色;(3)设置背景色;(4)向上滚动一行。
入口参数:用ah寄存器传递功能号,0表示清屏,1表示设置前景色,2表示设置背景色,3表示向上滚动一行;对于2、3号功能,用al传递颜色值,(al)∈{0,1,2,3,4,5,6,7}。
各个功能说明:
清屏,将显存中当前屏幕中的字符设为空格符;
设置前景色,设置显存中当前屏幕中处于奇地址的属性字节的第0、1、2位;
设置背景色,设置显存中当前屏幕中处于奇地址的属性字节的第4、5、6位;
向上滚动一行,依次将第n+1行的内容复制到第n行处,最后一行为空。
分别实现这4个子程序。
;名称,cls
;功能,清屏
;参数,无
cls: push bx
push cx
push es
mov bx,0b800h
mov es,bx
mov bx,0
mov cx,2000
clslp: mov byte ptr es:[bx],’ ‘
add bx,2
loop clslp
pop es
pop cx
pop bx
ret
;名称,foreg
;功能,设置前景色
;参数,(al)=颜色值
foreg: push bx
push cx
push es
mov bx,0b800h
mov es,bx
mov bx,1
mov cx,2000
foreglp: and byte ptr es:[bx],11111000b
or es:[bx],al
add bx,2
loop foreglp
pop es
pop cx
pop bx
ret
;名称,backg
;功能,设置背景色
;参数,(al)=颜色值
backg: push bx
push cx
push es
mov cl,4
shl al,cl
mov bx,0b800h
mov es,bx
mov bx,1
mov cx,2000
backglp:
and byte ptr es:[bx],10001111b
or es:[bx],al
add bx,2
loop backglp
pop es
pop cx
pop bx
ret
;名称,scroll
;功能,向上滚一行
;参数,无
scroll: push cx
push si
push di
push es
push ds
mov si,0b800h
mov es,si
mov ds,si
mov si,160
mov di,0
cld
mov cx,24
scrollcope:
push cx
mov cx,160
rep movsb
pop cx
loop scrollcope
mov cx,80
mov si,0
clearend:
mov byte ptr [160*24+si],’ ‘
add si,2
loop clearend
pop ds
pop es
pop di
pop si
pop cx
ret
可以将这些功能子程序的入口地址存储在一个表中,它们在表中的位置和功能号相对应。对应关系:功能号*2=对应的功能子程序在地址表中的偏移,如下:
setscreen: jmp short set
table dw cls,foreg,backg,scroll
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使用比较值的方式实现,如下:
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 cls
jmp short sret
do2: call foreg
jmp short sret
do3: call backg
jmp short sret
do4: call scroll
sret: ret
通过比较功能号进行转移的方法,程序结构比较混乱,不利于功能的扩充。比如,在setscreen中再加入一个功能,则需要修改程序的设计,加入新的指令。而使用功能号查询表,程序的结构清晰,便于扩充。如果加入或者取消功能,那么只需要在地址表中修改它的入口地址就可以了。
十八、 使用BIOS进行键盘输入和磁盘读写
大多数有用的程序需要处理用户的输入,键盘输入是最基本的输入。程序和数据通常需要长期存储,持久化,磁盘是最常用的存储设别。BIOS为这两种外设的I/O提供了最基本的中断例程。
1. int 9中断例程对键盘输入的处理
键盘输入将引发9号中断,BIOS提供了int 9中断例程。Cpu在9号中断发生后,执行int 9中断例程,从60h端口读出扫描码,并将其转为相应的ascii码或状态信息,存储在内存的指定空间(键盘缓冲区或状态字)中。
一般的键盘输入,在cpu执行完int 9中断例程后,都放在了键盘缓冲区中。键盘缓冲区有16个字单元,可以存储15个按键的扫描码和对应的ascii码。
比如,通过键盘输入A、B、C、D、E、shift_A、A,int 9中断例程对键盘输入的处理过程为:
初始状态下,没有键盘输入,键盘缓冲区为空。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
按下A键,引发键盘中断;cpu执行int 9中断例程,从60h端口读出A键的通码;然后检测状态字节,看看是否有shift、ctrl等切换键按下;发现没有切换键按下,则将A键的扫描码1eh和对应的ascii码,即字母“a”的ascii码61h,写入键盘缓冲区。其中,高位字节存储扫描码,低位字节存储ascii码。
1E61 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
按下B、C、D、E键,同理
1E61 |
3062 |
2E63 |
2064 |
1265 |
|
|
|
|
|
|
|
|
|
|
|
按下shift键,引发键盘中断,int 9中断例程接收左shift键的通码,设置0040:17处的状态字的第1位为1,表示左shift键按下。
按下A键,按下A键,引发键盘中断;cpu执行int 9中断例程,从60h端口读出A键的通码;然后检测状态字节,看看是否有shift、ctrl等切换键按下;发现左shift键被按下,则将A键的扫描码1eh和shift_A对应的ascii码,即字母“A”的ascii码41h,写入键盘缓冲区。其中,高位字节存储扫描码,低位字节存储ascii码。
1E61 |
3062 |
2E63 |
2064 |
1265 |
1E41 |
|
|
|
|
|
|
|
|
|
|
松开shift键,引发键盘中断,int 9中断例程接收左shift键的断码,设置0040:17处的状态字的第1位为0,表示左shift键松开。
按下A键,引发键盘中断;cpu执行int 9中断例程,从60h端口读出A键的通码;然后检测状态字节,看看是否有shift、ctrl等切换键按下;发现没有切换键按下,则将A键的扫描码1eh和对应的ascii码,即字母“a”的ascii码61h,写入键盘缓冲区。其中,高位字节存储扫描码,低位字节存储ascii码。
1E61 |
3062 |
2E63 |
2064 |
1265 |
1E41 |
1E61 |
|
|
|
|
|
|
|
|
|
2. 使用int 16h中断例程读取键盘缓冲区
BIOS提供了int 16h中断例程提供程序员调用。int 16h中断例程中包含的一个重要功能是从键盘缓冲区读取一个键盘输入,功能编号为0。比如,从键盘缓冲区读取一个键盘输入,并且将其从键盘缓冲区删除:
mov ah,0
int 16h
执行后,(ah)=扫描码,(al)=ascii码。
比如,接着上述键盘输入过程,使用int 16h读取键盘缓冲区的过程为:
执行mov ah,0 int 16h后,缓冲区的内容为:
3062 |
2E63 |
2064 |
1265 |
1E41 |
1E61 |
|
|
|
|
|
|
|
|
|
|
ah中的内容为1Eh,al中的内容为61h。
执行mov ah,0 int 16h后,缓冲区的内容为:
2E63 |
2064 |
1265 |
1E41 |
1E61 |
|
|
|
|
|
|
|
|
|
|
|
ah中的内容为30h,al中的内容为62h。
执行mov ah,0 int 16h后,缓冲区的内容为:
2064 |
1265 |
1E41 |
1E61 |
|
|
|
|
|
|
|
|
|
|
|
|
ah中的内容为2Eh,al中的内容为63h。
执行4次mov ah,0 int 16h后,缓冲区空。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ah中的内容为1Eh,al中的内容为61h。
执行mov ah,0 int 16h后,int 16h中断例程检测键盘缓冲区,发现缓冲区空,则循环等待,直到缓冲区中有数据。
按下A键后,缓冲区内容为:
1E61 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
循环等待的int 16h中断例程检测到键盘缓冲区中有数据,将其读出,缓冲区又为空。ah中的内容为1Eh,al中的内容为61h。
总结,int 16中断例程的0号功能,进行如下工作:
检测键盘缓冲区中是否有数据;
没有则继续做检测;
读取缓冲区第一个子单元中的键盘输入;
将读取的扫描码送入ah,ascii码送入al;
将已读取的键盘输入从缓冲区中删除。
BIOS的int 9中断例程和int 16h中断例程是一对相互配合的程序,int 9中断例程向键盘缓冲区中写入,int 16h中断例程从缓冲区读出。它们写入和读出的时机不同,int 9中断例程是在有键按下的时候向键盘缓冲区中写入数据;而int 16h中断例程是在应用程序对其进行调用的时候,将数据从键盘缓冲区中读出。编写一般的处理键盘输入的程序的时候,可以调用int 16h从键盘缓冲区中读取键盘的输入。
比如,编程,接收用户的键盘输入,输入“r”,将屏幕上的字符设置为红色;输入“g”,将屏幕上的字符设置为绿色;输入“b”,将屏幕上的字符设置为蓝色。
assume cs:code
code segment
start: mov ah,0
int 16h
mov ah,1
cmp al,’r’
je red
cmp al,’g’
je green
cmp al,’b’
je blue
jmp short sret
red: shl ah,1
green: shl ah,1
blue: mov bx,0b800h
mov es,bx
mov bx,1
mov cx,2000
s: and byte ptr es:[bx],11111000b
or es:[bx],ah
add bx,2
loop s
sret: mov ax,4c00h
int 21h
code ends
end start
3. 字符串的输入
用户通过键盘输入的通常不仅仅是单个字符而是字符串。最基本的字符串输入程序,需要具备下面的功能。
在输入的同时需要显示这个字符串;
一般在输入回车符后,字符串输入结束;
能够删除已经输入的字符。
编写一个接收字符串输入的子程序,实现上面3个基本功能。子程序的参数为:
(dh)、(dl)=字符串在屏幕上显示的行、列位置;
ds:si指向字符串的存储空间,字符串以0为结尾符。
字符的输入和删除的过程,每个新输入的字符都存储在前一个输入的字符之后,而删除是从最后面的字符进行的。比如输入abc的过程是,输入a,显示a,输入b,显示ab,输入c,显示abc,不考虑光标移动的前提下,删除从c开始,删除c,显示ab,删除b,显示a,删除a,空。
字符串的输入显示和删除,类似访问栈的规则,先进后出。可以用栈的方式来管理字符串的存储空间。字符串的存储空间实际上是一个字符栈。
输入回车后,字符输入结束。
在输入的同时需要显示这个字符串,每次有新的字符输入和删除一个字符的时候,都应该重新显示字符串,从字符栈的栈底到栈顶,显示所有的字符。
那么,程序的处理过程为:
用int 16h读取键盘输入;
如果是字符,进入字符栈,显示字符栈中的所有字符,继续执行上一步;
如果是退格键,从字符栈中弹出一个字符,显示字符栈中的所有字符;继续执行int 16h读取键盘输入;
如果是enter键,向字符栈中压入0,返回。
将这个程序过程,写为子程序。
;名称,charstack
;功能,字符栈的入栈、出栈和显示
;参数,(ah)=功能号,0表示入栈,1表示出栈,2表示显示,
;ds:si指向字符栈空间;
;对于0号功能,(al)=入栈字符;
;对于1号功能,(al)=返回字符;
;对于2号字符,(dh)、(dl)=字符串在屏幕上显示的行、列位置。
charstack: jmp short charstart
table dw charpush,charpop,charshow
top dw 0
charstart: push bx
push dx
push di
push es
cmp ah,2
ja sret
mov bl,ah
mov bh,0
add bx,bx
jmp word ptr table[bx]
charpush: mov bx,top ;将字符的总数传递给bx保存
mov [si][bx],al ;同时bx也表示栈顶的字符的偏移地址
inc top ;top位置的数递增,计算当前字符栈中字符的总数
jmp sret
charpop: cmp top,0 ;查看字符栈中的总数是否为0,为0则退出
je sret
dec top ;不为0,修改栈中的字符总数减1
mov bx,top ;将弹出字符后,栈中字符总数保存在bx中
mov al,[si][bx]
jmp sret
charshow: mov bx,0b800h
mov es,bx
mov al,160
mov ah,0
mul dh
mov di,ax
add dl,dl
mov dh,0
add di,dx
mov bx,0
charshows: cmp bx,top ;起初bx中的值为0,将top中存储的总数与bx比较,直到总数与bx值相等,则说明显示字符完毕
jne noempty
mov byte ptr es:[di],’ ‘
jmp sret
noempty: mov al,[si][bx]
mov es:[di],al
mov byte ptr es:[di+2],’ ‘
inc bx ;依次显示字符,并统计显示的个数,以便和top的总数比较,找到结束的位置
add di,2
jmp charshows
sret: pop es
pop di
pop dx
pop bx
ret
完整的接收字符串的子程序
getstr: push ax
getstrs: mov ah,0
int 16h
cmp al,20h
jb nochar
mov ah,0
call charstack
mov ah,2
call charstack
jmp getstrs
nochar: cmp ah,0eh
je backspace
cmp ah,1ch
je enter
cmp ah,01h
je quit
jmp getstrs
quit: mov ax,4c00h
int 21h
backspace: mov ah,1
call charstack
mov ah,2
call charstack
jmp getstrs
enter: mov al,0
mov ah,0
call charstack
mov ah,2
call charstack
pop ax
ret
4. 应用int 13h中断例程对磁盘进行读写
以3.5英文软盘为例。3.5英寸软盘分为上下两面,每面有80个磁道,每个磁道又分为18个扇区,每个扇区的大小为512字节。那么一张盘的容量:2*80*18*512=1440bk,约1.44mb。
磁盘的实际访问由磁盘控制器进行,可以通过控制磁盘控制器访问磁盘。只能以扇区为单位对磁盘进行读写。在读写磁盘的时候,要给出面号、磁道号和扇区号。面号和磁道号从0开始,扇区号从1开始。
如果通过直接控制磁盘控制器来访问磁盘,需要涉及许多硬件细节。BIOS提供了对扇区进行读写的中断例程,这些中断例程完成了许多复杂的和硬件相关的工作。可以通过调用BIOS中断例程来访问磁盘。这个中断例程为int 13h。比如,
1) 读取0面0道1扇区的内容到0:200的程序如下,
mov ax,0
mov es,ax
mov bx,200h
mov al,1
mov ch,0
mov cl,1
mov dl,0
mov dh,0
mov ah,2
int 13h
入口参数:
(ah)=int 13h的功能号(2表示读扇区)
(al)=读取的扇区数
(ch)=磁道号
(cl)=扇区号
(dh)=磁头号(对于软盘即面号,因为一个面用一个磁头来读写)
(dl)=驱动器号
关于驱动器号,软驱从0开始,0:软驱A,1:软驱B;硬盘从80h开始,80h:硬盘C,81h:硬盘D。
es:bx指向接收从扇区读入数据的内存区。
返回参数
操作成功:(ah)=0,(al)=读入的扇区数
操作失败:(ah)=出错代码
将0:200中的内容写入0面0道1扇区。
2) 将0:200中的内容写入0面0道1扇区
mov ax,0
mov es,ax
mov bx,200h
mov al,1
mov ch,0
mov cl,1
mov dl,0
mov dh,0
mov ah,3
int 13h
入口参数:
(ah)=int 13h的功能号(3表示写扇区)
(al)=写入的扇区数
(ch)=磁道号
(cl)=扇区号
(dh)=磁头号(对于软盘即面号,因为一个面用一个磁头来读写)
(dl)=驱动器号
关于驱动器号,软驱从0开始,0:软驱A,1:软驱B;硬盘从80h开始,80h:硬盘C,81h:硬盘D。
es:bx指向将写入磁盘的数据。
返回参数
操作成功:(ah)=0,(al)=写入的扇区数
操作失败:(ah)=出错代码
如果使用int 13h中断例程对软盘进行读写,直接向磁盘扇区写入数据是很危险的,可能覆盖掉重要的数据。如果向软盘的0面0道1扇区写入了数据,要使软盘在现有的操作系统下可以使用,必须要重新格式化。使用int 13h中断例程时主要驱动器号是否正确,不能随便对硬盘中的扇区进行写入。
比如,编程,将当前屏幕的内容保存在磁盘上。
1屏的内容占4000个字节,需要8个扇区,用0面0道的1~8扇区存储显存中的内容。
assume cs:code
code segment
start: mov ax,0b800h
mov es,ax
mov bx,0
mov al,8
mov ch,0
mov cl,1
mov dl,0
mov dh,0
mov ah,3
int 13h
mov ax,4c00h
int 21h
code ends
end start
5. 综合案例
开机后,cpu自动进入到ffff:0单元处执行,此处有一条跳转指令。Cpu执行这指令后,转去执行BIOS中的硬件系统检测和初始化程序。初始化程序将建立BIOS所支持的中断向量,即将BIOS提供的中断例程的入口地址登记在中断向量表中。硬件系统检测和初始化完成后,调用int 19h进行操作系统的引导。如果设置为从软盘启动操作系统,则int 19h将要完成以下工作:
控制0号软驱,读取软盘0道0面1扇区的内容到0:7c00;将cs:ip指向0:7c00。
软盘的0道0面1扇区中装有操作系统引导程序。int 19h将其装到0:7c00处后,设置cpu从0:7c00开始执行此处的引导程序,操作系统被激活,控制计算机。
如0号软驱中没有软盘,或发生软盘I/O错误,则int 19h将主要完成以下工作:
控制0号软驱,读取C盘0道0面1扇区的内容到0:7c00;将cs:ip指向0:7c00。
任务需求,编写一个可以自行启动计算机,不需要在现有操作系统环境中运行的程序。程序的功能为:
(1)列出功能选项,让用户通过键盘进行选择,界面如下。
1)reset pc ;重启计算机
2)start system ;引导现有的操作系统
3)clock ;进入时钟程序
4)set clock ;设置时间
(2)用户输入1后重新启动计算机(考虑ffff:0单元)
(3)用户输入2后引导现有的操作系统(考虑硬盘c的0道0面1扇区)
(4)用户输入3后,执行动态显示当前日期、时间和程序。
显示格式为:年/月/日 时:分:秒
进入这个功能后,一直动态显示当前的时间,屏幕上出现时间按秒变化的效果。
当按下F1键后,改变显示颜色;按下esc键后,返回到主选单
(5)用户输入4后可更改当前的日期、时间,更改后返回到主选单。
assume cs:code
code segment
;=========================================================
;功能,将代码写入0面0道1扇区
;入口参数:
;(ah)=int 13h的功能号(2表示读扇区,3表示写扇区)
;(al)=写入的扇区数
;(ch)=磁道号
;(cl)=扇区号
;(dh)=磁头号(面号)
;(dl)=驱动器号
;es:bx指向将写入磁盘的数据或指向接收从扇区读入数据的内存区
;返回参数
;操作成功,(ah)=0,(al)=写入的扇区数
;操作失败,(ah)=出错代码
;==========================================================
start: mov ax,floppyend-floppy
mov dx,0
mov bx,512
div bx ;商ax为所需的扇区数
inc al ;写入的扇区数
push cs
pop es
mov bx,offset floppy ;es:bx指向要被写入的内存单元
mov ch,0 ;磁道号
mov cl,1 ;扇区号
mov dl,0 ;驱动器号,软盘a
mov dh,0 ;磁头号(面号)
mov ah,3 ;int 13h的功能号(3表示写扇区)
int 13h ;将代码写入0面0道1扇区
mov ax,4c00h
int 21h
floppy: jmp read
;直接定址表
table dw function1-floppy,function2-floppy
dw function3-floppy,function4-floppy
menu db ‘***main menu***’,0
db ‘1) reset pc ‘,0
db ‘2) start system‘,0
db ‘3) clock ‘,0
db ‘4) set clock ‘,0
db ‘please choose ‘,0
time db ‘yy/mm/dd hh:mm:ss’,0
cmos db 9,8,7,4,2,0
hint db ‘press f1 to change the color,press esc toreturn’,0
hint1 db ‘please input: yy/mm/dd hh:mm:ss’,0
char db ‘ / / : : ‘,0
;================================================================
;功能,将0面0道2扇区的内容读入0:7e00h
;入口参数:
;(ah)=int 13h的功能号(2表示读扇区,3表示写扇区)
;(al)=写入的扇区数
;(ch)=磁道号
;(cl)=扇区号
;(dh)=磁头号(面号)
;(dl)=驱动器号
;es:bx指向将写入磁盘的数据
;返回参数
;操作成功,(ah)=0,(al)=写入的扇区数
;操作失败,(ah)=出错代码
;==========================================================
start: mov ax,floppyend-floppy
mov dx,0
mov bx,512
div bx ;商ax为所需的扇区数
inc al ;写入的扇区数
mov bx,0
mov es,bx
mov bx,7e00h ;es:bx指向要被读入的内存单元
mov ch,0 ;磁道号
mov cl,2 ;扇区号
mov dl,0 ;驱动器号,软盘a
mov dh,0 ;磁头号(面号)
mov ah,2 ;int 13h的功能号(2表示读扇区)
int 13h ;读取0面0道2扇区的内容到0:7e00h处
mov ax,7c0h
push ax
mov ax, showmenu-floppy
push ax
retf
;=================================================
;显示主菜单,调用show_str、clean子程序
;==================================================
showmenu: call clean ;清屏
push cs
pop ds
mov si,menu-floppy
mov dh,8
mov dl,30
mov cx,6
showmenu0:
push cx
mov cl,2
call show_str
add si,16
inc dh
pop cx
loopshowmenu0
;==========================================================
;接收键盘输入,跳转相应功能程序段
;调用bios用来提供读取键盘缓冲区功能的int 16h中断例程
;将读取的扫描码送入ah,ascii码送入al
;==========================================================
go: mov ax,0
int 16h
cmp al,’1’
jb showmenu
cmp al,’4’
ja showmenu
sub al,31h
mov bl,al
mov bh,0
add bx,bx
add bx,3 ;计算相应子程序在table中的位移
call word ptr cs:[bx]
jmp showmenu
;=================================================================
;功能1:重启计算机
;=================================================================
function1: mov ax,0ffffh
push ax
mov ax,0
push ax
ref ;jmp ffff:0
;==================================================================
;功能3:进入时钟程序
;==================================================================
function3: push ax
push bx
push cx
push dx
push si
push ds
push es
call clean
mov dh,0
mov dl,0
mov cl,2
mov si,offset hint-floppy
call show_str
;=================================================================
;名称:clock
;功能:动态显示当前日期、时间
;==================================================================
mov cx,2
clock: mov bx,offset cmos-floppy
mov si,offset time-floppy
push cx
push cx,6
clock0: push cx
mov al,[bx]
out 70h,al
int al,71h
mov ah,al
shr al,cl
add ah,0fh
add ax,3030h
mov [si],ax
inc bx
add si,3
pop cx
loop clock0
;=================================================================
;按下f1键后,改变显示颜色
;按下esc键后,返回主菜单,其他键照常处理
;=================================================================
mov al,0
in al,60h
pop cx
cmp al,3bh
je colour
cmp al,1
je clockend
jmp show_clock
col_1: mov cx,1 ;cl∈[1,7]
jmp show_clock
colour: cmp cx,7
je col_1
inc cx
show_clock: mov dh,12
mov dl,30
mov si,offset time-floppy
call show_str
jmp clock
clockend: pop es
pop ds
pop si
pop dx
pop cx
pop bx
pop ax
ret
;=================================================================
;功能4:设置时间
;=================================================================
function4: push ax
push bx
push cx
push dx
push si
call clean
mov dh,8
mov dl,30
mov si,offset hint1-floppy
call show_str
add dh,1
add dl,14
mov si,offset char-floppy
call show_str
mov di,0
call getstrs
call witein
call cleanchar
pop si
pop dx
pop cx
pop bx
pop ax
ret
;=================================================================
;清除char内输入数据,还原环境
;=================================================================
cleanchar: push cx
cleanchar1: mov cx,di
jcxz cleanchar2
call charpop
jmp cleanchar1
cleanchar2: pop cx
ret
;==================================================================
;ascii=>bcd,写入cmos
;==================================================================
witein: push si
mov cx,6
mov bx,offset cmos-floppy
wite: push cx
mov al,[bx]
out 70h,al
mov ax,[si]
sub ah,30h
sub al,30h
mov cl,4
shl al,cl
add al,ah
out 71h,al
add si,3
inc bx
pop cx
loop wite
pop si
ret
;=================================================================
;子程序,接收数字输入
;参数说明,di=char栈顶(字符地址,个数记录器)
;==================================================================
getstrs: push ax
push bx
getstr: mov ax,0
int 16h
cmp ah,0eh
je backspace
cmp ah,1ch
je enter1
cmp al,’0’
jb getstr
cmp al,’9’
ja getstr
cmp di,16
ja enter1
call charpush
call show_str
jmp getstr
backspace: call charpop
call show_str
jmp getstr
enter1: call show_str
pop bx
pop ax
ret
;=================================================================
;子程序,数字的入栈
;参数说明,ds:si指向char栈空间,(al)=入栈字符
;==================================================================
charpush: mov bx,di
mov [si][bx],al
inc di
cmp di,2
je adds
cmp di,5
je adds
cmp di,8
je adds
cmp di,11
je adds
cmp di,14
je adds
ret
adds: inc di
ret
;=================================================================
;子程序,数字的出栈
;参数说明,ds:si指向char栈空间,(al)=入栈字符
;==================================================================
charpop: cmp di,0
je sret
cmp di,3
je subs
cmp di,6
je subs
cmp di,9
je subs
cmp di,12
je subs
cmp di,15
je subs
dec di
mov bx,di
mov al,’ ‘
mov [si][bx],al
ret
subs: sub di,2
mov bx,di
mov al,’ ‘
mov [si][bx],al
ret
sret: ret
;=================================================================
;名称,show_str
;功能,在指定的位置,用指定的颜色,显示一个用0结束的字符串
;参数,(dh)=行号,(dl)=列号,(cl)=颜色,ds:si指向字符串首地址
;返回,无
;=================================================================
show_str: push ax
push bx
push cx
push dx
push si
push es
mov ax,0b800h
mov es,ax
mov ax,160
mul dh
mov bx,ax
mov ax,2
mul dl
add bx,ax
mov al,cl
mov cl,0
show0: mov ch,[si]
jcxz show1
mov es:[bx],ch
mov es:[bx].1,al
inc si
add bx,2
jmp show0
show1: pop es
pop si
pop dx
pop cx
pop bx
pop ax
ret
;=================================================================
;名称,clean
;功能,清屏
;==================================================================
clean: push bx
push cx
push es
mov bx,0b800h
mov es,bx
mov bx,0
mov cx,2000
clean0: mov byte ptr es:[bx],’ ‘
add bx,2
loop clean0;
pop es
pop cx
pop bx
ret
;=================================================================
;功能2,引导现有操作系统
;实现引导现有的操作系统,需要将硬盘的0面0道1扇区读入0:7c00
;会覆盖从软盘读到0:7c00的第一个扇区,所以功能2代码不能写到第一个扇区
;==================================================================
function2: call clean
mov ax,0
mov es,ax
mov bx,7c00h
mov al,1
mov ch,0
mov cl,1
mov dl,80h
mov dh,0
mov ah,2
int 13h
mov ax,0
push ax,
mov ax,7c00h
push ax
retf
floppyend: nop
code ends
end start
参考文献:王爽著,《汇编语言》(第2版),清华大学出版社。