汇编语言伪指令

一、处理机选择伪指令#

80x86 的所有处理器都支持 8086 指令系统,随着处理器的升级也增加了一些新的指令。为了能使用这些新增指令,在编写程序时要用处理机选择伪指令对所用的处理机作出选择,也就是说,要告诉汇编程序应该选择哪一种指令系统。

处理机选择伪指令有以下几种:

  • ·8086  选择 8086 指令系统
  • ·286  选择 80286 指令系统
  • ·286P  选择保护方式下的 80286 指令系统
  • ·386  选择 80386 指令系统
  • ·386P  选择保护方式下的 80386 指令系统
  • ·486  选择 80486 指令系统
  • ·486P  选择保护方式下的 80486 指令系统
  • ·586  选择 Pentium 指令系统
  • ·586P  选择保护方式下的 Pentium 指令系统

指令中的点“·”是需要的。这类伪指令一般放在代码段中的第一条指令前即可。如不给出,则汇编程序认为其默认选择是 8086 指令系统。

二、段定义伪指令#

我们结合之前已介绍的程序实例 2(稍作修改)来看段定义和选择 80386 指令,注意有分号的注释行,程序如下:

例 1 段定义和支持 386 指令。

data  segment       ; 定义数据段data

buff  db 'hello,world!$'

data  ends

code  segment       ; 定义代码段code

assume cs:code,ds:data    ; 指定段寄存器和段的关系

·386             ; 选择386指令

start: mov ax,data      ; 对ds赋data段基地址

mov ds,ax

lea bx,buff

mov eax,'ABCD'    ; eax是386指令系统中的32位寄存器

mov[bx],eax

mov dx,offset buff

mov ah,9

int 21h

code  ends

end start

图 1 为程序的调试,注意:程序中的第 4、第 5 这两条 32 位机指令,是把‘ABCD’作为 32 位常数送到 buff,由图中可见,这两条指令在 Debug 下已变成面目全非的 6 条指令(偏移地址从 0008 至 000F),这是因为 Debug 下不能显示 32 位机指令,但并不影响程序的执行。

支持 386 指令的程序

图 1 支持 386 指令的程序

  1. 段定义伪指令

汇编程序在把源程序转换为目标程序时,只能自动确定标号和变量(代码段和数据段的符号地址)的偏移地址,程序中对于段地址也要作出说明,段地址一旦说明,该段内的指令、标号和变量都属于这个段。

段定义伪指令格式:

segment_name  SEGMENT

…

segment_name  ENDS

其中 segment_name 由用户确定,大写的为关键字。段定义伪指令两句成对出现,两句之间为其他指令。

为了确定用户定义的段和哪个段寄存器相关联,用 ASSUME 伪指令来实现。

ASSUME 伪指令格式:

ASSUME register_name:segment_name …,register_name:segment_name

其中 register_name 为段寄存器名,必须是 CS,DS,ES 和 SS。而 segment_name 则必须是由段定义伪指令定义的段中的段名。

ASSUME 伪指令只是指定把某个段分配给哪一个段寄存器,它并不能把段地址装入段寄存器中,所以在代码段中,还必须把段地址装入相应的段寄存器中。为此,还需要用两条 MOV 指令完成这一操作。但是,代码段不需要这样做,代码段的这一操作是在程序初始化时完成的。应该还记得,不允许对 CS 寄存器赋值。

  1. 简化的段定义伪指令

MASM 5.0 以上版本还支持一种简化的段定义方法,可把例 1 程序用简化的段定义方法改写如下:

例 2

·model  small      ; 定义存储模型为small

·data          ; 定义数据段data

string  db 'hello,world!$ '

·code          ; 定义代码段code

start: mov ax,@data   ; 对ds赋data段基地址

mov ds,ax

mov dx,offset string

mov ah,9

int 21h

mov ah,4ch

int 21h

end start

