assembly

蔚落 2022-06-04 03:27 366阅读 0赞

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

一、 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

  1. 字在寄存器中的存储

一个字由两个字节组成,这个两个字节分别称为字的高位字节和低位字节。一个字可以存在一个16位寄存器中。这个字的高位字节和低位字节就存在这个寄存器的高8位寄存器和低8位寄存器中。

  1. 汇编指令的例子

汇编指令控制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的含义相同。

  1. 物理地址

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使用的物理地址的计算方式使得可以用分段的方式来管理内存。

  1. 段寄存器、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。

  1. 代码段

在8086中,可以根据需要,将一组内存单元定义为一个段。比如,将长度N(N<64kb)的一组代码,存在一组地址连续、起始地址为16的倍数的内存单元中,可以将这段内存看作是用来存放代码的,这就定义了一个代码段。比如将一个长度为10个字节的命令存放在123A0H~123A9H的一组内存单元中,可以认为123A0H~123A9H这段内存是用来存放代码的,是一个代码段,它的段地址为123AH,长度为10个字节。Cpu不会自动执行我们认为定义的代码段,cpu只认cs:ip指向的内存单元中的内容。必须将cs:ip指向所定义的代码段中的第一条指令的首地址。比如上面的例子,可以设cs=123AH,ip=0000H。

  1. 使用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地址字单元。

  1. 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传送数据。

  1. 字的传送

8086cpu是16位结构的,一次性可以传送16位数据,正好是一个字。比如:

  1. mov bx,1000H
  2. mov ds,bx
  3. mov ax,[0] ;1000:0处的字型数据传入ax
  4. mov [0],cx ;cx中的16位数据送到1000:0
  1. 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一样的操作形式。

  1. 数据段

在编程时,可以将一组内存单元定义为一个段。比如可以将一组长度为N(N<=64kb)、地址连续、起始地址为16的倍数的内存单元当作存储数据的内存空间,这就是一个数据段。比如用134A0H~123A9H这段内存空间来存放数据,这段内存就是一个数据段,它的段地址为123AH,长度为10个字节。

  1. 栈和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并不能保证和防止栈顶越界的问题出现。只能靠编程者编程的时候要小心谨慎注意防止栈顶越界。

  1. push、pop指令

push和pop指令的常用形式:




























Push  寄存器

将一个寄存器中的数据入栈

Pop   寄存器

用一个寄存器接收出栈的数据

Push  段寄存器

将一个段寄存器中的数据入栈

Pop   段寄存器

用一个段寄存器接收出栈的数据

Push  内存单元

将一个内存单元处的字入栈,栈操作以字为单位

Pop   内存单元

用一个内存单元接收出栈的数据

比如:

  1. mov ax,1000H
  2. mov ds,ax
  3. push [0]
  4. pop [2]

与数据段类似,可以将长度为N(N<=64kb)的一组地址连续、起始地址为16的倍数的内存单元,当作栈空间来用,这就定义了一个栈段。比如,将10010H~1001FH这段长度为16字节的内存空间当作栈来用,以栈的方式进行访问。这段空间就称为栈段,段地址为1001H,大小为16字节。如果要对这个栈段进行push和pop操作,需要将ss:sp指向这个栈段。

五、 汇编程序的编写

编写一个完整的汇编语言程序,用编译和连接程序将它们编译连接为可执行文件(如.exe文件),在操作系统中运行。首先需要了解,源程序从写出到执行的过程。

  1. 源程序从写出到执行的过程

基本步骤是:

第一步,编写汇编源程序,使用文本编辑器,用汇编语言编写源程序。

第二步,对源程序进行编译连接。使用汇编语言编译程序对源程序文件中的源程序进行编译,产生目标文件,再用连接程序对目标文件进行连接,生成可以在操作系统中直接运行的可执行文件。

第三步,在操作系统中执行可执行文件中的程序。

  1. 源程序的编写

源程序的组成与编辑

完整的源程序由指令和标号组成。指令包含二种,汇编指令和伪指令。

汇编指令有对应的机器码的指令,可以被编译为机器指令,由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就是一个标号,指代一个地址。

总体上,源程序由若干段构成。段中存放代码、数据、或将某个段当作栈空间。比如一段简单的源程序,

  1. assume cs:aaa
  2. aaa segment
  3. mov ax,22
  4. add ax,ax
  5. mov ax,bx
  6. aaa ends
  7. end

一个程序结束后,将cpu的控制权交还给使它得以运行的程序,这个过程为程序返回。声明程序返回的指令是:

mov ax,4c00H

int 21H

一个完整的源程序,需要在段中最后声明程序返回。

比如:

  1. assume cs:aaa
  2. aaa segment
  3. mov ax,22
  4. add ax,ax
  5. mov ax,bx
  6. mov ax,4c00H
  7. int 21H
  8. aaa ends
  9. end

在文本编辑器中编辑好源程序,将其存储为纯文本文件,就完成了源程序的编辑。比如,将一段源程序存储为new.asm。

  1. 源程序的编译

使用编译器对源程序文件进行编译,可以生成包含机器代码的目标文件。汇编编译器有多种。可以使用微软的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。然后,显示编译结束的信息。编译完成。

  1. 编译后目标文件的连接

在对源程序进行编译得到目标文件后,对目标文件进行连接后就可得到执行文件。使用的是微软的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),一个源程序编译后,得到存有机器码的目标文件,目标文件中的有些内容不能直接生成可执行文件,使用连接程序将这些内容处理为最终的可执行文件。

  1. 使用简化的方式进行编译和连接

进入dos,在编译软件的目录下,直接masm c:\xx\1;注意加分号,然后enter,编辑器就对c:\1.asm进行编译,在当前路径下生成目标文件1.obj。

进入dos,在连接软件的目录下,直接link c:\xx\1.obj;注意加分号,然后enter,连接器就在指定路径下1.obj文件进行处理,在当前路径下生成可执行文件1.exe。

  1. xxx.exe文件的执行

在编译器和连接器的软件目录下,直接输入可执行文件的路径和文件名,如果在软件目录下可以省略路径,比如1,或者c:\xx\1。

执行后界面上看不到任何过程的提示和结果的提示,是在后台执行的。

任何通用的操作系统,都要提供一个为shell(外壳)的程序,使用这个程序来操作计算机系统进行工作。Dos中的cmd命令行,在dos中称为命令解释器,就是dos的shell。比如,在dos中执行得到的可执行文件1.exe时,cmd将1.exe中的程序加载入内存。然后cmd设置cpu的cs:ip指向程序的第一条指令,使得程序得以运行,程序运行结束,返回cmd。

  1. 跟踪程序执行过程

使用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中。

  1. Loop

Loop是循环指令。功能是循环执行一个程序段。Loop指令的格式是:loop标号,标号是一个段的标记。Cpu执行loop指令时,分两步,首先(cx)=(cx)-1;然后判断cx中的值,如果不为0则转至标号处执行程序,如果为0则向下执行。Cx中的值影响loop的次数。Cx中存放的是loop的次数。

比如使用loop计算2的10次方的程序,

  1. assume cs:mode
  2. code segment
  3. mov ax,2
  4. mov cx,9
  5. s: add ax,ax
  6. loop s
  7. mov ax,4c00h
  8. int 21h
  9. code ends
  10. end

上述代码中s就是一个标号,实际上代表一个地址,这个地址处有一条指令:add ax,ax。

使用cx和loop配合实现循环功能的框架为:

  1. mov cx,循环次数

s:

  1. 循环执行的程序段
  2. loop s

比如用加法计算123*236,结果存在ax中:

  1. assume cs:plus
  2. plus segment
  3. mov ax,0
  4. mov cx,123
  5. s: add ax,236
  6. loop s
  7. mov ax,4c00h
  8. int 21h
  9. plus ends
  10. end

注意:在汇编程序中,数据不能以字母开头,比如写A000H时,要写为0A000H,mov ax,0A0000H。

在使用debug跟踪循环的时候,如果不需要跟踪循环段之前的程序,可以使用g命令,比如循环段从cs:0012开始,使用g 0012,可以直接执行到循环段的位置。如果循环的次数很多,不方便一次一次的跟踪,可以使用p命令,当执行到loop指令时,输入p,那么就自动重复完循环中的指令,直到(cx)=0为止。也可以使用g命令执行完循环,比如循环结束后的位置为0016,那么g 0016也可以一次执行完循环。

  1. Debug和masm对指令的不同处理

对于mov ax,[0]。在debug中执行,表示将ds:0处的数据传入ax中。在masm编译这段时,会当作mov ax,0处理。那么在源程序中将内存中的数据,比如2000:0单元中的数据送入al的方式是,向将偏移地址送入bx,再使用[bx]的方式访问内存单元。比如:

  1. mov ax,2000h
  2. mov ds,ax
  3. mov bx,0
  4. mov al,[bx]

也可以不使用[bx]的方式,而是使用ds:[0]的方式,比如,mov al,ds:[0]

  1. loop和[bx]的联合使用

需求,计算ffff:0~ffff:b单元中的数据的和,结果存储在dx中。

  1. assume cs:lianhe
  2. lianhe segment
  3. mov ax,0ffffh
  4. mov ds,ax
  5. mov bx,0
  6. mov dx,0
  7. mov cx,12
  8. s: mov al,[bx]
  9. mov ah,0
  10. add dx,ax
  11. inc bx
  12. loop s
  13. mov ax,4c00h
  14. int 21h
  15. lianhe ends
  16. end

inc,指向的寄存器中的内容加1。

  1. 段前缀

一般用于访问内存单元的指令中,显示地指明内存单元的段地址的寄存器名字,称为段前缀。比如,

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单元中。

  1. assume cs:duanpre
  2. duanpre segment
  3. mov ax,0ffffh
  4. mov ds,ax
  5. mov ax,0020h
  6. mov es,ax
  7. mov bx,0
  8. mov cx,12
  9. s: mov dl,[bx]
  10. mov es:[bx],dl
  11. int bx
  12. loop s
  13. mov ax,4c00h
  14. inc 21h
  15. duanpre ends
  16. end
  1. 一段内存空间的安全性

在8086模式中,随意向一段内存中写入内容是不安全的,这段空间可能存着重要的系统数据或代码。

为了安全起见,一般的pc的dos下,dos和其他合法的程序一般都不会使用0:200~0:2ff的256个字节的空间。这段空间是相对安全的,如果不确定,可以使用debug查一个这段空间的内容是否都是0,都是0说明没有被使用,安全。

  1. 练习

向内存0:200~0:23F依次传送数据0~63(3FH)

  1. assume cs:shisi
  2. shisi segment
  3. mov ax,0020h
  4. mov ds,ax
  5. mov bx,0
  6. mov cx,64
  7. s: mov ds:[bx],bl
  8. inc bl
  9. loop s
  10. mov ax,4c00h
  11. int 21h
  12. shisi ends
  13. end

七、 包含多个段的程序

内存中的0:200~0:2ff空间只是相对安全的,而且容量有限,不能要求大的需求。为了解决安全和容量,在操作系统中,合法地通过操作系统取得空间是安全的,而且在系统允许的情况下,可以取得任意容量的空间。有两种方法,在加载程序的时候为程序分配,在执行的过程中向系统申请。如果要在加载的时候取得所需的空间,必须在源程序中做出说明。一般,通过源程序中的定义段来获取需要的内存空间。

除了获取内存空间要使用定义段外,从程序设计的角度,为了是程序结构清晰,一般通过不同的段来存放要处理的数据,运行的代码以及对于栈空间的使用。

  1. 在代码段中使用数据

比如,编一个程序,计算这8个数据的和,让结果存在ax寄存器中。

0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h

当程序中定义了这些数据后,这些数据会被编译、连接程序作为程序的一部分写到可执行文件。当可执行文件中的程序加载如内存时,这些数据也同时载入内存。比如:

  1. assume cs:code
  2. code segment
  3. dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
  4. mov bx,0
  5. mov ax,0
  6. mov cx,8
  7. s: add ax,cs:[bx]
  8. add bx,2
  9. loop s
  10. mov ax,4c00h
  11. int 21h
  12. code ends
  13. end

上面的dw(define word)表示定义字型数据,也可以使用db(define byte)表示定义字节型数据。因为程序执行时是加载到cs段指向的内存空间的,一般从0000开始。所以,程序的开头,也就是数据部分就加载到了cs段指向的,偏移地址为0的的内存空间,也就是从cs:0处载入这些数据。所以执行,add ax,cs:[bx],可以从内存中取得对应的载入的数据。

一般,一段程序执行加载到内存后,是从程序的第一行开始执行的,当向程序开头的地方写入了数据后,程序的第一行就是数据,不是要执行的代码,这样会出错。所以,需要指明程序的入口,比如:

  1. assume cs:code
  2. code segment
  3. dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
  4. start: mov bx,0
  5. mov ax,0
  6. mov cx,8
  7. s: add ax,cs:[bx]
  8. add bx,2
  9. loop s
  10. mov ax,4c00h
  11. int 21h
  12. code ends
  13. end start

在程序的第一条指令前加上一个标号start,将这个标号写到end后面,end指令就可以指明程序的入口。

  1. 在代码段中使用栈

根据程序中写入数据,然后在程序运行时,系统给这些数据分配内存空间的特点,可以在程序中写入栈需要的容量的数据以便在程序执行时可以得到一段空间。然后将这段空间作为栈使用,只需要将这部分数据的段地址赋值给栈的段地址,设置适当的栈指针,就相当于将这段空间作为栈。比如,利用栈,将程序中定义的数据逆序存放:

  1. assume cs:code
  2. code segment
  3. dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
  4. dw 0,0,0,0,0,0,0,0,0,0
  5. start: mov ax,cs
  6. mov ss,ax
  7. mov sp,30h
  8. mov bx,0
  9. mov cx,8
  10. s: push cs:[bx]
  11. add bx,2
  12. loop s
  13. mov bx,0
  14. mov cx,8
  15. s1: pop cs:[bx]
  16. add bx,2
  17. loop s1
  18. mov ax,4c00h
  19. int 21h
  20. code ends
  21. end start
  1. 将数据、代码、栈放入不同的段

为了是程序看起来结构清晰,以及8086段的最大长度为64kb的制约,需要采用多个段来存放数据、代码和段。使用和定义代码段一样的方式可以定义存放数据和取得栈空间的段。

比如:

  1. assume cs:code
  2. data segment
  3. dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
  4. data ends
  5. stack segment
  6. dw 0,0,0,0,0,0,0,0,0,0
  7. stack ends
  8. code segment
  9. start: mov ax,stack
  10. mov ss,ax
  11. mov sp,30h
  12. mov ax,data
  13. mov ds,ax
  14. mov bx,0
  15. mov cx,8
  16. s: push [bx]
  17. add bx,2
  18. loop s
  19. mov bx,0
  20. mov cx,8
  21. s1: pop [bx]
  22. add bx,2
  23. loop s1
  24. mov ax,4c00h
  25. int 21h
  26. code ends
  27. end start

所有定义在段中的内容,在载入内存后,都有内存地址,而这个段的标号也是指向这个段的内存的段地址,使用mov ax,data或者mov ax,stack,可以将这个段的内存段地址放入累加寄存器,以便放入ds或者ss。

八、 更灵活的定位内存地址的方法

使用[0]、[1]、[bx]的方法可以访问内存,除此之外还有更灵活的定位内存地址的编程方法。

  1. and和or指令

and,逻辑与指令,按位进行与运算。比如,

  1. mov al,01100011B
  2. and al,00111011B

执行后,al=00100011B。

通过该指令可以将操作对象的相应位设为0,其他位不变。因为与运算全真才为真,有假就是假。比如如果al的第6位不是0,那么执行,add al,10111111B后第6位为0。

or,逻辑或指令,按位进行或运算。比如:

  1. mov al,01100011B
  2. or al,00111011B

执行后,al=01111011B。

通过该指令可以将操作对象的相应位设为1,其他位不变。因为或运算全假才为假,有真就是真。比如如果al的第6位不是1,那么执行,or,01000000B后第6位为1。

  1. 字符形式的数据以及字符的大小写转换

1) 字符形式给出数据

在汇编程序中,以’……’的方式指明数据是以字符的形式给出的,编译器会把它们转化为对应的ascii码。比如,

  1. assume cs:code,ds:data
  2. data segment
  3. db unHa
  4. db fasS
  5. data ends
  6. code segment
  7. start: mov al,’a
  8. mov bl,’b
  9. mov ax,4c00h
  10. int 21h
  11. code ends
  12. end start

db ‘unHa’,就相当于定义了字节型,即,db 75h,6Eh,48h,61h。

mov al,’a’,就相当于放入字节型,即,mov al,61h。

2) 字符的大小写转换

一般字符的大小写转换其实就是改变大小写字符对应的ascii码的值。而在汇编中习惯使用16进制。小写字母的16进制ascii码值比大写字母的16进制ascii码值大20。

比如,将数据段中的两串数据,

  1. data segment
  2. db asdCsDg
  3. db nsdSEFSEfs
  4. data ends

