目录
指令和伪指令区别是,指令是在程序运行期间由计算机CPU执行的,伪指令是在汇编程序对源程序进行汇编器件由汇编程序处理的,这里只介绍一些常用的伪命令
1.伪指令
(1)段定义伪指令
用一段程序作为例子来引出:
.386 ;选择80386指令系统
DATA SEGMENT
BUFF DB 'hello,world$'
DATA ENDS
CODE SEGMENT
ASSUME CS:CODE, DS:DATA
START: MOV AX,DATA
MOV DS,AX
LEA BX,BUFF
MOV EAX,'ABCD' ;EAX是80386指令系统中的32位寄存器
MOV [BX],EAX
MOV DX,OFFSET BUFF
MOV AH,9
INT 21H
CODE ENDS
END START
程序中的第10行,第11行因为是32位机指令所以在Debug命令下无法显示但并不影响程序执行
完整的段定义伪指令
汇编程序在将源程序转换为目标程序时,只能自动确定标号和变量的偏移地址
程序中对段基址要做出说明
段定义伪指令格式:
段名称 SEGMENT
......
段名称 ENDS
为了指明用户定义的段和哪个段寄存器相关联,提出ASSUME伪指令
ASSUME伪指令格式:
ASSUME register_name:segment..., register_name:segment_name;
regiment_name为段寄存器名,必须是CS,DS,ES和SS
segment_name是由段定义伪指令定义的段名
指令只是指定把某个段分配给一个段寄存器,但是并不能把段基址装入段寄存器
在代码段中必须用两条MOV指令把段基址装入相应的段寄存器
简化的段定义伪指令
MASM5.0以上的版本还支持一种简化的段定义伪指令
.MODEL SMALL ;定义存储模型为SMALL
.DATA ;定义数据段
STRING DB 'HELLO,WORLD$';数据段自动结束
.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存储类型
- tiny(微型模式):cs、ss、ds的初值将被设置为1个,意味着只能使用一个段。程序大小不能超过64kb
- small(小型模式):只能至多有一个代码段和一个数据段。因此程序总大小不能超过128kb。
- compact(紧凑模式):代码段仅有一个,数据段可以有多个。数据的指针默认为远转移。
- medium(中型模式):代码段可以有多个,数据段仅有一个。调用类型默认为远转移。
- large(大型模式):代码段和数据段都可以是多个。但是一开始定义好的静态数据段仍然只能只能不超过64kb,保证在一个段内。
- huge(巨型模式):代码段和数据段都可以是多个。无限制。
- flat(平展模式):创建一个32位的程序。DOSBOX不能运行该程序。
.DATA伪指令用来定义数据段,但没有给出段名,默认段名是_DATA,@DATA在程序中表示数据段基址
段自动结束
(2)程序结束伪指令
命令格式为: END [LABEL]
在遇到END语句(并不是遇到END就结束)时程序结束,同时程序从[LABEL]指向的地址开始执行
当有多个程序模块连接时,只有主程序需要使用标号,其他子程序只用END
这是因为主程序作为程序的入口点,需要清晰的标识;而子程序则专注于完成特定功能,其结束使用END
,方便编译器识别程序结尾。
(3)数据定义与存储单元分配伪指令
伪指令语句的一般格式:
[变量] 操作码 N个操作数 [;注释]
变量可有可无,用符号地址表示,用来表示指令在内存中的位置
伪指令的操作码是所用伪指令的助记符,说明所定义的数据类型
常见的操作码有八个:
- DB:伪指令用来定义字节类型数据,每个操作数占有1个字节(8位)
- DW:定义字类型数据,每个操作数占有1个字(16位)(数据低位在低地址,数据高位在高地址)
- DD:定义双字类型数据,每个操作数占有2个字(32位)
- DF:定义六个字节类型数据,每个操作数占有6个字节(48位)
- DQ:定义四个字类型数据,每个操作数占有4个字(64位)可以用来表示双精度浮点数
- DT:定义十个字节类型数据,每个操作数占有10个字节(压缩的BCD码)
masm6允许用BYTE,WORD,DWORD,FWORD,QWORD,TBYTE代替上述指令
这些伪指令可以把指令的操作数存入指定的存储单元,形成初始化数据
也可以只分配存储单元,不确定数值
例如:
操作数为常数或数据表达式时:
D-BYTE DB 10,10H ;10的符号地址为D-BYTE,10H的符号地址为D-BYTE+1
D-WORD DW 14, 100H, -5, 0ABCDH ;DW定义的数据的值不能超过1个字表示的范围
D-DWORD DD 4 X 8 ;操作数为数据表达式
数据可以是负数,以补码形式存放,如果
数据第一位不是数字需在前面加0
编译结果在内存单元中如下:
注意低地址对低位,高地址对高位
操作数为字符串时
MESSAGE DB 'HELLO?' , ? ;指令中的?通常被系统置0
DB 'AB' , ? ;
DW 'AB' ;注意这里字符串以字的形式存储,要遵循低位为低地址
当操作数为字符串时,以ASCII码存入内存单元,且注意字节和字存入的区别
MESSAGE
48 | 45 | 4C | 4C | 4F | 3F | -- | 41 | 42 | -- | 42 | 41 |
用伪指令DUP复制操作数
ARRAY DB 2 DUP(1 , 3 , 2 DUP(4 , 5))
ARRAY
01 | 03 | 02 | 04 | 05 | 04 | 05 | 01 | 03 | 02 | 04 | 05 | 04 | 05 |
(3)类型属性伪指令
访问内存变量知道地址用于定位,知道类型(长度)用于匹配
如果指令中出现两个长度不匹配的操作数,可以用类型属性伪指令指定访问类型
命令格式:
WORD PTR ;字类型
BYTE PTR ;字节类型
指令只是改变了变量的访问类型,并没有改变变量本身的类型
例如:
OPER 1 DB 3,4
OPER 2 DW 5678H,9
...
MOV AX , OPER1 ;操作数类型不匹配
MOV [DI], 0 ;操作数类型不明确
正确的mov指令如下:
MOV AX, BYTE PTR OPER1
MOV BYTE PTR [DI] , 0 ;常数0送到内存字节单元
(4)THIS伪指令和LABEL伪指令
一个变量可以定义成不同的访问类型,通过this和label指令实现
THIS指令格式: THIS TYPE
LABEL指令格式: NAME LABEL TYPE
实现指定一个类型type的操作数,使该操作数的地址与下一个存储单元地址相同
例如:
BUF = THIS WORD ; 使BUF和DAT指向同一个内存单元
DAT DB 8 , 9
OPR_B LABEL BYTE ;使OPR_B和OPR_W指向同一个内存单元
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
(5)表达式赋值伪指令EQU和=
可以用赋值伪指令给表达式赋予一个常量或名字
命令格式为:EXPRESSION_NAME EQU EXPRESSION
EXPRESSION_NAME = EXPRESSION
表达式必须是有效的操作数格式或有效的指令助记符
例如:
VALUE EQU 4
DATA EQU VALUE+5
ADDR EQU [BP+VALUE] ;提高了程序的可读性也易于程序修改
表达式中如果有变量等其他符号,必须先定义才能引用
“=”和EQU用法类似,区别是EQU的表达式名不允许重复定义,=允许重复定义
(6)汇编地址计数器“$”与定位伪指令
地址计数器$
使用地址计数器来设置当前正在汇编的指令的偏移地址
在每一段的开始地址计数器初始化为0
每处理一条指令地址计数器就增加一个值(指令所需字节数)
地址计数器不是硬件构成的,当$用在伪指令的参数字段时,表示地址计数器的当前值
例如:
ARRAY DW 3 ,$+7, 7 ;命令中¥经过第一个3的指令后变为2
COU = $
NEW DW COU
ARRAY NEW
03 | 00 | 09 | 00 | 07 | 00 | 06 | 00 |
a.定位伪指令ORG
ORG伪指令用来设置当前地址计数器$的值
命令格式为: ORG CONSTANT EXPRESSION
该操作指示下一个字节的存放地址为n
b.定位伪指令EVEN
EVEN伪指令使下一个指令或变量开始于偶数地址
对于16位的变量,偶数地址读取只需要用一次读写操作(奇数地址读取需要两次读写操作)
原因:
地址对齐
-
对齐指的是变量的起始地址是其自身大小的整数倍。例如:
-
16位(2字节)变量对齐的地址是
0, 2, 4, 6,...
(即偶数地址)。 -
32位(4字节)变量对齐的地址是
0, 4, 8, 12,...
。
-
-
未对齐的地址则不符合这一规则(如16位变量存储在奇数地址
1, 3, 5,...
)。
对齐的16位变量只需一次读取
-
内存总线的工作方式:
-
处理器通常通过内存总线以固定大小的块(如32位或64位)读取数据,且这些块的起始地址是对齐的。
-
如果16位变量存储在偶数地址(对齐),它完全落在单个内存总线传输的块内(例如
[地址0-1]
或[地址2-3]
),因此只需一次访问即可读取完整数据。
-
-
未对齐访问的问题:
-
如果16位变量存储在奇数地址(如地址
1
),它会跨越两个总线传输块(例如[地址0-1]
和[地址2-3]
)。 -
处理器需要执行两次内存访问(读取
[0-1]
和[2-3]
),然后拼接出目标数据(从第一个块取高字节,第二个块取低字节)。这会显著降低效率。
-
指令格式为 : EVEN
例如:
EVEN ;下一条指令开始于偶数地址
ARRAY DW 800 DUP(?) ;变量格式为字(16位)
c.定位伪指令ALIGN
该伪指令使下一个变量的地址从4的倍数开始
(可以保证双字数组边界从4的倍数开始)
指令格式为: ALIGN BOUNDARY
其中boundary必须是2的幂(因为至少得2字节才是4的倍数.)
例如:
ALIGN 8
ARRAY DW 800 DUP(?)
(6)基数控制伪指令RADIX
汇编语言默认进制为十进制数,因此在程序中用其他基数表示的常数时要有后缀(B,D,H...)
RADIX伪指令可以把默认的基数改为2~16的任意值
指令格式为: .RADIX EXPRESSION
EXPRESSION表示的是基数值(用十进制表示,例如16进制就16)
(在把基数改成十六进制后,十进制数后面要加D,同时如果最后一位为D要加后缀H避免混淆)
(7)过程定义伪指令
子程序又称过程,可以把一个程序写成多个过程使程序结构更加清晰
指令格式为: PROCEDURE_NAME PROC ATTRIBUTE
...
PROCEDURE_NAME ENDP
其中过程名(PROCEDURE_NAME)为标识符(子程序入口的符号地址)
属性(ATTRIBUTE)指类型属性,可以是NEAR或FAR
NEAR:调用程序和子程序在同一个代码段
FAR:调用程序和子程序不在一个代码段
例如:
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
2.表达式和操作符
程序中用的最多的使指令和有关数据定义的伪指令,这些语句由4项组成,格式为:
[NAME] OPERATION OPERAND [;COMMENT]
[名字] 操作 操作数 [;注释]
1.名字可以是变量名也可以是标号,如果是标号后面必须加冒号(例如start:)
一个地址符号显然具有三个基本属性:段,偏移和类型
- 段属性:定义该地址符号的段起始地址(在一个寄存器中)
- 偏移属性:偏移地址是从段起始地址到定义该地址符号的位置之间的字节数(16位段为无符号数)
- 类型属性:对于标号来说,用来指出该标号的引用范围,段内引用为NEAR,段外引用为FAR16位段既要表达16位段基址,又要表达16位的偏移地址;对于变量,用来定义该变量所保留的字节数(在数据定义伪指令中所定义的)
同一个程序中,变量或标号不能重复出现
2.操作可以是指令,伪指令或宏指令的助记符
指令汇编语言会将其翻译成机器语言;伪指令汇编程序根据要求进行执行
3.操作数由一个或多个表达式构成
对于指令,操作数一般给出操作数地址;对于伪指令给出所需要的参数
操作数可以是常数,寄存器,标号,变量,表达式(前四项与一些运算符组合的序列)
汇编程序根据一定的优先规则对表达式进行计算后可得到数字/地址表达式
下面介绍一些常用的操作符:
1.算术操作符
算术操作符由+、-、*、/和MOD(求余)
算术操作符在表达式中的使用,结果必须有明确的物理意义才是有效的
定义:
ORG = 0
VAL = 4
DA1 DW 6,2,9,3
DA2 DW 15,17,24
COU = $ - DA2
则以下的操作:
MOV AX , DA1 * 4 ;错,地址相乘没有意义
MOV AX, DA1*DA2 ;错,地址相乘没有意义
MOV AX, DA1+DA2 ;错,地址相加没有意义
MOV AX, BX+VAL ;错,BX+VAL需要指令实现(将BX赋值才行)
MOV AX, [BX+VAL] ;地址表达式,将地址[BX+4]的内容传给AX
MOV AX, [DA1+VAL] ; 地址表达式,将地址[4]的内容传给AX
MOV AX, DA1+VAL ;地址表达式,系统编译问题,和上面指令相同功能
MOV AX, VAL*4/2 ;数字表达式,把8赋值给AX
MOV AX, [VAL*4/2] ;和上面指令功能相同
MOV CX, (DA1-DA2)/2 ; 得到DA1区数据个数,相当于MOV CX, 4
MOV BX, COU ;得到DA2区的字节数,相当于MOV BX, 6
2.逻辑操作符与逻辑移位操作符
逻辑操作符有AND、OR、XOR、NOT,逻辑移位操作符有SHL和SHR
都是按位进行操作,只能用于数字表达式
命令格式为: EXPRESSION 操作符 NUMBER
关于运算规则,在常用指令系统中的逻辑指令提到了:
3.关系操作符
用于对两个操作数的大小关系做出判断
6个关系操作符:EQ(相等),NE(不等),LT(小于),GT(大于),LE(小于等于),GE(大于等于)
命令格式为: OPR1 操作符 OPR2
关系操作符的两个操作数必须都是数字或是同一段内的两个存储器地址
结果为逻辑值,真表示为0FFFFH,假表示为0
4.数值回送操作符
数回送操作符主要有TYPE、LENGTH、SIZE、OFFSET、SEG等
a.TYPE
指令格式为: TYPE EXPRESSION
如果表达式是变量,则返回该变量以字节数表示的类型(DB为1,DW为2类推)
如果表达式是标号,则返回代表该标号类型的数值(NEAR=-1,FAR=-2,常数表达式=0)
b.LENGTH
指令格式: LENGTH VARIABLE
若变量用DUP复制,返回总变量数(嵌套的DUP不计),其他情况为1
嵌套的DUP复制的数据无法得到正确的总变量数
c.SIZE
指令格式为: SIZE VARIABLE
若变量用DUP复制,返回总字节数(嵌套的DUP不计),其他情况为1
嵌套的DUP复制的数据无法得到正确的总变量数
d.OFFSET
指令格式为: OFFSET VARIABLE或LABEL
返回变量或标号的偏移地址
e.SEG
指令格式为: SEG VARIABLE或LABEL
返回变量或标号的段基址
数值回送操作符的举例
设有如下定义:
ORG = 0
VAL = 4
ARR DW 4 DUP(3)
BUF DW 4 DUP(4 DUP(3))
DAT DW 15,17,24
STR DB 'ABCDEF'
则通过数值回送操作符之后
MOV AX , TYPE ARR ;汇编结果为MOV AX, 2
MOV AX,, LENGTH ARR ;汇编结果为MOV AX, 4
MOV AX, LENGTH BUF ;汇编结果为MOV AX, 4
MOV AX, LENGTH DAT ;汇编结果为MOV AX, 1
MOV AX, SIZE ARR ;汇编结果为MOV AX, 8
MOV AX, SIZE BUF ;汇编结果为MOV AX, 8(不是32)
MOV AX, SIZE DAT ;汇编结果为MOV AX, 2
MOV AX, SIZE STR ;汇编结果为MOV AX, 1
MOV AX, OFFSET ARR ; 不完整的机器指令
MOV BX, SEG ARR ;不完整的机器指令
操作符的优先级
在计算表达式时,根据操作符的优先级和括号,从左到右进行计算
从高到低排列有11级
- 圆括号中的项,方括号中的项,结构变量,LENGTH,SIZE,WIDTH
- 段名:表示段超越前缀取代
- PTR,OFFSET,SEG,TYPE,THIS:用于段超越前缀
- 分离高字节和低字节操作符HIGH和LOW
- *,/,MOD,SHL,SHR
- +,-
- 关系操作: EQ,NE,LT,LE,GT,GE
- 逻辑:NOT
- 逻辑:AND
- 逻辑:OR,XOR
- 段内短转移操作符: SHORT
3.EXE文件和COM 文件
1.EXE文件
EXE文件允许多个段,可以指定任一条指令为执行的起始地址,除了程序本身,还有文件头
文件头由LINK程序生成(包括程序的重定位信息,供DOS装入文件时用)
程序执行前调入内存是,DOS首先在其装入的起始地址处建立一个程序段前缀(PSP)
接着装入程序
地址[PSP : 0]处存放INT 20H指令(程序返回的中断调用指令)
EXE文件装入内存后,有关寄存器的值如下:
DS = ES = PSP段基址
CS : IP =程序执行的起始地址
SS : SP = 堆栈段的栈底地址
这里我一开始搞混了一些东西(段基址和程序执行的起始地址)
下面是两者的区别
-
抽象层次:段基址是内存管理概念,起始地址是程序加载概念
-
使用方式:段基址用于地址转换,起始地址确定执行起点
-
存储位置:段基址在段寄存器中,起始地址在可执行文件头中
-
关系:程序执行起始地址 = 代码段基址 + 入口点偏移
程序在装入前无法确定其在内存中的物理位置,连接后的数据段基址只是一个相对地址
程序装入内存时,DOS将下数据段的相对地址转为绝对地址(需要程序对段寄存器赋值)
地址 PSP:80H~PSP:0FFH处存放命令行参数,参数直接提供给可执行程序
如执行 PROG.EXE ,可在DOS提示符下输入:
PROG PAR1, PAR2
这时命令行参数域为0bh,"par1,par2"0dh
0bh表示参数常数长度,0dh表示回车符
如果把程序写成过程,可以用RET指令结束程序
执行RET指令时,会把栈中原先存入的地址出栈,并送入到ip寄存器里,ret指令等价于”pop ip“的效果 (POP IP是非法的)
DATA SEGMENT
STRING DB 'hello,world!$'
DATA ENDS
CODE SEGMENT
ASSUME CS:CODE,DS:DATA
MAIN PROC FAR
PUSH DS ;DS进栈
MOV AX,0
PUSH AX ;0进栈
MOV AX,DATA
MOV DS,AX
MOV DX,OFFSET STRING
MOV AH,9
INT 21H
RET ;相当于把0弹出给寄存器IP,但是我搜到的是RETF才能将DS弹给CS
MAIN ENDP
CODE ENDS
END MAIN
按照上述程序执行[CS:IP]即[PSP:0]处的指令,即INT 20H
RET命令必须写在过程当中,用于返回到调用程序,此处RET命令返回到DOS
程序的调试结果如下:
显然当执行完RET命令后会转到075A:0000执行INT 20H 指令
(DS:0L2指的是-u命令执行的范围为DS:0000到DS:0002)
2.COM文件
COM文件也是一种可执行文件,COM文件由本身的二进制代码组成
并没有文件头,占用空间比EXE文件的小得多(只包含一个代码段,即CS=DS=ES=SS)
如有过程调用,类型为NEAR,要求程序从100H开始执行
例如:
CODE SEGMENT
ASSUME CS:CODE
ORG 100H ;使程序从100H开始执行
START:
MOV AX,CS
MOV DS,AX
MOV DX,OFFSET STRING
MOV AH,9
INT 21H
MOV AH,4CH
INT 21H
STRING DB 'Hello,world!$' ;因为没有数据段,所以直接在代码段中进行数据定义
CODE ENDS
END START
COM文件的生成:
通过LINK程序生成,连接命令后面需要加上/T,如:
LINK COM_NAME/T