汇编语言实战精解_第1页
汇编语言实战精解_第2页
汇编语言实战精解_第3页
汇编语言实战精解_第4页
汇编语言实战精解_第5页
已阅读5页,还剩296页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

第1章汇编语言基础知识1.1汇编语言的特点所谓汇编语言,其实质就是机器语言的一个高级的形式。我们知道,机器语言是CPU唯一可以真正"理解"的语言,它是用一些由"0"和"1"两个数字组成的一组数字来表示的。例如:1011000000000001(意思是将数字1放入累加器)。

这样的一组数字非常难以理解和记忆,毕竟程序员不是一块CPU。为了使程序设计人员能够很好地记忆这些机器指令,简化程序设计工作,技术人员将这些怪异的数字用一些取自人类语言的简短的文字符号来表示,于是就产生了汇编语言。这些简短的文字符号称为指令助记符。例如上面的那个机器指令用汇编语言表达出来,就是"MOVAL,1"。

同高级语言相比,汇编语言具有一些极其突出的特点:①汇编语言是一种完全面向硬件的语言,这同BASIC,C之类的高级语言截然不同。多数高级语言都是面向问题的,例如:如果需要在屏幕上显示一串文字时,我们可以直接应用BASIC语言中的PRINT语句,或用C语言中的PRINTF函数,这个问题就迎刃而解了。而使用汇编语言编程,解决这个问题的最终操作是"将这些文字的ASCII码写入显示缓冲存储器中"。可见,汇编语言将这个问题转化成了对硬件(显示缓冲存储器)的操作(写入)。这是汇编语言的一个极其突出的特点,也是汇编语言同高级语言的最显著的差别;

②同高级语言相比,汇编语言编写的程序结构十分紧凑,运行速度很快。汇编语言同机器指令直接对应,编译速度快,同时,CPU"理解"其"母语"的速度远高于"翻译"高级语言的速度。因此,汇编语言是所有程序设计语言中运行效率最高的。这是汇编语言的一个最为突出的优点。当需要编写高速运行的软件时,例如编写图像处理程序,就往往使用汇编语言编写软件中的关键部分;

③用汇编语言编制程序十分费时,而且程序的质量直接受到程序员技术水平的影响,程序的可读性也很差。就象前面所举的输出文字的例子,用高级语言编程只需写一条语句,简单明了,极其直观。而用汇编语言编程则需写出一系列指令,这些指令都是些对硬件的操作,同"文字输出"这个问题没有明显的直接联系,因此程序的可读性很差。

④由于汇编语言是面向硬件的,所以用汇编语言编制的程序可移植性很差。显而易见,不同的CPU都有相互独立的指令系统,相互间无任何关系,就算是使用同一系列CPU的机器,因其外围硬件可能有差别,这也会使相同的程序在不同的机器上无法通用。

不难看出,汇编语言存在很多的弱点,但由于它具有一些高级语言所不具备的突出优点,所以汇编语言的应用范围还是很广的。特别是当用户需要研究计算机具体的工作原理的时候,还必须要掌握汇编语言。1.2汇编语言中的数

高级语言中也多用十进制数。十进制数由0-9十个数字组合而成,逢10进1。但由于汇编语言是面向硬件的,因此,在汇编语言中使用的数字也是和硬件结合紧密的二进制数。

二进制数只由0和1而个数字组成,逢2进1,也就是说,在十进制数中计算1+1时将得到一位数字的结果--2,而在二进制数中计算时将得到一个二位二进制数--10,表1-1列出了四位二进制数与十进制数间的对应关系。

在十进制数字中还有"数位"之分,个位,十位,百位,在二进制数中也分各个数位。以四位二进制数1010为例,从右数第一位,称为bit0,第二位称bit1,以此类推。因此,1010的bit3,bit1位是1,bit2,bit0位是0。与十进制数一样,二进制数自右向左数位逐渐升高。最左端的数位为最高位,1010的最高位为1。二进制数00000001001000110100010101100111十进制数01234567二进制数10001001101010111100110111101111十进制数89101112131415 表1-1四位二进制数与十进制数对照表我们知道,计算机利用电路来记忆1和0,那么1和0究竟和电路的工作状态有什么关系呢?通俗的讲,1,0反应了电路输出(入)电压的高和低。如果电路输出(入)的电压高,比如达到了电源电压的幅度,此时电路的输出(入)就为1。如果电路输出(入)的电压很低,比如接近0V,那么这时电路的输出(入)就为0。

二进制数字在实际应用中还有一些缺点,比如不便于书写,难于记忆等。为此,在汇编语言中还常用另一种数制--十六进制。十六进制数字由十六个数组成,逢16进1。前十个数字和十进制一样,是0-9,后六个数字用英文字母A-F来表示。表1-2列出了十六进制与十进制数间的对应关系。十进制01234567十六进制01234567十进制89101112131415十六进制89ABCDEF表1-2十六进制数与十进制数对照表从表1-2可以看出,有些十六进制数与十进制数是一样的,区分不开。这个问题对于1位十六进制数而言到还没有什么关系,可对于多位十六进制数来说就有些麻烦了。比如给出一个数"79",它是十进制的"79"还是十六进制的"79(相当于十进制121)"呢?所以为区分十六进制数与十进制数,特别规定了十六进制数尾部应加字母"H"(Hexadecimal)。十六进制数79应该写成"79H"才对。

对照表1-1和表1-2,可以看出每个四位二进制数都有相应的十六进制数与之对应。若用十六进制数代替二进制数,在书写时可以使数位减少四倍,简化了书写。同时也可以看到十六进制数同二进制数之间没有实质上的差别,只是两种不同的表达形式而已。下面的例子介绍了二-十-十六进制数字间的转换方法,请大家自己总结其中的规律。将十进制数83转换成二进制数。解:采用短除法计算2|

83

余数:1←──bit02|

41

余数:1←──bit1

2|

20

余数:0←──bit22|

10

余数:0←──bit32|

5

余数:1←──bit4计算结果为7位二进制数:1010011例1.2将8位二进制数10110110转换成十进制数。

解:8位二进制数各个数位的权①如下所示:bit76543210权2726252423222120∴10110110=1×27+0×26+1×25+1×24+0×23+1×22+1×21+0×20=182例1.3将十进制数34567转换成十六进制数。

解:仿照例1.1采用短除法计算16|

34567

余数:7←──第0位16|

2160

余数:0←──第1位 16|

135

余数:7←──第2位

8

←────────第3位∴结果是一个4位十六进制数:8707例1.4将16位二进制数1101101001100011转换成十六进制数。

解:将此二进制数按每4位为一组分成4组1101101011000011↓↓↓↓DAC3∴1101101011000011=DAC31.3数学运算和逻辑操作

同十进制数一样,二进制数同样有相应的加,减,乘,除之类的运算,这些运算称为数学运算。由于数学运算还涉及到二进制的其它一些重要的知识,因此在这里暂时不做深入讨论。现在主要来讨论二进制数的逻辑操作。

这里所说的逻辑不同于广义的逻辑,事实上在计算机中的逻辑关系十分简单,只有四种--与逻辑,或逻辑,非逻辑和异或逻辑。与、或和非的关系可以通过一个电路的例子来说明,见图1-1: 图1-1三种逻辑关系示意图在三个图中,灯被点亮的条件是什么呢?很明显,当A点电压和电源电压一致时(即A点输出为1时),灯就会亮。看来主要的问题就是如何使A点输出1?对于(a)图,只有当开关K1,K2都闭合时,A点才会与电源接通,此时灯亮。若把"开关闭合"这一动作用"1"表示,把"开关断开"用"0"表示,则可以说,在(a)图中只有两个开关都是"1"时,A点才会输出"1"。这种开关状态与输出之间的关系就是"与"逻辑关系。