第一串转换为大写,第二串转换为小写。如果使用十六进制码值,那么在转换时就要判断一个字符的值是大于还是小于61H,对于第一串,如果是大于61H,则sub 20H,如果不是则不改变。但是,在没有了解汇编中使用判断的语法前,这样做是行不通的。通过观察大小写字符的2进制码值,发现规律,同一个大小写字符,比如A和a的二进制码值中除了第5位,其他位都相同,大写的是0,小写的是1。那么要转为小写,只需将这个字符的第5位设置为1,其他位不变,这可以使用逻辑或。要转为大写,只需将这个字符的第5位转为0,其他位不变,这可以使用逻辑与。

那么,将上述需求编码

  1. assume cs:code,ds:data
  2. data segment
  3. db asdCsDg
  4. db nsdSEFSEfs
  5. data ends
  6. code segment
  7. start: mov ax,data
  8. mov ds,ax
  9. mov bx,0
  10. mov cx,7
  11. s: mov al,[bx]
  12. and al,11011111B
  13. mov [bx],al
  14. loop s
  15. inc bx
  16. mov cx,10
  17. s1: mov al,[bx]
  18. or al,00100000B
  19. mov [bx],al
  20. inc bx
  21. loop s1
  22. mov ax,4c00h
  23. int 21h
  24. code ends
  25. end start
  1. 使用[bx+idata]

使用[bx]可以指明一个内存单元。使用[bx+idata]可以更灵活的指明内存单元,它的偏移地址为bx中的值加上idata。比如,

mov ax,[bx+200]

表示将偏移地址为bx+200,段地址为ds中的值,处的数据放入ax中。该指令还可以书写为:

  1. mov ax,[200+bx]
  2. mov ax,200[bx]
  3. mov ax,[bx].200

合理使用[bx+idata],可以实现以类似数组的方式,处理数据段中的数据。比如,对于将两串字节数一样的字符串,第一串转为大写,第二串转为小写,使用[bx+idata]编程为:

  1. assume cs:code,ds:data
  2. data segment
  3. db adbEd
  4. db ESEDa
  5. data ends
  6. code segment
  7. start: mov ax,data
  8. mov ds,ax
  9. mov bx,0
  10. mov cx,5
  11. s: mov al,[bx]
  12. and al,11011111B
  13. mov [bx],al
  14. mov al,[bx+5]
  15. or al,00100000B
  16. mov [bx+5],al
  17. inc bx
  18. loop s
  19. mov ax,4c00h
  20. int 21h
  21. code ends
  22. end start
  1. SI和DI

si和di的功能和bx相似,bx是基址寄存器,si是存放相对于ds段的源变址寄存器,di是存放相对于es段的目的变址寄存器,这两个不能分成两个8位寄存器使用。实际上,这两个地址寄存器,是为了操作的方便而多提供的存放偏移地址的寄存器,它们和bx有几乎相似的功能。比如:

  1. mov bx,0
  2. mov ax,[bx]
  3. mov si,0
  4. mov ax,[si]
  5. mov di,0
  6. mov ax,[di]

上述3组指令的功能是相同的。同样的,

  1. mov bx,0
  2. mov ax,[bx+12]
  3. mov si,0
  4. mov ax,[si+12]
  5. mov di,0
  6. mov ax,[di+12]

这3组指令的功能也是相同的。

使用si和di实现将字符串hello world haha复制到它后面的数据区中。数据段如下:

  1. assume cs:code,ds:data
  2. data segment
  3. db hello world haha
  4. db ‘…………….’
  5. data ends

这两串数据中,第一串存放的地址,也就是源地址为:data:0,长度为16,这串数据后面的数据区的偏移地址为16,也就是目的地址为:data:16。所以,使用ds:si指向数据源,ds:di指向要复制的目的空间。编码如下:

  1. assume cs:code,ds:data
  2. data segment
  3. db hello worldhaha
  4. db ‘…………….’
  5. data ends
  6. code segment
  7. start: mov ax,data
  8. mov ds,ax
  9. mov si,0
  10. mov di,16
  11. mov cx,8
  12. s: mov ax,[si]
  13. mov [di],ax
  14. add si,2
  15. add di,2
  16. loop s
  17. mov ax,4c00h
  18. int 21h
  19. code ends
  20. end start

使用[bx(si或di)+idata]的方式,简化上述代码

  1. assume cs:code,ds:data
  2. data segment
  3. db hello worldhaha
  4. db ‘…………….’
  5. data ends
  6. code segment
  7. start: mov ax,data
  8. mov ds,ax
  9. mov si,0
  10. mov cx,8
  11. s: mov ax,[si]
  12. mov [si+16],ax
  13. add si,2
  14. loop s
  15. mov ax,4c00h
  16. int 21h
  17. code ends
  18. end start
  1. [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]

  1. [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。该指令也可以写成:

  1. mov ax,[bx+20+si]
  2. mov ax,[20+bx+si]
  3. mov ax,200[bx][si]
  4. mov ax,[bx].200[si]
  5. mov ax,[bx][si].200
  1. 不同的寻址方式的应用

总结几种定位内存地址的方法的特点:
























[idata]

用一个常量来表示地址,可用于直接定位一个内存单元

[bx]

用一个变量来表示内存地址,用于间接定位一个内存单元

[bx+idata]

用一个变量和常量表示地址,可在一个起始地址的基础上用变量间接定位一个内存单元

[bx+si]和[bx+di]

用两个变量表示地址

[bx+si+idata]和[bx+di+idata]

用两个变量和一个常量表示地址

例子1,将data段中每个单词的头一个字母改为大写字母。

  1. assume cs:code,ds:data
  2. data segment
  3. db '1, file '
  4. db '1, edie '
  5. db '1, search '
  6. db '1, view '
  7. db '1, options '
  8. db '1, help '
  9. data ends
  10. code segment
  11. start: mov ax,data
  12. mov ds,ax
  13. mov bx,3
  14. mov cx,6
  15. s: mov al,[bx]
  16. and al,11011111b
  17. mov [bx],al
  18. add bx,16
  19. loop s
  20. mov ax,4c00h
  21. int 21h
  22. code ends
  23. end start

例子2,将data段中每个单词改为大写字母。

使用嵌套循环实现。

  1. assume cs:code,ds:data
  2. data segment
  3. db len
  4. db hah
  5. db aad
  6. db yxs
  7. data ends
  8. code segment
  9. start: mov ax,data
  10. mov ds,ax
  11. mov bx,0
  12. mov cx,4
  13. s1: mov si,0
  14. mov cx,3
  15. s2: mov al,[bx+si]
  16. and al,11011111b
  17. mov [bx+si],al
  18. inc si
  19. loop s2
  20. add bx,16
  21. loop s1
  22. mov ax,4c00h
  23. int 21h
  24. code ends
  25. end start

上面的程序是有问题的,因为内层循环将外层循环的计数器cx中的值清零了,导致外层循环无法按预计需求执行。可以采用dx,数据寄存器,在内存循环开始前保存cx的值,等外层循环结束后,再将dx中保存的值恢复给cx。改写后代码如下:

  1. assume cs:code,ds:data
  2. data segment
  3. db 'len '
  4. db 'hah '
  5. db 'aad '
  6. db 'yxs '
  7. data ends
  8. code segment
  9. start: mov ax,data
  10. mov ds,ax
  11. mov bx,0
  12. mov cx,4
  13. s1: mov dx,cx
  14. mov si,0
  15. mov cx,3
  16. s2: mov al,[bx+si]
  17. and al,11011111b
  18. mov [bx+si],al
  19. inc si
  20. loop s2
  21. add bx,16
  22. mov cx,dx
  23. loop s1
  24. mov ax,4c00h
  25. int 21h
  26. code ends
  27. end start

虽然,上述方法中采用dx作为暂存cx数据的中介是可行的。但是由于cpu中的寄存器的数量是有限的,而且在复杂的运算中,可能出现作为中介的寄存器不够用的情况,那么,这就需要采用更具有广泛性的通用性的办法作为暂时存放数据的中介。除了cpu的寄存器,内存也是保存数据的好地方。可以在程序中开辟一段内存空间,来作为暂时存放数据的中介。比如:

  1. assume cs:code,ds:data
  2. data segment
  3. db 'len '
  4. db 'hah '
  5. db 'aad '
  6. db 'yxs '
  7. dw 0
  8. data ends
  9. code segment
  10. start: mov ax,data
  11. mov ds,ax
  12. mov bx,0
  13. mov cx,4
  14. s1: mov ds:[40h],cx
  15. mov si,0
  16. mov cx,3
  17. s2: mov al,[bx+si]
  18. and al,11011111b
  19. mov [bx+si],al
  20. inc si
  21. loop s2
  22. add bx,16
  23. mov cx,ds:[40h]
  24. loop s1
  25. mov ax,4c00h
  26. int 21h
  27. code ends
  28. end start

虽然使用内存作为中介保存数据,增强了通用性,但是操作起来并不方便,因为要记住数据放在哪个内存单元,不够简单明了。一般来说,在需要暂存数据的时候,应该使用栈,栈的操作只需要push、pop等,就可以方便的放入、取出。如果使用栈作为中介,程序如下:

  1. assume cs:code,ds:data,ss:stack
  2. data segment
  3. db 'len '
  4. db 'hah '
  5. db 'aad '
  6. db 'yxs '
  7. data ends
  8. stack segment
  9. dw 0,0,0,0,0,0,0,0
  10. stack ends
  11. code segment
  12. start: mov ax,data
  13. mov ds,ax
  14. mov bx,0
  15. mov ax,stack
  16. mov ss,ax
  17. mov sp,16
  18. mov cx,4
  19. s1: push cx
  20. mov si,0
  21. mov cx,3
  22. s2: mov al,[bx+si]
  23. and al,11011111b
  24. mov [bx+si],al
  25. inc si
  26. loop s2
  27. add bx,16
  28. pop cx
  29. loop s1
  30. mov ax,4c00h
  31. int 21h
  32. code ends
  33. end start

例子3,将data段中每个单词的前4个字母改为大写字母。

  1. assume cs:code,ds:data,ss:stack
  2. data segment
  3. db '1. lendiss '
  4. db '2. hsdah '
  5. db '3. aasdfd '
  6. db '4. yxsdfs '
  7. data ends
  8. stack segment
  9. dw 0,0,0,0,0,0,0,0
  10. stack ends
  11. code segment
  12. start: mov ax,data
  13. mov ds,ax
  14. mov bx,0
  15. mov ax,stack
  16. mov ss,ax
  17. mov sp,16
  18. mov cx,4
  19. s1: push cx
  20. mov si,0
  21. mov cx,4
  22. s2: mov al,[bx+si+3]
  23. and al,11011111b
  24. mov [bx+si+3],al
  25. inc si
  26. loop s2
  27. add bx,16
  28. pop cx
  29. loop s1
  30. mov ax,4c00h
  31. int 21h
  32. code ends
  33. end start

九、 数据处理的两个基本问题

数据处理的基本问题是:处理的数据具体在哪里?处理的数据容量有多长?

  1. 数据在哪里?

首先数据在内存里,cpu中的段寄存器ds、ss、ds、es和地址寄存器bx、si、di、bp可以用来指向详细到单元字节的内存地址。

在8086中,只有bx\si\di\bp这四个寄存器可以用在[…]中进行内存单元的寻址,比如:

  1. mov ax,[bx]
  2. mov ax,[bx+si]
  3. mov ax,[bx+di]
  4. mov ax,[bp]
  5. mov ax,[bp+si]
  6. mov ax,[bp+di]

注意:其他的寄存器不能用在[…]中做内存寻址,比如mov ax,[cx]是错误的。

在[…]中,这4个寄存器可以单独使用,如果组合使用只能是bx+si、bx+di、bp+si、bp+di。比如:

  1. mov ax,[bx]
  2. mov ax,[si]
  3. mov ax,[di]
  4. mov ax,[bp]
  5. mov ax,[bx+si]
  6. mov ax,[bx+di]
  7. mov ax,[bp+si]
  8. mov ax,[bp+di]
  9. mov ax,[bx+si+idata]
  10. mov ax,[bx+di+idata]
  11. mov ax,[bp+si+idata]
  12. mov ax,[bp+di+idata]

注意:其他组合是错误的,比如[bx+bp],[si+di]。

只要在[…]中使用了寄存器bp,如果指令中没有显性给出段地址,段地址默认在ss中。比如,

mov ax,[bp]

指向的内存空间是ss中的值*16+bp中的值。

  1. 机器指令处理的数据在哪里

一般,机器指令对数据的处理,可以分为3类,读取、写入、运算。对于机器指令来将,要执行前必须得到要处理的数据所在的位置。一般,所要处理的数据所在的位置,见表:
























机器码

汇编指令

执行前数据位置

8E1E0000

mov  bx,[0]

内存中,ds:0单元

89C3

mov  bx,ax

CPU内部,ax寄存器

BB0100

mov  ax,1

CPU内部,指令寄存器

  1. 汇编中数据位置的表达方式


















立即数

直接包含在机器指令中的数据,执行前在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]

等,是显性指明段寄存器。

  1. 关于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]

  1. 指令要处理的数据的长度

8086cpu可以处理两种尺寸的数据,byte和word。在指令中要指明处理的是字还是字节。常见的情况有3种。

1) 通过寄存器指明要处理的数据的尺寸

如果源或者目的寄存器为8位,则处理的数据为byte,如果源或目的寄存器为16位,则处理的数据为word。

比如,

  1. mov ax,1
  2. mov bx,ds:[0]
  3. mov ds,ax
  4. mov ds:[0],ax
  5. inc ax
  6. add ax,1000

进行的是字操作。

  1. mov al,1
  2. mov al,bl
  3. mov bl,ds:[0]
  4. mov ds:[0],al
  5. inc al
  6. add al,100

进行的是字节操作。

2) 使用x ptr指明内存单元的长度

在没有寄存器名存在的情况下,用操作符x ptr指明内存单元的长度,x可以为word或byte。

用word ptr指明指令访问的内存单元是一个字单元的例子,

  1. mov word ptr ds:[0],1
  2. inc word ptr [bx]
  3. inc word ptr ds:[0]
  4. add word ptr [bx],2

用byte ptr指明指令访问的内存单元是一个字节单元的例子,

  1. mov byte ptr ds:[0],1
  2. inc byte ptr [bx]
  3. inc byte ptr ds:[0]
  4. add byte ptr [bx],2

对于

2000:1000内存单元中数据为:aa aa aa aa …

执行

  1. mov ax,2000h
  2. mov ds,ax
  3. mov byte ptr [1000h],1

的结果为,2000:1000 01 aa aa aa…

执行

  1. mov ax,2000h
  2. mov ds,ax
  3. mov word ptr [1000h],1

的结果为,2000:1000 01 00 aa aa …

3) 其他方式

有的指令默认了访问的是子单元还是字节单元,比如,push [1000h]访问的是字单元。因为push只进行字操作。

  1. 寻址方式的例子

有一条公司的记录如下,

公司名称:HAHA

总裁姓名:heihei

排名:123

收入:20

著名产品:HEHE

假设数据存放在data段,偏移地址从60开始的内存中。数据的存储图示为:
























+00

‘HAHA’

+04

heihei

+0c

123

+0e

20

+10

HEHE

后来,公司的情况发生了变化,公司排名上升至15位,收入增加了40,著名产品变为XIXI。请汇编编程修改公司数据为新的数据。

假设data段是已知的,那么关键代码为:

  1. mov ax,data
  2. mov ds,ax
  3. mov bx,60h
  4. mov word ptr [bx+0ch],15
  5. add word ptr [60+0eh],40
  6. mov si,0
  7. mov byte ptr [bx+10h+si],’H
  8. inc si
  9. mov byte ptr [bx+10h+si],’E
  10. inc si
  11. mov byte ptr [bx+10h+si],’H
  12. inc si
  13. mov byte ptr [bx+10h+si],’E

使用bx,指定公司信息的基值,使用地址大小,指定要访问的数据在公司信息中的位置,使用源变址,指定要访问的数据中的某个字节在这个数据的位置。

如果使用c语言描述这个程序,大致为:

  1. struct compamy{
  2. char cn[4];
  3. char hn[6];
  4. int pm;
  5. int st;
  6. char cp[4];
  7. };
  8. struct company dec={“HAHA”,”heihei”,123,20,”HEHE”};
  9. main(){
  10. int i;
  11. dec.pm = 15;
  12. dec.sr = dec.sr + 40;
  13. i = 0;
  14. dec.cp[i] = H’;
  15. i++;
  16. dec.cp[i] = E’;
  17. i++;
  18. dec.cp[i] = H’;
  19. i++;
  20. dec.cp[i] = E’;
  21. return 0;
  22. }

仿照c语言的语法格式,可将上述汇编语言修改为:

  1. mov ax,data
  2. mov ds,ax
  3. mov bx,60h
  4. mov word ptr [bx].0ch,15
  5. add word ptr [60].0eh,40
  6. mov si,0
  7. mov byte ptr [bx].10h[si],’H
  8. inc si
  9. mov byte ptr [bx].10h[si],’E
  10. inc si
  11. mov byte ptr [bx].10h[si],’H
  12. inc si
  13. mov byte ptr [bx].10h[si],’E
  1. 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位寄存器来存放除数。

关键代码为:

  1. mov dx,1h
  2. mov ax,86a1h
  3. mov bx,100
  4. div bx

