assembly

《汇编语言》第二版,王爽著,汇编语言学习笔记。

一、           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版),清华大学出版社。

你可能感兴趣的:(ASSEMBLY)