对于(b)图来讲,两个开关或者K1为"1"(接通),或者K2为"1",或者两者都为"1",均可以使A点输出为"1",这两个开关与输出之间的逻辑关系就称为"或"逻辑关系。

对于(c)图而言,当K为"0"时A点才会输出"1",K为"1"时电源被短路,此时A点输出"0"。这种逻辑关系称为"非"逻辑关系。"异或"关系不大好用图表达,但是异或关系有一个重要的特点,就是当进行异或操作的两个数"相同"时所得结果就是"0",而两个数"不同"时就得"1"。这是一个十分重要的特性,大家需牢牢记住。

所谓逻辑操作,就是把两个数按照选定的某种逻辑关系加以处理并得出结果的过程。逻辑操作通常用于使一个二进制数中的某些数位的状态变成我们需要的其它状态,而不改变其它位。

在汇编语言中,基本的逻辑操作有四种:与操作、或操作、非操作和异或操作。分别记作AND、OR、NOT和XOR。表1-3给出了这四种操作的具体情况。表1-3四种逻辑操作执行的结果进行逻辑操作的两个数值不同的逻辑操作及其结果ABANDORNOT*XOR000010010111100101111100*注:"非"操作只对一个数进行,表中选择的是A。下面的例子说明了这四种逻辑操作的应用例1.5给定一个八位二进制数10110100

①求一个八位二进制数,与给定的数作OR操作,要求结果为10111101。

②求一个八位进制数,与给定的数作AND操作,要求结果为00110000。③对给定的数作什么操作可得到二进制数01001011。

④若把给定的数同00100010作XOR操作,将得到什么结果。解:①将OR操作的结果同给定的数相比,不难发现只要把给定数字的bit0,bit3位置成1,其它位状态保持不变,即可得出结果。因此可很容易求出八位二进制数00001001,并可验证:10110100OR)

0000100110111101可见,OR操作可以方便的将某个二进制数的特定位置成"1"而保存其它位不变。只要取另一个二进制数,让这个数中的相应数位--即和给定的数中要改变的数位相对应的位--为"1",而其它位为"0",即可达到目的。②将AND操作的结果同给定的数相比,可以看出只要把给定数字的bit2,bit7位置成0,其它位保持不变,即得出结果。因此可很容易求出八位二进制数01111011,并且可以验证:10110100AND)

0111101100110000可见,和OR操作相对应,AND操作可以把某个二进制数的特定位置"0"而保持其它位不变。只要取另一个二进制数,让这个数的相应位为"0",而其它位为"1",即可达到目的。③比较结果和给定数,可看出将已知数所有位取相反状态,可得到结果。因此可用NOT操作,即NOT)

1011010001001011④将两个数作XOR操作10110100XOR)

0010001010010110把得到的结果10010110同已知数相比较,可以看出只要将已知数的bit1,bit5两位取反,就能得出结果。将(3)和(4)进行比较,可以发现这样一个规律:NOT操作可以将给定数的所有位取反,而XOR操作可以将给定数的特定位取反;进一步分析(4),不难看出若把所得到的结果10010110和00100010再作一次XOR操作:10010110XOR)

0010001010110100又能得到了已知的数。即取反后的数位又重新恢复原状态。因此我们说,XOR操作可以反复改变给定数中的特定位状态。1.4汇编语言中的文字和符号习惯上通常把文字和符号统称为"字符",字符在机器中是如何表示和存储的呢?我们知道计算机可以方便地处理数字,因此如果把字符用数字来表示,就可以很方便的在计算机中储存和处理。所以在计算机中一般采用ASCII(美国信息交换代码AmericanStandardCodeforInformationInterchange)码来表示字符。

计算机中所用的字符包括:

字母:A、B、...Z,a、b、...z;

数字:0、1、...9;

专用符号:+、-、*、/、=...

控制符号:CR(CarriageReturn回车)、LF(LineFeed换行)...

这些字符的ASCII码都列在下面表1-4和表1-5中。表1-4ASCII基本字符对照表ASCII值00000100200300400500600700800900A00B00C00D00E00F01001101201301401501601701801901A01B01C01D01E01F字符NULLSOHSTXETXEOTENDACKBELBSHTLFVTFFCRSOSIDLEDC1DC2DC3DC4NAKSYNETBCANEMSUBESCFSGSRSUSASCII值02002102202302402502602702802902A02B02C02D02E02F03003103203303403503603703803903A03B03C03D03E03F字符SPACE!"#$%&'()*+,-./0123456789:?<=>?ASCII值04004104204304404504604704804904A04B04C04D04E04F05005105205305405505605705805905A05B05C05D05E05F字符@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]_ASCII值06006106206306406506606706806906A06B06C06D06E06F07007107207307407507607707807907A07B07C07D07E07F字符`abcdefghijklmnopqrstuvwxyz{|}~|注:000H-01FH本为控制码,但在PC机屏幕上有时也会显示出特殊字形,比如0DH是控制码"CR(回车)",直接写入屏幕时会显示出"x"。表1-5ASCII扩展字符对照表ASCII值字符*ASCII值字符ASCII值字符ASCII值字符080

081

082

083

084

085

086

087

088

089

08A

08B

08C

08D

08E

08F

090

091

092

093

094

095

096

097

098

099

09A

09B

09C

09D

09E

09F

ü

é

à

a

à

a

ê

è

ё

ì

A

A

E

ò

ù

Pt0A0

0A1

0A2

0A3

0A4

0A5

0A6

0A7

0A8

0A9

0AA

0AB

0AC

0AD

0AE

0AF

0B0

0B1

0B2

0B3

0B4

0B5

0B6

0B7

0B8

0B9

0BA

0BB

0BC

0BD

0BE

0BFá

í

ó

ú

n

N

a

o

1/2

1/4

?br>》

┐0C0

0C1

0C2

0C3

0C4

0C5

0C6

0C7

0C8

0C9

0CA

0CB

0CC

0CD

0CE

0CF

0D0

0D1

0D2

0D3

0D4

0D5

0D6

0D7

0D8

0D9

0DA

0DB

0DC

0DD

0DE

0DF└

▅0E0

0E1

0E2

0E3

0E4

0E5

0E6

0E7

0E8

0E9

0EA

0EB

0EC

0ED

0EE

0EF

0F0

0F1

0F2

0F3

0F4

0F5

0F6

0F7

0F8

0F9

0FA

0FB

0FC

0FD

0FE

0FF

Д

σ

Φ

Ω

±

÷

°

·

˙

Ζ