案例2,编程,利用除法指令计算1001/100

1001小于16位,可以用ax寄存器存放,被除数为16位,除数100小于255,可用8位寄存器存放。

  1. mov ax,1001
  2. mov bl,100
  3. div bl
  1. 伪指令dd

dd是用来定义dword(double word,双字)型数据的,占据的字节为32位。比如:

  1. data segment
  2. db 1
  3. dw 1
  4. dd 1
  5. data ends

在data中定义了3个数据,第一个数据为01H,在data:0处,占一个字节;第二个数据为0001H,在data:1处,占一个字;第三个数据为00000001H,在data:3处,占2个字。

案例,用下面data段中第一个数据除以第二个数据后的结果,商存在第三个数据的存储单元中。

  1. data segment
  2. dd 100001
  3. dw 100
  4. dw 0
  5. data ends

第一个为双字型数据,长度为4个字节,32位,位置为data:0,作为被除数应该将低16位存在ax中,高16位存在dx中;

第二个字型数据,长度为2个字节,位置为:data:4,作为除数,应该存放在16位寄存器中;

第三个字型数据,长度为2个字节,位置为data:6。

关键程序为:

  1. mov ax,data
  2. mov ds,ax
  3. mov ax,[0]
  4. mov dx,[2]
  5. mov bx,[4]
  6. div bx
  7. mov [6],ax

也可以直接除即

div word ptr [4]

  1. 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可以大大简化编程,比如:

  1. stack segment
  2. db 200 dup (0)
  3. stack ends
  1. 综合使用寻址方式处理结构化数据访问的例子

某公司从1975年成立一直到1995年的基本情况如下,




























年份

收入

雇员

人均收入

1975

16

3

1976

22

7

1977

356

9

给出数据材料如下:

  1. data segment
  2. db '1975','1976','1977','1978','1979','1980','1981','1982','1983'
  3. db '1984','1985','1986','1987','1988','1989','1990','1991','1992'
  4. db '1993','1994','1995'
  5. dd 16,22,382,1356,2390,8000,16000,24486,50065,97479,140417,197514
  6. dd 345980,590827,803530,1183000,1843000,2795000,3753000,4649000,5937000
  7. dw 3,7,9,12,28,38,130,220,476,778,1001,1442,2258,2793,4072,5646,8334
  8. dw 11542,14430,15257,17800
  9. data ends
  10. table segment
  11. db 21 dup ('year summ ne ?? ')
  12. 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

 

 

编程

  1. assume cs:code
  2. data segment
  3. db '1975','1976','1977','1978','1979','1980','1981','1982','1983'
  4. db '1984','1985','1986','1987','1988','1989','1990','1991','1992'
  5. db '1993','1994','1995'
  6. dd 16,22,382,1356,2390,8000,16000,24486,50065,97479,140417,197514
  7. dd 345980,590827,803530,1183000,1843000,2795000,3753000,4649000,5937000
  8. dw 3,7,9,12,28,38,130,220,476,778,1001,1442,2258,2793,4072,5646,8334
  9. dw 11542,14430,15257,17800
  10. data ends
  11. table segment
  12. db 21 dup ('year summ ne ?? ')
  13. table ends
  14. code segment
  15. start: mov ax,data
  16. mov ds,ax
  17. mov ax,table
  18. mov es,ax
  19. mov bx,0h
  20. mov si,0h
  21. mov di,0h
  22. mov cx,21h
  23. s: mov ax,ds:[0h+si]
  24. mov es:[bx+0h],ax
  25. mov ax,ds:[2h+si]
  26. mov es:[bx+2h],ax
  27. mov ax,ds:[54h+si]
  28. mov es:[bx+5h],ax
  29. mov dx,ds:[56h+si]
  30. mov es:[bx+7h],dx
  31. mov ax,ds:[0a8h+di]
  32. mov es:[bx+0ah],ax
  33. push cx
  34. mov cx,ds:[0a8h+di]
  35. div cx
  36. mov es:[bx+0dh],ax
  37. add si,4h
  38. add di,2h
  39. pop cx
  40. add bx,10h
  41. loop s
  42. mov ax,4c00h
  43. int 21h
  44. code ends
  45. 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在汇编语言中是由编译器处理的符号,它的功能是取得符号的偏移地址。比如程序:

  1. assume cs:code
  2. code segment
  3. start: mov ax,offset start
  4. s: mov ax,offset s
  5. code ends
  6. 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处。

  1. assume cs:code
  2. code segment
  3. s: mov ax,bx
  4. mov si,offset s
  5. mov di,offset s0
  6. mov ax,cs:[si]
  7. mov cs:[di],ax
  8. s0: nop
  9. nop
  10. code ends
  11. end s

由于当程序执行时,程序被加载在内存中的位置的段地址在cs中,所以使用cs,在结合偏移地址,可以获得当前程序在内存中的详细地址。

  1. jmp指令

jmp为无条件转移指令,可以只修改ip,也可以同时修改cs和ip。

使用jmp指令需要给出两种信息:转移的目的地址;转移的距离(段间转移,段内短转移,段内近转移)。不同的给出目的地址的方法,和不同的转移位置,对应不同格式的jmp指令。

1) 依据位移进行转移的jmp指令

jmp short 标号

转到标号处执行指令。这种格式的jmp指令实现的是段内短转移,对ip的修改范围为-128~127,向前转移时最多越过128个字节,向后转移时最多越过127个字节。Short表示进行短转移。标号是代码段中的标号,指明转移的目的地,转移结束后,cs:ip指向标号出的指令。

比如,程序

  1. assume cs:code
  2. code segment
  3. start: mov ax,0
  4. jmp short s
  5. add ax,1
  6. s: inc ax
  7. code ends
  8. 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在缓冲器中的地址加上这个差值。

比如上例中,假设偏移地址与汇编程序的对应如下:

  1. assume cs:code
  2. code segment
  3. start:0000h mov ax,0
  4. 0003h jmp short s
  5. 0005h add ax,1
  6. s: 0008h inc ax
  7. code ends
  8. 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位位移由编译程序在编译时算出。

  1. 转移的目的地址在指令中的jmp指令

jmp far ptr 标号,实现的是段间转移,又称为远转移。功能是:

将cs指向标号所在段的段地址;ip指向标号所在段中的偏移地址。实际上是修改了cs和ip的值。

比如:

  1. assume cs:code
  2. code segment
  3. start: mov ax,0
  4. mov bx,0
  5. jmp far ptr s
  6. db 256 dup(0)
  7. s: add ax,1
  8. inc ax
  9. code ends
  10. end start

在debug中将这段程序翻译为机器码,可以看到jmp far ptr s,对应的机器码为EA0B016A07,076A标明了转移目的地址的段地址,010B标明了转移目的地址的偏移地址。

  1. 转移地址在寄存器中的jmp指令

jmp 16位的寄存器,功能是:(ip)=(16位寄存器)。比如,jmp ax。将ip的值改为ax中的值。

  1. 转移地址在内存中的jmp指令

转移地址在内存中的jmp指令有两种格式。

1) jmp word ptr 内存单元地址(段内转移)

功能是从内存单元地址处开始的一个字,作为转移目的偏移地址。内存单元地址可用任意合法格式的寻址方式给出。

比如1,

  1. mov ax,0133h
  2. mov ds:[0],ax
  3. jmp word ptr ds:[0]

比如2,

  1. mov ax,0134h
  2. mov [bx],ax
  3. jmp word ptr [bx]

2) jmp dword ptr 内存单元地址(段间转移)

功能是从内存单元地址处开始存放的两个字,高地址处的字是转移目的段地址,低地址处是转移目的偏移地址。即,CS=内存单元地址+2,IP=内存单元地址。内存单元地址可以使用任意合法格式的寻址方式。

比如1,

  1. mov ax,0123h
  2. mov ds:[0],ax
  3. mov word ptr ds:[2],0
  4. jmp dword ptr ds:[0]

执行上述程序后,cs中的值=0,ip中的值=0123h。cs:ip指向0000:0123。

比如2,

  1. mov ax,0123h
  2. mov [bx],ax
  3. mov word ptr [bx+2],0
  4. jmp dword ptr [bx]

执行后,cs中的值=0,ip中的值=0123h。cs:ip指向0000:0123。

  1. 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中。

  1. assume cs:code
  2. code segment
  3. start: mov ax,2000h
  4. mov ds,ax
  5. mov bx,0
  6. s: mov cx,0
  7. mov cl,[bx]
  8. jmp ok
  9. inc bx
  10. jmp short s
  11. ok: mov dx,bx
  12. mov ax,4c00h
  13. int 21h
  14. code ends
  15. end start
  1. 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中。

  1. assume cs:code
  2. code segment
  3. start: mov ax,2000h
  4. mov ds,ax
  5. mov bx,0
  6. s: mov cl,[bx]
  7. mov ch,0
  8. inc cx
  9. inc bx
  10. loop s
  11. ok: dec bx
  12. mov dx,bx
  13. mov ax,4c00h
  14. int 21h
  15. code ends
  16. end start
  1. 根据位移转移的意义和对转移位移超界的检测

1) 根据位移转移的意义

jmp short 标号

jmp near ptr 标号

jcxz 标号

loop 标号

等指令,都是根据转移目的地址和转移起始地址之间的位移来修改ip的值。这样有利于实现程序段在内存中的浮动装配。使用位移来得到目的地址,只涉及ip要做的位移,而不是固定住ip的地址。如果执行转移指令的机器码是固定的目的地址,就是对内存中的偏移地址做了严格的限制,如果这个固定的目的地址上没有要转移到的指令,那么程序的执行就会出错。如果使用位移就可以避免这个问题,因为位移是根据指令的实际情况计算得出的。即使发生了意外,仍然根据实际情况计算得出意外后的位移数,增强了程序的健壮性。

2) 编译器对转移位移超界的检测

根据位移进行转移的指令,它们的转移范围受到转移位移的限制,如果在源程序中出现了转移范围超界的问题,在编译的时候,编译器将报错。

比如,

  1. assume cs:code
  2. code segment
  3. start: jmp short s
  4. db 128 dup (0)
  5. s: mov ax,0ffffh
  6. code ends
  7. end start
  1. 综合练习

在屏幕中间分别显示绿色、绿底红色、白底蓝色的字符串“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进入全屏。

编程:

  1. assume cs:code,ds:data
  2. data segment
  3. db 'hello assembly !'
  4. data ends
  5. code segment
  6. start: mov ax,data
  7. mov ds,ax
  8. mov bx,0
  9. mov ax,0b800h
  10. mov es,ax
  11. mov si,64
  12. mov cx,16
  13. s: mov al,[bx]
  14. mov ah,01000010b
  15. mov es:[si],ax
  16. inc bx
  17. add si,2
  18. loop s
  19. mov bx,0
  20. mov si,1*160+64
  21. mov cx,16
  22. s1: mov al,[bx]
  23. mov ah,01100010b
  24. mov es:[si],ax
  25. inc bx
  26. add si,2
  27. loop s1
  28. mov bx,0
  29. mov si,2*160+64
  30. mov cx,16
  31. s1: mov al,[bx]
  32. mov ah,01100010b
  33. mov es:[si],ax
  34. inc bx
  35. add si,2
  36. loop s1
  37. mov ax,4c00h
  38. int 21h
  39. code ends
  40. 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处开始执行指令。

  1. assume cs:code
  2. stack segment
  3. db 16 dup (0)
  4. stack ends
  5. code segment
  6. start: mov ax,stack
  7. mov ss,ax
  8. mov sp,16
  9. mov ax,1000h
  10. push ax
  11. mov ax,0
  12. push ax
  13. retf
  14. code ends
  15. end start
  1. 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 内存单元地址。

比如,

  1. mov sp,10h
  2. mov ax,0123h
  3. mov ds:[0],ax
  4. call word ptr ds:[0]

执行后,(ip)=0123h,(sp)=0eH。

第二种,call dword ptr 内存单元地址

Cpu执行这种指令相当于执行了,push cs和push ip和jmp dword ptr 内存单元地址。

比如,

  1. mov sp,10h
  2. mov ax,0123h
  3. mov ds:[0],ax
  4. mov word ptr ds:[2],0
  5. call word ptr ds:[0]

执行后,(cs)=0,(ip)=0123h。

  1. call和ret的配合使用

配合使用ret和call可以实现子程序。

1) 案例1,程序返回前,bx中的值是多少?

  1. assume cs:code
  2. code segment
  3. start: mov ax,1
  4. mov cx,3
  5. call s
  6. mov bx,ax
  7. mov ax,4c00h
  8. int 21h
  9. s: add ax,ax
  10. loop s
  11. ret
  12. code ends
  13. end start

程序返回前,bx中的值是8。

根据上面程序的特点,可以写一个具有一定功能的程序段,作为子程序,在需要的时候,用call指令转去执行这个子程序。Call指令转去执行子程序之前,call指令后面的指令的地址将存储在栈中,所以可以在子程序的后面使用ret指令,用栈中的数据设置ip的值,从而转到call指令后面的代码继续执行。

使用call和ret来实现的子程序的框架如下,

  1. assume cs:code
  2. code segment
  3. main:
  4. call sub1
  5. mov ax,4c00h
  6. int 21h
  7. sub1:
  8. call sub2
  9. ret
  10. sub2:
  11. ret
  12. code ends
  13. end main
  1. 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位乘法。程序片段,

  1. mov al,100
  2. mov bl,10
  3. mul bl

案例2,计算99*1000。

100小于255,但是1000大于255,所以必须做16位乘法,程序片段,

  1. mov ax,100
  2. mov bx,1000
  3. mul bx
  1. 模块化程序设计

使用call和ret指令可以实现汇编语言编程中的模块化设计。在实际编程中,程序的模块化是必不可少的。现实问题复杂,把复杂的问题转化成为相互联系、不同层次的子问题,是必须的解决方法。利用call和ret,可以用简洁的方法,实现多个相互联系、功能独立的子程序来解决一个复杂的问题。

有关子程序设计中的相关问题和解决方法。

1) 参数和结果传递的问题

通常,子程序要根据提供的参数处理一定的事务,处理后,将结果提供给调用者。这就是参数和结果传递的问题。比如,设计一个子程序,根据提供的n,计算n的3次方。

这个程序涉及到参数n的存储地方和计算得到的值存储在什么地方的问题。实际上,可以使用寄存器来存储。比如,

  1. cube: mov ax,bx
  2. mul bx
  3. mul bx
  4. ret

使用寄存器来存储参数和结果是最常用的方法。调用者将参数送入参数寄存器,从结果寄存器中取得返回值;子程序从参数寄存器中取得参数,将返回值送入结果寄存器。

案例,计算data段中第一组数据的3次方,结果保存在后面一组dword单元中。

  1. assume cs:code
  2. data segment
  3. dw 1,2,3,4,5,6,7,8
  4. dd 0,0,0,0,0,0,0,0
  5. data ends
  6. code segment
  7. start: mov ax,data
  8. mov ds,ax
  9. mov si,0
  10. mov di,16
  11. mov cx,8
  12. s: mov bx,ds:[si]
  13. call cube
  14. mov ds:[di],ax
  15. mov ds:[di+2],dx
  16. add si,2
  17. add di,4
  18. loop s
  19. mov ax,4c00h
  20. int 21h
  21. cube: mov ax,bx
  22. mul bx
  23. mul bx
  24. ret
  25. code ends
  26. end start

2) 批量数据的传递

如果子程序只有一个参数,可以放在bx中,如果有更多个时,就不适合放在寄存器中了,因为寄存器的数量是有限的。对于返回值,也有同样的问题。

解决的方式可以是,将批量数据放到内存中,然后将它们所在内存空间的首地址存放在寄存器中,传递给需要这些数据的子程序。对于具有批量数据的返回结果,同此。

比如,设计一个子程序,将一个全是字母的字符串转化为大写形式。

因为字符串可能很长,不方便将整个字符串中的所有字母全部直接传递给子程序。但是,可以将字符串在内存中的首地址放在寄存器中传递给子程序。然后在子程序中做循环,循环的长度就是字符串的长度。

  1. assume cs:code
  2. data segment
  3. db sdfbsodjfleojfljsldjf
  4. data ends
  5. code segment
  6. start: mov ax,data
  7. mov ds,ax
  8. mov bx,0
  9. mov cx,21
  10. call cap
  11. mov ax,4c00h
  12. int 21h
  13. cap: and byte ptr [bx],11011111b
  14. inc bx
  15. loop cap
  16. ret
  17. code ends
  18. end start

注意,除了使用寄存器传递参数外,还有一种通用的方法是用栈来传递参数。

3) 寄存器冲突的问题

案例1

设计一个子程序,将一个全是字母,以0结尾的字符串,转化为大写。

由于字符串后面有一个0标记字符串的结束。所以,在转化的时候,要先判断,如果不是0就转化,如果是0就结束。可以用jcxz来检测0,做判断结束处理。

  1. assume cs:code
  2. data segment
  3. db sdfbsodjfleojfljsldjf’,0
  4. data ends
  5. code segment
  6. start: mov ax,data
  7. mov ds,ax
  8. mov bx,0
  9. call cap
  10. mov ax,4c00h
  11. int 21h
  12. cap: mov cl,[bx]
  13. mov ch,0
  14. jcxz ok
  15. add byte ptr [bx],11011111b
  16. inc bx
  17. jmp short cap
  18. ok: ret
  19. code ends
  20. end start

