第3章 汇编语言基础 汇编语言使用指令进行编程,是一种面向机器的低级语言。 指令是对计算机硬件发出的指示、命令。例如,下面的一组二进制代码是一条指令: 10001011 11000011 它要求8086微处理器把BX寄存器的内容传送到AX寄存器中,其中左面的8位二进 制代码10001011表示本条指令要进行的操作:在两个16位寄存器之间进行数据传送,称 为操作码。后面的一个字节指出两个用于传送数据的寄存器,称为操作数。 用二进制代码表示的指令可以由CPU 直接执行,称为机器指令。用机器指令编写程 序的规范称为机器语言,编写出来的程序称为机器语言程序。 机器指令难以记忆,容易出错,不便于阅读和维护,为此,用一些符号来表示上面的操作 码和操作数。例如,用MOV 表示进行数据传送的操作码,用AX、BX 表示各自的寄存器。 这样,上面的指令可以另写为 MOV AX, BX 显然这样的表达方式更为清晰,更便于记忆和使用。符号MOV 因此称为助记符。用 符号、助记符书写的指令称为符号指令。用符号指令书写程序的规范称为汇编语言,用它编 写的程序称为汇编语言源程序。 微处理器不能直接识别和执行符号指令,所以汇编语言源程序要经过翻译,转换成对应 的机器语言程序,才能够交计算机执行。这个翻译的操作称作汇编,由汇编程序完成。 汇编语言和机器语言都是面向机器的低级语言,是计算机的母语。使用汇编语言编程 可以对计算机的硬件直接进行操控,实现计算机硬件能够实现的所有功能。此外,汇编语言 编写的程序具有占用内存空间小,执行效率高的优点。 3.1 数据定义与传送 现代计算机可以处理数值数据、文字、声音、图形信息,这些信息在计算机内都是用一组 二进制代码来表示的,统称为数据。计算机运行的过程,就是数据的传输、加工的过程。 本节介绍数据在计算机内的表示、数据在程序中的定义、数据传送指令及其应用。 3.1.1 计算机内数据的表示 1.数据组织 计算机内的信息按一定的规则组织存放。 1)位 位(bit,b)是信息的最小表示单位。一位二进制可以描述一个开关的状态(称为开关 量),例如:用“1”表示接通,“0”表示断开。 ·43· 2)字节 字节(Byte,B)是计算机内信息读写、处理的基本单位,由8位二进制数组成。8位二进 制代码有28 种不同的组合,可以表示256(28)个不同的值,可以用来存放一个范围较小的整 数,一个西文字符,或者8个开关量。 一个字节内的8位从右(低位)向左(高位)从0开始编号,依次为b0,b1,…,b7,如 图3-1(a)所示。其中,b0 称为最低有效位(LeastSignificantBit,LSB),b7 称为最高有效位 (MostSignificantBit,MSB )。 3)字(Word)和双字(DoubleWord) 字和双字分别由16 和32 位二进制组成,或者说,分别由2或4字节组成。 字由16 位二进制(2字节)组成,可以存放一个范围较大的整数或一个汉字的编码。它 的16 个二进制位仍然从右(低位)向左(高位)从0开始编号,依次为b0,b1,…,b15,如 图3-1(b)所示。其中,b0~b7 称为低位字节,b8~b15 称为高位字节。 双字由32 位二进制组成(4字节), 可以存放范围更大的整数或者一个浮点格式表示的 单精度实数。它的32 个二进制位中,b0~b7,b8~b15,b16~b23,b24~b31,分别称为低位字 节、次低位字节、次高位字节、高位字节,如图3-1(c)所示。 图3-1 数据组织 2. 无符号数的表示 所谓无符号数是正数和零的集合。储存一个正数或零时,所有的位都用来存放这个数 的各位数字,无须考虑它的符号,无符号数因此得名。 可以用字节、字、双字或者更多的字节来存储和表示一个无符号数。 用 N 位二进制表示一个无符号数时,最小的数是0,最大的数是2N -1( N 位二进制数 111…111 )。一个字节、字、双字无符号数的表示范围分别是0~255 、0~65535 、 0~4294967295 。 一个无符号数需要增加位数时,可以在它的左侧添加若干个“0,(”) 称为零扩展。例如,将 8位无符号数10110011 扩展为一个字时,低位字节置入这个无符号数,高位字节填“0,(”) 结 果为0000000010110011(插入空格是为了便于阅读,书写时没有这个要求)。 3. 有符号数的表示 计算机内用“补码”来表示一个有符号数,可以用字节、字、双字或者更多的字节来存储 一个有符号数的补码。 补码表示法用最高有效位(MSB)表示一个有符号数的符号,“1”表示符号为负,“0”表 示符号为正。 ·44· 符号位之后的其他二进制位用来存储这个数的有效数字。正数的有效数字不变,负数 的有效数字取反后最低位加1。用字节记录一个有符号数时,[+11011]补=[+0011011]补= 00011011,[-11011]补=[-0011011]补=11100100+1=11100101。 对于正数X =+dn-2dn-3…d2d1d0 来说,[X ]补=0dn-2dn-3…d2d1d0=X 。 对于负数Y=-dn-2dn-3…d2d1d0,[Y]补=1dn-2dn-3…d2d1d0+1=1111…111-|Y|+ 1=2n -|Y|=2n +Y。表3-1列出了用8位二进制代码表示的部分数值的补码。 表3-1 部分数值的补码 真值(十进制) 二进制表示补 码 +127 +1111111 01111111 +1 +0000001 00000001 +0 +0000000 00000000 -0 -0000000 00000000 -1 -0000001 11111111 -2 -0000010 11111110 -127 -1111111 10000001 -128 -10000000 10000000 用一个字节存储有符号数补码时,可以表示127个正数(1~127),128个负数(-1~ -128),1个“0”(00000000)。其中,[-1]补=11111111,[-128]补=10000000。 如果把一个数的补码的所有位(包括符号位)“取反加1”,将得到这个数相反数的补码。 把“取反加1”这个操作称为“求补”,[[X ]补]求补=[-X ]补。例如,[5]补=00000101, [[5]补]求补=[00000101]求补=11111011=[-5]补。 已知一个负数的补码,求这个数自身(真值)时,可以先求出这个数相反数(正数)的补 码。例如:已知[X ]补=10101110,符号位为1,X 是一个负数。求X 的真值可以遵循以 下步骤。 (1)[-X ]补=[[X ]补]求补=[10101110]求补=01010001+1=01010010(一个正数 的补码)。 (2)-X =[+1010010]2=+82D(后缀'D'表示该数为十进制数)。 (3)X =-82。 一个补码表示的有符号数需要增加它的位数时,对于正数,在左侧添加若干个“0”,对于 负数,则添加若干个“1”。上述操作实质上是用它的符号位来填充增加的“高位”(无论该数 是正是负),称为符号扩展。例如,[-5]补=11111011(8 位)=1111111111111011 (16位),[+5]补=00000101(8位)=0000000000000101(16位)。 4.字符编码 计算机处理的对象除了数值数据之外,还有大量的文字信息。文字信息以字符为基本 单元。计算机内用若干位二进制来表示一个字符,这组二进制代码称为该字符的编码。 计算机内常用的字符编码是ASCII(AmericanStandard CodeforInformation ·45· Interchange,美国信息交换标准编码)。它规定用7位二进制表示一个字母、数字或符号, 包含128个不同的编码。由于计算机用8位二进制组成的字节作为基本存储单位,一个 字符的ASCII码一般占用一个字节,低7位是它的ASCII码,最高位置“0”,或者用作“校 验位”。 ASCII编码的前32个(编码00H~1FH)用来表示控制字符,例如CR(回车,编码 0DH),LF(换行,编码0AH)。 ASCII编码30H~39H 用来表示数字字符0~9。它们的高3位为011,低4位就是这 个数字字符对应的二进制表示。例如,5' '=0110000B +0101B=0110101B=35H。 ASCII编码41H~5AH 用来表示大写字母A~Z。它们的高2位为10。 ASCII编码61H~7AH 用来表示小写字母a~z。它们的高2位为11。小写字母的编 码比对应的大写字母大20H。例如,'A'=41H,a' '=61H,a' '-'A'=b' '-B' '=…=20H。 完整的ASCII编码请参阅附录。 5.BCD 码 用一组二进制来表述一位十进制数,组间仍然按照逢十进一的规则进行。这种用二进 制表示的十进制数编码称为BCD码(BinaryCodedDecimal)。 有两种不同的BCD码。 压缩BCD码用一字节存储2位十进制数,高4位二进制表示高位十进制数,低4位二 进制表示低位十进制数。例如,[25]压缩BCD=00100101B。可以用相同数字的十六进制数来 书写压缩BCD码。例如,用25H 表示十进制数25的压缩BCD码。 非压缩BCD 码用一字节存储1位十进制数,低4位二进制表示该位十进制数,对高 4位的内容不作规定。例如,数字字符7' '的ASCII码37H 就是数7的非压缩BCD码。 从上面的叙述可以看出,计算机内的一组二进制编码和它们的“原型”之间存在着“一对 多”的关系。计算机内的一字节代码96H,可以代表十进制数96D 的压缩BCD 编码,代表 有符号数-106的补码、无符号数150,甚至还可以是通信用的同步字符(SYN)的偶校验 ASCII码。所以,面对计算机内的一组二进制编码,可能无法准确地知道它究竟代表什么。 知道它真面目的应该是这组二进制信息的主人,如汇编语言程序员。 3.1.2 数据的定义 1.数据段 汇编语言程序以段为单位书写。常见的情况是,数据定义在数据段里,程序写在代码 段内。每 个段有一个开始语句,一个结束语句。下面是一个例子: DATA SEGMENT ;DATA 数据段开始 ;在这里定义数据; .D ATA ENDS ;DATA 数据段结束 汇编语言对大小写字母不加区分,DATA 与data被认为是相同的名字。 在汇编语言里,一行只能写一个语句。一个语句是一条指令,或者是一条伪指令,或者 是对程序的注释。 ·46· 第一行的语句 DATA SEGMENT ;DATA 数据段开始 告诉汇编程序:一个名为DATA 的段从本行开始了。 标识符SEGMENT表示一个段的开始。这个单词已经被汇编语言固定使用,称为保留 字,用户不能把它用作其他用途。这个语句形式上与一条符号指令类似,但是汇编后不会产 生机器指令代码,这样的语句称为伪指令,对应的操作称为伪操作。 DATA 是程序员给这个段起的名字。 程序员应该给每个段起一个含义清晰的名字。本章中,你会看到命名为DATA_DSEG 的数据段,命名为CSEG_CODE的程序段,命名为STACK_SSEG 的堆栈段。段的名字用 字母或下划线开始,不能与保留字重名。 汇编语言把分号后面的文字看作是对程序的说明,称为注释,它不参加汇编,也不产生 结果。注释可以跟在指令或伪指令的后面,也可以是分号开始的独立的一行。 段定义的最后一行DATA ENDS表示命名为DATA 的一个段到此结束。ENDS是 一个保留字,它也是一个伪操作,汇编时不产生代码。 数据的定义语句就写在这两行的中间。不能在一个段的内部再定义另一个段,不同的 图3-2 字节数据的定义 段互相独立。 2.数据定义 1)伪操作DB(DefineByte,定义字节数据) 用来定义字节数据。所谓定义数据,就是给出数据,为它分 配存储单元,把它们用标准的格式存储到数据段中。例如,下面 的定义将产生图3-2所示的结果。 DATA SEGMENT X DB -1, 255, 'A', 3+2, ? DB "ABC", 0FFH,11001010B Y DB 3 dup(? ) DATA ENDS 上面的例子里,定义了多项数据。 (1)用DB定义的第2行表示在数据段存储5B的数据,数据 之间用逗号分隔。数据按照它们出现的先后顺序存储在数据段 里。所有数据由汇编程序翻译成等值的二进制代码存储。 (2)DB定义的数据,每个数据占用1B存储空间。如果是无符号数,应为[0~255]。 有符号数用补码存储,应为[-128~127]。综合起来,在[-128~255]的数据都可以用 DB来定义和储存,超出以上范围则无法存入,汇编程序将报告错误。 (3)可以出现用单/双引号括起来的单个/多个字符,每个字符占1B空间,按照它们出 现的顺序用ASCII代码存储。 (4)可以出现简单的可以求出值的表达式,如第2行的“3+2”,与直接写“5”效果相同。 “?”表示一个尚未确定的值,在程序运行时写入,一般先用“0”填充这个单元。 (5)X是程序员起的一个名字,代表了后面的若干个数据。因为这些数据的值在程序 ·47· 里可以被改变,所以X也称为变量名。和代数里的含义不同,汇编语言里变量名的真正含 义不是它所代表的变量的值,而是表示后面第一个数据的地址。 (6)数据在一行写不下时,可以另起一行,仍用DB定义,不要重复写相同的变量名。 (7)一般的情况下,段内的偏移地址从0开始,但是也可以不从0开始。无论怎样,数 据之间的相对顺序是固定的。 (8)DUP称为重复定义符,表示定义若干个相同的数据。本例中 DB 3 DUP(?) 等效于 DB ?, ?, ? 同样 DB 2 DUP(5) 等效于 DB 5, 5 DB 2 DUP(2, 3, 4 DUP(? )) 等效于 DB 2, 3, ? , ? , ? , ? , 2, 3, ? , ? , ? , ? 图3-3 字/双字数据的定义 2)伪操作DW(DefineWord,定义字数据) 用来定义字数据,每个数据占用2B空间,数据的高位存放在 地址较大的单元里。用DW 定义的数据范围应为[-32768~ 65535]。 3)伪操作DD(DefineDoubleword,定义双字数据) 用来定义双字数据,每个数据占用4B 空间,数据的高 位存放在地址较大的单元里。用DD 定义的数据范围应为 [-231~232-1]。 下面的定义将产生如图3-3所示的结果。 DSEG SEGMENT Z DW -2, -32768, 65535, 'AB' W DD 12345678H, -400000 DW Z, W-Z DSEG ENDS (1)有符号数据自动转换成它的补码,如DW 定义的-2 成为0FFFEH(以A~F开始的十六进制数都会在首部添加 一个0,以便与其他标识符区别)。高8位0FFH 存放在偏移 地址为0001H 的存储单元里,低8位0FEH 存放在偏移地址 为0000H 的存储单元里,用0000H 代表这个字数据的地址 (字地址)。 ·48· (2)字符串AB构成一个“字”数据,'A'的ASCII码成为高位,'B'的ASCII码成为低位, 这和DB定义时有些区别。 (3)“DW Z,W-Z”把Z的偏移地址,W 和Z偏移地址之差存放到存储器中。W 和 Z的偏移地址之差实际上代表了用变量名“Z”定义的所有数据占用存储器的字节数。类似 地,还可以把一个变量名写在用DD定义的一行中,它占用4B空间,地址较小的2B内容存 放这个变量名的偏移地址,地址较大的2B内容存放这个变量名所在段的段基址。 4)伪操作DQ 和DT用来定义8B、10B数据。 例如: DQ 12345678ABCDEF00H DT 112233445566778899AAH 大多数情况下,数据定义在一个独立的段里。有时候,为了某种需要,数据也可以定义 在其他段内,比如说,定义在程序段中。 3.1.3 数据的传送 据统计,在机器指令程序中,大约有30%的指令属于数据传送指令。本小节从数据传 送指令入手,介绍汇编语言程序的基本格式和编写方法。 1.指令格式 1)8086指令格式 汇编语言程序由若干个语句组成,每个语句占据源程序的一行。这些语句分成以下 3类。 (1)指令语句:包含一条符号指令,与一条机器指令相对应,汇编以后成为这条机器指 令的二进制代码,这个代码称为目标(Object)。 (2)伪指令语句:一条说明性的语句。有的伪指令语句汇编后没有结果,例如用 SEGMENT定义一个段的开始,汇编程序只是在内部对定义的段名进行登记,不产生目标。 有的伪指令汇编后产生目标,例如用DB定义的一个/若干个字节数据,汇编后产生与这些 数据对应的二进制代码。 (3)注释行:书写说明性文字,不进行汇编,也不产生目标。 下面就是一个指令行的例子: BEGIN: MOV AX, 0 ;将AX 寄存器清"0" 指令语句的一般格式如下: [标号:] 操作码 [操作数] [;注释] 标号是程序员给这一行起的名字,如上例的BEGIN,后面必须加上冒号。大多数的行不 需要标号。标号用字母开始,不允许使用保留字作为标号。方括号表示这项内容可以不出现。 操作码是这条指令需要完成的操作,用指令助记符表示,如上例中的MOV。操作码本 身就是保留字。 操作数是指令的操作对象。指令的操作数可以是0~3个,操作数之间用逗号隔开。大 多数指令有两个操作数,右面的操作数称为源操作数,左面的操作数称为目的操作数。源操 ·49· 作数参与指令操作,但是不存入结果,因此内容不会改变。目的操作数参与指令操作,还保 存指令的操作结果。指令执行后,目的操作数的内容被更新。上例中,常数0是源操作数, 寄存器AX是目的操作数,AX的内容在指令执行后被改变。 [;注释]用来添加一些说明,例如说明本行指令的功能。在重要指令处添加注释是一 个良好的习惯。 2)操作数 操作数有寄存器操作数、立即数操作数和存储器操作数3种类型。 (1)寄存器操作数。寄存器操作数包括段寄存器,通用数据、地址寄存器。例如,把寄 存器AX的内容送入DS寄存器可以用下面的指令: MOV DS, AX 其中,AX是源操作数,写在右边,指令执行后,它的内容不会被改变。DS是目的操作数,写 在左边,指令执行后,它的内容被改变。 寄存器IP和FLAG不能作为操作数出现在指令中。 (2)立即数操作数。二进制/十进制/十六进制常数,可求出值的表达式,字符,标号等 都可以用作操作数。例如,把常数300送入BX寄存器可以用下面的指令: MOV BX, 300 ;也可以写成MOV BX, 140*2+20 立即数不能用作目的操作数。 存储器操作数的表示方法比较灵活,将在下面单独介绍。 3)存储器操作数 为了对存储器的一个单元进行访问,需要给出这个单元的段基址和偏移地址。 大多数情况下,指令自动使用DS寄存器的内容作为操作数的段基址,为此编写汇编源 程序时,首先做的事情就是把数据段的段基址装入DS寄存器。 存储器操作数的偏移地址可以由几个部分组合而成,合成后得到的偏移地址称做有效 地址(EffectiveAddress,EA)。 给出偏移地址的方法有直接和间接两种。 (1)直接(偏移)地址。顾名思义,所谓直接地址就是在指令里直接写出存储单元的偏 移地址。例如: MOV AL, [1200H] ;从DS:1200H 读1B 内容,送入AL,方括号不能少 MOV AX, [1200H] ;从DS:1200H 读1W 内容([1200H]和[1201H]2B 内容),送入AX 从上面的例子可以看到,用常数书写的地址,可以代表1B的地址,也可以代表一个字 的地址,没有固定的属性。 上面的常数地址虽然一目了然,但一般情况下没有实用价值,因为一般都不知道一个具 体的地址单元里存放的是哪一个变量。这种写法容易导致错误,可读性也不好。 假设已经进行如下定义: DATA SEGMENT A DB 12, 34, 56 ARRAY DW 55, 66, 77, 88, 99 ·50· DATA ENDS 把DATA 代表的段基址装入DS后,现在需要取出变量A 的前两个数据12,34分别送 入BL,BH 寄存器,可以用下面的指令: MOV BL, A ;也可以写作 MOV BL, [A] MOV BH, A+1 ;也可以写作 MOV BH, [A+1]或MOV BH, A [1] 这里的A 代表数据12的偏移地址,它如同上面的[1200H]一样,是一个直接地址,A+1 是数据34的偏移地址。经过上面的数据段定义之后,A,A+1都是已知的地址。如注释所 说明的,这两项都可以加上方括号,效果相同。 可能会想到用一条指令同时取出2B内容: MOV BX, A ;把变量[A]送入BL,变量[A+1]送入BH 这条指令是错误的。这是因为源操作数是字节变量,而目的操作数是字,两个不同类型 的操作数不能直接传送。 (2)间接(偏移)地址。所谓间接地址,就是把存储单元的偏移地址事先装入某个寄存 器,需要时通过这个寄存器来找到这个存储单元,所以也称为寄存器间接寻址。如上例所 示,为了把这两个数据装入BL,BH 寄存器,可以这样编程: MOV SI, OFFSET A ;把变量A 的偏移地址装入SI ;OFFSET 是保留字,表示取出后面变量的偏移地址 MOV BL, [SI] ;SI 中存放变量"A"的地址,变量A 的第一个值送入BL MOV BH, [SI+1] ;第二个值送入BH,也可以写作MOV BH, 1[SI] 对于16位80x86微处理器,只有BX、BP、SI、DI这4个寄存器可以用做间接寻址。不 另加说明的话,使用BP时自动用SS的值作为段基址,使用BX、SI、DI时自动用DS的值作 为段基址。 如果要取出字数组ARRAY的第3个成员“77”送入AX,下面3种方法效果相同。 方法1: MOV AX, ARRAY [4] ;ARRAY 代表数组首地址,位移量=4,直接寻址 ;也可以写作"MOV AX, ARRAY+4" 方法2: MOV BX, OFFSET ARRAY ;数组首地址装入BX MOV AX, [BX+4] ;第3 个元素距数组首元素4 个字节 方法3: MOV BX, 4 ;数组第3 个成员距数组首地址的位移量装入BX MOV AX, ARRAY [BX] ;ARRAY 代表数组首地址,BX 中是位移量 可以用两个寄存器联合起来寻址。但是只能分别从(BX/BP)和(SI/DI)中各选出一个 使用。同样,出现BP意味着使用SS作为段基址寄存器。 下面都是合法的寻址方式例子: ·51· MOV AX, ARRAY[4] ;直接寻址,EA=ARRAY+4,使用DS MOV AX, [BX] ;寄存器间接寻址,使用DS MOV AX, [BP+2] ;寄存器相对寻址,BP 中存放首地址,位移量2,使用SS MOV AX, ARRAY [BX] ;寄存器相对寻址,ARRAY 为首地址,BX 中存放位移量,使用DS MOV AX, [BX+SI] ;基址(BX)变址(SI)寻址,使用DS MOV AX, [BP+DI+2] ;相对基址变址寻址,使用SS 2.程序段 假设已定义数据段为DATA,程序段的常见格式如下: CODE SEGMENT ASSUME CS: CODE, DS: DATA START: MOV AX, DATA MOV DS, AX ;其他指令 MOV AX, 4C00H INT 21H CODE ENDS END START 代码段的开始、结束和数据段类似。这里定义了一个名为CODE的程序段。 START是第一条指令的标号。应注意标号与变量名的区别:标号出现在指令行前面, 标号与指令之间用冒号( )分开。本程序的执行从标有START的第一条指令开始,它的地 址称为这个程序的入口地址。 标号START开始的两条指令用于装载数据段寄存器DS。进入程序后,代码段寄存器 CS已经由操作系统设置为代码段的段基址,数据段的段基址则需要由用户装入到DS中。 程序中,ASSUME伪指令用来指定段和段寄存器之间的对应关系,供汇编程序使用。 使用多个数据段时,可以清晰地看出ASSUME伪指令的作用。 假设有两个数据段定义如下: DATA SEGMENT A DB 55 DATA ENDS DSEG SEGMENT X DB 10 DSEG ENDS 代码段中对段的说明如下: ASSUME DS: DATA, ES: DSEG, CS: CODE 假设各段的段基址已经装入对应寄存器,并假设变量A 和X的偏移地址都是0000H。 指令 MOV AL, A 自动按照 ·52· MOV AL, DS:[0000H] 的格式汇编,结果正确。 指令 MOV DL, X 自动按照 MOV DL, ES:[0000H] 的格式汇编,结果正确。 指令中的DS:和ES:指出数据所在的段。上面两条指令汇编以后,都能够正确地执行, 取到正确的数据:(AL)=55、(DL)=10。 但是,如果这样来取数据: MOV SI, OFFSET A ;将A 的偏移地址装入SI MOV DI, OFFSET X ;将X 的偏移地址装入DI MOV AL, [SI] ;取A 的值送给AL MOV DL, [DI] ;取X 的值送给DL 执行的结果:(AL)=55、(DL)=55,这不是预期的结果。 出现错误的原因在于,虽然已经把X 的偏移地址装入DI,但是执行指令MOV DL, [DI]时,仍然会使用DS寄存器中的段基址。为了避免上述错误,取X值的指令要改为 MOV DL, ES: [DI] 这条指令显式地指定了段基址,汇编出来的机器指令比MOVDL,[DI]多1B,称为段 跨越前缀。 注意:ASSUME伪指令仅仅说明了段和段寄存器之间的对应关系,段基址的装入仍然 需要程序员通过指令实现。 程序的最后两条指令用来结束程序运行,返回操作系统。指令INT21H 表示调用由操 作系统提供的21H 号服务程序。这个程序可以提供从键盘输入、显示器输出、文件操作等 许多的服务,本次需要完成的服务的种类由AH 中的功能号指定。本例中AH=4CH,表示 返回操作系统的操作。装入AL中的代码称为返回代码,用00H 表示正常返回。 与数据段相似,伪指令CODEENDS表示代码段CODE结束。 最后一行END伪指令表示整个程序到此结束,在它下面书写的任何代码都不会被汇 编成目标。因此,所有的段都应该写在END伪指令之前。这一行里的标号START定义这 个程序的入口地址。如果在END 之后没有写上入口标号,汇编程序会把整个源程序第一 行作为入口,不管这第一行究竟是指令还是数据,这可能导致程序不能正常地执行。 综上所述,一个较完整的汇编语言源程序可以包含如下内容: . 数据段定义; . 代码段定义; . 程序结束伪指令。 3.基本传送指令 传送指令是使用最频繁的指令,要熟练地掌握使用。 ·53· 1)MOV(Move,传送)指令 MOV 指令的一般格式如下: MOV dest, src MOV 指令把一个源操作数(source,src)传送到目的操作数(destination,dest)。指令 执行后,源操作数的内容不变,目的操作数的内容与源操作数相同。例如,指令执行前, (AX)=2345H,(BX)=1111H。指令MOV AX,BX 执行后,(AX)=1111H,(BX)= 1111H。BX的内容复制到AX寄存器内。 源操作数可以是寄存器操作数、存储器操作数、立即数。 目的操作数可以是寄存器操作数、存储器操作数。 图3-4列出了正确的和错误的数据传送方向。图中,I、R、M、S分别代表立即数、寄存 器、存储器、段寄存器操作数。 图3-4 数据传送操作 MOV 指令的使用有如下限制。 (1)源操作数与目的操作数可以是字节、字或双字,但必须具有相同的类型。 (2)源操作数与目的操作数不能同时为存储器操作数。 (3)目的操作数不能是立即数。 (4)FLAG、IP不能用作操作数。 (5)对于段寄存器作为操作数的MOV 指令。 ① 源操作数与目的操作数不能同时为段寄存器。 ② 目的操作数是段寄存器时,源操作数只能是寄存器或存储器,不能是立即数。 ③ CS不能用作目的操作数。 假设变量X_BYTE用DB定义,变量Y_WORD用DW 定义,它们所在段在ASSUME 伪指令中与DS寄存器相对应。下面都是正确的传送指令: MOV AL, 30H ;字节传送指令,执行后(AL)=30H MOV AX, 30H ;字传送指令,30H=0030H,执行后(AX)=0030H MOV AL, -5 ;字节传送指令,[-5]补=0FBH,执行后(AL)=0FBH MOV AX, -5 ;字传送指令,[-5]补=0FFFBH,执行后(AX)=0FFFBH MOV CX, DX ;字传送指令,将DX 寄存器内容送入CX MOV AX, CS ;字传送指令,将CS 寄存器内容送入AX MOV X_BYTE, 30H ;字节传送指令,执行后存储单元(X_BYTE)=30H MOV [BX], AX ;字传送指令,将AL 内容送入DS:[BX],将AH 内容送入DS:[BX+1] ·54· MOV CX, Y_WORD ;字传送指令,将存储单元DS:[Y_WORD]内容送入CL ;将存储单元DS:[Y_WORD +1]内容送入CH MOV DX, [SI] ;字传送指令,将DS: [SI]内容送入DL,将DS: [SI+1]内容送入DH MOV [BP], BL ;字节传送指令,将BL 寄存器内容送入SS: [BP]处一个字节 使用立即数作为源操作数时,该立即数会按照目的操作数的类型进行扩展。如果立即 数本身没有符号,进行零扩展,如果立即数本身有符号,进行符号扩展。 仍然假设变量X_BYTE 用DB 定义,变量Y_WORD 用DW 定义,下面是错误使用 MOV 指令的例子: MOV AX, X_BYTE ;类型不匹配 MOV X_BYTE, [BX] ;不允许同时为内存操作数 MOV CS, AX ;CS 不允许作为目的操作数 MOV DS, CS ;不允许同时为段寄存器 MOV DS, 2300H ;目的操作数为段寄存器时,源操作数不能为立即数 MOV DS, DATA ;DATA 是已定义的数据段名,相当于立即数 MOV AX, [DX] ;不能用DX 进行存储器间接寻址 MOV CL, 300 ;源操作数超出范围 MOV [BX], 20 ;无法确定操作数类型 可以用“类型PTR”指定,或强行改变操作数的类型: MOV BYTE PTR[BX], 20H ;将1B 立即数20H 送入DS:[BX] MOV WORD PTR[BX], 20H ;将立即数20H 送入DS:[BX],将00H 送入DS:[BX+1] MOV BYTE PTR[Y_WORD], 20H ;将立即数20H 送入字变量Y_WORD 的第一字节 MOV AL, BYTE PTR[Y_WORD] ;将字变量Y_WORD 的第一字节送入AL 寄存器 MOV WORD PTR[X_BYTE], 20H ;将2B 立即数00 20H 送入变量X_BYTE 开始的2 字节 变量名一经定义,就已经具有明确的类型,要谨慎使用“类型PTR 变量名”操作数。 MOV 指令执行之后,FLAG寄存器内各标志位的状态不会发生变化。 2)LEA(LoadEffectiveAddress,装载有效地址)指令 LEA 把源操作数的偏移地址装入目的操作数。它的一般格式如下: LEA REG16, MEM REG16表示一个16位通用寄存器,MEM 是一个存储器操作数。上面的指令把存储 器操作数的有效地址EA 存入指定的16位寄存器。 假设变量X的偏移地址为048CH,(BP)=1820H,(SI)=0068H LEA DX, X ;执行后,(DX)=048CH LEA BX, 4[BP][SI] ;执行后,(BX)=4+1820H+0068H=188CH 上面第一条指令的功能等同于 MOV DX, OFFSET X 但是它们是两条不同的指令。 利用MOV 和LEA 指令,可以编写简单的数据传送程序。 【例3-1】 编写程序,把字节数组ARRAY的4个元素清“0”。 ·55· ;第一个汇编语言源程序,文件名EX301.ASM ;程序的功能:把字节数组ARRAY 的4 个元素清"0" DATA SEGMENT ARRAY DB 4 DUP (0FFH) DATA ENDS ; CODE SEGMENT ASSUM E CS: CODE, DS: DATA START: MOV AX, DATA MOV DS, AX MOV ARRAY, 0 ;第1 个元素清"0" MOV ARRAY+1, 0 ;第2 个元素清"0" MOV ARRAY+2, 0 ;第3 个元素清"0" MOV ARRAY+3, 0 ;第4 个元素清"0" MOV AX, 4C00H INT 21H ;正常返回操作系统 CODE ENDS END START 这个程序里,源操作数使用立即数,目的操作数使用直接地址的寻址方式。 如果把数组ARRAY的首地址事先装入地址寄存器,常数0装入AX,则程序更简捷: MOV AX, 0 LEA BX, ARRAY ;数组ARRAY 首地址装入BX MOV WORD PTR [BX], AX ;第1、第2 个元素清"0" ;"WORD PTR"可省略 MOV [BX+2], AX ;第3、第4 个元素清"0" 上面的程序里,BX寄存器存放数组ARRAY的首地址,可以通过BX访问数组的各个 元素。这种存放地址的寄存器或者存储单元称为地址指针,简称指针(Pointer)。 4.其他传送指令 1)地址传送指令LDS,LES 指令格式如下: LDS REG16, MEM32 ;从存储器取出4B,分别送入REG16 和DS 寄存器 LES REG16, MEM32 ;从存储器取出4B,分别送入REG16 和ES 寄存器 地址传送指令从存储器取出4B,前面的2B(地址较低)送入指定的16位寄存器,后面 的2B(地址较高)送入由指令操作码包含的段寄存器。 例如,指令LDS SI,[BX]从DS:[BX]处取出32位二进制,两个低地址字节送入SI, 两个高地址字节送入DS寄存器。指令执行后DS寄存器的内容被刷新。 这两条指令的执行不影响标志位。 2)扩展传送指令CBW,CWD 把累加器(AL/AX)的操作数符号扩展为16/32位,送入目的寄存器。指令格式如下: CBW ;将AL 寄存器内容符号扩展成16 位,送入AX ·56· CWD ;将AX 寄存器内容符号扩展成32 位,送入DX(高位)和AX(低位) 指令助记符CBW 是ConvertByteto Word的缩写,其余类似。这组指令主要用于有 符号数除法前对被除数的位数进行扩展。 设有(AX)=8060H,分别执行下面指令后的结果如下: CWD ;(DX)=0FFFFH,(AX)=8060H CBW ;(AX)=0060H 3)交换指令XCHG XCHG指令交换源、目的操作数的内容,要求两个操作数有相同的类型,不能为立即 数,不能同时为存储器操作数。指令格式如下: XCHG REG/MEM, REG/MEM 例如,(AX)=5678H,下面指令执行后的结果如下面右侧所示: XCHG AH, AL ;执行后(AX)=7856H 4)换码指令XLAT 换码指令用AL寄存器的内容查表,结果存回AL寄存器。要求表格的首地址事先存 放在DS:BX中。指令格式如下: XLAT ;AL←DS: [BX+AL] XLAT MEM16 ;以MEM16 所在段寄存器为段基址,以BX 为偏移地址查表 设(AL)=00001011B=0BH,下面程序执行后,AL中的二进制数改变为对应的十六进 制数字符的ASCII代码01000010('B')。 TABLE DB "0123456789ABCDEF" .P USH DS ;保护DS 寄存器内容 MOV BX, SEG TABLE ;取出TABLE 所在的段基址送入BX,"SEG"表示取"段基址" MOV DS, BX ;段基址从BX 转送入DS LEA BX, TABLE ;取出TABLE 的偏移地址 XLAT ;查表,(AL)=0100 0010('B') POP DS ;恢复DS 寄存器内容 5.堆栈 和数据段、代码段一样,堆栈(STACK)也是用户使用的存储器的一部分,用来存放一些 临时性的数据和其他信息,例如函数使用的局部变量、调用子程序的入口参数、返回地址等。 1)堆栈段结构 下面是一个堆栈段的定义: SSEG SEGMENT STACK DW 6 DUP(? ) SSEG ENDS 在SEGMENT伪指令中增加STACK表示该段是堆栈。有了这项说明,操作系统在装 ·57· 入这个程序时,会自动地把SSEG 的段基址置入SS,堆栈段的字节数(本例中为12= 000CH)置入SP。这样,SP指向了这个堆栈的栈顶。 堆栈段的使用和数据段、代码段有以下不同。 ① 从较大地址开始分配和使用(数据段、代码段从较小地址开始分配和使用)。 ② SP中地址指出的存储单元称为栈顶。进行堆栈操作时,数据总是在栈顶位置存入 (称为压入),取出(称为弹出)。 ③ 最先进入的数据最后被弹出(FirstInLastOut,FILO),最后进入的数据最先被弹 出(LastInFirstOut,LIFO)。 以上面的定义为例,堆栈的初始状态,装入、弹出数据后的状态如图3-5所示。 图3-5 堆栈段结构 上面定义的堆栈,占用SS:0000H~SS:000BH 共12个字节单元。 堆栈尚未使用时,堆栈为空,SP指向堆栈下面的单元,如图3-5(a)所示,(SP)=0012H。 对于8086CPU,进、出堆栈只能是2B数据。压入一个2B数据的操作如下: SP←(SP)-2 SS: [SP]←数据 例如,数据1122H 压入堆栈后,如图3-5(b)所示,由SP指出的栈顶位置上移,(SP)= 000AH,SS:[SP]=22H,SS:[SP+1]=11H。 第二个数据3344H 进栈后堆栈的状态如图3-5(c)所示。 从堆栈弹出一个数据的操作如下: 目的操作数←SS: [SP] SP←(SP)+2 如图3-5(d)所示,弹出一个数据后,栈顶的位置下移,(SP)=000AH,堆栈段存储器的 内容实际上没有发生变化,但是从逻辑上可以认为,堆栈中只有一项数据:1122H。 2)8086CPU 堆栈指令和标志寄存器指令 8086CPU 堆栈指令和标志寄存器指令如表3-2所示。 表3-2 8086CPU 堆栈指令和标志寄存器指令 指令名称操作码指令格式功 能主要用途 压栈PUSH PUSH REG16/ MEM16/SEG SP←(SP)-2, SS:[SP]←字数据 数据入栈保护 ·58· 续表 指令名称操作码指令格式功 能主要用途 出栈POP POPREG16/ MEM16/SEG 目的操作数←SS:[SP], SP←(SP)+2 数据从堆栈恢复 标志寄存器入栈PUSHF PUSHF SP←(SP)-2, SS:[SP]←FLAGS 标志寄存器入栈 保护 标志寄存器出栈POPF POPF FLAGS←SS:[SP], SP←(SP)+2 从堆栈恢复标志寄 存器 装载FLAGS LAHF LAHF FLAGS低8位←AH 装载FLAGS低8位 卸载FLAGS SAHF SAHF AH←FLAGS低8位保存FLAGS低8位 下面的程序段把CS寄存器内容存入DS: PUSH CS POP DS 下面的程序段把TF标志位置位(置“1”): PUSHF POP AX ;AX←Flags OR AX, 0100H ;在AX 内将b8(TF 位)置"1" PUSH AX POPF ;Flags←AX 6.操作数表达式 指令中的操作数,包括立即数和存储器操作数都可以用一个表达式来代替,这个表达式 在汇编成目标的时候进行计算,它的结果用来产生目标代码。例如,设变量X 的偏移地址 为1020H,汇编指令MOV AL,X+5时,把X 的偏移地址1020H 和5相加,得到结果 1025H,产生MOV AL,[1025H]对应的机器指令代码。 1)符号定义伪指令 可以用EQU 和“=”来定义一个符号,这个符号在后面的指令中用作立即操作数或者 存储器操作数。符号定义伪指令的格式如下: 符号名 EQU 表达式 符号名 = 常数表达式 汇编时,对EQU 定义的符号名用对应的表达式进行替换。例如,有以下定义: NUM EQU 3+2 ERR_MSG EQU "Data Override" POINTER EQU BUFFER [DI] WP EQU WORD PTR 下面是这些符号名使用的例子: MESSAGE DB ERR_MSG ;等价于MESSAGE DB "Data Override " MOV BX, POINTER ;等价于MOV BX, BUFFER [DI] ·59· MOV CX, NUM*4 ;等价于MOV CX, 3+2*4,11 送入CX MOV WP POINTER, 0 ;等价于MOV WORD PTR BUFFER[DI], 0 使用“=”定义符号名时,只能使用常数表达式,而且对一个符号名可以多次定义。一个 新的定义出现后,原来的定义自动终止。例如: TIMES=0 .T IMES=TIMES+1 . 用EQU 定义的符号名不允许重复定义。 将多次出现、不便记忆的常数、表达式定义为符号名,有助于提高程序的可读性和可靠 性。这个常数/表达式内容需要修改时,只需要修改一条符号定义伪指令,而不需要搜索整 个程序多处修改它。两种符号定义有一个共同的规则:先定义,后使用。所以,符号定义伪 指令一般出现在源程序的首部。 2)地址表达式 指令中的存储器操作数最终都是以偏移地址为结果,产生对应的有效地址(Effective Address,EA)。用于计算有效地址的地址表达式有3个运算符:+、-、[]。 +和-运算符对构成有效地址的各个分量进行加、减操作。仍然设变量X的偏移地址 为1020H,X+5产生EA=1025H,指令MOVBL,X-10H 产生EA=1010H。 []称为索引运算符,用来括起组成有效地址的一个分量,各分量相加,得到最后的有效 地址。例如,指令MOV AX,2[BX][DI]等效于MOV AX,[BX+DI+2],指令MOV AX,BUFFER[BX][2]等效于MOV AX,[BUFFER+BX+2]。 3)立即数表达式 立即数表达式在汇编源程序时进行计算,它的结果用作指令中的立即数操作数。这种 表达式中的运算对象必须是已知的,否则无法进行计算。用于产生立即数操作数的表达式 有4类运算符:算术运算符、逻辑运算符、关系运算符和地址运算符。 (1)算术运算符。算术运算符有+(相加)、-(相减)、*(相乘)、/(整除运算)和MOD (取余数)。运算优先级从高到低依次为(*,/)→(MOD)→(+,-)。允许使用圆括号改 变运算顺序。 例如,指令MOV BX,32+13/6MOD3中,表达式计算顺序是32+((13/6)MOD3),得 到结果34,该指令汇编后产生与MOVBX,0022H 相同的机器指令代码。 (2)逻辑运算符。逻辑运算符有SHR(右移)、SHL(左移)、AND(逻辑与)、OR(逻辑 加)、XOR(异或,半加)和NOT(逻辑非、取反)。例如30SHR1产生结果15。 (3)关系运算符。关系运算符用于两个数的比较,结果为“真(-1)”或“假(0)”。运算 符有GT(大于)、GE(大于或等于)、LT(小于)、LE(小于等于)、EQ(等于)和NE(不等于)。 例如,指令MOV AX,6000H GE5000H 中的表达式结果为“真”,产生指令MOV AX,0FFFFH 对应的机器代码。指令MOV AX,-3EQ2中的表达式结果为“假”,产生 指令MOVAX,0000H 对应的机器代码。 (4)地址运算符。地址运算符对变量名、标号、地址表达式进行计算,得到作为立即数 ·60· 的运算结果。 SEG取地址表达式所在段的段基址。设变量LIST定义在DATA 段中,下面3条指令 都是把DATA 段的段基址装入AX: MOV AX, DATA ;符号名DATA 代表该段的段基址,是一个立即数 MOV AX, SEG DATA ;取DATA 的段基址,结果是立即数 MOV AX, SEG LIST ;取LIST 的段基址,结果是立即数 OFFSET取地址表达式的偏移地址,下面两条指令进行了不同的操作: MOV AX, LIST ;取出字变量LIST 第一个元素(2B)送入AX MOV AX, OFFSET LIST ;取出变量LIST 的偏移地址送入AX TYPE、LENGTH 和SIZE这3个运算符仅仅对变量名、标号进行操作,分别用于取变 量、标号的类型,取变量定义时的元素个数,取变量占用的字节数。例如: X DB "ABCDE" ;TYPE=1, LENGTH=1, SIZE=1 Y DW 3 DUP(5), 4 DUP(-1) ;TYPE=2, LENGTH=3, SIZE=6 Z DD 34, 49, 18 ;TYPE=4, LENGTH=1, SIZE=4 不同的汇编语言版本对上面例子的处理结果可能不同,本书以BorlandTASM 5.x 为例。再 次强调一下:上面所有的表达式都必须是汇编期间可以求值的。MOV AX,BX+ 2是一条错误的指令,汇编时将报告错误,原因在于BX的值是未知的,可变的,在汇编阶段 无法进行相关的计算。需要把BX的值与常数2相加并存入AX的操作只能在程序执行阶 段由以下两条指令完成: MOV AX, BX ;BX 寄存器的值存入AX 寄存器 ADD AX, 2 ;AX 寄存器的值加上2,结果存入AX 3.1.4 简化段格式 除了前面所述的汇编语言源程序格式之外,还有一种称为简化段格式。整个源程序组 成如下: 内存模式说明 [数据段定义] [堆栈段定义] [代码段定义] END 入口标号 其中,带有方括号的内容不是必需的,根据需要选择使用,也可以改变出现的次序。 1.内存模式说明 内存模式说明的格式如下: .MODEL 内存模式 可选择的内存模式如下。 ·61· (1)Tiny:微型,整个程序只有一个段,供.COM 格式程序使用。 (2)Small:小型,程序由一个代码段、一个数据段(包括堆栈段)组成。 (3)Medium:中型,有多个代码段,但只有一个数据段。 (4)Large:大型,有多个代码段和多个数据段。 (5)Flat:平坦,供编写32位微处理器的汇编语言源程序,在Windows下运行。 2.数据段定义 数据段定义格式如下。 .DATA ;常用的格式 或者 .FARDATA [数据段段名] ;定义第二个数据段 或者 .DATA? ;定义一个没有初始值的数据段 只有一个数据段时,.DATA 表示数据段的开始,随后可以定义各项数据。DATA 是保 留字,不能随意更换。使用.DATA? 可以减少可执行文件大小。 3.堆栈段定义 堆栈段定义格式如下: .STACK [堆栈段大小] 省略堆栈段大小时,堆栈段自动设置为1024B(1KB)。程序装入执行时,根据所定义的 堆栈段自动装载SS,SP寄存器。 4.代码段定义 代码段定义格式如下: .CODE [代码段段名] 只有一个代码段时可以省略代码段段名。 使用上述格式时,一个段的开始同时意味着上一个段的结束,也就是说,只需要声明段 的开始,不需要声明段的结束。 使用简化段定义格式重写例3-1如下: .MODEL SMALL .DATA ARRAY DB 4 DUP (0FFH) .CODE START: MOV AX, @DATA ;@DATA 代表.DATA 定义的数据段段基址 MOV DS, AX ;装载DS … ;第1 个元素清"0" . ;…… MOV AX, 4C00H INT 21H END START ·62·