BLANK用ASCII码表示字符可以很方便地由计算机处理,也便于在机器之间交换文字信息。当我们在键盘上按下字母"a"时,计算机究竟收到怎样的信息呢?实际上计主机从键盘收到的信息就是一个二进制数01100001。由于字符与数字之间的对应关系在ASCII码表中规定死了,计算机当然知道键盘传输来的这个数字意味着什么,所以它会将这个数重新转换为人所能看懂的形式并将其显示在屏幕上。因此"键盘输入-主机-显示输出"的过程实际上就是"文字-数字-文字"的转换过程,这种转换由计算机自动完成。注意标准的ASCII码表内只有128个字符和控制码,用一个7位二进制数就可以表示。不过我们实际使用的都是8位二进制数,最高位用作校验位。不过在PC电脑中ASCII码表被扩展了,最高位不再用于校验,这样一来就多出了128个字符,这128个字符通常被称为"扩展ASCII码",我们平常所看到的表格线,以及""、""这样的字符都存在于扩展ASCII码表中。1.5数据的存储计算机之所以被广泛的应用,其主要原因就在于它能够"记忆"。即将数据储存在它的"存储器"中。那么数据究竟是以何种形式存于存储器中的呢?前面已经讨论过,一位二进制数只能表达0和1两种状态,用于表示数字也只能表达0和1两个数字。而我们平时应用的数的范围是很大的。因此,若数据在存储器中是按"位"存放,即CPU每次仅能从存储器中取得1bit数据进行处理,那么这样的计算机效率是极低的。所以,"位"(bit)并不是存储器中所使用的最小存储单元。

实际上,数据是按照每八个"位"为一组的形式存于存储器中,也就是说,CPU每次从存储器中取出或放入数据的最小宽度是八个bit。我们把这八个bit单独取了个名字--"字节"(BYTE)。所以说,存储器的最小单元就是"字节"。

有时我们嫌"字节"这个单位太小,因此在实际应用中还常用"千字节"(KB),"兆字节"(MB)和"千兆字节"(GB)。1KB=1024B,1MB=1024KB,1GB=1024MB。①

不同的CPU所能配备的存储器的容量是不同的,例如8086/88最多可配备1MB存储器,而80286最多可配备16MB存储器,为什么有这样的差别呢?图1-2表示了CPU同存储器之间的联接形式:图1-2存储器与CPU之间的信号传输可以看到,存储器中的每个字节都有一个编号,这个编号在技术上称为存储单元的"地址"(Address)。这就象是生活中的门牌号码一样。假如要把一封信邮寄到收信人手中,势必要写出收信人的地址。同样,CPU要想把数据发到存储器的某个单元中,也要给出这个存储单元的"地址"。因此,在CPU和存储器之间就有一组专门传送"地址"的线路,这组线路称为"地址总线"。

地址总线不是一根,而是一组。每根地址线都有0或1两种状态,这一系列的0和1组成一个二进制数,当CPU把这个二进制数传到存储器后,哪个存储单元的编号恰好和这个数相等,则这个存储单元就被CPU"选中",那么CPU就可以把这个存储单元所存的数据通过另一组线路(数据总线)取出,或向这个存储单元存入新的数据。不同的CPU所具有的地址线数量是不一样的,像8086/88只有20根地址线,所能给出的地址范围是0-220-1(1048575),所以8086/88只能配备1MB存储器。而286有24根地址线,当然可配备224个字节的存储器。386/486有32根地址线,所以最多可配备4GB存储器。

前面所说的存储器都是直接和CPU相联接的,这些存储器和CPU往往安装在同一块电路板上,因此通常把这类存储器称为"内存储器"(内存)。在机器中还有另一类存储器-"外存储器",这类存储器一般指得是软盘、硬盘、磁带和光盘等容量较大的存储设备。

外存储器和内存相联接而不和CPU直接联接,即CPU无法直接将外存中的数据取到自己内部进行处理,而只能先通过另一部分控制电路将外存中的数据取到内存中,再进行进一步的处理。处理后的数据也是先放在内存中,再传至外存。

正是由于这个原因,外存的容量并不受CPU地址线数量的限制。其容量可从几百KB直至几个GB不等。而且外存往往是机械--电子结合的设备,其数据存取速度远低于内存。1.6寄存器寄存器又分为内部寄存器与外部寄存器,所谓内部寄存器,其实也是一些小的存储单元,也能存储数据。但同存储器相比,寄存器又有自己独有的特点:

①寄存器位于CPU内部,数量很少,仅十四个;

②寄存器所能存储的数据不一定是8bit,有一些寄存器可以存储16bit数据,对于386/486处理器中的一些寄存器则能存储32bit数据;

③每个内部寄存器都有一个名字,而没有类似存储器的地址编号。

寄存器的功能十分重要,CPU对存储器中的数据进行处理时,往往先把数据取到内部寄存器中,而后再作处理。关于各个寄存器的具体问题后面会深入讨论。

外部寄存器是计算机中其它一些部件上用于暂存数据的寄存器,它与CPU之间通过"端口"交换数据,所以外部寄存器具有寄存器和内存储器双重特点。有些时候我们常把外部寄存器就称为"端口",这种说法不太严格,但经常这样说。

外部寄存器虽然也用于存放数据,但是它保存的数据具有特殊的用途。某些寄存器中各个位的0、1状态反映了外部设备的工作状态或方式;还有一些寄存器中的各个位可对外部设备进行控制;也有一些端口作为CPU同外部设备交换数据的通路。所以说,端口是CPU和外设间的联系桥梁。

CPU对端口的访问也是依据端口的"编号"(地址),这一点又和访问存储器一样。不过考虑到机器所联接的外设数量并不多,所以在设计机器的时候仅安排了1024个端口地址,端口地址范围为0--3FFH。图1-3反映了CPU、内存、端口和外设间的联系关系。图1-5CPU、内存、端口和外设的联接关系本章结束语我们说汇编语言是面向硬件的语言,其实汇编语言所真正面对的就是存储器、寄存器和端口。而它所完成的工作无非是在内存中保存数据,在寄存器中"加工"数据,通过端口传输数据或控制设备。这一特点随着后文对汇编语言更深入地探讨就会逐渐显露出来。

汇编语言中的每一条指令都会控制电脑完成一个细微的动作,这些细微的动作组合在一起就会产生一种宏观的效果。CPU的一举一动都在程序的精确控制之下完成。这样的能力可不是高级语言所能拥有的。这也正是汇编语言能够吸引人的地方。第2章开始设计程序前一章提到汇编语言的一个用途是通过端口控制外部设备,本章我们就要用汇编语言编制一些小程序来控制机器中最富于趣味性的一个小设备--喇叭。我们的程序将使这个小部件发出各种各样的声音,从简单的嘀嘀声直至叮叮咚咚的音乐,还有乒乒乓乓的枪声。同时我们也会学到许多指令和程序设计技术。这对于我们今后的学习有很大的帮助。好,下面就让我们带着好奇的心情开始这艰难的开端吧。2.1如何发出声音2.1.1喇叭的构造喇叭的构造大概如图2-1所示,主要由纸盆、线圈、永久磁铁等组成。当有电流流过线圈时,线圈产生的磁场将和永磁铁的磁场相互作用,从而使线圈产生移动。和线圈相联的纸盆也会随之移动。若通过线圈的电流是连续变化的,则线圈移动的幅度也会变化,从而牵动纸盆振动,产生声音。图2-1喇叭的构造图2-2喇叭与机器的连接那么PC机中的小喇叭是怎么与机器相联的呢?我们能否改变流过喇叭线圈的电流呢?图2-2表示了喇叭与机器间简单的联接情况(实际情况要复杂些)。喇叭的一端接在电源正极上,另一端与机器中的61H端口的bit1位相联。可以想象,若能连续改变61H端口的bit1位的0,1状态,就可以使喇叭线圈内的电流时有时无,从而使喇叭发出声音。我们编制的汇编程序的工作,就是连续改变61H端口的bit1位状态。2.1.2汇编伴侣--DEBUG.EXE如何将汇编程序输入计算机?各种高级语言都有自己的一套解释或编译程序,汇编语言也同样有自己的编译器。然而我们现在还没有良好的基础,因此现在就开始介绍汇编语言的编译系统还有些为时过早。好在DOS给我们提供的一个"迷你"汇编器--DEBUG。任何版本的DOS都提供了这个小软件。它的主要用途是用来排除.EXE和.COM类的可执行程序中的逻辑错误。这可以从它的名字看出来。"BUG",英文原意是"小虫子",计算机界将其引申为程序中隐藏的逻辑错误。前缀"DE-"有"排除"的意思。