案例2,将如下data段中的字符串全部转化为大写

  1. assume cs:code
  2. data segment
  3. db haha’,0
  4. db hehe’,0
  5. db heih’,0
  6. db hell’,0
  7. data ends
  8. code segment
  9. start: mov ax,data
  10. mov ds,ax
  11. mov bx,0
  12. mov cx,4
  13. s: call cap
  14. inc bx
  15. loop s
  16. mov ax,4c00h
  17. int 21h
  18. change: mov cl,[bx]
  19. mov ch,0
  20. jcxz ok
  21. add byte ptr [bx],11011111b
  22. inc bx
  23. jmp short change
  24. ok: ret
  25. code ends
  26. end start

上述程序中,出现了主程序和子程序寄存器冲突的问题。一般解决思路是,编写子程序的时候主要看看是否使用了冲突的寄存器,如果有,调用者使用别的寄存器或者子程序中不要使用产生冲突的寄存器。然而,这样的解决思路是行不通的,因为子程序很多时候是具有广泛使用性的,有时候编写的子程序到底会被哪个程序调用具有可变性的。这个问题的关键在于是调用者和子程序相互不用关心对方使用了哪些寄存器。通用的解决思路是,在调用子程序的时候,将所有用到的寄存器中的内容保存起来,到栈中,在子程序返回前再恢复这些寄存器中内容。可以很好的实现相互的隔离性。

使用栈保存子程序开始时用到的寄存器中的内容

  1. assume cs:code
  2. data segment
  3. db haha’,0
  4. db hehe’,0
  5. db heih’,0
  6. db hell’,0
  7. data ends
  8. stack segment
  9. dw 0,0,0,0
  10. stack ends
  11. code segment
  12. start: mov ax,stack
  13. mov ss,ax
  14. mov sp,4
  15. mov ax,data
  16. mov ds,ax
  17. mov bx,0
  18. mov cx,4
  19. s: call cap
  20. add bx,5
  21. loop s
  22. mov ax,4c00h
  23. int 21h
  24. cap: push cx
  25. push bx
  26. change: mov cl,[bx]
  27. mov ch,0
  28. jcxz ok
  29. and byte ptr [bx],11011111b
  30. inc bx
  31. jmp short change
  32. ok: pop bx
  33. pop cx
  34. ret
  35. code ends
  36. end start

注意出入栈数据的先后顺序。

引入栈作为隔离机制后,子程序的标准框架为:

子程序开始: 子程序中使用的寄存器入栈

  1. 子程序内容
  2. 子程序中使用的寄存器出栈
  3. 返回(retretf
  1. 子程序综合案例

1) 按行列和颜色要求显示字符串

功能需求,在指定的位置,用指定的颜色,显示一个用0结束的字符串。

参数,(dh)=行号(取值范围0~24),(dl)=列号(取值范围0~79),(cl)=颜色,ds:si指向字符串的首地址。

返回值,无。

比如,在屏幕的8行3列,用绿色显示data段中的字符串。

  1. assume cs:code
  2. data segment
  3. db hello assembly!’,0
  4. data ends
  5. code segment
  6. start: mov dh,8
  7. mov dl,3
  8. mov cl,2
  9. mov ax,data
  10. mov ds,ax
  11. mov si,0
  12. call show_str
  13. mov ax,4c00h
  14. int 21h
  15. show_str: push dx
  16. push cx
  17. mov ax,0b800h
  18. mov es,ax
  19. mov al,160
  20. dec dh
  21. mul dh
  22. mov dh,0
  23. mov bx,ax
  24. mov al,2
  25. dec dl
  26. mul dl
  27. mov dl,al
  28. add bx,dx
  29. mov di,bx
  30. change: mov cl,[si]
  31. mov bl,cl
  32. mov ch,0
  33. jcxz ok
  34. pop cx
  35. mov es:[di],bl
  36. mov es:[di+1],cl
  37. inc si
  38. add di,2
  39. push cx
  40. jmp short change
  41. ok: pop cx
  42. pop dx
  43. ret
  44. code ends
  45. end start

2) 解决除法溢出的问题

用div做除法的时候,是可能发生溢出的。8位除法,al存储商;16位除法,ax存储商。如果商大于al或ax所能存储的最大值,就会溢出。比如

  1. mov bh,1
  2. mov ax,1000
  3. div bh

结果商为1000,大于al所能存放的最大值255。

比如,

  1. mov ax,1000h
  2. mov dx,1
  3. mov bx,1
  4. 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指令来做,不会导致除法溢出。

  1. assume cs:code
  2. stack segment
  3. dw 0,0,0,0,0,0,0,0
  4. stack ends
  5. code segment
  6. start: mov ax,stack
  7. mov ss,ax
  8. mov sp,16
  9. mov ax,4240h
  10. mov dx,000fh
  11. mov cx,0ah
  12. call divdw
  13. mov ax,4c00h
  14. int 21h
  15. divdw: push bx
  16. push ax
  17. mov ax,dx
  18. mov dx,0
  19. div cx
  20. mov bx,ax
  21. pop ax
  22. div cx
  23. mov cx,dx
  24. mov dx,bx
  25. pop bx
  26. ret
  27. code ends
  28. end start

3) 数值显示

关于数字的数值,有多种表示方式,数据在机器中存储为二进制形式的,显卡遵循的是ascii编码,外界在显示器上看到的字符在内存中的形式为16进制的,为了能够以常用10进制的形式在显存中看到这个数值,需要编写程序实现数据以十进制形式显示。这个程序分为两步,将使用二进制存储的数据转变为十进制形式的字符串,显示十进制形式的字符串。关于显示字符串,上述已经用子程序实现。所以,还需要编写一个将二进制数据转为十进制形式的字符串的子程序。

功能,将word型数据转变为表示十进制数的字符串,字符串以0为结尾符。

参数,(ax)=word型数据,ds:si指向字符串的首地址。

返回,无。

比如,编程,将数据12345以十进制的形式在屏幕的8行3列,用绿色显示。

  1. assume cs:code
  2. data segment
  3. db 10 dup (0)
  4. data ends
  5. code segment
  6. start: mov ax,12345
  7. mov bx,data
  8. mov ds,bx
  9. mov si,0
  10. mov di,0
  11. call dtoc
  12. mov dh,8
  13. mov dl,3
  14. mov cl,2
  15. call show_str
  16. mov ax,4c00h
  17. int 21h
  18. show_str: push dx
  19. push cx
  20. mov ax,0b800h
  21. mov es,ax
  22. mov al,160
  23. dec dh
  24. mul dh
  25. mov dh,0
  26. mov bx,ax
  27. mov al,2
  28. dec dl
  29. mul dl
  30. mov dl,al
  31. add bx,dx
  32. mov di,bx
  33. change: mov cl,[si]
  34. mov bl,cl
  35. mov ch,0
  36. jcxz ok
  37. pop cx
  38. mov es:[di],bl
  39. mov es:[di+1],cl
  40. inc si
  41. add di,2
  42. push cx
  43. jmp short change
  44. ok: pop cx
  45. pop dx
  46. ret
  47. dtoc: push ax
  48. push cx
  49. push bx
  50. push si
  51. push dx
  52. mov bx,0
  53. divdw: mov dx,0
  54. mov cx,10
  55. div cx
  56. add dx,'0'
  57. push dx
  58. mov cx,ax
  59. inc bx
  60. jcxz ok2
  61. jmp short divdw
  62. ok2: mov cx,bx
  63. x1: pop ds:[si]
  64. inc si
  65. loop x1
  66. pop dx
  67. pop si
  68. pop bx
  69. pop cx
  70. pop ax
  71. ret
  72. code ends
  73. end start
  1. 综合设计

任务,将前面做过的某公司的数据按照图中所示的格式在屏幕上显示出来。注意除法的溢出问题和字符串的长度问题。














































1975

16

3

5

1976

22

7

3

1977

343

9

43

1978

2345

13

134

……

……

 

 

 

1994

46750000

15467

304

1995

59780000

17809

356

编程

  1. assume cs:code
  2. data segment
  3. db '1975','1976','1977','1978','1979','1980','1981','1982','1983'
  4. db '1984','1985','1986','1987','1988','1989','1990','1991','1992'
  5. db '1993','1994','1995'
  6. dd 16,22,382,1356,2390,8000,16000,24486,50065,97479,140417,197514
  7. dd 345980,590827,803530,1183000,1843000,2795000,3753000,4649000,5937000
  8. dw 3,7,9,12,28,38,130,220,476,778,1001,1442,2258,2793,4072,5646,8334
  9. dw 11542,14430,15257,17800
  10. data ends
  11. agency segment
  12. db 8 dup (0)
  13. agency ends
  14. code segment
  15. start: mov ax,0b800h
  16. mov es,ax
  17. mov di,0
  18. mov cx,80*24
  19. cls: mov byte ptr es:[di],‘ ’;将屏幕清空
  20. mov byte ptr es:[di+1],0
  21. inc di
  22. inc di
  23. loop cls
  24. mov ax,data
  25. mov es,ax
  26. mov di,0
  27. mov bx,0
  28. mov ax,agency
  29. mov ds,ax
  30. mov si,0
  31. mov dh,4
  32. mov cx,21
  33. x1: push cx
  34. mov ax,es:[di]
  35. mov [si],ax
  36. mov ax,es:[di+2]
  37. mov [si+2],ax
  38. mov byte ptr [si+4],0 ;显示年份
  39. mov dl,0
  40. mov cl,2
  41. call show_str
  42. mov ax,es:[84+di]
  43. push dx
  44. mov dx,es:[84+di+2]
  45. call dtoc_dword ;显示收入
  46. pop dx
  47. mov dl,20
  48. mov cl,2
  49. call show_str
  50. mov ax,es:[84+84+bx]
  51. call dtoc_word ;显示雇员数
  52. mov dl,40
  53. mov cl,2
  54. call show_str
  55. mov ax,es:[84+di]
  56. push dx
  57. mov dx,es:[84+di+2]
  58. div word ptr es:[84+84+bx] ;显示人均收入
  59. call dtoc_word
  60. pop dx
  61. mov dl,60
  62. mov cl,2
  63. call show_str
  64. add di,4
  65. add bx,2
  66. add dh,1
  67. pop cx
  68. loop x1
  69. mov ax,4c00h
  70. int 21h
  71. ;show_str
  72. ;功能,在屏幕的指定位置,用指定颜色,显示一个用0结尾的字符串
  73. ;参数,(dh)=行号,(dl)=列号,(cl)=颜色,ds:si,该字符串的首地址
  74. ;将指定内存中的字符串显示在屏幕上
  75. show_str:
  76. push ax
  77. push cx
  78. push dx
  79. push di
  80. push si
  81. push es
  82. mov ax,0b800h
  83. mov es,ax
  84. mov al,160
  85. mul dh
  86. add bl,bl
  87. mov dh,0
  88. add ax,bx
  89. mov di,ax
  90. mov ah,cl
  91. show_str_x:
  92. mov cl,[si]
  93. mov ch,0
  94. jcxz show_str_f
  95. mov al,cl
  96. mov se:[di],ax
  97. inc si
  98. add di,2
  99. jmp show_str_x
  100. show_str_f:
  101. pop di
  102. pop si
  103. pop dx
  104. pop cx
  105. pop ax
  106. ret
  107. ;dtoc_dword
  108. ;功能,将32位的二进制转化为对应的十进制字符串
  109. ;参数,(ax)=32位的低16位,(dx)=32位的高16
  110. ;将转化的字符串放入ds:si指向的内存单元,以0结尾
  111. dtoc_dword:
  112. push ax
  113. push bx
  114. push cx
  115. push dx
  116. push si
  117. mov bx,0
  118. dtoc_dword_x:
  119. mov cx,10
  120. call divdw
  121. push cx
  122. inc bx
  123. cmp ax,0
  124. jne dtoc_dword_x
  125. cmp dx,0
  126. jne dtoc_dword_x
  127. mov cx,bx
  128. dtoc_dword_x1:
  129. pop ds:[si]
  130. add byte ptr ds:[si],’0
  131. inc si
  132. loop dtoc_dword_x1
  133. pop si
  134. pop dx
  135. pop cx
  136. pop bx
  137. pop ax
  138. ret
  139. ;dtoc_word
  140. ;功能,将一个word型数转化为字符串
  141. ;参数,(ax)=word型的数据,ds:si指向字符串的首地址
  142. ;返回,ds:[si]存放转化后的字符串,以0结尾
  143. dtoc_word:
  144. push ax
  145. push bx
  146. push cx
  147. push dx
  148. push si
  149. mov bx,0
  150. dtoc_word_x:
  151. mov dx,0
  152. mov cx,10
  153. div cx
  154. mov cx,ax
  155. add dx,’0
  156. push dx
  157. inc bx
  158. jcxz dtoc_word_f
  159. jmp dtoc_word_x
  160. dtoc_word_f:
  161. mov cx,bx
  162. dtoc_word_x1:
  163. pop ds:[si]
  164. inc si
  165. loop dtoc_word_x1
  166. pop si
  167. pop dx
  168. pop cx
  169. pop bx
  170. pop ax
  171. ret
  172. ;divdw
  173. ;功能,除法,被除数32位,除数16位,商32位,余数16位,不会溢出
  174. ;参数,(ax)=被除数低16位,(dx)=被除数高16位,(cx)=除数
  175. ;返回,(dx)=商高16位,(ax)=商低16位,(cx)=余数
  176. divdw:
  177. push bx
  178. push ax
  179. mov ax,dx
  180. mov dx,0
  181. div cx
  182. mov bx,ax
  183. pop ax
  184. div cx
  185. mov cx,dx
  186. mov dx,bx
  187. pop bx
  188. ret
  189. code ends
  190. 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等,大都是传送指令。使用指令的时候,主要指令的全部功能,包括执行结果对标志寄存器的哪些标志位造成影响。

  1. 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。

  1. 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。

特别注意,只有那些会影响标志寄存器的指令执行后,标志寄存器的值才会变化,而不是某个非标志寄存器的值变化了就一定变化。

  1. 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。

  1. 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所表示的进位和溢出,是分别对无符号数和有符号数运算而言的,它们之间没有任何关系。

  1. abc指令

abc是带进位加法指令,利用了cf位上记录的进位值。

格式,abc操作对象1,操作对象2

功能,操作对象1=操作对象1+操作对象2+cf

比如,abc ax,bx实现的功能是,(ax)=(ax)+(bx)+cf。

比如,

  1. mov ax,2
  2. mov bx,1
  3. sub bx,ax
  4. adc ax,1

执行后,cf=1,(ax)=2+1+1=4。

  1. mov ax,1
  2. add ax,ax
  3. 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位和进位值相加。

  1. mov ax,001fh
  2. mov bx,1000h
  3. add bx,e000h
  4. adc ax,0020h

adc指令执行后,也可能产生进位值,也会对cf进行设置。这样,使得可以对任意大的数据进行加法运算。比如,编程,计算1ef0001000h+2010001ef0h。结果放在ax(最高16位),bx(次高16位),cx(低16位)中。

计算分3步,先计算低16位,再使用adc计算次高16位加上cf中的值,再使用adc计算最高16位加上cf中的值。

  1. mov cx,1000h
  2. mov bx,f000h
  3. mov ax,1eh
  4. adc cx,1ef0h
  5. adc bx,1000h
  6. adc ax,20h

利用adc,编写一个子程序,对两个128位数据进行相加。

;名称:add128

;功能,两个128位数据进行相加

;参数,ds:si指向存储第一个数的内存空间,因数据为128位,需要8个字单元,由低地址单元向高地址单元依次存放这128位数据由低到高的各个字。ds:di指向存储第二个数的内存空间,运算结果存储在第一个数的存储空间中。

  1. add128: push ax
  2. push cx
  3. push si
  4. push di
  5. mov ax,data
  6. mov ds,ax
  7. mov si,0
  8. mov di,128
  9. sub ax,ax ;将cf值为0
  10. mov cx,8
  11. ladc: mov ax,[si]
  12. adc ax,[di]
  13. mov [si],ax
  14. inc si
  15. inc si
  16. inc di
  17. inc di
  18. loop ladc
  19. pop di
  20. pop si
  21. pop cx
  22. pop ax
  23. ret

注意,inc和loop指令不影响cf的值。由于add指令会影响cf的值,如果上面的inc指令换成add,在上题的数据环境中,会将cf置为0,从而可能导致错误的结果。

  1. sbb指令

sbb是带借位减法指令,利用了cf上记录的借位值。

指令格式:sbb操作对象1,操作对象2

功能,操作对象1=操作对象1-操作对象2-cf。

比如,sbb ax,bx,实现的功能是:(ax)=(ax)-(bx)-cf。

sbb指令执行后,将对cf进行设置。利用sbb指令可以对任意大的数据进行减法运算。比如,计算003e1000h-00202000h,结果放在ax,bx中。

  1. mov bx,1000h
  2. mov ax,003eh
  3. sub bx,2000h
  4. sbb ax,0020h
  1. 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)。

  1. 检测比较结果的条件转移指令