首先用 ·MODEL 伪指令说明在内存中如何安排各个段,存储模型为 SMALL 的意思是:所有数据都放在一个 64KB 的数据段,所有代码都放在另一个 64KB 的代码段,数据和代码都为近访问。这是最常用的一种模型。

·DATA 伪指令用来定义数据段,但没有给出段名,默认段名是 _DATA。

@DATA 表示段名 _DATA,在指令中表示段地址。

简化段定义的表达能力不如 SEGMENT 伪指令那样完整而清楚,所以很多时候还是用 SEGMENT 伪指令。

三、程序开始和结束伪指令#

表示源程序结束的伪操作的格式为:

END[label]

汇编程序将在遇到 END 时结束汇编。其中标号 label 指示程序开始执行的起始地址。如果是多个程序模块相连接,则只有主程序需要使用标号,其他子程序模块则只用 END 而不能指定标号。

四、数据定义与存储器单元分配伪指令#

我们知道,指令语句的一般格式是:

[标号:] 操作码  操作数 [; 注释]

和指令语句格式类似,这一类伪指令的格式是:

[变量]  操作码  N个操作数 [; 注释]

其中变量字段是可有可无的,它用符号地址表示。其作用与指令语句前的标号相同。但它的后面不跟冒号。

操作码字段说明所用伪操作的助记符,即伪操作,说明所定义的数据类型。常用的有以下几种。

DB:伪操作用来定义字节,其后的每个操作数都占有一个字节(8 位)。

DW:伪操作用来定义字,其后的每个操作数占有一个字(16 位,其低位字节在第一个字节地址中,高位字节在第二个字节地址中,即数据低位在低地址,数据高位在高地址)。

DD:伪操作用来定义双字,其后的每个操作数占有两个字(32 位)。

DF:伪操作用来定义 6 个字节的字,其后的每个操作数占有 48 位。

DQ:伪操作用来定义4个字,其后的每个操作数占有 4 个字(64 位),可用来存放双精度浮点数。

DT:伪操作用来定义 10 个字节,其后的每个操作数占有 10 个字节,为压缩的 BCD 码。

(需要说明的是,MASM6 允许 DB,DW,DD,DF,DQ,DT 伪操作分别用 BYTE,WORD,DWORD,FWORD,QWORD,TBYTE 代替)。

这些伪操作可以把其后跟着的数据存入指定的存储单元,形成初始化数据;或者只分配存储空间而并不确定数值。下面举例说明各种用法。

例 3 操作数为常数、数据表达式。

D_BYTE  DB 10,10H

D_WORD  DW 14,100H,-5,0ABCDH

D_DWORD  DD 4×8

程序中默认的数据为十进制数,10H 为十六进制数,用 DB 定义的数据的值不能超出一个字节所能表示的范围。数据 10 的符号地址是 D_BYTE,数据 10H 的符号地址是 D_BYTE+1。

数据可以是负数,均为补码形式存放。允许数据表达式,如 4×8,等价为 32。若数据第一位不是数字,应在前面加 0,如 0ABCDH。数据在内存中的存放如图 2 所示。

例 3 的汇编结果

图 2 例 3 的汇编结果

例 4 操作数为字符串。问号‘?’仅预留空间。数据在内存中的存放如图 3 所示。

MESSAGE  DB 'HELLO?',?    ; 问号?通常被系统置0

DB 'AB',?

DW 'AB'      ; 注意这里’AB’作为串常量按字类型存放
例 4 的汇编结果

图 3 例 4 的汇编结果

例 5 用操作符复制操作数。数据在内存中的存放如图 4 所示。

例 5 的汇编结果

图 4 例 5 的汇编结果

例 6 根据需要自己定义的各类数据,含义由自己决定。数据在内存中的存放如图 5 所示。

X1  DB 14,3      ; 十进制小数3.14

Y2  DW 1234H,5678H   ; 32位数据十六进制数56781234H

Y3  DW 22,9      ; 32位数据十六进制数00090016H
例 6 的汇编结果

图 5 例 6 的汇编结果

五、类型属性操作符#

WORD PTR  ; 字类型