这个小软件还提供了将汇编语言的指令直接翻译成机器码的功能,这是我们现在所需要的。它还有一些其他特殊的本领,后面会逐渐介绍到。2.1.3细看PC机编制程序前我们应首先调入DEBUG。很简单,在DOS提示符下敲入DEBUG并回车即可。随着磁盘的转动,DEBUG被装入内存执行。一番忙碌后,屏幕上将出现一个"-",后面是跳动的光标。

屏幕上出现的"-"其实是DEBUG给出的提示符,它的出现说明DEBUG此时已经完全作好了为你服务的准备。和DOS一样,DEBUG也提供了一个"命令行"界面,只有输入它自己定义的一些命令才能调动它好好工作。现在我们就来试着打入一个命令--在"-"后面敲入一个字母"R"并回车,看看屏幕上出现些什么东西?-r输入的命令通用寄存器堆栈指针、基指针、索引寄存器AX=0000BX=0000CX=0000DX=0000SP=FFEEBP=0000SI=0000DI=0000DS=0B01ES=0B01SS=0B01CS=0B01IP=0100NVUPEIPLNZNAPONC0B01:01004FDECDI起始地址指令段寄存器指令指针标志寄存器"R"命令的作用是"列出寄存器目前所存的数值",字母"R"取自单词Register。这个命令可以加一个寄存器名做为参数,如RBX、RDI等,其中BX,DI都是寄存器的名字(见下文)。使用这种用法时不仅可以观察某个寄存器的值,还可以任意修改寄存器的内容。有关实例将在后面给出。我们将显示出的内容分成几组,分别介绍各个部分的含义。(1)通用寄存器(Generalregister)

8086/88内部有14个寄存器。其中有四个寄存器最常用。这四个寄存器称为通用寄存器。名字分别是AX、BX、CX和DX。它们具有一个通用的功能--保存数据,但每个寄存器还有自己专门的用途,下面分别介绍。A取自单词Accumulator,所以AX也被称为"累加器"。不过请不要望文生意,它的专门用途可不是作加法。今天我们介绍它的一个专门用途,就是CPU与端口交换数据的唯一通路。除此之外,它还有一系列专门的用途,这些用途以后会陆续谈到。BX也被称为"基地址寄存器"(Baseaddress)。这也是一个十分重要的寄存器,它可用来作为指针使用。这方面的内容将在第三章介绍。本节的程序不涉及BX的专门用途。CX也称"计数器"(Counter)。这个寄存器主要用于为循环指令(LOOP)计循环次数,也用于计数据移位的位数。在"串处理"指令中也有应用。后面的程序就是用CX为LOOP指令计数的。DX是一个一般用途的数据寄存器,通常用于临时保存数据。有时它也和AX一起应用,用于记录32bit数据的高16bit。同时DX在端口输入/输出时也有特殊用途,此用途将在后交介绍。前面所讲的四个寄存器都是16位的,有时程序也需要使用8位的数据,因此,这四个16位通用寄存器都可以拆分成两个8位寄存器。即它们的高8位和低8位可单独使用。这两个部分都有专门的名字,我们用字母"H(High)"表示高8位,用"L(Low)"表示低8位。因此,对于AX来讲,它的高8位被称为AH,低8位称为AL。同理,BX、CX、DX均可分成"*H"、"*L"分别使用。(2)逻辑地址

出现在左下角的16进制数字就是目前即将存放程序代码的内存起始地址。在前面一章里讨论数据存储时曾说到过,每个内存单元都有一个地址。对于8086/88来说,它的内存地址范围是00000--0FFFFFH。即它产生20位的地址。这20位的地址我们一般称其为"物理地址"。但实际上CPU内部并没有20位的寄存器来保存它所需的地址,这是因为在一个硅片上制造20位的寄存器是很不方便的。因此在设计8086/88时,技术人员对这1MB的内存做了如下所述的重新编排。

首先,我们把1MB内存分成了一些相互重叠的存储块,从地址00000开始,每隔16个字节做为一个块的开始,整个内存分为64K个块,这些存储块被称为"存储段"。

每个块的长度是64KB。对于相邻的两段而言,前一段的后64K-16个字节和后一段的前64K-16字节是重叠的。

每一个段都有一个编号,这个编号就是"段地址"。因此如果想找到内存中的某一单元,首先应给出这个单元所在的任一个段的段地址,然后再给出这个单元在所选段内的"偏移量"(距段首的距离),就可找到这个内存单元。习惯上一般把这种用"段地址:偏移量"表示内存单元所在位置的形式称为"逻辑地址"。因为各个段间有部分重叠,所以同样的一个内存单元有多种不同的"段:偏移"表示。例如对于物理地址为00010H的内存单元,若在内存第0段中表示,应该是0000:0010H;而在内存第1段中表示,则成了0001:0000H。

逻辑地址和物理地址之间的转换转换关系也很简单,由于段地址起始于16字节的整数倍,因此若要找到某个段的起始地址,只须将该段的段地址乘以16即可;不同的单元有不同的段内偏移量,将偏移量和段起始物理地址相加,就得到任一内存单元的物理地址。

例如逻辑地址0000:0010H对应的物理地址是0′10H+10H=00010H;而逻辑地址0001:0000对应的物理地址是1′10H+0=00010H,这两个逻辑地址表示内存中同一个单元。注意这种转换是由CPU内部电路自动完成的,对于汇编程序而言则只需使用逻辑地址即可。图2-3内存的组织(3)段寄存器(Segmentregister)

段寄存器就是用来存储内存单元的段地址的。CPU中共有四个段寄存器DS、ES、CS和SS,分别介绍如下:DS称为数据段地址寄存器(Datasegment),它保存数据段的段地址。如果在程序中有数据的存取操作时,若不"显式"地指定段寄存器,则将在DS指定的段寻找相应的存储单元。ES称为附加段地址寄存器(Exterialsegment),也可以称为扩展段地址寄存器,它用于指向内存中的任一段。一般情况下我们常使用这个段寄存器取得内存中某个单元的数据而不需修改DS寄存器。CS是代码段地址寄存器(Codesegment),它和IP(指令指针)寄存器一起指向目前正在执行的指令。也就是说,CPU永远在CS:IP指向的内存单元中取得指令机器码并执行。CS寄存器的数值有变化,CPU所取的指令就有变化。SS是堆栈段地址寄存器(Stacksegment),它保存了堆栈存储区的段地址。堆栈是开辟在内存中的一段特殊空间,数据在堆栈中存取不同于在其它内存空间。有关堆栈的概念将在后面做更详细地介绍。以上所讲的所有段寄存器都是16位的,无法拆分成8位使用,同时,段寄存器不能参与任何数学运算和逻辑操作。(4)指令指针寄存器(InstructionPointer)

IP是CPU内部一个16位的寄存器,它用于记录CPU将要执行的指令的偏移地址。指令的段地址由CS保存。IP不同于其它寄存器,它不能随意的修改,不能参与任何运算或逻辑操作。它的唯一用途就是为CPU取得指令提供偏移地址(CS提供段地址)。也就是说,CPU永远从CS:IP指定的内存地址处取得所要执行的指令。