转移,表示能够修改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)。

  1. cmp ah,bh
  2. je s1
  3. add ah,bh
  4. jmp short ok
  5. s1:
  6. add ah,ah
  7. ok:
  8. ……

当然,上述的条件转移指令,不一定要用在比较条件中,只要检测到相关的标志位符合转移发生的值就转移。比如,

  1. mov ax,0
  2. add ax,0
  3. je s
  4. inc ax
  5. s:
  6. inc ax

虽然上述程序没有进行比较,但是zf=1,je指令发生转移。

综合使用cmp和条件转移指令的例子,

比如,data段中的8个字节如下:

  1. data segment
  2. db 5,11,2,1,5,5,65,49
  3. data ends

1) 统计data段中数据为5的字节的个数,用ax保存统计结果。

  1. assume cs:code
  2. data segment
  3. db 5,11,2,1,5,5,65,49
  4. data ends
  5. code segment
  6. start: mov ax,data
  7. mov ds,ax
  8. mov si,0
  9. mov ax,0
  10. mov bl,5
  11. mov cx,8
  12. s0:
  13. cmp bl,[si]
  14. je s1
  15. goon:
  16. inc si
  17. loop s0
  18. jmp short ok
  19. s1:
  20. inc ax
  21. jmp short goon
  22. ok:
  23. mov ax,4c00h
  24. int 21h
  25. code ends
  26. end start

或者

  1. assume cs:code
  2. data segment
  3. db 5,11,2,1,5,5,65,49
  4. data ends
  5. code segment
  6. start: mov ax,data
  7. mov ds,ax
  8. mov si,0
  9. mov ax,0
  10. mov bl,5
  11. mov cx,8
  12. s0:
  13. cmp bl,[si]
  14. jne s1
  15. inc ax
  16. s1:
  17. inc si
  18. loop s0
  19. mov ax,4c00h
  20. int 21h
  21. code ends
  22. end start

2) 统计data段中数值大于5的字节的个数,用ax保存统计结果

  1. assume cs:code
  2. data segment
  3. db 5,11,2,1,5,5,65,49
  4. data ends
  5. code segment
  6. start: mov ax,data
  7. mov ds,ax
  8. mov si,0
  9. mov ax,0
  10. mov bl,5
  11. mov cx,8
  12. s0:
  13. cmp bl,[si]
  14. jna s1
  15. inc ax
  16. s1:
  17. inc si
  18. loop s0
  19. mov ax,4c00h
  20. int 21h
  21. code ends
  22. end start

3) 统计data段中数值小于5的字节的个数,用ax保存统计结果

  1. assume cs:code
  2. data segment
  3. db 5,11,2,1,5,5,65,49
  4. data ends
  5. code segment
  6. start: mov ax,data
  7. mov ds,ax
  8. mov si,0
  9. mov ax,0
  10. mov bl,5
  11. mov cx,8
  12. s0:
  13. cmp bl,[si]
  14. jnb s1
  15. inc ax
  16. s1:
  17. inc si
  18. loop s0
  19. mov ax,4c00h
  20. int 21h
  21. code ends
  22. end start
  1. 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。

编程:

  1. assume cs:code,ds:data
  2. data segment
  3. db hello assembly!‘
  4. db 16 dup (0)
  5. data ends
  6. code segment
  7. start: mov ax,data
  8. mov ds,ax
  9. mov es,ax
  10. mov si,0
  11. mov di,16
  12. mov cx,16
  13. cld
  14. rep movsb
  15. mov ax,4c00h
  16. int 21h
  17. code ends
  18. end start

6) 案例2,编程,用串传送指令,将f000h段中的最后16个字符复制到data段中。

要传送的字符串位于f000h段的最后16个单元中,它的最后一个字符的位置,f000:ffff,将ds:si指向f000h段的最后一个单元,将es:si指向data中的最后一个单元,然后逆向传送16个字节。

  1. assume cs:code,ds:data
  2. data segment
  3. db 16 dup (0)
  4. data ends
  5. code segment
  6. start: mov ax,0f000h
  7. mov ds,ax
  8. mov si,0ffffh
  9. mov ax,data
  10. mov es,ax
  11. mov di,0fh
  12. mov cx,16
  13. std
  14. rep movsb
  15. mov ax,4c00h
  16. int 21h
  17. code ends
  18. end start
  1. pushf和popf

pushf的功能是将标志寄存器的值压栈,而popf是从栈中弹出数据,送入标志寄存器中。pushf和popf,为直接访问标志寄存器提供了一种方法。

栈一次性操作的对象是字,标志寄存器刚好16位,就是一个字。pushf就是将标志寄存器的16位二进制数压入栈,popf是从栈中弹出一个数据,转化16位二进制数,对应标志寄存器的各个位。

  1. 标志寄存器在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

  1. 综合案例

编写一个子程序,将包含任意字符,以0结尾的字符串中的小写字母转变成大写字母。

子程序描述

名称:uppercase

功能:将以0结尾的字符串中的小写字母转变成大写字母

参数:ds:si指向字符串首地址

  1. assume cs:code
  2. data segment
  3. db beginnersall-puipose haha asembly 1234 inc >>0++coder world.”,0
  4. data ends
  5. code segment
  6. start: mov ax,data
  7. mov ds,ax
  8. mov si,0
  9. call uppercase
  10. mov ax,4c00h
  11. int 21h
  12. uppercase: push cx
  13. push si
  14. s0: mov cl,ds:[si]
  15. cmp cl,0
  16. jne s1
  17. pop si
  18. pop cx
  19. ret
  20. s1: cmp cl,61h
  21. jb s2
  22. cmp cl,7ah
  23. ja s2
  24. and cl,11011111b
  25. mov ds:[si],cl
  26. s2: inc si
  27. jmp short s0
  28. code ends
  29. end start

十三、 内中断

任何通用的cpu,都具备一种能力,可以在执行当前正在执行的指令之后,检测到从cpu外部发送过来的或内部产生的一种特殊信息,并且可以立即对所接收到的信息进行处理。这种特殊的信息,称为中断信息。中断的意思是,cpu不再接着向下执行,而是转去处理这个特殊信息。

  1. 内中断的产生

对于8086cpu,当内部发生如下情况,将产生中断信息。

除法错误,比如,执行div指令产生的除法溢出;

单步执行;

执行into指令;

执行int指令。

8086cpu采用称为中断类型码的数据来标志中断信息的来源。中断类型码为一个字节型数据,可以表示256种中断信息的来源。中断信息的来源,简称中断源。这4种中断源,在8086cpu中的中断类型码如下,

除法错误,0

单步执行,1

执行into指令,4

执行int指令,该指令的格式为int n,指令中的n为字节型立即数,是提供给cpu的中断类型码。

  1. 中断处理程序

Cpu收到中断信息后,需要对中断信息进行处理。可以编程来对中断程序处理,这种程序称为中断处理程序。对于不同的中断信息,需要编写不同的处理程序。Cpu收到中断信息后,要将cs:ip指向中断信息处理程序的入口。中断类型码的作用就是来定位中断处理程序。比如cpu根据中断类型码4,可以找到4号中断的处理程序。

Cpu用8位的中断类型码通过中断向量表找到相应的中断处理程序的入口地址。中断向量表是中断向量的列表。中断向量就是中断处理程序的入口地址。中断向量表在内存中保存,其中存放着256个中断源所对应的中断处理程序的入口。Cpu只要知道了中断类型码,就可以将中断类型码作为中断向量表的表项号,定位相应的表项,从而得到中断处理程序的入口地址。

8086cpu中,中断向量表指定存放在内存地址0处。从内存0000:0000到0000:03ff的1024个单元中存放着中断向量表。一个表项存放一个中断向量,即一个中断处理程序的入口地址,这个入口地址包括段地址和偏移地址,一个表项占两个字,高地址字存放段地址,低地址字存放偏移地址。

  1. 中断过程

用中断类型码找到中断向量,并用它设置cs和ip,这个由cpu硬件自动完成。Cpu硬件完成这个工作的过程被称为中断过程。Cpu执行完中断处理程序后,应该返回原来的执行点继续执行下面的指令。在执行中断过程中,设置cs:ip之前,还要将原来的cs和ip的值保存起来。

中断过程的执行过程是:

从中断信息中取得中断类型码;

标志寄存器的值入栈,因为中断过程中要改变标志寄存器的值;

设置标志寄存器的第8位TF和第9位IF的值为0;

Cs的内容入栈;

Ip的内容入栈;

从内存地址为中断类型码*4和中断类型码*4+2的两个字单元中读取中断处理程序的入口地址设置ip和cs。

简洁描述中断过程是:

取得中断类型码N;

  1. pushf
  2. TF=0IF=0
  3. push cs
  4. push ip
  5. (IP)=(N*4),(CS)=(N*4+2)
  1. 中断处理程序的编写和iret指令

中断处理程序的编写方法和子程序的编写类似,一般步骤:

保存用到的寄存器;

处理中断;

恢复用到的寄存器;

用iret指令返回。

iret指令的功能是:pop ip pop cs popf

iret通常和硬件自动完成的中断过程配合使用。在中断过程中,寄存器入栈的顺序是标志寄存器、CS、IP,iret的出栈顺序是IP、CS、标志寄存器,刚好对应入栈,实现执行中断处理程序前的cpu恢复标志寄存器和CS、IP的工作。iret指令执行后,cpu回到执行中断处理程序前的执行点继续执行程序。

  1. 除法错误中断的处理

除法错误中断,即0号中断。当cpu执行div等除法指令的时候,如果发生了除法溢出错误,将产生中断类型码为0的中断信息,cpu检测到这个信息,然后引发中断过程,执行0号中断所对应的中断处理程序。比如:

  1. mov ax,1000h
  2. mov bh,1
  3. div bh

cpu执行0号中断处理程序,显示提示信息后,然后返回到操作系统。

可以自定义编写一个0号中断处理程序,它的功能是在屏幕中间显示“overflow!”后,返回到操作系统。

经过分析,自定义中断处理程序包括以下工作:

编写可以显示”overflow!”的中断处理程序do0;

将do0送入内存0000:0200处;

将do0的入口地址0000:0200存储在中断向量表0号表项中。

程序的框架为:

  1. assume cs:code
  2. code segment
  3. start: do0安装程序
  4. 设置中断向量表
  5. mov ax,4c00h
  6. int 21h
  7. do: 显示字符串”overflow!”
  8. mov ax,4c00h
  9. int 21h
  10. code ends
  11. end start

这个程序可以分为两部分:安装do0,设置中间向量的程序;do0。

  1. 安装do0

安装do0就是使用movsb将do0的代码送入0:200处。程序如下:

  1. assume cs:code
  2. code segment
  3. start: mov ax,cx
  4. mov ds,ax
  5. mov si,offset do0
  6. mov ax,0
  7. mov es,ax
  8. mov di,200h
  9. mov cx,offset do0end-offset do0
  10. cld
  11. rep movsb
  12. 设置中断向量表
  13. mov ax,4c00h
  14. int 21h
  15. do0: 显示字符串”overflow!”
  16. mov ax,4c00h
  17. int 21h
  18. do0end: nop
  19. code ends
  20. end start

-符号是编译器可以识别的运算符号,编译器可以用它来进行两个常数的减法。

  1. do0

do0程序的作用是显示字符串,这就涉及到字符串存放位置的问题。这个字符串不能放到程序段中,比如

  1. data segment
  2. db overflow!”
  3. data ends

这样就是在do0程序中设置ds指向当前程序所在的内存段的起始位置,然后将使用这段内存。然后,当执行完这个安装do0的过程后,这个安装程序占用的内存空间将释放,data段中的字符串也就被覆盖。那么当发生除法溢出时,难以保证cs仍然指向这个安装程序当初的段,即使恰好是指向这个段,也难以保证这个段中的字符串仍然存在。所以,应该将字符串存放在不会覆盖,并且相对于安装的do0的内存位置相对固定的内存空间中。

  1. assume cs:code
  2. code segment
  3. start: mov ax,cs
  4. mov ds,ax
  5. mov si,offset do0
  6. mov ax,0
  7. mov es,ax
  8. mov di,200h
  9. mov cx,offset do0end-offset do0
  10. cld
  11. rep movsb
  12. 设置中断向量表
  13. mov ax,4c00h
  14. int 21h
  15. do0: jmp short do0start
  16. db overflow!”
  17. do0start: mov ax,cs
  18. mov ds,ax
  19. mov si,202h
  20. mov ax,0b800h
  21. mov es,ax
  22. mov di,12*160+36*2
  23. mov cx,9
  24. s: mov al,[si]
  25. mov es:[di],al
  26. inc si
  27. add di,2
  28. loop s
  29. mov ax,4c00h
  30. int 21h
  31. do0end: nop
  32. code ends
  33. end start
  1. 设置中断向量

将do0的入口地址0:200,写入中断向量表的0号表项中,使do0成为0号中断的处理程序。

  1. mov ax,0
  2. mov ds,ax
  3. mov si,0
  4. mov word ptr ds:[si],200h
  5. mov word ptr ds:[si+2],0h
  1. 关于单步中断

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提供单步中断功能,为单步跟踪程序的执行过程,提供了实现机制。

  1. 关于响应中断的特殊情况

有些情况下,cpu在执行完当前指令后,即便是发生中断,也不会响应。比如,在执行完ss寄存器传送数据的指令后,即便发生中断,cpu也不会响应。因为ss:sp联合指向栈顶,对它们的设置要连续完成。如果在执行完设置ss的指令后,cpu响应中断,要在栈中压入标志寄存器、cs和ip的值。这将导致执行到sp后,ss改变,ss:sp指向不正确的栈顶,引起错误。比如设置栈顶为1000:0应该:

  1. mov ax,1000h
  2. mov ss,ax
  3. mov sp,0

而不是

  1. mov ax,1000
  2. mov ss,ax
  3. mov ax,0
  4. mov sp,0
  1. 自定义0号中断的完整代码

    assume cs:code

    code segment

    start: mov ax,cs

    1. mov ds,ax
    2. mov si,offset do0 ;安装do0
    3. mov ax,0
    4. mov es,ax
    5. mov di,200h
    6. mov cx,offset do0end-offset do0
    7. cld
    8. rep movsb
  1. mov ax,0
  2. mov ds,ax ;设置中断向量表
  3. mov si,0
  4. mov word ptr ds:[si],200h
  5. mov word ptr ds:[si+2],0h
  6. mov ax,4c00h
  7. int 21h
  8. do0: jmp short do0start ;do0程序
  9. db overflow!”
  10. do0start: mov ax,cs
  11. mov ds,ax
  12. mov si,202h
  13. mov ax,0b800h
  14. mov es,ax
  15. mov di,12*160+36*2
  16. mov cx,9
  17. s: mov al,[si]
  18. mov es:[di],al
  19. inc si
  20. add di,2
  21. loop s
  22. mov ax,4c00h
  23. int 21h
  24. do0end: nop
  25. code ends
  26. 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指令调用任何一个中断处理程序。比如,

  1. assume cs:code
  2. code segment
  3. start: mov ax,0b800h
  4. mov es,ax
  5. mov byte ptr es:[12*160+40*2],’!’
  6. int 0
  7. code ends
  8. end start

一般,系统将一些具有一定功能的子程序,以中断程序的方式提供给应用程序调用。编程的时候,可以用int指令调用这些子程序。也可以自己编写一些中断处理程序供别人使用。将中断处理程序简称为中断例程。

  1. 编程供应用程序调用的中断例程

1) 编写安装中断7ch的中断例程

功能,求一个word型数据的平方。

参数,(ax)=要计算的数据。

返回值,dx、ax中存放结果的高16位和低16位。

比如,求2*34562的程序,其中int调用7ch计算ax中数的平方。

  1. assume cs:code
  2. code segment
  3. start: mov ax,3456
  4. int 7ch
  5. add ax,ax
  6. adc dx,dx
  7. mov ax,4c00h
  8. int 21h
  9. code ends
  10. end start

编写7ch的中断例程,包括3部分内容,

编写实现求平方功能的程序;

安装程序,将其安装在0:200处;

设置中断向量表,将程序的入口地址保存在7ch表项中,使其称为中断7ch的中断例程。

安装7ch程序的代码:

  1. assume cs:code
  2. code segment
  3. start: mov ax,cs
  4. mov ds,ax
  5. mov si,offset sqr
  6. mov ax,0
  7. mov es,ax
  8. mov di,200h
  9. mov cx,offset sqrend-offset sqr
  10. cld
  11. rep movsb
  12. mov ax,0
  13. mov es,ax
  14. mov word ptr es:[7ch*4],200h
  15. mov word ptr es:[7ch*4+2],0
  16. mov ax,4c00h
  17. int 21h
  18. sqr: mul ax
  19. iret
  20. sqrend: nop
  21. code ends
  22. end start

2) 编写、安装中断7ch的中断例程,实现字母转换

功能,将一个全是字母,以0结尾的字符串,转化为大写。

参数,ds:si指向字符串的首地址。

比如,将data段中的字符串转化为大写。

  1. assume cs:code
  2. data segment
  3. db conhahvassembly’,0
  4. data ends
  5. code segment
  6. start: mov ax,data
  7. mov ds,ax
  8. mov si,0
  9. int 7ch
  10. mov ax,4c00h
  11. int 21h
  12. code ends
  13. end start