BYTE PTR  ; 字节类型

通常访问内存变量要知道它的符号地址,以便定位,还要知道它的类型(长度),以便匹配,如果指令中不可避免地出现两个类型(长度)不匹配的操作数时,可以在指令中对该内存变量使用类型属性操作符指定访问类型。这里注意仅是“访问类型”,并不是改变了变量本身的类型,访问类型的作用只是增加了一种访问方式,例如,对一个 8 位(或 16 位)的变量可以用 16 位(或 8 位)方式访问。

例 7 在指令中用类型属性操作符指定对内存变量的访问类型,以匹配两个操作数。

OPER1  DB 3,4

OPER2  DW 5678H,9

┇

MOV  AX,OPER1   ; 操作数类型不匹配

MOV  BL,OPER2   ; 操作数类型不匹配

MOV [DI],0    ; 操作数类型不明确

前两条指令操作数类型不匹配,第三条指令的两个操作数类型都不明确,所以都是错误的。解决的办法是可在指令中对操作数为内存变量的指定访问类型,以使操作数类型匹配和明确。这三条指令可改为:

MOV  AX,WORD PTR OPER1   ; 从OPER1处取一个字使AX=0403H

MOV  BL,BYTE PTR OPER2   ; 从OPER2处取一个字节使BL=78H

MOV  BYTE PTR[DI],0     ; 常数0送到内存字节单元

六、THIS 操作符和 LABEL 伪操作#

在例 7 中使用类型属性操作符,使指令书写过长。实际上一个变量也可以定义成不同的访问类型,通过 THIS 操作符或 LABEL 伪操作都可以实现。

使用 THIS 操作符:

格式:THIS type

使用 LABEL 伪操作:

格式:name LABEL type

只是指定一个类型为 type 的操作数,使该操作数的地址与下一个存储单元地址相同。type 在这里是 BYTE 或者 WORD。

例 8 把变量定义成不同访问类型,以便指令中可灵活选用。指令执行结果如图 6 所示。

BUF=THIS WORD

DAT  DB 8,9

OPR_B  LABEL BYTE

OPR_W  DW 4 DUP(2)

┇

MOV  AX, 1234H

MOV  OPR_B,AL

MOV  OPR_W+2,AX

MOV  DAT+1,AL

MOV  BUF,AX

表达式 BUF=THIS WORD 使 BUF 和 DAT 指向同一个内存单元。

LABEL 伪操作使得 OPR_B 和 OPR_W 指向同一个内存单元。

使用 THIS 和 LABEL

图 6 使用 THIS 和 LABEL

七、表达式赋值伪指令“EQU”和“=”#

可以用赋值伪操作给表达式赋予一个常量或名字。其格式如下:

Expression_name EQU Expression

Expression_name=Expression

上式中的表达式必须是有效的操作数格式或有效的指令助记符,此后,程序中凡需要用到该表达式之处,就可以用表达式名来代替了。举例如下:

VALUE   EQU 4

DATA   EQU VALUE+5

ADDR   EQU [BP+VALUE]

此后,指令 MOV AX,ADDR 就代表 MOV AX,[BP+4],可见,EQU 伪操作的引入提高了程序的可读性,也更加易于程序的修改。

在 EQU 语句的表达式中,如果有变量或标号等其他符号出现在表达式中,必须先定义这些符号才能引用。

另一个更为简洁的赋值伪操作是“=”,格式同“EQU”,它们之间的区别是 EQU 伪操作中的表达式名是不允许重复定义的,而“=”伪操作则允许重复定义。例如:

VALUE=53

VALUE=VALUE+89

如果把上面的两条赋值伪指令的“=”改成“EQU”,则为重复定义表达式名 VALUE,是不允许的。

八、汇编地址计数器 $ 与定位伪指令#

  1. 地址计数器 $