如果CS:IP指向的位置没有合乎逻辑的指令,而是一些杂乱无章的数据,那么CPU就会傻乎乎地把这些乱七八糟的数据当成指令来胡乱执行,其结果将是未知的。通常情况下会导致"死机"。

在进入DEBUG环境后IP究竟指向什么位置?屏幕上显示IP指向偏移100H的位置。也就是说,我们后面要编制的程序也必须从偏移100H处开始。为什么要空出前面256字节内存呢?这是因为在这256字节的内存中保存有DOS系统提供的重要数据。关于这些数据的说明会在后面陆续介绍。(5)变址、基指针、堆栈指针和标志寄存器(Index,BasePointer,StackPoint&Flag)

这部分内容现在暂时不用,后面用到时会详细加以说明。

一个看似简单的R命令引出了这么多枯燥的东西,实在让人头痛。不过这可是些极其重要的基本知识,不了解这些细节内容就无法继续学习下去。

实际上与学习高级语言相比,这些知识更应使人感到新奇有趣。毕竟,我们只能看到高级语言执行的结果,而看不到它执行的过程。2.2编制第一个程序2.2.1程序的输入和保存了解了上面这些知识,我们就可以开始编写第一个程序了。编程之前先要命令DEBUG作好翻译汇编指令的准备--在"-"后面打入一个字母"A"并回车。看到了什么有趣的东西吗?-a输入的命令0B01:0100程序的起始地址"A"命令的作用是告诉DEBUG现在开始编写程序,它可以后加一个地址作为参数,即可以从代码段的任何位置开始输入程序。对于新编一个程序来说,必须从100H开始。因此我们也可打入"A100[?]"。对于修改一个已经存在的程序中的部分指令,可在A后面打入将要修改指令的偏移地址即可。例如要重写位于偏移量200H处的指令,可打入"A200[?]"。此时屏幕上将显示0B01:0200。输入了"A"命令之后,屏幕上应出现一个以"段:偏移"形式表示的逻辑地址。这个地址就是程序的起始地址。它和CPU中CS:IP指向的位置相同。

在不同的机器,不同的DOS版本中,用户程序的起始段地址是不同的。如果看到的段地址不是0B01,那并没有任何关系。关于偏移地址必为0100的原因前面说过,这是DOS的安排。