安装程序如下,

  1. assume cs:code
  2. code segment
  3. start: mov ax,cs
  4. mov ds,ax
  5. mov si,offset uppercase
  6. mov ax,0
  7. mov es,ax
  8. mov di,200h
  9. mov cx,offset uppercaseend-offset uppercase
  10. cld
  11. rep movsb
  12. mov ax,0
  13. mov es,ax
  14. mov word ptr es:[7ch*4],200h
  15. mov word ptr es:[7ch*4+2],0
  16. mov ax,4c00h
  17. int 21h
  18. uppercase: push cx
  19. push si
  20. change: mov cl,ds:[si]
  21. mov ch,0
  22. jcxz ok
  23. and byte ptr ds:[si],11011111b
  24. inc si
  25. jmp short change
  26. ok: pop si
  27. pop cx
  28. iret
  29. uppercaseend:
  30. nop
  31. code ends
  32. end start
  1. 对int、iret和栈的深入理解

用7ch中断例程完成loop指令的功能。

loop s的执行需要两个信息,循环次数和到s的位移。如果7ch中断例程要完成loop指令的功能,也需要这两个信息作为参数。用cx存放循环次数,用bx存放位移。

比如,编程,在屏幕中间显示80个‘!’。

  1. assume cs:code
  2. code segment
  3. start: mov ax,0b800h
  4. mov es,ax
  5. mov di,12*160
  6. mov bx,offset s-offset se
  7. mov cx,80
  8. s: mov byte ptr es:[di],’!’
  9. add di,2
  10. int 7ch
  11. se: nop
  12. mov ax,4c00h
  13. int 21h
  14. code ends
  15. 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。

  1. lp: push bp
  2. mov bp,sp
  3. dec cx
  4. jcxz lpret
  5. add [bp+2],bx
  6. lpret: pop bp
  7. iret

也可以不使用bp访问栈,而是直接出栈,修改后入栈。

  1. lp: pop ax
  2. dec cx
  3. jcxz lpret
  4. add ax,bx
  5. lpret: push ax
  6. iret

案例,用7ch中断例程完成jmp near ptr s指令的功能,用bx向中断例程转移位移。

比如,在屏幕的第15行,显示data段中以0结尾的字符串。

  1. assume cs:code
  2. data segment
  3. db conversation’,0
  4. data ends
  5. code segment
  6. start: mov ax,data
  7. mov ds,ax
  8. mov si,0
  9. mov ax,0b800h
  10. mov es,ax
  11. mov di,15*160
  12. mov bx,offset s-offset s1
  13. s: cmp byte ptr [si],0
  14. je s1
  15. mov al,ds:[si]
  16. mov es:[di],al
  17. inc si
  18. add di,2
  19. int 7ch
  20. s1: mov ax,4c00h
  21. int 21h
  22. code ends
  23. end start

7ch中断例程要做的事情就是将执行中断,压入栈中的ip的值加上bx的值就可以。然后使用iret返回段cx和修改后的ip。

  1. jmpto: pop ax
  2. add ax,bx
  3. push ax
  4. iret

或者使用bp

  1. jmpto: push bp
  2. mov bp,sp
  3. add [bp+2],bx
  4. pop bp
  5. iret
  1. BIOS和DOS所提供的中断例程

在系统的rom中存放着一套程序,称为BIOS(基本输入输出系统),BIOS主要包含以下内容:

硬件系统的检测和初始化程序;

外部中断和内部中断的中断例程;

用于硬件设备进行I/O操作的中断例程;

其他和硬件系统相关的中断例程。

操作系统DOS也提供了中断例程,从操作系统的角度看,dos的中断例程就是操作系统向程序员提供的编程资源。它们提供的中断程序中包含了许多子程序,这些子程序实现了很多功能。可以用int指令直接调用它们提供的中断例程,来完成某些工作。

和硬件相关的dos中断例程中,一般都是调用了bios的中断例程。

  1. BIOS和DOS中断例程的安装过程

BIOS和DOS提供的中断例程安装到内存中的过程:

开机后,cpu一入电,就初始化(cs)=0ffffh,(ip)=0,自动从ffff:0单元开始执行程序。ffff:0处有一条跳转指令,cpu执行这指令后,转去执行BIOS中的硬件系统检测和初始化程序。

初始化程序将建立BIOS所支持的中断向量,即将BIOS提供的中断例程,只需将入口地址登记在中断向量表中即可。它们是固化到rom中的程序,一直在内存中存在。

硬件系统检测和初始化完成后,调用int 19h进行操作系统的引导。从此将计算机交由操作系统控制。

Dos启动后,除完成其他工作外,还将它所提供的中断例程装入内存,并建立相应的中断向量。

  1. BIOS中断例程应用

int 10h中断例程是BIOS提供的中断例程,其中包含了多个和屏幕输出相关的子程序。一般,一个可以使用的中断例程包含多个子程序,中断例程内部用传递进来的参数来决定执行哪一个子程序。BIOS和DOS提供的中断例程,都用ah来传递内部子程序的编号。

比如,

  1. mov ah,2 ;置光标
  2. mov bh,0 ;第0
  3. mov dh,5 ;dh中放行号
  4. mov dl,12 ;dl中放列号
  5. int 21h

(ah)=2表示调用第10h号中断例程的2号子程序,功能是设置光标位置。光标的位置参数为80*25彩色字符模式的显示缓冲区中的位置。

比如,

  1. mov ah,9 ;在光标位置显示字符
  2. mov al,’a ;要显示的字符
  3. mov bl,7 ;颜色属性
  4. mov bh,0 ;第0
  5. mov cx,3 ;字符重复个数
  6. int 10h

(ah)=9表示调用第10h号中断例程的9号子程序,功能是在光标位置显示字符。

比如,使用BIOS的int 10h,实现在屏幕的8行12列显示4个红底高亮闪烁绿色的’c’。

  1. assume cs:code
  2. code segment
  3. start: mov ah,2
  4. mov bh,0
  5. mov dh,8
  6. mov dl,12
  7. int 10h
  8. mov ah,9
  9. mov al,’c
  10. mov bl,11001010b
  11. mov bh,0
  12. mov cx,3
  13. int 10h
  14. mov ax,4c00h
  15. int 21h
  16. code ends
  17. end start
  1. DOS中断例程应用

int 21h中断例程就是DOS提供的中断例程,其中包含了DOS提供给程序员在编程时调用的子程序。前述一直使用的是int21h中断例程的4ch号功能,如下,

  1. mov ah,4ch ;程序返回
  2. mov al,0 ;返回值
  3. 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 !”。

  1. assume cs:code
  2. data segment
  3. db hello assembly!’,’$
  4. data ends
  5. code segment
  6. start: mov ah,2
  7. mov bh,0
  8. mov dh,8
  9. mov dl,12
  10. int 10h
  11. mov ax,data
  12. mov ds,ax
  13. mov dx,0
  14. mov ah,9
  15. int 21h
  16. mov ax,4c00h
  17. int 21h
  18. code ends
  19. end start
  1. 案例

编写并安装int 7ch中断例程,功能为显示一个用0结束的字符串,中断例程安装在0:200处。

参数,(dh)=行号,(dl)=列号,(cl)=颜色,ds:si指向字符串首地址。

  1. assume cs:code
  2. code segment
  3. start: mov ax,cs
  4. mov ds,ax
  5. mov si,offset string
  6. mov ax,0
  7. mov es,ax
  8. mov di,200h
  9. mov cx,offset stringend-offset string
  10. cld
  11. rep movsb
  12. mov ax,0
  13. mov es,ax
  14. mov word ptr es:[7ch*4],200h
  15. mov word ptr es:[7ch*4+2],0
  16. mov ax,4c00h
  17. int 21h
  18. string: mov ax,0b800h
  19. mov es,ax
  20. mov ax,160
  21. mul dh
  22. add dl,dl
  23. mov dh,0
  24. add ax,dx
  25. mov di,ax
  26. s: mov al,ds:[si]
  27. cmp al,0
  28. je ok
  29. mov ah,cl
  30. mov es:[di],ax
  31. inc si
  32. add di,2
  33. jmp short s
  34. ok: iret
  35. stringend: nop
  36. code ends
  37. end start

测试安装的7ch

  1. assume cs:code
  2. data segment
  3. db hello assembly!”,0
  4. data ends
  5. code segment
  6. start: mov dh,10
  7. mov dl,10
  8. mov cl,2
  9. mov ax,data
  10. mov ds,ax
  11. mov si,0
  12. int 7ch
  13. mov ax,4c00h
  14. int 21h
  15. code ends
  16. 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端口写入一个字节

  1. 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号单元的内容

  1. assume cs:code
  2. code segment
  3. start: mov al,2
  4. out 70h,al
  5. in al,71h
  6. mov ax,4c00h
  7. int 21h
  8. code ends
  9. end start

编程,向CMOS RAM的2号单元写入0。

  1. assume cs:code
  2. code segment
  3. start: mov al,2
  4. out 70h,al
  5. mov al,0
  6. out 71h,al
  7. mov ax,4c00h
  8. int 21h
  9. code ends
  10. end start
  1. 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中。比如,指令:

  1. mov al,01010001b
  2. mov cl,3
  3. shl al,cl

执行后,(al)=10001000b,因为最后移出的一位是0,所以cf=0。

将x逻辑左移一位,相当于执行x=x*2。

比如,

  1. Mov al,00000001b ;执行后(al)=00000001b=1
  2. Shl al,1 ;执行后(al)=00000010b=2
  3. Shl al,1 ;执行后(al)=00000100b=4
  4. Shl al,1 ;执行后(al)=00001000b=8
  5. Mov cl,3
  6. 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中。

比如,指令:

  1. mov al,01010001b
  2. mov cl,3
  3. shr al,cl

执行后(al)=00001010b,因为最后移出的一位是0,所以cf=0。

将x逻辑右移一位,相当于执行x=x/2。

3) 案例,编程用加法和移位指令计算(ax)=(ax)*10。

  1. assume cs:code
  2. code segment
  3. start: mov bx,ax
  4. shl bx,1
  5. mov cl,3
  6. shl ax,cl
  7. add ax,bx
  8. mov dx,0
  9. adc dx,0
  10. mov ax,4c00h
  11. int 21h
  12. code ends
  13. end start

或者

  1. assume cs:code
  2. code segment
  3. start: shl ax,1
  4. mov bx,ax
  5. mov cl,2
  6. shl ax,cl
  7. add ax,bx
  8. mov dx,0
  9. adc dx,0
  10. mov ax,4c00h
  11. int 21h
  12. code ends
  13. end start
  1. 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中为月份的个位数码值

完整代码:

  1. assume cs:code
  2. code segment
  3. start: mov ax,0b800h
  4. mov ds,ax
  5. mov si,12*160+4
  6. mov al,8
  7. out 70h,al
  8. in al,71h
  9. mov ah,al
  10. mov cl,4
  11. shr ah,cl
  12. add ah,30h
  13. and al,00001111b
  14. add al,30h
  15. mov ch,2
  16. mov cl,ah
  17. mov [si],cx
  18. mov cl,al
  19. mov [si+2],cx
  20. mov ax,4c00h
  21. int 21h
  22. code ends
  23. end start
  1. 综合案例

编程,以“年/月/日 时:分:秒”的格式,显示当前的日期、时间。

  1. assume cs:code
  2. data segment
  3. db 9,8,7,4,2,0
  4. db " / / : : "
  5. data ends
  6. code segment
  7. start: mov ax,0b800h
  8. mov es,ax
  9. mov di,12*160+4
  10. mov ax,data
  11. mov ds,ax
  12. mov si,0
  13. mov bx,6
  14. mov cx,6
  15. s: push cx
  16. mov al,[si]
  17. out 70h,al ;从CMOS RAM中取出时间
  18. in al,71h
  19. mov ah,al
  20. mov cl,4
  21. shr ah,cl
  22. add ah,30h
  23. and al,00001111b
  24. add al,30h
  25. mov dh,al
  26. mov dl,ah
  27. mov [bx],dx
  28. inc si
  29. add bx,3
  30. pop cx
  31. loop s
  32. mov cx,17
  33. mov si,6
  34. s1: mov al,[si]
  35. mov ah,02h
  36. mov es:[di],ax
  37. inc si
  38. add di,2
  39. loop s1
  40. mov ax,4c00h
  41. int 21h
  42. code ends
  43. end start

也可以使用,int 21h显示字符串,要以$结尾。

  1. assume cs:code
  2. data segment
  3. table db 9,8,7,4,2,0
  4. time db " / / : : $"
  5. data ends
  6. code segment
  7. start: mov ax,0b800h
  8. mov es,ax
  9. mov di,12*160+4
  10. mov ax,data
  11. mov ds,ax
  12. mov si,offset table
  13. mov bx,offset time
  14. mov cx,6
  15. s: push cx
  16. mov al,[si]
  17. out 70h,al ;从CMOS RAM中取出时间
  18. in al,71h
  19. mov ah,al
  20. mov cl,4
  21. shr ah,cl
  22. add ah,30h
  23. and al,00001111b
  24. add al,30h
  25. mov es:[bx],ah
  26. mov es:[bx+1],al
  27. inc si
  28. add bx,3
  29. pop cx
  30. loop s
  31. mov ah,0
  32. mov bh,0
  33. mov dh,10 ;将光标置于104
  34. mov dl,4
  35. int 10h
  36. mov dx,offset time
  37. mov ah,9 ;显示字符串
  38. int 21h
  39. mov ax,4c00h
  40. int 21h
  41. code ends
  42. end start

十六、 外中断

Cpu在计算机系统中,除了能够执行指令,进行运算以外,还应该能够对外部设备进行控制,接收它们的输入,向它们进行输出。Cpu除了有运算能力外,还要有I/O(input/output)能力。比如,按下键盘上的一个键,cpu最终要能够处理这个键。在使用文本编辑器时,按下b,屏幕上出现b。

Cpu要处理外设的输入,要处理两个问题,外设的输入随时可能发生,cpu怎么知道发生了;cpu从哪里得到外设的输入。

  1. 接口芯片和端口

外设的输入不直接送入内存和cpu,而是送入相关的接口芯片的端口中,cpu向外设的输出也不是直接送入外设,而是先送入端口中,再由相关的芯片送到外设。Cpu还可以向外设输出控制命令,而这些控制命令也是先送到相关芯片的端口中,然后再由相关的芯片根据命令对外设进行控制。

Cpu通过端口和外部设备进行联系。

  1. 外中断信息

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的中断信息。

  1. 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表示处于删除态;

  1. 编写int 9中断例程

键盘输入的处理过程:键盘产生扫描码;扫描码送入60h端口;引发9号中断;cpu执行int 9中断例程处理键盘输入。

这个过程中,第1、2、3步都是由硬件系统完成的。能改的只有int 9中断处理程序。可以重新编写int 9中断例程,按照自己的意图来处理键盘的输入。

案例,在屏幕中间一次显示“a”~“z”,并可以让人看清。在显示的过程中,按下esc键,改变显示的颜色。

首先,依次显示“a”~“z”的程序。

  1. assume cs:code
  2. code segment
  3. start: mov ax,0b800h
  4. mov es,ax
  5. mov ah,’a
  6. s: mov es:[160*12+40*2],ah
  7. inc ah
  8. cmp ah,’z
  9. jna s
  10. mov ax,4c00h
  11. int 21h
  12. code ends
  13. end start

这个程序的执行过程中,人们是无法看全屏幕上的显示的,因为一个字母刚显示到屏幕上,cpu执行后,就编程另一个字母,字母间切换太快,无法看清。应该在每显示一个字母后,延时一段时间,看清后,再显示下一个字母。可以让cpu执行一段时间的空循环。循环的次数一定要大,用两个16位寄存器来存放32位的循环次数。如,

  1. mov dx,10h
  2. mov ax,0
  3. s:
  4. sub ax,1
  5. sbb bx,0
  6. cmp ax,0
  7. jne s
  8. cmp dx,0
  9. jne s

这个循环了100000h次。

也可以这样设计一个循环。

  1. mov dx,1000h ;循环2000000h
  2. s2:mov ax,200h
  3. s1:
  4. sub ax,1
  5. cmp ax,0
  6. jne s1
  7. sub dx,1
  8. cmp dx,0
  9. jne s2

我们可以将循环延时的程序段写为一个子程序。

将上述程序改进如下,

  1. assume cs:code
  2. code segment
  3. start: mov ax,0b800h
  4. mov es,ax
  5. mov ah,’a
  6. s: mov es:[160*12+40*2],ah
  7. call delay
  8. inc ah
  9. cmp ah,’z
  10. jna s
  11. mov ax,4c00h
  12. int 21h
  13. delay: push ax
  14. push dx
  15. mov dx,1000h ;循环10000000h次,可以根据实际调节
  16. mov ax,0
  17. s1:
  18. sub ax,1
  19. sbb dx,0
  20. cmp ax,0
  21. jne s1
  22. cmp dx,0
  23. jne s1
  24. pop dx
  25. pop ax
  26. ret
  27. code ends
  28. 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中断例程的入口地址恢复为原来的地址。