在汇编程序对源程序汇编的过程中,为了按序存放程序中定义的数据变量和指令,使用地址计数器(location counter)来设置当前正在汇编的指令的偏移地址。每一段的开始,地址计数器初始化为零,接着每处理一条指令,地址计数器就增加一个值,此值为该指令所需要的字节数,以安排下条指令的存放位置。

地址计数器不是硬件构成,其实就是一个 16 位的变量,可用 $ 来表示。当 $ 用在伪操作的参数字段时,它所表示的是地址计数器的当前值。

汇编语言允许用户直接用 $ 来引用地址计数器的值。如在指令中引用 $,JMP $+8 的转向地址是本条指令的首地址加上 8。显然 $+8 必须是另一条指令的首地址,否则汇编程序将指示出错。

例 9 考察 $ 的作用,假定 $ 初值=0,数据在内存中的存放如图 7 所示。

ARRAY  DW 3,$+7,7

COU=$

NEW   DW COU
例 9 的汇编结果

图 7 例 9 的汇编结果

  1. ORG 伪操作

ORG 伪操作用来设置当前地址计数器 $ 的值,其格式为:

ORG constant expression

如常数表达式的值为 n,则该操作指示下一个字节的存放地址为 n。

例 10 考察 ORG 伪操作,数据在内存中的存放如图 8 所示。

ORG  0

DB  3

ORG  4

BUFF  DB 6

ORG  $+6

VAL  DB 9
例 10 的汇编结果

图 8 例 10 的汇编结果

  1. EVEN 伪操作

EVEN 伪操作使下一个变量或指令开始于偶数地址。对于 16 位的变量来说,其地址为偶数时,机器内部只用一次读写操作,如果地址为奇数时要二次读写操作。程序员没有必要如此斤斤计较,但对于程序中要大量访问字单元变量时,变量始于偶数地址还是有利的。例如:

EVEN

ARRAY DW 800 DUP(?)
  1. ALIGN 伪操作

ALIGN 伪操作使下一个变量的地址从 4 的倍数开始,这可以用来保证双字数组边界从 4 的倍数开始,其格式为:

ALIGN boundary

其中 boundary 必须是 2 的幂。例如:

ALIGN 8

ARRAY DW 800 DUP(?)

九、基数控制伪指令#

汇编程序默认的数为十进制数,所以在程序中使用其他基数表示的常数时,需要专门给以标记如下。

(1)二进制数:由一串 0 和 1 组成,其后跟以字母 B,如 00101001B。

(2)十进制数:由 0~9 的数字组成的数,一般情况下,后面不必加上标记,在指定了其他基数的情况下,后面跟字母 D,例如 23D。

(3)十六进制数:由 0~9 及 A~F 组成的数,后面跟字母 H。这个数的第一个字符必须是 0~9,所以如果第一个字符是 A~F 时,应在其前面加上数字 0,如 0FFFFH。

RADIX 伪操作可以把默认的基数改变为 2~16 范围内的任何基数。其格式如下:

.RADIX expression

其中表达式用来表示基数值(用十进制数表示)。

注意

在用 .RADIX 把基数定为十六进制后,十进制数后面都应跟字母 D。在这种情况下,如果某个十六进制数的末字符为 D,则应在其后跟字母 H,以免与十进制数发生混淆。

十、过程定义伪指令#

子程序又称过程,可以把一个程序写成一个过程或多个过程,这样可以使程序结构更加清晰,基本的过程定义伪指令的格式为:

procedure_name  PROC  Attribute

┇

procedure_name  ENDP

其中过程名(procedure_name)为标识符,起到标号的作用,是子程序入口的符号地址。属性(Attribute)是指类型属性,可以是 NEAR 或 FAR。

例 11

data  segment        ; 定义数据段data

string  db 'hello,world!$ '

data  ends

code  segment        ; 定义代码段code

assume cs:code,ds:data

main  proc  far      ; 定义过程main

mov ax,data

mov ds,ax

mov dx,offset string

mov ah,9

int 21h

mov ah,4ch

int 21h

main  endp

code  ends

end  main      ; 汇编结束,程序起始点main

(完)

comments powered by Disqus