看到逻辑地址后,我们即可敲入程序1。DEBUG会自动把输入的汇编指令译成机器码并保存在相应存储单元中。每条指令译完后DEBUG会自动列出下条指令的逻辑地址,因此我们只要打入程序右侧的那些指令即可。左边的逻辑地址是自动给出的。-a[Enter]0B01:0100inal,610B01:0102xoral,20B01:0104out61,al0B01:0106movcx,400用户输入的内容0B01:0109loop1090B01:010Bjmp1000B01:010D←--此处直接回车在运行这个程序前,我们应先将此程序存盘。因为程序中的一个细小的"BUG"都可导致"死机"的后果。那如何保存此程序呢?既然要存盘,总要给程序取个名字。程序命名要用到DEBUG的第三条命令--N,操作如下:-NPROG1.COM[Enter]"N"命令实际来自于英文单词"Name"。有些参考书讨论这个命令时常提醒用户注意不要在"N"后面加空格,其实有无空格并没有关系,至少在6.0以上版的DOS系统没有这个要求。当然,文件扩展名必须是"COM",不能是"EXE"或其它的。有了名字后,我们还应告诉DEBUG此程序的字节数。由于程序从100H开始,因此我们只要把最后一条指令的下一个地址减去100H即可得到程序长度。前面那个程序的长度应该是010D-100=0D。DEBUG规定存盘程序的长度应放在"BX"和"CX"寄存器中,也就是说把BX,CX两个16位寄存器联合起来存放一个32位的长度值,高16位在BX中。因此,若要存盘的内容不足64K,则应将BX置0,而将长度放入CX中。这就要用到前面介绍的"R"命令。操作如下:-rbx[Enter]←输入的命令BX0000←DEBUG显示BX寄存器的原值:0[Enter]←用户输入的新值,如果直接回车将不改变寄存器的原值-rcx[Enter]←同上述CX0000:0d[Enter]有了名字和长度,即可通知DEBUG存盘。这可以使用DEBUG的第四条命令--W。操作很简单,只需在"-"后敲入"W(Write)"并回车,DEBUG会报告存到磁盘上的字节数。程序已经存盘,我们可以大胆地运行这个程序。程序的细节我们先不必理会,咱们先要个宏观结果。DEBUG给我们提供了一个命令用于运行程序,命令码是"G"(GO)。由于我们应从偏移100H处执行,所以我们这样应用G命令:-G=100[Enter],注意等号是不能省略的。听到声音了吗?结果应该是令人兴奋的。喇叭里真的发出了连续的声音。不过兴奋之余,大家还应该清楚一点,就是在机器不断电和热启动的情况下,此声音将永远"连续"。也就是说,机器此时已经进入"死锁"状态。不过不必担心,出现这种情况并非是程序中有"BUG",而是我们用的指令过于少了,以至于不可能让这个程序正确结束。这个问题我们会在后面加以解决。当然要用到更多的指令和知识。请按下"Ctrl+Alt+Del"来结束这无休止的叫声。我们已经得到了宏观结果,下面就来仔细分析一下程序的微观结构--每个指令的作用。2.2.2程序透析还记得我们前面的讨论吗?若要让喇叭发声,关键是连续改变61H端口的bit1位的状态。但由于8086/88在设计时规定端口不能直接参与任何运算和逻辑操作,因此程序必须首先将61H端口内各个位的当前状态取到CPU中。这就是第一条指令的作用。助记符:IN(In)用途:将某端口的数据取至累加器格式:IN累加器,端口号执行:端口内各个位的状态传送至累加器(AL或AX)IN是这条指令的助记符,此指令的作用如上所述。由于累加器是CPU和端口之间的"唯一"通路,因此在此程序的这条指令中只能使用AX寄存器。而且程序现在要从端口中取得8bit数据,则我们必须要用AX的低8位--AL。注意不能用AH寄存器。像这样的用法--"INAH,61"或"INBL,61"都是不对的。十六进制数字"61"就是提供数据的端口地址。对于地址小于0FFH的端口都可以在指令中直接引用地址,而超过0FFH的端口不能这样应用。例如"INAL,1C0"这样的用法是错误的。至于如何取得地址大于0FFH的端口状态,将在后面介绍。这里有三个问题应注意:①助记符后的空格只能多而不能省,指令中的逗号不能丢;②DEBUG只接收16进制数,如果使用其它进制数应先转换成16进制;③数据进入累加器后,各个位排列排列顺序与端口是一一对应的,即AL的bit0对应于端口的bit0,而AL中的bit7也对应于端口的bit7。取得此端口的状态后,我们要改变bit1位的0、1状态,而端口的原状态是不确定的,况且我们还想使第一位的状态能够连续"翻转",所以我们选择了异或操作(见前一章)。这就是程序中的第二条指令--XOR。采用异或操作改变某个位时要注意必须用"1"作为操作数,只有用"1"才能达到让某个位连续"翻转"的目的。在给出的程序中由于要让AL的bit1位改变,所以操作数的bit1位是"1"。寄存器同IN指令相比,XOR的用法要丰富得多。首先我们可以把一个寄存器和一个二进制数作XOR操作,也可以把一个放在内存中的数据和一个二进制数作异或,还可以把这个操作码放入一个寄存器或内存中。助记符:XOR用途:对寄存器或内存中的数据作"异或"操作格式:XOR寄存器,操作码XOR存储器,操作码XOR寄存器,寄存器XOR存储器,寄存器XOR寄存器,存储器执行:两个操作数进行"XOR"操作后,结果存入左边的寄存器或存储单元中。使用这条指令有两个要点要注意:①操作结果都保存在左边的寄存器或内存单元中,右边的数据不变。这也就是说,左边必须是寄存器或内存单元;②两边数据的位宽要一致。像这样的用法--XORAX,BL是不对的。至于如何应用此指令"异或"内存单元中的数据将在后面介绍。当务之急是掌握第一种用法。执行过此指令后,AL寄存器的bit1位状态发生了翻转。但此时端口中的各个位状态并没有改变。因此,程序需要把AL中的数据返回到61H端口中。这就要用下面介绍的OUT指令:助记符:OUT用途:将累加器中的数据返回到端口中格式:OUT端口,累加器执行:端口内各个位原状态被新值代替(AL或AX)应用这条指令的注意事项和IN指令相同,不再多讲。CPU连续执行这样三条指令就可以使61H端口第一位发生状态变化,如果这三条指令反复的被执行,那么XOR指令就会使61H端口的bit1位"连续"翻转。所以程序中最后一条指令的作用就是返回到100H继续执行这三条指令。助记符:JMP(Jump)用途:从本指令开始的地址转移到内存其它位置继续执行程序格式:JMP目的地址执行:CPU从CS:IP指向的新地址继续取指令执行这条指令的实际功能是改变"CS:IP"的指向,前面我们已经讨论过CPU执行的指令都来自于"CS:IP"指向的内存单元,因此"CS:IP"的改变就意味CPU执行的指令要发生改变。所谓"跳转"的含义也就是如此。为了反复执行前三条指令,我们在程序最后用"JMP100"使CPU重新从"CS:0100"开始执行指令。有关JMP还有一些便深入的内容,例如在这个程序中CS寄存器的内容并没有改变,即转移发生在一段之内。那么如果要"跨段"转移又应如何应用JMP指令呢?这个问题暂且留作后话。这四条指令构成一个"循环",使61H端口第一位的状态在0、1之间连续变化。但是,如果没有第4,第5条指令,那是无法听到任何声音的。这是因为CPU执行指令的速度极快,61H端口的bit1位变化频率过高,己超出了听觉范围。因此程序在改变了端口61H的状态后应该"拖延"一段时间,再做第二次改变。第4,5条指令的作用就是"拖时间"。助记符:MOV(Move)用途:传递数据格式:MOV寄存器,数字MOV存储器,数字MOV寄存器,存储器MOV存储器,寄存器MOV寄存器,寄存器执行:右边的数据被"拷贝"到左边的寄存器或存储单元中。同IN,OUT指令有些类似,数据仍然是从"右"传到"左",这实际上汇编语言的一个规律,和高级语言中的"赋值"是一样的。还要注意右边的数据是"拷贝"到左边的寄存器或存储单元中的,因此指令执行以后右边的寄存器或存储美元中的数据没有改变。从应用格式上看没有"MOV存储器,存储器"的用法,即MOV指令不能在内存单元之间移动数据。要想直接在内存中移动数据需要使用后面讲的"串操作"指令。另外,段地址寄存器之间也不能进行数据传送,比如"MOVDS,ES"这样的用法也是不对的。如果需要在段寄存器之间传送数据,应该借助其它通用寄存器或内存单元。第4条指令把一个数字500H放到CX寄存器中,为"拖时间"作准备。实际上如果把"MOV"指令比作高级语言中的"赋值"语句,那么500H就是一个"常量",在汇编语言中将这种直接出现在指令中的常数称为"立即数"。将被"赋值"的寄存器就相当于是"变量",在C语言中称这种变量为寄存器变量。真正在"拖时间"的是第5条指令--LOOP:助记符:LOOP(Loop)用途:根据CX中设定的计数值循环执行一些指令格式:LOOP目的地址执行:CX内的循环次数减去1,若CX≠0则CPU转移到指定的目的地址继续执行程序;若CX=0则CPU顺序执行LOOP下面的指令。循环是程序设计中重要的技术。学过高级语言的朋友知道,对于有限次循环,应具备的两个主要因素是循环次数和循环体。汇编语言也是这样。只不过它有一些特别的规定:①如果使用LOOP指令来完成循环(还有别的方法),在进入循环体前必须把循环的次数放入CX寄存器中;②LOOP指令所带的目的地址必须"小于或等于"它本身的地址,目的地址只能是偏移量且它减去LOOP指令下面的地址之差必须在[0-128]范围内;③CPU每次执行LOOP指令时,总是自动地把CX中的计数值减1,根据减的结果是0还是非0来决定是否转移到目的地址去执行指令,一旦CX被减至0,则CPU不再转移,而是执行LOOP下面的指令。图2-3LOOP指令的执行过程2-3反映图了循环的流程。在这个程序中LOOP指令的目的地址就是它自身的地址,这样的循环是空循环,它可以完成"延时"。有了第4,5条指令后,我们就能听到喇叭发出的声音了。经过这一系列的分析,我们可以画出整个程序的流程图(如图2-4)。图2-4PROG1程序的流程图请务必注意,每一条汇编指令所完成的工作都是微观上的,每条指令单独作用并不能产生宏观效果。声音的产生是所有指令共同作用的结果。所以不要象学习高级语言那样将发出声音归功于程序中的某一条指令。图2-5反映出这些指令间的有机配合。图2-5各指令的配合2.2.3程序的缺点至此,我们已经把这个程序完整地分析了一遍。下面的工作就是看看这个程序有什么缺点。毫无疑问,无法返回DOS是程序的最大问题;不过还要注意这样的事实:同样的程序,在使用不同CPU的机器上发出声音的频率不一样。原因在于CPU执行指令的速度有快有慢,486完成LOOP的速度肯定比286快,因此这个程序在486上执行时产生的声音比286机发出的声音尖。说白了,此程序不可能精确地控制声音频率。下面一节的内容就是来改变这些缺点的。2.3回到DOS的怀抱通过图2-4可以看出,整个程序由两层循环构成。内层循环由"LOOP"指令控制完成,外层循环由"JMP"完成。这两层循环有着极大的差别,CPU执行LOOP指令时根据CX减量结果控制转移,这称为条件转移;而执行JMP指令时没有任何条件需要CPU判断。因此,用JMP完成的外层循环是"死"的,没有出口。我们要想返回操作系统,首先要解决"死循环"问题。这是很容易做到的,显而易见,用LOOP指令代替JMP指令,把程序改成PROG1-A那样,外层循环不就"活"了吗?PROG1-A0B01:0100movcx,8000B01:0103inal,610B01:0105xoral,20B01:0107out61,al0B01:0109movcx,5000B01:010Cloop10C0B01:010Eloop1030B01:0110真能如我们所想的吗?如果用G命令执行这个新程序,那么后果仍然是"灾难性"的。让我们仔细分析这个程序:第一条指令在CX中放了一个数字800H作为外层循环的计数值,也就是说CPU在首次执行最后一行的LOOP时CX中应该是800才对。可是不要忘记,程序中还有个内层循环,它同样用到了CX寄存器。事实上在执行到第五条指令时CX寄存器中的800就已经不复存在了。而且退出内循环后CX已经是0,最后一个LOOP指令已经失去了正确的计数值。看来问题有点棘手,内循环破坏了外循环的计数值,怎么办?问题并不难解决。很容易想到只要在进入内循环之前把CX寄存器中的数据做个副本,在退出内循环后恢复CX寄存器中的数据就可以了。程序PROG1-B就是依据此想法改进的。我们新增加了两条指令:助记符:PUSH/POP用途:将寄存器或存存储单元中的16位数据压入/弹出"堆栈"格式:PUSH/POP寄存器(16bit)PUSH/POP存储器(16bit)执行:PUSH指令使"堆栈"中存入了寄存器或存储单元中数据的副本,POP指令会使"堆栈"中最顶端的数据出栈并进入给出的寄存器或存储单元中"堆栈"是内存中一段连续的存储单元,它主要用于临时存放数据。堆栈在内存中的位置可以是任何一段空闲存储单元,它的段地址由SS寄存器指示,而数据存放的位置由堆栈指针SP寄存器指示。那么堆栈段在应用上与一段存储段有何差别呢?①堆栈中保存的数据都是16位的,我们把16位的数据称为"字(WORD)"。8位数据无法用PUSH指令放入堆栈;②其次,和程序不一样,堆栈的起始于内存"高地址"位置,这可以通过R命令观察到。进入DEBUG之后使用R命令查看各个寄存器的值,可以看到指令指针IP寄存器指向偏移0100H,而"堆栈指针"SP寄存器则指向0FFEEH。③PUSH进栈的数据总是从高地址向低地址方向排列,每执行一次PUSH指令,SP寄存器就会自动减2,同时数据存入SP指向的位置。也就是说,只要不人为修改SP寄存器,那么SP将永远指向最后一个进入堆栈的数据;④和PUSH指令相反,POP指令会把SP寄存器指向的数据取到指定寄存器中,同时SP会自动加2。所以,POP指令总是取出堆栈最后一个数据,即堆栈具有"后进先出"的性质。RO图2-6DEBUG状态下的堆栈设置PROG1-B0B1D:0100MOVCX,08000B1D:0103PUSHCX0B1D:0104INAL,610B1D:0106XORAL,020B1D:0108OUT61,AL0B1D:010AMOVCX,05000B1D:010DLOOP010D0B1D:010FPOPCX0B1D:0110LOOP01030B1D:0112图2-6表示了进入DEBUG后代码和堆栈的位置。为了更好地说明堆栈的特性,我们用DEBUG编一小段程序:C:\ASM\>DEBUG[Enter]-a100[Enter]0B01:0100movax,12340B01:0103movbx,abcd输入这些内容0B01:0106pushax0B01:0107pushbx0B01:0108popax0B01:0109popbx0B01:010A[Enter]为了能够看到程序执行的细节情况,我们先学习DEBUG的一个新命令--T。T命令主要用于"跟踪"(TRACE)程序,这是DEBUG最重要的功能。利用T命令,我们可以一次执行一条指令,每执行一条指令后DEBUG都会把所有的寄存器内容列出来,这样即可看到指令执行的结果。我们现在跟踪这个程序。程序输入完毕,我们首先使用"R"命令将所有寄存器的当前值列出,重点观察一下SP寄存器与AX寄存器的值。-rAX寄存器的初值SP寄存器的指向AX=0000BX=0000CX=000ADX=0000SP=FFEEBP=0000SI=0000DI=0000DS=0B01ES=0B01SS=0B01CS=0B01IP=0100NVUPEIPLNZNAPONC0B01:0100B83412MOVAX,1234第一条待执行的指令现在我们输入一个"T"命令,CPU就会将第一条指令"MOVAX,1234"执行,执行之后DEBUG会将执行结果显示出来。-t注意此寄存器的变化指令指针的增加AX=1234BX=0000CX=000ADX=0000SP=FFEEBP=0000SI=0000DI=0000DS=0B01ES=0B01SS=0B01CS=0B01IP=0103NVUPEIPLNZNAPONC0B01:0103BBCDABMOVBX,ABCD第二条待执行的指令以上显示的内容就是执行了指令"MOVAX,1234"之后的各寄存器的状态,可以看到AX寄存器中确实存入了1234。另外一点,请注意IP寄存器的变化情况。再次输入"T"命令,则指令"MOVBX,ABCD"也会被执行。-tBX寄存器存入ABCD指令指针继续增长AX=1234BX=ABCDCX=000ADX=0000SP=FFEEBP=0000SI=0000DI=0000DS=0B01ES=0B01SS=0B01CS=0B01IP=0106NVUPEIPLNZNAPONC0B01:010650PUSHAX需要重点观察的指令"PUSH"指令的执行情况是我们要点要观察的,单步执行此指令之后请注意SP寄存器的变化。-tSP寄存器减量AX=1234BX=ABCDCX=000ADX=0000SP=FFECBP=0000SI=0000DI=0000DS=0B01ES=0B01SS=0B01CS=0B01IP=0107 NVUPEIPLNZNAPONC这是第一次执行PUSH指令,可以观察到SP寄存器由原来的FFEE变成了FFEC,恰好是减了2。0B01:010753PUSHBX-tSP寄存器继续减量AX=1234BX=ABCDCX=000ADX=0000SP=FFEABP=0000SI=0000DI=0000DS=0B01ES=0B01SS=0B01CS=0B01IP=0108NVUPEIPLNZNAPONC执行了"PUSHBX"之后SP寄存器继续减2。0B01:010858POPAXPOP指令执行与PUSH恰好相反-t注意AX寄存器的改变执行POP指令后SP寄存器增量AX=ABCDBX=ABCDCX=000ADX=0000SP=FFECBP=0000SI=0000DI=0000DS=0B01ES=0B01SS=0B01CS=0B01IP=0109NVUPEIPLNZNAPONC现在指令"POPAX"执行完华,注意SP寄存器的变化,它加了2。另外看一看AX寄存器与BX的关系,究竟那个数从堆栈中跳出了呢?0B01:01095BPOPBX-tAX、BX寄存器内容交换SP寄存器复原AX=ABCDBX=1234CX=000ADX=0000SP=FFEEBP=0000SI=0000DI=0000DS=0B01ES=0B01SS=0B01CS=0B01IP=010ANVUPEIPLNZNAPONC0B01:010A0883745BOR[BP+DI+5B74],AL最后一条指令"POPBX"执行完了,我们可以很清楚地看到SP寄存器的变化情况,同时也会发现,AX寄存器和BX寄存器中的数据换了位置,这就是堆栈中数据"后进先出"的结果。应用PUSH和POP指令时必须记住使用16位数据,PUSHAH、POPDL这样的用法都是不对的。新增了两条指令,"救活"了一个循环,现在这个程序应该可以结束了吧?不过现在的回答仍然是否定的。很多高级语言都不要求程序结尾必须有表示结束的语句,然而汇编语言可不行。除非你输入了要求返回DOS的指令,否则CPU是不会自行结束执行程序的。因此我们需要再学一条指令:助记符:RET(Return)用途:从子程序返回到主程序格式:RET执行:结束子程序的执行,返回主程序关于这条指令的使用还有很多更深入的内容,但凭我们现在这点功底还不宜讲得过深,现在只需记住这条指令能够让我们的程序正常的结束就行了。PROG2就是程序最终改成的样子。PROG20B1D:0100MOVCX,08000B1D:0103PUSHCX0B1D:0104INAL,610B1D:0106XORAL,020B1D:0108OUT61,AL0B1D:010AMOVCX,05000B1D:010DLOOP010D0B1D:010FPOPCX0B1D:0110LOOP01030B1D:0112RET运行PROG2,喇叭里会发出一声鸣叫,然后屏幕上会重新出现DEBUG的提示符。改变外循环的计数值,就可以任意改变声音的长短。-g=100Programterminatednormally(程序正常结束)这个程序看来已经接近完美了,然而新的问题又出现了:想要听到0.5秒钟的声音,外循环的计数值应是多少?非常遗憾,用LOOP指令精确控制时间十分困难,这和我们前面所讨论的控制频率的问题一样。而要想解决这两个问题,我们还需了解更多的PC原理,所以这个问题只能留到后面再研究了。2.4其它的改进方法上一节的程序中我们用堆栈来保存CX寄存器的拷贝,这并非是唯一的解决办法。就"保存CX寄存器副本"这一问题而言,很容易想到可以用一个未使用的寄存器来保存CX寄存器中的计数值。程序PROG2-A中应用了BX寄存器(当然也可应用DX或其它寄存器)。有一点要注意:段寄存器,SP寄存器不可作为暂存数据用。这里需要插入一点有关"寻址方式"的讨论。很多专业教材都谈到了这个问题。所谓寻址方式,就是CPU"寻找"被处理数据的方法。例如在指令"MOVCX,500"中,500H是一个"立即数",它最终将出现在指令编译后的机器码中。CPU在取指令的同时就已经取到了数据。这样的寻址方式称为立即数寻址。而在指令"MOVCX,BX"中,BX是一个寄存器,CPU在取指令时只是取到了表示"BX"寄存器编码,而实际所需的数据并未取到,只有在指令执行完后,BX中的数据才会进入CX。这样的寻址方式就称为"寄存器寻址"。当然还有其它的寻址方式,在本书的后文中会逐渐谈到。PROG2-A-a100[Enter]0B01:0100MOVCX,08000B01:0103MOVBX,CX0B01:0105INAL,610B01:0107XORAL,020B01:0109OUT61,AL0B01:010BMOVCX,05000B01:010ELOOP010E0B01:0110MOVCX,BX0B01:0112LOOP01030B01:0114RET关于PROG2我们还有别的改进方法。PROG2中的两个循环都用LOOP指令完成,都要用CX计数,这是造成"冲突"的根源。若我们不用LOOP指令而改用其它指令来完成"活"循环,那么这种冲突就不存在了。程序PROG2-B就是采用了这种思想。在讨论新增的指令前,我们先看看LOOP指令的动作流程,图2-3反映了CPU执行LOOP指令的过程。注意CX寄存器先减1,而后判断是否减至0。这两个步骤是CPU自动完成的。由此我们可以设想把某个循环的计数值放在除CX外的其它寄存器中,在执行循环时我们用"专门的指令"来把计数值减1,再用"专门的指令"判断减1的结果来控制转移,即可避免两个LOOP间的冲突。这就要用到PROG2-B中新增的两条指令DEC和JNZ。助记符:DEC(Decrement)用途:将指定的寄存器或内存单元中的数据的值减去1格式:DEC寄存器(8bit或16bit)DEC内存单元执行:原数据减1,结果仍保存于这个寄存器或内存单元中这条指令和C语言的那个"--"运算符一样。由于所有段寄存器都不参与运算,所以没有"DECDS"这样的用法。PROG2-B-a1000B01:0100movbx,8000B01:0103inal,610B01:0105xoral,20B01:0107out61,al0B01:0109movcx,5000B01:010Cloop10c0B01:010Edecbx0B01:010Fjnz1030B01:0111int200B01:0112在程序PROG2-B中我们一开始用BX来保存外循环的计数值,在内循环执行完后,我们用"DECBX"使得BX中的计数值减1。下面的问题就是看BX是否被减成0。这由新增的JNZ指令完成。助记符:JNZ(JUMPIFNOTZERO)用途:根据运算结果是否非0来控制转移格式:JNZ目的地址执行:若ZF=0,则转移至目的地址执行指令;若ZF=1,则继续执行下面的指令新的问题出现了,ZF是什么?是新的寄存器吗?这个问题不会再留到后面讨论了,因为它本身就是个遗留下来的问题。请看第一节内"细看PC机"中的2.2.5,在前文我们刚刚讨论了堆栈指针(SP)寄存器,还留了3个小问题,现在我们就来讨论"标志寄存器"。对于一般数学运算我们不仅要关心运算的结果,同时要关心"运算后果"。以"DECBX"为例,若BX〉1,则执行一次DEC指令不会使BX减为0,即运算没有造成变量变成0的"后果";若BX=1,则执行一次DEC即可造成"BX=0"的后果。因此对于DEC指令而言,它可以造成两种不同的后果:(1)被减的变量仍大于0;(2)被减的变量恰好成了0。我们所编的程序并不关心"DECBX"的结果是什么,但是要关心"DECBX"的后果。因为BX被减至0实际意味着外循环的结束。那么运算的后果记录在什么地方呢?在CPU中有一个特殊的16bit寄存器,此寄存器称为"标志寄存器(FLAGRegister)"。和通用寄存器、段寄存器不同,它并不用于保存数据,CPU用它某些位的0,1状态来记录运算的后果。它的结构如图2-7所示。图2-7标志寄存器的结构所用到的各个位的解释都列在表2-1中,其中的ZF位就是我们现在所关心的。ZF被称为"零标志位"(ZeroFlag),当某次运算的结果恰好是0时,ZF将被"置位(SET)",即ZF=1;否则ZF"复位(RESET)"。JNZ指令根据ZF的状态来转移。表2-1标志寄存器各个位的功能表 标志位功能解释清0置1CF进位(借位)标志,某次运算有进位或借位时此标志置1NCCYPF奇偶标志,若运算结果中为1的位有偶数个此标志置1PEPOAF辅助进位标志*,记录运算中低4位向高4位或低8位向高8位的进位ACNAZF零标志,若运算结果等于0此标志置1ZRNZSF符号标志,运算结果为负数此标志置1NGPLTF陷井标志,当此标志位置1后CPU处于单步执行方式--IF中断标志,此标志置1时允许CPU响应中断,置0时将屏蔽中断**EIDIDF方向标志,用于控制串处理指令的步进方向DNUPOF溢出标志,若运算发生溢出时此标志置1OVNV注:*也称为半进位标志;**对不可屏蔽中断(NMI)没有影响不难看出JNZ指令的转移与否也是有条件的,这样的转移指令被称为"条件转移指令"。在汇编语言中还有大量的条件转移指令,这些指令大部分都受到标志寄存器中某些位的控制。而且条件转移指令有很多都有"对立面"--转移条件恰好相反的指令。如与JNZ相对的指令:助记符:JZ(JUMPIFZERO)用途:根据运算结果是否为0来控制转移格式:JZ目的地址执行:若ZF=1则转移至目的地址执行指令;ZF=0时继续执行下面的指令这两条指令应用格式完全一致,而转移条件正相反。还有一点需要注意:条件转移指令只能在-128-+127字节范围内进行跳转,即目的地址与转移指令后面一条指令的起始地址之差不能大于+127或小于-128。这个性质很容易验证:输入命令A100[],然后输入"JZ182[Enter]",DEBUG马上会显示"error",这是因为182H-102H=80H=128〉+127的缘故。同理,"JZ81[Enter]"也会出错。标志寄存器还有第二个用途,就是控制CPU内部的工作状

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论