最终,程序为:

  1. assume cs:code
  2. stack segment
  3. db 128 dup (0)
  4. stack ends
  5. data segment
  6. dw 0,0
  7. data ends
  8. code segment
  9. start: mov ax,stack
  10. mov ss,ax
  11. mov sp,128
  12. mov ax,data
  13. mov ds,ax
  14. mov ax,0
  15. mov es,ax
  16. push es:[9*4]
  17. pop ds:[0]
  18. push es:[9*4+2]
  19. pop ds:[2] ;将原来的int 9中断例程的入口地址保存在ds:0ds:2单元中
  20. mov word ptr es:[9*4],offset int9
  21. mov es:[9*4+2],cs ;在中断向量表中设置新的int 9中断例程的入口地址
  22. mov ax,0b800h
  23. mov es,ax
  24. mov ah,'a'
  25. s: mov es:[160*12+40*2],ah
  26. call delay
  27. inc ah
  28. cmp ah,'z'
  29. jna s
  30. mov ax,0
  31. mov es,ax
  32. push ds:[0]
  33. pop es:[9*4]
  34. push ds:[2]
  35. pop es:[9*4+2] ;将中断向量表中int 9中断例程的入口恢复为原来的地址
  36. mov ax,4c00h
  37. int 21h
  38. delay: push ax
  39. push dx
  40. mov dx,1000h
  41. s2: mov ax,200h
  42. s3: dec ax
  43. cmp ax,0
  44. jne s3
  45. dec dx
  46. cmp dx,0
  47. jne s2
  48. pop dx
  49. pop ax
  50. ret
  51. ;--------------新的int 9中断例程---------------
  52. int9: push ax
  53. push bx
  54. push es
  55. in al,60h
  56. pushf
  57. pushf
  58. pop bx
  59. and bh,11111100b
  60. push bx
  61. popf
  62. call dword ptr ds:[0] ;模拟int 9调用原来的int 9中断例程
  63. cmp al,1
  64. jne int9ret
  65. mov ax,0b800h
  66. mov es,ax
  67. inc byte ptr es:[160*12+40*2+1] ;改变属性值
  68. int9ret: pop es
  69. pop bx
  70. pop ax
  71. iret
  72. code ends
  73. end start
  1. 安装新的int 9中断例程

安装一个新的int 9中断例程。功能是在dos下,按下F1键后改变当前屏幕的显示颜色,其他键照常处理。

这个程序主要包括以下工作:

改变屏幕颜色,就是改变从b800h开始的4000个字节中所有奇地址单元中的内容,当前屏幕的显示颜色即发生改变。

其他键照常处理,可以调用原来int 9中断例程,来处理。

原int 9中断例程入口地址的保存。不能保存在安装程序中,因为安装程序返回后地址将丢失,可以保存在0:200单元处。

新的int 9中断例程的安装,可以将新的int 9中断例程安装在0:204处。

  1. assume cs:code
  2. stack segment
  3. db 128 dup (0)
  4. stack ends
  5. code segment
  6. start: mov ax,stack
  7. mov ss,ax
  8. mov sp,128
  9. push cs
  10. pop ds
  11. mov ax,0
  12. mov es,ax
  13. mov si,offset int9
  14. mov di,204h
  15. mov cx,offset int9end-offset int9
  16. cld
  17. rep movsb
  18. push es:[9*4]
  19. pop es:[200h]
  20. push es:[9*4+2]
  21. pop es:[202h]
  22. mov word ptr es:[9*4],204h
  23. mov word ptr es:[9*4+2],0
  24. mov ax,4c00h
  25. int 21h
  26. int9: push ax
  27. push bx
  28. push cx
  29. push es
  30. in al,60h
  31. pushf
  32. call dword ptr cs:[200h] ;当此中断例程执行时(cs)=0
  33. cmp al,3bh
  34. jne int9ret
  35. mov ax,0b800h
  36. mov es,ax
  37. mov bx,1
  38. mov cx,2000
  39. s: inc byte ptr es:[bx]
  40. add bx,2
  41. loop s
  42. int9ret: pop es
  43. pop cx
  44. pop bx
  45. pop ax
  46. iret
  47. int9end: nop
  48. code ends
  49. end start
  1. 综合案例

安装一个新的int 9中断例程,功能,在dos下,按下“A”键后,除非不再松开,如果松开,就显示满屏幕的“A”;其他的键照常处理。

  1. assume cs:code
  2. stack segment
  3. db 128 dup (0)
  4. stack ends
  5. code segment
  6. start: mov ax,stack
  7. mov ss,ax
  8. mov sp,128
  9. push cs
  10. pop ds
  11. mov ax,0
  12. mov es,ax
  13. mov si,offset int9
  14. mov di,204h
  15. mov cx,offset int9end-offset int9
  16. cld
  17. rep movsb
  18. push es:[9*4]
  19. pop es:[200h]
  20. push es:[9*4+2]
  21. pop es:[202h]
  22. mov word ptr es:[9*4],204h
  23. mov word ptr es:[9*4+2],0
  24. mov ax,4c00h
  25. int 21h
  26. int9: push ax
  27. push bx
  28. push cx
  29. push es
  30. in al,60h
  31. pushf
  32. call dword ptr cs:[200h] ;当此中断例程执行时(cs)=0
  33. cmp al,9eh
  34. jne int9ret
  35. mov ax,0b800h
  36. mov es,ax
  37. mov bx,0
  38. mov cx,2000
  39. s: mov byte ptr es:[bx],”A
  40. add bx,2
  41. loop s
  42. int9ret: pop es
  43. pop cx
  44. pop bx
  45. pop ax
  46. iret
  47. int9end: nop
  48. code ends
  49. end start

十七、 直接定址表

  1. 描述单元长度的标号

一般,在代码段中,标号用来标记指令、数据、段的起始地址。比如,

  1. assume cs:code
  2. code segment
  3. a: db 1,2,3,4,5,6,7,8
  4. b: dw 0
  5. start: mov si,offset a
  6. mov bx,offset b
  7. mov cx,8
  8. s: mov al,cs:[si]
  9. mov ah,0
  10. add cs:[bx],ax
  11. inc si
  12. loop s
  13. mov ax,4c00h
  14. int 21h
  15. code ends
  16. end start

程序中,code、a、b、start、s都是标号。这些标号仅仅表示了内存单元的地址。还有一种标号,不但可以表示内存单元的地址,还表示了内存单元的长度,也就是表示在此处标号处的单元,是一个字节单元,还是字单元,还是双字单元。比如,

  1. assume cs:code
  2. code segment
  3. a db 1,2,3,4,5,6,7,8
  4. b dw 0
  5. start: mov si,0
  6. mov cx,8
  7. s: mov al,a[si]
  8. mov ah,0
  9. add b,ax
  10. inc si
  11. loop s
  12. mov ax,4c00h
  13. int 21h
  14. code ends
  15. 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]。

这种标号一般称为数据标号,它标记了存储数据的单元的地址和长度。

  1. 在其他段中使用数据标号

一般,不在代码段中定义数据,而将数据定义到其他段中。也可以使用数据标号来描述存储数据的单元的地址和长度。

在后面加有“:”的地址标号,只能在代码段中使用,不能在其他段中使用。

如下的程序将data段中a标号处的8个数据累加,结果存储到b标号处的字中。

  1. assume cs:code,ds:data
  2. data segment
  3. a db 1,2,3,4,5,6,7,8
  4. b dw 0
  5. data ends
  6. code segment
  7. start: mov ax,data
  8. mov ds,ax
  9. mov si,0
  10. mov cx,8
  11. s: mov al,a[si]
  12. mov ah,0
  13. add b,ax
  14. inc si
  15. loop s
  16. mov ax,4c00h
  17. int 21h
  18. code ends
  19. end start

如果在代码段中直接使用数据标号访问数据,需要用伪指令assume将标号所在的段和一个段寄存器联系起来。在程序中还要使用指令对段寄存器进行设置。

比如,指令mov al,a[si]编译为mov al,[si+0]。指令add b,ax编译为add [8],ax。

还可以将标号当作数据来定义,此时,编译器将标号所表示的地址当作数据的值。

比如,

  1. data segment
  2. a db 1,2,3,4,5,6,7,8
  3. b dw 0
  4. c dw a,b
  5. data ends

数据标号c处存储的两个字型数据为标号a、b的偏移地址,相当于:

  1. data segment
  2. a db 1,2,3,4,5,6,7,8
  3. b dw 0
  4. c dw offset a,offset b
  5. data ends

比如,

  1. data segment
  2. a db 1,2,3,4,5,6,7,8
  3. b dw 0
  4. c dd a,b
  5. data ends

数据标号c处存储的两个双字型数据为标号a的偏移地址和段地址、标号b的偏移地址和段地址,相当于:

  1. data segment
  2. a db 1,2,3,4,5,6,7,8
  3. b dw 0
  4. c dw offset a,seg a,offset b,seg b
  5. data ends

seg的功能是取得某一个标号的段地址。

  1. 直接定址表

要求,编写子程序,以十六进制的形式在屏幕中间显示给定的字节型数据。

一个字节需要用两个十六进制数码来表示,子程序要在屏幕上显示两个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可直接查找到对应的字符。子程序如下:

  1. ;名称,showbyte
  2. ;功能,将一个字节数据用十六进制显示在屏幕中间
  3. ;参数,(al)=要显示的数据
  4. ;返回,将十六进制显示在屏幕中间
  5. showbyte: jmp short show
  6. table db 0123456789abcdef
  7. show: push bx
  8. push es
  9. mov ah,al
  10. mov cl,4
  11. shr ah,cl
  12. and al,00001111b ;右移4位,ah中得到高4位的值,al中逻辑与高4位为0,得到低4位的值
  13. mov bl,ah
  14. mov bh,0
  15. mov ah,table[bx] ;用高4位的值作为相对于table的偏移,取得对应的字符
  16. mov bx,0b800h
  17. mov es,bx
  18. mov es:[160*12+40*2],ah
  19. mov bl,al
  20. mov bh,0
  21. mov al,table[bx] ;用低4位的值作为相对于table的偏移,取得对应的字符
  22. mov es:[160*12+40*2+2],al
  23. pop es
  24. pop bx
  25. ret

利用表,在两个数据集合之间建立一种映射关系,使得可以用查表的方法根据给出的数据得到其在另一集合中的对应数据。目的有以下3个,

为了算法的清晰和简洁;为了加快运算速度;为了使程序易于扩充。

比如,为了加快运算速度的例子,编写一个子程序,计算sin(x),x∈{0°,30°,60°,90°,120°,150°,180°},并在屏幕中间显示计算结果,比如sin(30)的结果显示为“0.5”。

可以使用麦克劳林公式来计算sin(x),但是其中涉及多次乘法和除法,乘除法是费时的,执行时间大约是加法、比较指令的5倍。如果不使用乘除法,通过观察x等于集合中角度时的sin的值,可以建立一张角度值和sin值的表的子程序。如下:

  1. ;名称,showsin
  2. ;功能,根据由ax传送来的角度值,在屏幕显示对应sin
  3. ;参数,(ax)=角度
  4. ;返回,无
  5. showsin: jmp short show
  6. table dw ag0,ag30,ag60,ag90,ag120,ag150,ag180
  7. ag0 db 0’,0
  8. ag30 db 0.5’,0
  9. ag60 db 0.866’,0
  10. ag90 db 1’,0
  11. ag120 db 0.866’,0
  12. ag150 db 0.5’,0
  13. ag180 db 0’,0
  14. show: push bx
  15. push es
  16. push si
  17. mov bx,0b800h
  18. mov es,bx
  19. mov ah,0
  20. mov bl,30
  21. div bl
  22. mov bl,al
  23. mov bh,0
  24. add bx,bx ;用dw存放偏移地址,所以bx要乘以2
  25. mov bx,table[bx]
  26. mov si,160*12+40*2
  27. shows: mov ah,cs:[bx]
  28. cmp ah,0
  29. je showret
  30. mov es:[si],ah
  31. inc bx
  32. add si,2
  33. jmp short shows
  34. showret: pop si
  35. pop es
  36. pop bx
  37. ret

上述通过依据数据,直接计算出所要找的元素的位置的表,称为直接定址表。

  1. 程序入口地址的直接定址表

可以在直接定址表中存储子程序的地址,从而方便地实现不同子程序的调用。比如,实现一个子程序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个子程序。

  1. ;名称,cls
  2. ;功能,清屏
  3. ;参数,无
  4. cls: push bx
  5. push cx
  6. push es
  7. mov bx,0b800h
  8. mov es,bx
  9. mov bx,0
  10. mov cx,2000
  11. clslp: mov byte ptr es:[bx],’
  12. add bx,2
  13. loop clslp
  14. pop es
  15. pop cx
  16. pop bx
  17. ret
  18. ;名称,foreg
  19. ;功能,设置前景色
  20. ;参数,(al)=颜色值
  21. foreg: push bx
  22. push cx
  23. push es
  24. mov bx,0b800h
  25. mov es,bx
  26. mov bx,1
  27. mov cx,2000
  28. foreglp: and byte ptr es:[bx],11111000b
  29. or es:[bx],al
  30. add bx,2
  31. loop foreglp
  32. pop es
  33. pop cx
  34. pop bx
  35. ret
  36. ;名称,backg
  37. ;功能,设置背景色
  38. ;参数,(al)=颜色值
  39. backg: push bx
  40. push cx
  41. push es
  42. mov cl,4
  43. shl al,cl
  44. mov bx,0b800h
  45. mov es,bx
  46. mov bx,1
  47. mov cx,2000
  48. backglp:
  49. and byte ptr es:[bx],10001111b
  50. or es:[bx],al
  51. add bx,2
  52. loop backglp
  53. pop es
  54. pop cx
  55. pop bx
  56. ret
  57. ;名称,scroll
  58. ;功能,向上滚一行
  59. ;参数,无
  60. scroll: push cx
  61. push si
  62. push di
  63. push es
  64. push ds
  65. mov si,0b800h
  66. mov es,si
  67. mov ds,si
  68. mov si,160
  69. mov di,0
  70. cld
  71. mov cx,24
  72. scrollcope:
  73. push cx
  74. mov cx,160
  75. rep movsb
  76. pop cx
  77. loop scrollcope
  78. mov cx,80
  79. mov si,0
  80. clearend:
  81. mov byte ptr [160*24+si],’
  82. add si,2
  83. loop clearend
  84. pop ds
  85. pop es
  86. pop di
  87. pop si
  88. pop cx
  89. ret

可以将这些功能子程序的入口地址存储在一个表中,它们在表中的位置和功能号相对应。对应关系:功能号*2=对应的功能子程序在地址表中的偏移,如下:

  1. setscreen: jmp short set
  2. table dw cls,foreg,backg,scroll
  3. set: push bx
  4. cmp ah,3 ;判断功能号是否大于3
  5. ja sret
  6. mov bl,ah
  7. mov bh,0
  8. add bx,bx
  9. call word ptr table[bx]
  10. sret: pop bx
  11. ret

也可以将子程序setscreen使用比较值的方式实现,如下:

  1. setscreen: cmp ah,0
  2. je do1
  3. cmp ah,1
  4. je do2
  5. cmp ah,2
  6. je do3
  7. cmp ah,3
  8. je do4
  9. jmp short sret
  10. do1: call cls
  11. jmp short sret
  12. do2: call foreg
  13. jmp short sret
  14. do3: call backg
  15. jmp short sret
  16. do4: call scroll
  17. 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

 

 

 

 

 

 

 

 

 

  1. 使用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”,将屏幕上的字符设置为蓝色。

  1. assume cs:code
  2. code segment
  3. start: mov ah,0
  4. int 16h
  5. mov ah,1
  6. cmp al,’r
  7. je red
  8. cmp al,’g
  9. je green
  10. cmp al,’b
  11. je blue
  12. jmp short sret
  13. red: shl ah,1
  14. green: shl ah,1
  15. blue: mov bx,0b800h
  16. mov es,bx
  17. mov bx,1
  18. mov cx,2000
  19. s: and byte ptr es:[bx],11111000b
  20. or es:[bx],ah
  21. add bx,2
  22. loop s
  23. sret: mov ax,4c00h
  24. int 21h
  25. code ends
  26. end start
  1. 字符串的输入

用户通过键盘输入的通常不仅仅是单个字符而是字符串。最基本的字符串输入程序,需要具备下面的功能。

在输入的同时需要显示这个字符串;

一般在输入回车符后,字符串输入结束;

能够删除已经输入的字符。

编写一个接收字符串输入的子程序,实现上面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,返回。

将这个程序过程,写为子程序。

  1. ;名称,charstack
  2. ;功能,字符栈的入栈、出栈和显示
  3. ;参数,(ah)=功能号,0表示入栈,1表示出栈,2表示显示,
  4. ;ds:si指向字符栈空间;
  5. ;对于0号功能,(al)=入栈字符;
  6. ;对于1号功能,(al)=返回字符;
  7. ;对于2号字符,(dh)、(dl)=字符串在屏幕上显示的行、列位置。
  8. charstack: jmp short charstart
  9. table dw charpush,charpop,charshow
  10. top dw 0
  11. charstart: push bx
  12. push dx
  13. push di
  14. push es
  15. cmp ah,2
  16. ja sret
  17. mov bl,ah
  18. mov bh,0
  19. add bx,bx
  20. jmp word ptr table[bx]
  21. charpush: mov bx,top ;将字符的总数传递给bx保存
  22. mov [si][bx],al ;同时bx也表示栈顶的字符的偏移地址
  23. inc top ;top位置的数递增,计算当前字符栈中字符的总数
  24. jmp sret
  25. charpop: cmp top,0 ;查看字符栈中的总数是否为0,为0则退出
  26. je sret
  27. dec top ;不为0,修改栈中的字符总数减1
  28. mov bx,top ;将弹出字符后,栈中字符总数保存在bx
  29. mov al,[si][bx]
  30. jmp sret
  31. charshow: mov bx,0b800h
  32. mov es,bx
  33. mov al,160
  34. mov ah,0
  35. mul dh
  36. mov di,ax
  37. add dl,dl
  38. mov dh,0
  39. add di,dx
  40. mov bx,0
  41. charshows: cmp bx,top ;起初bx中的值为0,将top中存储的总数与bx比较,直到总数与bx值相等,则说明显示字符完毕
  42. jne noempty
  43. mov byte ptr es:[di],’
  44. jmp sret
  45. noempty: mov al,[si][bx]
  46. mov es:[di],al
  47. mov byte ptr es:[di+2],’
  48. inc bx ;依次显示字符,并统计显示的个数,以便和top的总数比较,找到结束的位置
  49. add di,2
  50. jmp charshows
  51. sret: pop es
  52. pop di
  53. pop dx
  54. pop bx
  55. ret

完整的接收字符串的子程序

  1. getstr: push ax
  2. getstrs: mov ah,0
  3. int 16h
  4. cmp al,20h
  5. jb nochar
  6. mov ah,0
  7. call charstack
  8. mov ah,2
  9. call charstack
  10. jmp getstrs
  11. nochar: cmp ah,0eh
  12. je backspace
  13. cmp ah,1ch
  14. je enter
  15. cmp ah,01h
  16. je quit
  17. jmp getstrs
  18. quit: mov ax,4c00h
  19. int 21h
  20. backspace: mov ah,1
  21. call charstack
  22. mov ah,2
  23. call charstack
  24. jmp getstrs
  25. enter: mov al,0
  26. mov ah,0
  27. call charstack
  28. mov ah,2
  29. call charstack
  30. pop ax
  31. ret
  1. 应用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的程序如下,

  1. mov ax,0
  2. mov es,ax
  3. mov bx,200h
  4. mov al,1
  5. mov ch,0
  6. mov cl,1
  7. mov dl,0
  8. mov dh,0
  9. mov ah,2
  10. 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扇区

  1. mov ax,0
  2. mov es,ax
  3. mov bx,200h
  4. mov al,1
  5. mov ch,0
  6. mov cl,1
  7. mov dl,0
  8. mov dh,0
  9. mov ah,3
  10. 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扇区存储显存中的内容。

  1. assume cs:code
  2. code segment
  3. start: mov ax,0b800h
  4. mov es,ax
  5. mov bx,0
  6. mov al,8
  7. mov ch,0
  8. mov cl,1
  9. mov dl,0
  10. mov dh,0
  11. mov ah,3
  12. int 13h
  13. mov ax,4c00h
  14. int 21h
  15. code ends
  16. end start
  1. 综合案例

开机后,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后可更改当前的日期、时间,更改后返回到主选单。

  1. assume cs:code
  2. code segment
  3. ;=========================================================
  4. ;功能,将代码写入001扇区
  5. ;入口参数:
  6. ;(ah)=int 13h的功能号(2表示读扇区,3表示写扇区)
  7. ;(al)=写入的扇区数
  8. ;(ch)=磁道号
  9. ;(cl)=扇区号
  10. ;(dh)=磁头号(面号)
  11. ;(dl)=驱动器号
  12. ;es:bx指向将写入磁盘的数据或指向接收从扇区读入数据的内存区
  13. ;返回参数
  14. ;操作成功,(ah)=0,(al)=写入的扇区数
  15. ;操作失败,(ah)=出错代码
  16. ;==========================================================
  17. start: mov ax,floppyend-floppy
  18. mov dx,0
  19. mov bx,512
  20. div bx ;商ax为所需的扇区数
  21. inc al ;写入的扇区数
  22. push cs
  23. pop es
  24. mov bx,offset floppy ;es:bx指向要被写入的内存单元
  25. mov ch,0 ;磁道号
  26. mov cl,1 ;扇区号
  27. mov dl,0 ;驱动器号,软盘a
  28. mov dh,0 ;磁头号(面号)
  29. mov ah,3 ;int 13h的功能号(3表示写扇区)
  30. int 13h ;将代码写入001扇区
  31. mov ax,4c00h
  32. int 21h
  33. floppy: jmp read
  34. ;直接定址表
  35. table dw function1-floppy,function2-floppy
  36. dw function3-floppy,function4-floppy
  37. menu db ‘***main menu***’,0
  38. db 1) reset pc ‘,0
  39. db 2) start system‘,0
  40. db 3) clock ‘,0
  41. db 4) set clock ‘,0
  42. db please choose ‘,0
  43. time db yy/mm/dd hh:mm:ss’,0
  44. cmos db 9,8,7,4,2,0
  45. hint db press f1 to change the color,press esc toreturn’,0
  46. hint1 db please input: yy/mm/dd hh:mm:ss’,0
  47. char db / / : : ‘,0
  48. ;================================================================
  49. ;功能,将002扇区的内容读入0:7e00h
  50. ;入口参数:
  51. ;(ah)=int 13h的功能号(2表示读扇区,3表示写扇区)
  52. ;(al)=写入的扇区数
  53. ;(ch)=磁道号
  54. ;(cl)=扇区号
  55. ;(dh)=磁头号(面号)
  56. ;(dl)=驱动器号
  57. ;es:bx指向将写入磁盘的数据
  58. ;返回参数
  59. ;操作成功,(ah)=0,(al)=写入的扇区数
  60. ;操作失败,(ah)=出错代码
  61. ;==========================================================
  62. start: mov ax,floppyend-floppy
  63. mov dx,0
  64. mov bx,512
  65. div bx ;商ax为所需的扇区数
  66. inc al ;写入的扇区数
  67. mov bx,0
  68. mov es,bx
  69. mov bx,7e00h ;es:bx指向要被读入的内存单元
  70. mov ch,0 ;磁道号
  71. mov cl,2 ;扇区号
  72. mov dl,0 ;驱动器号,软盘a
  73. mov dh,0 ;磁头号(面号)
  74. mov ah,2 ;int 13h的功能号(2表示读扇区)
  75. int 13h ;读取002扇区的内容到0:7e00h
  76. mov ax,7c0h
  77. push ax
  78. mov ax, showmenu-floppy
  79. push ax
  80. retf
  81. ;=================================================
  82. ;显示主菜单,调用show_strclean子程序
  83. ;==================================================
  84. showmenu: call clean ;清屏
  85. push cs
  86. pop ds
  87. mov si,menu-floppy
  88. mov dh,8
  89. mov dl,30
  90. mov cx,6
  91. showmenu0:
  92. push cx
  93. mov cl,2
  94. call show_str
  95. add si,16
  96. inc dh
  97. pop cx
  98. loopshowmenu0
  99. ;==========================================================
  100. ;接收键盘输入,跳转相应功能程序段
  101. ;调用bios用来提供读取键盘缓冲区功能的int 16h中断例程
  102. ;将读取的扫描码送入ahascii码送入al
  103. ;==========================================================
  104. go: mov ax,0
  105. int 16h
  106. cmp al,’1
  107. jb showmenu
  108. cmp al,’4
  109. ja showmenu
  110. sub al,31h
  111. mov bl,al
  112. mov bh,0
  113. add bx,bx
  114. add bx,3 ;计算相应子程序在table中的位移
  115. call word ptr cs:[bx]
  116. jmp showmenu
  117. ;=================================================================
  118. ;功能1:重启计算机
  119. ;=================================================================
  120. function1: mov ax,0ffffh
  121. push ax
  122. mov ax,0
  123. push ax
  124. ref ;jmp ffff:0
  125. ;==================================================================
  126. ;功能3:进入时钟程序
  127. ;==================================================================
  128. function3: push ax
  129. push bx
  130. push cx
  131. push dx
  132. push si
  133. push ds
  134. push es
  135. call clean
  136. mov dh,0
  137. mov dl,0
  138. mov cl,2
  139. mov si,offset hint-floppy
  140. call show_str
  141. ;=================================================================
  142. ;名称:clock
  143. ;功能:动态显示当前日期、时间
  144. ;==================================================================
  145. mov cx,2
  146. clock: mov bx,offset cmos-floppy
  147. mov si,offset time-floppy
  148. push cx
  149. push cx,6
  150. clock0: push cx
  151. mov al,[bx]
  152. out 70h,al
  153. int al,71h
  154. mov ah,al
  155. shr al,cl
  156. add ah,0fh
  157. add ax,3030h
  158. mov [si],ax
  159. inc bx
  160. add si,3
  161. pop cx
  162. loop clock0
  163. ;=================================================================
  164. ;按下f1键后,改变显示颜色
  165. ;按下esc键后,返回主菜单,其他键照常处理
  166. ;=================================================================
  167. mov al,0
  168. in al,60h
  169. pop cx
  170. cmp al,3bh
  171. je colour
  172. cmp al,1
  173. je clockend
  174. jmp show_clock
  175. col_1: mov cx,1 ;cl∈[1,7]
  176. jmp show_clock
  177. colour: cmp cx,7
  178. je col_1
  179. inc cx
  180. show_clock: mov dh,12
  181. mov dl,30
  182. mov si,offset time-floppy
  183. call show_str
  184. jmp clock
  185. clockend: pop es
  186. pop ds
  187. pop si
  188. pop dx
  189. pop cx
  190. pop bx
  191. pop ax
  192. ret
  193. ;=================================================================
  194. ;功能4:设置时间
  195. ;=================================================================
  196. function4: push ax
  197. push bx
  198. push cx
  199. push dx
  200. push si
  201. call clean
  202. mov dh,8
  203. mov dl,30
  204. mov si,offset hint1-floppy
  205. call show_str
  206. add dh,1
  207. add dl,14
  208. mov si,offset char-floppy
  209. call show_str
  210. mov di,0
  211. call getstrs
  212. call witein
  213. call cleanchar
  214. pop si
  215. pop dx
  216. pop cx
  217. pop bx
  218. pop ax
  219. ret
  220. ;=================================================================
  221. ;清除char内输入数据,还原环境
  222. ;=================================================================
  223. cleanchar: push cx
  224. cleanchar1: mov cx,di
  225. jcxz cleanchar2
  226. call charpop
  227. jmp cleanchar1
  228. cleanchar2: pop cx
  229. ret
  230. ;==================================================================
  231. ;ascii=>bcd,写入cmos
  232. ;==================================================================
  233. witein: push si
  234. mov cx,6
  235. mov bx,offset cmos-floppy
  236. wite: push cx
  237. mov al,[bx]
  238. out 70h,al
  239. mov ax,[si]
  240. sub ah,30h
  241. sub al,30h
  242. mov cl,4
  243. shl al,cl
  244. add al,ah
  245. out 71h,al
  246. add si,3
  247. inc bx
  248. pop cx
  249. loop wite
  250. pop si
  251. ret
  252. ;=================================================================
  253. ;子程序,接收数字输入
  254. ;参数说明,di=char栈顶(字符地址,个数记录器)
  255. ;==================================================================
  256. getstrs: push ax
  257. push bx
  258. getstr: mov ax,0
  259. int 16h
  260. cmp ah,0eh
  261. je backspace
  262. cmp ah,1ch
  263. je enter1
  264. cmp al,’0
  265. jb getstr
  266. cmp al,’9
  267. ja getstr
  268. cmp di,16
  269. ja enter1
  270. call charpush
  271. call show_str
  272. jmp getstr
  273. backspace: call charpop
  274. call show_str
  275. jmp getstr
  276. enter1: call show_str
  277. pop bx
  278. pop ax
  279. ret
  280. ;=================================================================
  281. ;子程序,数字的入栈
  282. ;参数说明,ds:si指向char栈空间,(al)=入栈字符
  283. ;==================================================================
  284. charpush: mov bx,di
  285. mov [si][bx],al
  286. inc di
  287. cmp di,2
  288. je adds
  289. cmp di,5
  290. je adds
  291. cmp di,8
  292. je adds
  293. cmp di,11
  294. je adds
  295. cmp di,14
  296. je adds
  297. ret
  298. adds: inc di
  299. ret
  300. ;=================================================================
  301. ;子程序,数字的出栈
  302. ;参数说明,ds:si指向char栈空间,(al)=入栈字符
  303. ;==================================================================
  304. charpop: cmp di,0
  305. je sret
  306. cmp di,3
  307. je subs
  308. cmp di,6
  309. je subs
  310. cmp di,9
  311. je subs
  312. cmp di,12
  313. je subs
  314. cmp di,15
  315. je subs
  316. dec di
  317. mov bx,di
  318. mov al,’
  319. mov [si][bx],al
  320. ret
  321. subs: sub di,2
  322. mov bx,di
  323. mov al,’
  324. mov [si][bx],al
  325. ret
  326. sret: ret
  327. ;=================================================================
  328. ;名称,show_str
  329. ;功能,在指定的位置,用指定的颜色,显示一个用0结束的字符串
  330. ;参数,(dh)=行号,(dl)=列号,(cl)=颜色,ds:si指向字符串首地址
  331. ;返回,无
  332. ;=================================================================
  333. show_str: push ax
  334. push bx
  335. push cx
  336. push dx
  337. push si
  338. push es
  339. mov ax,0b800h
  340. mov es,ax
  341. mov ax,160
  342. mul dh
  343. mov bx,ax
  344. mov ax,2
  345. mul dl
  346. add bx,ax
  347. mov al,cl
  348. mov cl,0
  349. show0: mov ch,[si]
  350. jcxz show1
  351. mov es:[bx],ch
  352. mov es:[bx].1,al
  353. inc si
  354. add bx,2
  355. jmp show0
  356. show1: pop es
  357. pop si
  358. pop dx
  359. pop cx
  360. pop bx
  361. pop ax
  362. ret
  363. ;=================================================================
  364. ;名称,clean
  365. ;功能,清屏
  366. ;==================================================================
  367. clean: push bx
  368. push cx
  369. push es
  370. mov bx,0b800h
  371. mov es,bx
  372. mov bx,0
  373. mov cx,2000
  374. clean0: mov byte ptr es:[bx],’
  375. add bx,2
  376. loop clean0;
  377. pop es
  378. pop cx
  379. pop bx
  380. ret
  381. ;=================================================================
  382. ;功能2,引导现有操作系统
  383. ;实现引导现有的操作系统,需要将硬盘的001扇区读入0:7c00
  384. ;会覆盖从软盘读到0:7c00的第一个扇区,所以功能2代码不能写到第一个扇区
  385. ;==================================================================
  386. function2: call clean
  387. mov ax,0
  388. mov es,ax
  389. mov bx,7c00h
  390. mov al,1
  391. mov ch,0
  392. mov cl,1
  393. mov dl,80h
  394. mov dh,0
  395. mov ah,2
  396. int 13h
  397. mov ax,0
  398. push ax,
  399. mov ax,7c00h
  400. push ax
  401. retf
  402. floppyend: nop
  403. code ends
  404. end start

参考文献:王爽著,《汇编语言》(第2版),清华大学出版社。

发表评论

表情:
评论列表 (有 0 条评论,366人围观)

还没有评论,来说两句吧...

相关阅读

    相关 maven assembly记录

    1.pom.xml文件中会有如下一段代码,根据不同的环境,指定P参数进行打包。 如: clean package -DskipTests -Psit -DappName=my

    相关 Assembly Manifest详解

    \[现象\] 对这个问题的研究是起源于这么一个现象:当你用VC++2005(或者其它.NET)写程序后,在自己的计算机上能毫无问题地运行,但是当把此exe文件拷贝到别人电脑

    相关 assembly

    《汇编语言》第二版,王爽著,汇编语言学习笔记。 一、           Introduction 汇编语言,assembly language,是一种用于电子计算机、微处

    相关 Assembly

    System.Reflection.Assembly类是一个比较常用到的类,在反射中就经常用到。   由于这个类实在有太多的字段、属性与方法。实在不想将一个个属性方法从MSD

    相关 Assembly介绍

    微软Windows应用[程序][Link 1]经常受到动态链接库的拖累,这就是“DLL地狱”问题,在遇到此类麻烦的时候,应用[程序][Link 1]的某一个[组件][Link

    相关 .NET系统学习----Assembly

    Assembly学习心得 说明: 最近开始准备把学到的.NET知识重新整理一遍,眼过千遍不如手过一遍,所以我准备记下我的学习心得,已备参考。J 各位都是大虾了,如果有哪些

    相关 Linux宏:__ASSEMBLY__

    汇编:assembly 猜测:所以这个宏跟汇编有关?! 引用:某些常量宏会同时被C和asm引用,而C与asm在对立即数符号的处理上是不同的。asm中通过指令来区分其操作数是