开始学习汇编语言,对相关的所学知识做个总结,希望对自己可以有所提高。
1、在计算机中数的表示方式
因为计算机中只能存储二进制数,所以一般都是通过二进制直接进行存储,但是为了方便阅读和程序员的编码简单化,就出现了八进制、十进制、十六进制,一般在汇编的学习过程中以二、十、十六进制为主。
四种数据的表示形式符号是:B(二进制)、O(八进制),D(十进制),H(十六进制)
二进制、八进制、十六进制转化为十进制都是通过数值为乘以权值,然后求和得出;
十进制转换为相应的进制都是通过除以相应的基数取余后,逆序达到相关的表示法。
2、在计算中通常通过数值的原码、反码、补码来进行表示。
对于无符号数,讨论三码没有什么意义;
对于有符号数,一般第一位代表的是符号位,0代表+,1代表—,同时在表示的过程中,因为存储器的位数可能存在数据为的扩展,一般都是扩展符号位。
对于正整数而言,原码=反码=补码,符号扩展位为0;
对于负整数而言,原码 按位取反 = 反码,反码+1 = 补码,符号位保证不变。
求负数的补码一般有两种方法:
(1)先写出其绝对值的原码,然后把符号位也参加在内进行取反,在加1即可。
(2)按照上面给出负数的求补即可。
补码的相关规则
(a+b)补 = (a)补+(b)补
(a-b)补 = (a)补-(b)补
3、对8086体系结构中寄存器的认识
(1)存在14个16位的寄存器和8个8位寄存器
通用寄存器包括如下几类
通用寄存器:传送、暂存数据和接受相关的运算结果。
1、16位数据寄存器,保存操作数和操作结果,缩短了访问内存的时间和并且不会占用相关的系统总数
AX(累加器、被除数的低16位和除法结果) = AH + AL(8位)<两个独立的寄存器,下面的相同>
BX(基址寄存器) = BH + BL(8位)<用于基址寻址,唯一一个作为存储器指针使用的寄存器>
CX(字符串操作、控制循环次数) = CH + CL (移位的时候使用,保存移位的位数)(8位)
DX(32位乘除法,存放被除数的高16位,或者保留余数,AX保留结果) = DH + DL(8位)
8个八位寄存器
AH 、AL 、BH、 BL、 CH、 CL、 DH、 DL
指针寄存器:存储某个存储单元的地址或者是一段存储单元的起始地址
2、16位指针寄存器
BP(基址指针 base point 堆栈数据区基址的偏移 )
SP(堆栈指针 stack pointer <push pop 指令的时候使用,保存栈顶地址>)
上面两个指针一般是和SS合用
3、16位变值寄存器<一般在字符串操作的时候用的比较多>
DI (目的地址 destination )
SI(源地址 source)
上面的两个寄存器一般是和DS、ES合用
控制寄存器
IP(指令指针)<下一条指令的地址,但是不代表是下次将会执行的指令>
在计算机的组成原理中还有PC(程序计数器,始终指向下一条将要执行的指令,有时候PC和IP的内容相同,有时候又不同,个人理解?)
FLAG(标志寄存器),其中包含了9个标志,主要反映存储器的状态和相关的运算状态
前6个运算结果标志,后3个控制标志
0 CF (carry flag ) 进位标志 反映运算是否产生进位或者借位<加法和减法>
2 PF (parity flag ) 奇偶标志 反映运算结果中1的个数是奇数还是偶数(偶数则置为1)
4 AF(assist flag ) 辅助标志 在字节操作中,发生低半字节向高半字节进位或借位;在字操作中,低半字向高半字进位或者借位
6 ZF(zero flag ) 零标志 反映运算结果是否为0
7 SF(signed flag )符号标志 反映运算结果的符号位,与运算结果的最高位相同
11 OF(over flag) 溢出标志 反映有符号数加减运算是否引起溢出
8 TF(trace flag ) 跟踪标志 置为1后,cpu进入单步方式。主要用于调试,cpu执行一条指令后被中断
9 IF(interrupt flag)中断标志 决定CPU是否相应外部可屏蔽中断请求,1则响应,0则不响应
10 DF(direction flag) 方向标志 决定串操作指令执行时有关指针寄存器调整方向,为1,则串操作指令按减方式改变有关寄存器的值,反之则用加方式
16位段寄存器<寻址1M的物理地址空间的时候需要使用它,在计算机的内存中代码<指令>和数据是放在不同的存储空间中>
放操作的数据
DS(数据段,一般配合bx作为偏移 data segment)
ES(附加段 extend segment)
放指令
CS(代码段,一般和IP连用 code segment)
保存相关的寄存器值等,放在这里方便函数返回的时候在恢复现场
SS(堆栈段 stack segment)
2、地址分段和寻址
一、明确地址分段的原因
因为在8086中CPU的地址线是20位,那么实际可用的最大物理地址空间是1MB,但是因为寄存器都是只有16位和8位之分,最大寻址范围是64KB,为了寻找到所有的物理地址,需要对物理地址空间进行分段。
分段一般是由段首地址+段内偏移地址组成。
但是对于段的首地址不是随意乱取,通常都以“小段的起始地址为主”
“小段”即是在物理地址中从00000H开始,每16个字节而划分的,那么整个物理地址空间就可以划分为64K个小段,且首地址的最后四位均为0(用二进制表示时),所以是16的倍数。
进行分段后,段与段之间就会有重叠、相邻、不会相干的现象产生。
一般物理地址= 段首地址*16+段内偏移地址。
前者为物理地址,后者断首地址:偏移地址为逻辑地址。所以一个物理地址可能对应多个逻辑地址的表示。
二、寻址方式
(1)汇编代码是由两部分组成:操作码+操作数
一般操作码在相应的机器指令体系中有相关的表示,但是操作数的存储就会不同了。
操作数存储在如下地方:
一、直接在汇编代码中:那么这种寻址方式就是立即数寻址 mov ax, 1234H
二、存放在寄存器中:那么这种寻址方式就是寄存器寻址 mov ax,bx
三、存放在内存中,那么这种寻址方式就比较多了
寻址方式:(以源操作数的寻址为例)
1、立即数寻址 mov ax,1234H
2、寄存器寻址 mov ax,bx
3、直接寻址 mov ax,【1234H】 (ax) = (ds*16+1234H)
4、寄存器间接寻址
mov ax,【bx】 (ax) = (ds*16+bx)
mov ax,【BP】 (ax)=(ss*16+bp)
因为bp的默认是通过ss来寻址,不过也可以通过段地址前缀来进行强加了
mov ax,ds:[BP] (ax) = (ds*16+bp)
5、寄存器相对寻址
mov ax,[bx+1234H] (ax) = (ds*16+bx+1234H)
也可以表示为
mov ax,1234H【bx】
同4,也存在这样的关系
6、基址变址寻址
mov ax,【bx+si】 (ax)= (ds*16+bx+si) <bx是基址寄存器,默认是和ds合用>
也可以表示为
mov ax,【bx】【si】或者是mov ax,【si】【bx】
7、相对基址变址寻址
mov ax,【bx+si+1234H】 (ax)=(ds*16+bx+si+1234H)
因为对于其中的很多寄存器都是可以变换的,所以不在这里一一列举
但是对于以上7中寻址方式到底应该在什么情况下进行使用还需要进一步的学习。
汇编代码是由两部分组成:操作码(mov)+操作数,既然有操作数的参与,那么对于操作数必然需要存储。
在计算机中,对于操作数的存取至少有两种方式:寄存器和存储器,那么相对而言就产生了各种寻找操作数的方式,本文一一介绍
1、立即寻址方式
操作数就包含在指令代码中,它作为指令的一部分跟在操作码后放在代码段(CS)中。
这种操作数被称作立即数,立即数可以是8位的也可以是16位的。
如果立即数是16位的就按照“高高低低”的原则存储<主要是针对寄存器,与存储单元的大端和小端没有关系>
指令示例:mov ax,1234H<H代表的是16位>
AH = 12, AL=34
但是汇编指令在代码段中的存储方式需要依据存储器的大小端格式来定。
2、寄存器寻址方式
操作数放在CPU内部的寄存器中,指令指定寄存器的编号。
对于16位的操作数,寄存器可以如下:
数据寄存器(AX/BX/CX/DX)
地址寄存器(DI/SI)
指针寄存器(SP/BP)
对于8位的操作数,可以是第一节的8个8位的寄存器
因为操作数在寄存器中,不需要占有系统总线,访问速度快。
指令示例:mov ax,bx
如果执行前:(AX) = 3064H,(BX)=1234H,指令执行后
(AX)= 1234H
3、直接寻址方式
指令直接包含操作数的有效地址(偏移地址)
操作数的有效地址一般存储在代码段中,操作数放在数据段中,默认的是ds段
所以操作数的地址由DS加上偏移地址得到有效地址,取出有效地址中的数据进行操作。
指令示例:
MOV AX,[1234H]
假设DS = 4567H,内存中(468A4H) = 0001H,那么有效地址 = (4567H)*16+(1234H) = (468A4H)
那么寄存器AX = 0001H
因为默认的是DS寄存器,其实也可以指定前缀寄存器,即所谓的段超越前缀
MOV AX, SS:[1234]H
AX = SS*16+(1234H)
4、寄存器间接寻址方式
操作数在寄存器中,操作数的有效地址在SI\DI\BX(DS), BP(SS)
在这四个寄存器之一种,一般情况下如果有效地址在SI、DI、 BX中,则默认段地址是DS
如果在BP中,则默认是SS
不过和上面一样,指令中也可以指定段超越前缀来取得其他段中的数据
指令示例:
MOV AX,[SI] 引用的段寄存器是DS
MOV AX,ES:[SI],引用的段寄存器是ES
MOV [BP],AX,引用的段寄存器是SS
5、寄存器相对寻址方式
操作数放在存储器中,操作数的有效地址是一个基址寄存器(BX、BP)或者是变址寄存器的(SI 、DI)内容加上指令中给定的8位
或者是16位的位移偏移量之和
其中和上面的一样,引用段地址的时候,BX、SI、DI的段地址寄存器是DS,BP的是SS,不过也可以采用段地址超前缀。
其中给定的8位或者是16位采用补码形式表示。如果偏移量是8位,则需要进行符号扩展到16位
MOV AX,[BX+1234H] ,引用段寄存器是DS,有效物理地址=DS*16+BX+1234H,(AX)=(DS*16+BX+1234H)
MOV [BP+1234H],AX,引用段寄存器是SS
MOV ES:[SI+1234H],AX,引用段寄存器是ES
有时候指令也可以表示如左:MOV AX,1234H[BX]等。
6、基址加变址寻址方式
操作数在存储器中,操作数的有效地址由基址寄存器(BX、BP)的内容与变址寄存器(DI、SI)之一的内容相加
如果有BP则段寄存器是SS,其他的是DS
指令示例:
MOV AX,[BX][DI] 等价于 MOV AX,[BX+DI]
有效地址 = DS*16+BX+DI ,(AX) =(有效地址)
同时也允许段超越前缀
MOV DS:[BP+SI],AX
寻址方式适合于数组操作,利用基址存储数组的首地址,变址存储元素的相对地址
7、相对基址加变址寻址方式
操作数在存储器中,操作数的有效地址由基址寄存器之一的内容+变址寄存器之一的内容+8位或者是16位的偏移量之和。
段寄存器和相关的符号扩展同前面。
如果得到的有效地址超过FFFFH时,取64K的模。
大多数指令既可以处理字数据,也可以处理字节数据。
算术运算和逻辑运算不局限于寄存器,存储器操作数也可以直接参加算术逻辑运算。
指令系统分为如下六个功能组:
(1)数据传送
(2)算术运算
(3)逻辑运算
(4)串操作
(5)程序控制
(6)处理器控制
指令的一般格式分为四个部分
[标号:] 指令助记符 [操作数1][,操作数2][;注释]
指令是否带有操作数完全取决于指令
标号的使用取决于程序的需要,但是不被汇编程序识别,与指令系统无关。
标号有点类似于C语言中的goto语句中的标号,做为一个偏移。
指令助记符代表操作码,从二进制的操作码到助记符的一个翻译过程。
功能组一:数据传送指令
1、传送指令格式:
MOV DST(目的操作数),SRC(源操作数)
源操作数:累加器(AX),寄存器,存储单元和立即数
目的操作数:不能是立即数
操作后不改变源操作数。
(1)CPU内部之间的传送(两个都是寄存器)
MOV AH,AL
MOV DL,DH
MOV BP,SP
MOV AX,CS
源操作数和目的操作数两个不能都是段寄存器
代码段(CS)不能作为目的操作数
指令指针(IP)既不能作为源操作数也不能作为目的操作数
(2)立即数送通用寄存器和存储单元
MOV AL,3
MOV SI,-3
MOV VARB,-1;VARB是变量名,代表一个存储单元
MOV VARW,3456H;VARW是一个字变量
MOV [SI],6543H
主要:立即数不能直接传送给段寄存器
立即数不能是目的操作数
(3)寄存器与存储器之间的数据传送
MOV AX,VARW ;VARW是一个字变量,为直接寻址
MOV BH,[DI];为寄存器间接寻址
MOV DI,ES:[SI+3];为寄存器相对寻址,段超越前缀
MOV VARB,DL;为寄存器直接寻址
MOV DS:[BP],DI;寄存器基址变址寻址
MOV VARW,DS;寄存器直接寻址
MOV ES,VARW;直接寻址
注意:
源操作数和目的操作数类型一样(byte和word等),除了串操作外
不能同时是存储器操作数,两个操作数必须有一个寄存器除立即寻址以外。
如果需要在两个存储单元中进行数据传送,可以利用一个寄存器过渡
MOV AX,VARW1
MOV VARW2,AX
实现了VARW1->VARW2的数据传送。
操作数不能同时为段寄存器,那么同上也可以进行过渡。
MOV BX,OFFSET TABLE
把TABLE的偏移地址送到BX寄存器中,其中OFFSET为属性操作符。
传送指令不影响FLAG寄存器
2、交换指令
利用交换指令可以方便的实现通用寄存器之间或者是与存储器之间的数据交换
指令格式:
XCHAG OPRD1,OPRD2
此指令把操作数OPRD1与OPRD2的内容进行交换必须保证数据类型的一致。
通过上面的分析,操作指令中必须有一个寄存器,并且存储器之间,段寄存器之间不能直接通过MOV进行操作。
例如:
XCHAG AL,AH
XCHAG SI,BX
OPRD可是通用寄存器和存储单元,但是不能包括段寄存器<一定要通过通用寄存器来交换>
还不能有立即数,可以采用各种寄存器和存储器的寻址方式。
指令示例:
XCHAG BX,[BP+SI]; 基址加变址寻址方式,基址寄存器和存储器的数据呼唤[SS]
此指令不影响FLAG
3、地址传送指令
(1)指令LEA( load effective Address)
传送有效地址指令,格式如下:
LEA REG,OPRD
该指令把操作数OPRD的有效地址传送到操作数REG中。
操作数OPRD必须是一个存储器操作数
操作数REG必须是一个16位的通用寄存器(AX BX CX DX BP SP DI SI)
操作的结果是把偏移地址送给REG,记住不是物理地址,是偏移地址
指令示例:
LEA AX, BUFFER [AX]=BUFFER
LEA DS,[BS+SI] [DS] = BS+SI
LEA SI,[BP+DI+4] [SI] = BP+DI+4
(2)LDS( Load pointer into DS)
段值和段内偏移构成32位的地址指针。
该指令传送32为地址指针,其格式如下:
LDS REG, OPRD
执行操作: (REG) <- (SRC)
(DS) <- (SRC+2)
该指令把操作数OPRD作为基址所含的一个32位的内存中的内容前两个字节送到REG中,后两个字节送到数据段寄存器DS
操作数OPRD必须是一个32为的存储器操作数,
操作数REG可以是一个16位的通用寄存器,但实际使用的往往是变址寄存器或者是指针寄存器。
(3)LES(Load pointer into ES)
操作和上面的完全相同。
4、堆栈指令
在8086/8088系统中,堆栈实际是一段随机访问RAM区域。
称为栈底的一端地址较大,称为栈顶的一端地址较小。
堆栈的段值在堆栈寄存器SS中
堆栈的指针寄存器SP始终指向栈顶
堆栈是以“后进先出”方式工作
堆栈的存取必须以字为单位(16bit = 2Btye)
堆栈的指令分为如下两种:
(1)进栈指令PUSH
格式如下:PUSH SRC(源操作数)
该指令把源操作数SRC压栈。
执行过程是:先把栈顶指针SP值减2,SP = SP-2
再把SRC中的值放入SP所指的栈顶中即 [SS*16+SP] = [SRC]
SRC可以是通用寄存器和段寄存器,也可以是字存储单元
(2)出栈指令POP
格式如下:POP DST(目的操作数)
该指令把栈顶的元素放到DST中,然后把SP加2
执行过程如下:先把堆栈指针SP指的数据放到DST中,【DST】=【SS*16+SP】
再使SP = SP + 2
DST可以是通用寄存器和段寄存器(但是CS除外),也可以是字存储单元。
注意:
(1)上面两条指令PUSH和POP只能是字操作
(2)可以使用除立即寻址外的其他任何方式
(3)POP指令不允许使用CS寄存器
此两条指令不影响FLAG标志位
利用这两条指令可以是实现两个段寄存器的数据交换
例如:实现DS、ES的数据交换
PUSH DS
PUSH ES
POP DS
POP ES
在汇编的过程中,堆栈操一般实现“现场保存”和“现场恢复”,作为参数的传递缓冲区等。
汇总:
数据交换有三种方式:
传送指令、交换指令、堆栈指令
举例:交换DS、AX的数据
利用传送指令
MOV BX,AX
MOV AX,DS
MOV DS,BX
利用交换指令
XCHG AX,DS<不能同时是段寄存器>
利用堆栈操作指令如上面的示例。
5、标志操作指令
(1)标志传送指令
1、LAHF(Load AH Flags)
把FLAG寄存器的低八位送到AH,即把CF PF AF ZF SF送到AH中。
不影响标志寄存器。
2、SAHF(Store AH into Flags)
把AH寄存器的八位传送到FLAG寄存器的低八位中,刚好和上面的指令作用相反。
影响标志寄存器。但是不影响8-15中的标志位。
3、PUSHF和POPF
把FLAG的标志寄存器压入和压出。
可以通过他们的操作来改变FLAG中的标志位的值。主要可以改变TF标志。
1、加法指令ADD
格式:ADD OPRD1,OPRD2
(OPRD1) = (OPRD1)+(OPRD2)
例如:MOV AX,7896H; AX=7896H
即AH = 78H, AL=96H;各个标志寄存位保持不变
ADD AL,AH ;AL=0EH,AH = 78H,即AX = 780EH(0111100000001110)
此时如果FLAG寄存器的值分别为
CF = 1, ZF = 0,SF = 0,OF = 0,AF = 0,PF = 0
继续操作如下:
ADD DX,0F0F0H
执行前(DX) = 4652H,执行后(DX)=3742H,ZF=0,SF=0,CF=1,OF=0
如果执行如下操作:
ADD AX,4321H
执行前(AX)=62A0H,执行后(AX)=A5C1H SF=1,ZF=0,CF=0,OF=1
讲解一下执行的过程,为什么FLAG标志位的状态发生了变化。
在寄存器中一般数值都是用补码表示,最高位代表符号位。
但是在加法指令中,是不区分操作数的符号位的,因为补码的表示完全避开了这个符号位的概念<在下一篇目录中会有说明>,符号的概念只在编程语言级别才有区分(针对加法和减法)
根据上面的分析可以知道:加法指令影响标志位。
PF标志位表示结果包含的1的个数,如果为偶数个则为1,如果是奇数个则为0
2、带进位的加法指令ADC( ADD with carry )
格式如下:
ADC,OPRD1,OPRD2
OPRD1 = OPRD1+OPRD2+CF
例如:下列指令序列执行了两个双精度的加法。(双精度32位)
设目的操作数放在DX和AX寄存器中,其中DX存放高位字,AX存放低位字
源操作数放在BX,CX中,其中BX存放高位字
(DX) = 0002H (AX) = 0F365H
(BX) = 0005H (CX) = 0E024H
指令序列如下:
ADD AX,CX
ADC DX,BX
执行第一条指令后:
AX = 0D389H(1101001110001001),SF=1, ZF=0,CF=1,OF=0
执行第二条指令后:
DX = 00008H(0000000000001000),SF=0, ZF=0, CF=0, OF=0
从上面的例子可以看到:
为了实现双精度数的加法,必须用两条指令来完成,低位和低位相加,
产生进位,然后再使用ADC。
另外带符号的双进度数的溢出,需要根据ADC指令的OF来判断
ADD指令的OF没有作用。
通过上面例子可以看出:影响FLAG
3、加1指令INC(INCrement)
加1指令的格式如下:
INC OPRD
OPRD = OPRD + 1;
操作数可以是通用寄存器,也可以是存储单元
这条指令的执行结果影响标志位ZF,SF,OF,PF和AF,但它不影响CF
该指令主要用于调整地址指针和计数器。
两个数相加,如果最高有效位不同,那么肯定不会发生溢出,即OF=0,但是会有进位,CF的值根据是否有进位来判断
如果最高有效位相同,相加的结果的最高有效位如果与操作数相反,那么肯定有溢出了,则OF=1,证明发生了错误,CF的值根据是否有进位来判断
在高级的编程语言层面,这个就作为“截断”进行处理,然后可以根据OF和CF的值来判断结果是错误,还是正确。
如果OF = 1,则结果肯定是错误,不用理会CF
如果OF = 0,则结果可能是正确的,如果CF=0,则正确,如果CF=1,则发生了进位,结果被“截断”
4、减法指令SUB(SUBtraction)
格式如下:
SUB OPRD1,OPRD2
执行的操作:(OPRD1) = (OPRD1)-(OPRD2)
例如:
SUB [SI+14H],0136H
指令执行前 (DS) = 3000H, (SI)=0040H
(300054H) = 4336H
指令执行后 (30054H) = 4200H
SF=0, ZF=0, CF=0, OF=0
例子如下:
SUB DH,[BP+4]
指令执行前
(DH)=41H,(SS)=0000H,(BP)=00E4H,(000E8H)=5AH
指令执行后
(DH)=E1H,(SS)=0000H,(BP)=00E4H,(000E8H)=5AH
SF=1, ZF=0, CF=1<借位>, OF=0
5、带借位的减法SBB( SuBtrace with Borrow)
与带借位的加法刚好相反
SUC OPRD1,OPRD2
OPRD1 = OPRD1-OPRD2-CF
例如:
SBB AL,DL
SBB DX,AX
该条指令主要用于多字节想减的场合。
6、减1指令 DEC(DECrement)
格式如下:
DEC OPRD
(OPRD) = (OPRD)-1
例如:
DEC VARB ;VARB是字节变量
操作数OPRD可以是通用寄存器,也可以是存储单元。
减1指令,在相减的时候,把操作数作为一个无符号数对待。
这条指令执行的结果影响ZA,OF,PF,SF,AF但是不影响CF
该条指令主要用于调整地址指针和计数器。
两个操作数相减,如果最高有效位相同,则不会发生溢出,则OF=0,CF根据是否借位来判断,如果CF=1,则借位,如果CF=0,则没有借位。
如果最高有效位不同,且操作的结果和减数的最高有效位相同,则OF=1,CF根据是否借位来判断,如果CF=1,则借位,如果CF=0,则没有借位
7、取补指令NEG(NEGate)
格式如下:
NEC OPRD
这条指令对操作数取补,就是用零减去操作数OPRD,在把结果送到OPRD中
(OPRD) = -(OPRD)
操作数可以是通用寄存器,也可以是存储单元
此指令的执行结果影响CF/ZF/SF/OF/AF和PF
操作数为0时,求补的运算结果是CF=0,其它情况则均为1,都是借位操作
如果在字节操作的时候对-128取补,或在字操作的时候对-32768取补,则操作数不变,但是OF被置为1,
其它都是0
8、比较指令CMP(CoMPare)
格式如下:
CMP OPRD1,OPRD2
这条指令完成操作数OPRD1减去OPRD2,运算结果不送到OPRD1,
但是影响标志CF/ZF/SF/OF/AF和PF
记住双操作数中至少有一个寄存器。
比较指令主要用于比较两个数的关系,是否相等,谁大谁小
执行了比较指令后,可以根据ZF是否置位,来判断两者是否相等
如果两者都是无符号数,则可以根据CF判断大小,如果借位了则前者小
如果两个都是有符号数,则可以根据SF和OF判断大小,
若为无符号数,则根据CF判断大小:
若CF = 1,则OPRD1 < OPRD2
若CF = 0,则OPRD1 >= OPRD2,如果ZF不等于1则是大于
若为有符号数,则根据SF和OF判断大小:
若SF = 1, OF = 1,则OPRD1 > OPRD2,说明发生了溢出,相减后为负数1(正数-负数)
若SF = 1, OF = 0,则OPRD1 < OPRD2,说明没有发生溢出,相减后为负数
若SF = 0, OF = 1,则OPRD1 < OPRD2,说明发生了溢出,相减后为正数,但是发生了溢出(负数-正数)
若SF = 0, OF = 0,则OPRD1 > OPRD2,说明正常操作,且结果为正数
在汇编指令中,是不区分有符号数和无符号数,但是汇编指令中对于加减法指令是不区分有符号数和无符号数的。
但是乘除法指令是区分有符号数和无符号数。
一、只有一个标准!
在汇编语言层面,声明变量的时候,没有 signed 和 unsignde 之分,汇编器统统,将你输入的整数字面量当作有符号数处理成补码存入到计算机中,只有这一个标准!汇编器不会区分有符号还是无符号然后用两个标准来处理,它统统当作有符号的!并且统统汇编成补码!也就是说,db -20 汇编后为:EC ,而 db 236 汇编后也为 EC 。这里有一个小问题,思考深入的朋友会发现,db 是分配一个字节,那么一个字节能表示的有符号整数范围是:-128 ~ +127 ,那么 db 236 超过了这一范围,怎么可以?是的,+236 的补码的确超出了一个字节的表示范围,那么拿两个字节(当然更多的字节更好了)是可以装下的,应为:00 EC,也就是说 +236的补码应该是00 EC,一个字节装不下,但是,别忘了“截断”这个概念,就是说最后的结果被截断了,00 EC 是两个字节,被截断成 EC ,所以,这是个“美丽的错误”,为什么这么说?因为,当你把 236 当作无符号数时,它汇编后的结果正好也是 EC ,这下皆大欢喜了,虽然汇编器只用一个标准来处理,但是借用了“截断”这个美丽的错误后,得到的结果是符合两个标准的!也就是说,给你一个字节,你想输入有符号的数,比如 -20 那么汇编后的结果是正确的;如果你输入 236 那么你肯定当作无符号数来处理了(因为236不在一个字节能表示的有符号数的范围内啊),得到的结果也是正确的。于是给大家一个错觉:汇编器有两套标准,会区分有符号和无符号,然后分别汇编。其实,你们被骗了。:-)
二、存在两套指令!
第一点说明汇编器只用一个方法把整数字面量汇编成真正的机器数。但并不是说计算机不区分有符号数和无符号数,相反,计算机对有符号和无符号数区分的十分清晰,因为计算机进行某些同样功能的处理时有两套指令作为后备,这就是分别为有符号和无符号数准备的。但是,这里要强调一点,一个数到底是有符号数还是无符号数,计算机并不知道,这是由你来决定的,当你认为你要处理的数是有符号的,那么你就用那一套处理有符号数的指令,当你认为你要处理的数是无符号的,那就用处理无符号数的那一套指令。加减法只有一套指令,因为这一套指令同时适用于有符号和无符号。下面这些指令:mul div movzx … 是处理无符号数的,而这些:imul idiv movsx … 是处理有符号的。
举例来说:
内存里有 一个字节x 为:0x EC ,一个字节 y 为:0x 02 。当把x,y当作有符号数来看时,x = -20 ,y = +2 。当作无符号数看时,x = 236 ,y = 2 。下面进行加运算,用 add 指令,得到的结果为:0x EE ,那么这个 0x EE 当作有符号数就是:-18 ,无符号数就是 238 。所以,add 一个指令可以适用有符号和无符号两种情况。(呵呵,其实为什么要补码啊,就是为了这个呗,:-))
乘法运算就不行了,必须用两套指令,有符号的情况下用imul 得到的结果是:0x FF D8 就是 -40 。无符号的情况下用 mul ,得到:0x 01 D8 就是 472。
三、可爱又可怕的c语言。
为什么又扯到 c 了?因为大多数遇到有符号还是无符号问题的朋友,都是c里面的 signed 和 unsigned 声明引起的,那为什么开头是从汇编讲起呢?因为我们现在用的c编译器,无论gcc 也好,vc6 的cl 也好,都是将c语言代码编译成汇编语言代码,然后再用汇编器汇编成机器码的。搞清楚了汇编,就相当于从根本上明白了c,而且,用机器的思维去考虑问题,必须用汇编。(我一般遇到什么奇怪的c语言的问题都是把它编译成汇编来看。)
C 是可爱的,因为c符合kiss 原则,对机器的抽象程度刚刚好,让我们即提高了思维层面(比汇编的机器层面人性化多了),又不至于离机器太远 (像c# ,java之类就太远了)。当初K&R 版的c就是高级一点的汇编……:-)
C又是可怕的,因为它把机器层面的所有的东西都反应了出来,像这个有没有符号的问题就是一例(java就不存在这个问题,因为它被设计成所有的整数都是有符号的)。为了说明c的可怕特举一例:
#include <stdio.h>
#include <string.h>
int main()
{
int x = 2;
char * str = "abcd";
int y = (x - strlen(str) ) / 2;
printf("%d\n",y);
}
结果应该是 -1 但是却得到:2147483647 。为什么?因为strlen的返回值,类型是size_t,也就是unsigned int ,与 int 混合计算时类型被自动转换了,结果自然出乎意料。。。
观察编译后的代码,除法指令为 div ,意味无符号除法。
解决办法就是强制转换,变成 int y = (int)(x - strlen(str) ) / 2; 强制向有符号方向转换(编译器默认正好相反),这样一来,除法指令编译成 idiv 了。我们知道,就是同样状态的两个内存单位,用有符号处理指令 imul ,idiv 等得到的结果,与用 无符号处理指令mul,div等得到的结果,是截然不同的!所以牵扯到有符号无符号计算的问题,特别是存在讨厌的自动转换时,要倍加小心!(这里自动转换时,无论gcc还是cl都不提示!!!)
为了避免这些错误,建议,凡是在运算的时候,确保你的变量都是 signed 的。(完)
在前面一节谈到,在汇编中对于加法和减法指令是没有所谓的有符号数加法和有符号数减法,统一通过补码进行运算,然后再根据标识寄存器的相关标识位进行判断,来辨别运算结果是否正确,主要是以OF,SF和CF的对比来判断。
但是乘法指令和除法指令区分了相关的有符号操作和无符号操作,因为在运算的结果中需要进行符号位的扩展。
乘法指令和除法指令都分为字节和字的操作,如果是两个8bit位的操作数,则结果存放在AX中,如果是两个16bit的操作数,则操作数和结果放在AX和DX中。
(1)无符号数的乘法指令
指令格式如下:
MUL OPRD
如果OPRD是8bit位的无符号数,那么有一个隐藏数在AL中,最后的结果放在AX中。
如果OPRD是16bit位的无符号数,那么有一个隐藏数在AX中,最后的结果是低字放在AX中,高字放在DX中。
对于标志为的影响比较特殊:
如果高半部分不为0,则CF = 1,OF=1(说明操作结果有效),如果为0则CF=OF=0,说明CF和OF是对高半部分进行记录,但是对其它FLAG位的影响没有定义。
(2)有符号数的乘法指令
格式如下:
IMUL OPRD
如果OPRD是8bit位的无符号数,那么有一个隐藏数在AL中,最后的结果放在AX中。
如果OPRD是16bit位的无符号数,那么有一个隐藏数在AX中,最后的结果是低字放在AX中,高字放在DX中。
对于标志为的影响比较特殊:
如果高半部分是低半部分的符号扩展,则CF =OF=0(说明操作结果有效),如果不是符号扩展则CF=OF=1,说明CF和OF是对高半部分进行记录,但是对其它FLAG位的影响没有定义。
乘法指令适应于我们前面介绍的所有寻址方式。
(3)无符号的除法指令
格式如下:
DIV OPRD
如果OPRD是8bit位的无符号数,那么有一个隐藏数在AX中,最后的结果AL保存商,AH保存余数。
如果OPRD是16bit位的无符号数,那么有隐藏数在AX、Dx中,其中AX保存操作数的低字,DX保存操作数的高字,最后的结果是商放在AX中,余数放在DX中。
除法指令是状态标志没有意义,结果可能产生溢出,溢出时8086CPU中就产生编号为0的内部中断.实用中应该考虑这
个问题.
(4)有符号数的除法指令
指令格式如下:
IDIV OPRD
整个的操作过程和DIV一样,没有什么比较特殊的地方
汇编指令中的移位操作分为算术移位和逻辑移位
一般在进行左移操作的时候,算术移位和逻辑移位的处理过程都比较简单:移除左边的最高位,最低位补零
但是在进行右移操作的时候,算术移位移除右边的数字然后左边的最高位进行符号扩展,不过逻辑移位就是补零,则个需要注意一点。
对于需要进行左移和右移的操作,一般都是需要指定移动位数M,如果M=1则可以直接以立即数给出,如果移位超过1则需要把移位放在CL中。
移位操作主要分为如下几个指令:
SAL OPRD,M 算术左移
SHL OPRD,M 逻辑左移
SAR OPRD,M 算术右移
SHR OPRD,M 逻辑右移
循环移位没有符号位的扩展等性质
ROL OPRD,M 循环左移<如果操作数为Nbit位,则移动N次后可以还原>
ROR OPRD,M 循环右移
RCL OPRD,M 带进位的循环左移<CF作为循环移动的一部分,需要移动N+1次才可以复位>
RCR OPRD,M 带进位的循环右移
一般移位操作都是和逻辑运算结合进行操作数的结合与分解运算
右移操作一般是把最高位移动到CF中
带进位的循环移位操作也是对CF进行了操作,对其他标志位的影响根据相关性质来决定。
在汇编指令中跳转指令分为两种,一种是无条件跳转指令,一种是有条件跳转指令。
对于前者无条件跳转指令有点类似于高级语言C中的goto语句,goto标志符,无跳转指令的格式也是类似JMP 标号;
对于有条件跳转指令通常都是根据FLAG寄存器的相关状态值SF,OF,AF,PF,CF是否被设置为1或者是0来进行跳转的选择,这个就可以实现相关的分支语句。类似于高级语言中的if等。
(1)无条件跳转指令JMP
基本格式如下:
JMP 标号;
因为在操作系统中我们一般对程序进行分段处理,那么在不同的段就会设置不同的CS寄存器,执行不同指令的过程中实质是设置CS与IP寄存器的值,然后CPU以此来进行指令的取出,由此对于跳转指令我们就分为段内跳转和段间跳转,前者是在一个代码段中,后者是实现不同的代码段的跳转。
首先说一下段间无条件跳转。
段间的无条件跳转的实现原理是:汇编器根据JMP后面设置的标号,计算出标号对应的段内偏移与此时IP寄存器中的值得差值,然后让IP加上该差值,实质就是设置IP的值为该标号对应的段内偏移值。
根据差值所占位的大小我们又分为:无条件段内近转移和无条件段内短转移。
对于前者,偏移与IP的差值大小只占2个字节,后者占1个字节
指令格式分别如下:
无条件段内短转移:JMP SHORT 标号
无条件段内近转移:JMP NEAR PTR 标号
对于这个PTR什么时候需要添加我也不是很清楚,到后面的学习过程中明了后在进行修改。
对于无条件转移指令,此时的IP值是通过标号直接设置的,在汇编器解析的时候进行设置,但是有时候我们可以把需要设置的IP值放到通用寄存器或者是存储器中,那么这样就可以实现无条件段内间接跳转指令。
指令的格式如下:
JMP OPRD
其中OPRD可以是通用寄存器,也可是存储单元,寻址方式除了是立即数寻址外,可以是其它的寻址方式。
例如:
JMP AX;JMP [AX];JMP WORD PTR [1234H]等
(2)无条件段间跳转
所谓的无条件段间跳转就是通过相关的操作直接设置CS和IP寄存器的值,使得执行不同代码段中的代码
指令格式和上面的大同小异,但是汇编器在进行解析的时候会设置CS和IP的值。
指令分为两种:一种是直接跳转
JMP FAR PTR 标号
一种是间接跳转,通过直接寻址的方式,把存储器中的低字放到IP中,高字放到CS中,指令格式如下
JMP DWORD PTR OPRD
例如:JMP DWORD PTR [1234H],则执行后IP = DS*16+[1234H],CS = DS*16+[1236H]
总结了一些JMP跳转指令的相关格式:
格式 |
描述 |
举例 |
类别 |
说明 |
jmp 16位寄存器 |
以16位寄存器的值改变IP |
jmp ax |
段内转移 |
|
jmp 段地址:偏移地址 |
以立即数改变段地址和偏移地址 |
jmp 0045H:0020H |
段间转移 |
|
jmp short 标号 |
以标号地址后第一个字节的地址来改变IP,实际上这个功能可以作如下描述: |
jmp short sign |
段内短转移 |
对IP的修改范围是-128->127,实际算法是编译器根据当前IP指针的指向来计算到底偏移多少个字节来指向下一条指令,下面这段代码就会出编译错误 |
jmp near ptr 标号 |
以标号地址后第一个字的地址来改变IP, |
jmp near ptr sign |
段内近转移 |
对IP的修改范围是-32768->32767 |
jmp far ptr标号 |
以标号的段地址和指令地址同时改变CS和IP |
jmp far ptr sign |
段间转移 |
|
jmp word ptr 内存地址 |
以内存地址单元处的字修改IP,内存单元可以以任何合法的方式给出 |
jmp word ptr ds:[si] |
段内转移 |
|
jmp dword ptr内存地址 |
以内存地址单元处的双字来修改指令,高地址内容修改CS,低地址内容修改IP,内存地址可以以任何合法的方式给出 |
jmp dword ptr [bx] |
段间转移 |
s1 segment |
对于JMP 段地址:偏移地址,并不是所有的MASM都支持的,需要依据实际的形式来判断。
上面我们看到了段内的无条件跳转指令,但是和很多高级语言进行对比,我们在很多时候都是通过条件的判断来决定是否需要进行跳转,同样在汇编指令中也提供了相关的条件跳转指令,我们现在一一进行介绍:
明确一下,在汇编指令中N代表的是否。同时进行条件跳转的指令都是段内跳转,因此有短和近跳转了撒!
(1)根据标识FLAG寄存器来判断是否需要进行跳转
我们根据前面的需要知道相关的算术运算、逻辑运算、移位运算(部分指令会影响CF)都会影响FLAG寄存器中的部分标识位的值,那么根据这些标志位我们可以判断是否需要进行跳转,譬如CMP AX,BX是判断AX和BX的大小,那么通过相关设置的标志位我们就可以判断是AX大还是BX大,然后决定是否需要转移嘛,这就是所谓的分支语句了撒!
对于有符号数分大于(G-great),等于(E-equal),小于(L-light)
对于无符号数分为大于(A),等于(E),小于(B)
根据标志位跳转的指令:
JE ;等于则跳转 JNE ;不等于则跳转 JZ ;为 0 则跳转 (ZF) JNZ ;不为 0 则跳转 JS ;为负则跳转 (SF) JNS ;不为负则跳转 JC ;进位则跳转 (CF) JNC ;不进位则跳转 JO ;溢出则跳转(OF) JNO ;不溢出则跳转 JA ;无符号大于则跳转 JNA ;无符号不大于则跳转 JAE ;无符号大于等于则跳转 JNAE ;无符号不大于等于则跳转 JG ;有符号大于则跳转 JNG ;有符号不大于则跳转 JGE ;有符号大于等于则跳转 JNGE ;有符号不大于等于则跳转 JB ;无符号小于则跳转 JNB ;无符号不小于则跳转 JBE ;无符号小于等于则跳转 JNBE ;无符号不小于等于则跳转 JL ;有符号小于则跳转 JNL ;有符号不小于则跳转 JLE ;有符号小于等于则跳转 JNLE ;有符号不小于等于则跳转 JP ;奇偶位置位则跳转 (PF) JNP ;奇偶位清除则跳转 JPE ;奇偶位相等则跳转 JPO ;奇偶位不等则跳转跳转相关的标志位:
11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|---|---|---|
OF | DF | IF | TF | SF | ZF | AF | PF | CF | |||
溢 出 |
符 号 |
零 | 未 用 |
辅 助 |
未 用 |
奇 偶 |
未 用 |
进 位 |
通过上面的跳转指令我们就可以实现简单的分支和循环,例如
MOV CX,10H
NEXT:
........
DEC CX
JNZ NEXT
实现的是执行NEXT中的代码段10次
但是通过自己手动的写相关的循环语句有时候很复杂,增加了编码的难度,因此在汇编指令中有了如下的专门的循环指令,如下所示:
LOOP = CX不为零的时候进行跳转
LOOPE/LOOPZ = CX不为零并且相等的时候跳转
LOOPNE/LOOPNZ = CX不为零并且不相等的时候跳转
LCXZ CX为零的时候跳转
通过相关的循环+跳转语句就可以实现高级语言中的分支语句和循环语句的执行了
程序设计语言是实现人机交换信息(对话)的最基本工具,可分为机器语言、汇编语言和高级语言。本章重占介绍汇编语言。
(1)汇编语言是一种面向机器的程序设计语言,其基本内容是机器语言的符合化描述;
(2)通常汇编语言的执行语句与机器语言的执行指令是一一对应的;
(3)汇编语言允许程序直接使用寄存器,标志等微处理器芯片内部的特性;
(4)同高级语言程序相比,与其等效的汇编语言执行速度要块,目标代码所占的内存要少;
(5)汇编语言是系统软件和实时控制系统程序员必须掌握的。
1.机器语言
机器语言用二进制编码表示每条指令,它是计算机能只别和执行的语言。用机器语言编写的程序称为机器语言程序或指令程序(机器码程序)。因为机器只能直接识别和执行这种机器码程序,所以又称它为目标程序。显然,用机器语言缩写程序不易记忆、不易查错与不易修改。为了克服机器语言的上述缺点,可采用有一定含义的符号即指令助记符来表示指令。一般都采用某些有关的英文单词的缩写,这样就出现了另一种程序语言――汇编语言。
2.汇编语言
汇编语言是用指令的助记符、符号地址、标号等来表示指令的程序语言,简称符号语言。它的特点是易读、易写、易记。
它与机器语言指令是一一对应的。汇编语言不像高级语言(如BASIC)那样通用性强,而是性某种计算机所独有,与计算机的内部硬件结构密切相关。用汇编语言缩写的程序叫汇编语言程序。
把汇编语言源程序翻译成目标程序的过程称为汇编过程,简称汇编。完成这个任务有两种方法:
①手工汇编。所谓手工汇编是程序设计人员根据机器语言指令与汇编语言指令对照表,把编好的汇编语言程序翻译成目标程序。
汇编语言程序 机器语言程序
MOV AL,0AH B0H 0AH
ADD AL,14H 04H 14H
②机器汇编。所谓机器汇编就是由汇编程序自动将用户编写的汇编语言源程序翻译成目标程序。
这里,汇编程序是由厂家为计算机配置的担任把汇编源程序成目标程序的一种系统软件。
以上两种程序语言都是低级语言。尽管汇编语言具有执行速度快和易于实现对硬件的控制等优点,但它仍存在着机器语言的某些缺点:与CPU的硬件结构紧密相关,不同的CPU其汇编语言是不同的,这使得汇编语言程序不能移植,使用不便;其次,要用汇编语言进行程序设计,必须了解所使用的CPU硬件的结构与性能,对程序设计人员有较高的要求,为此又出现了所谓的高级语言。
3.高级语言
高级语言是脱离具体机器(即独立于机器)的通用语言,不依赖于特定计算机的结构与指令系统。用同一种高级语言缩写的源程序,一般可以在不同计算机上运行而获得同一结果。
使用高级语言编程与计算机的硬件结构没有多大关系。目前常用的高级语言有BASIC、FORTRAN、COBOL、PASCAL、PL/M、C等。一般来说,高级语言是独立于机器的,在编程时不需要对机器结构及其指令系统有深入的了解,而且用高级语言缩写的程序通用性好、便于移植。
综上所述,比较3种语言,各有优缺点。应用时,需根据具体应用场合加以选用。一般,在科学计算方面采用高级语言比较合适;而在实时控制中,特别是在对程序的空间和时间要求很高的场合,以及需要直接控制设备的应用场合,通常要用汇编语言。
8086汇编语言程序设计(二)
各种机器的汇编语言,其语法规则不尽相同,但基本语法结构形式类似。现以8086/8088汇编语言为例加以具体讨论。
4.2.1 汇编语言的数据与表达式
1.汇编语言的数据
数据是汇编语言中操作数的基本组成成分,汇编语言能识别的数据有常数、变量和标号。
(1)常数
常数是指那些在汇编过程中已经有确定数值的量,它主要用作指令语句中的立即操作数、变址寻址和基址加变址中的位移量DISP或在伪指令语句中用于给变量赋初值。
常数可以分数值常数和字符串常数两类。
数值常数:以各种进位制数值形式表示,以后缀字符区分各种进位制,后缀字符H表示十六进制,O或Q表示八进制,B表示二进制,D表示十进制,十进制常省略后缀。
字符串常数:用单引号括起来的一串ASⅡC码字符。如字符串"ABC"等效为41H、42H、43H一组数值常数,如"179"等效为31H、37H、39H一组数值常数。
变量是代表存放在某些存储单元的数据,这些数据在程序运行期间可以随时修改。变量是通过变量名在程序中引用的,变量名在是存放数据存储单元的符号地址,它可以作为指令中的存储器操作数来引用。
(2)变量
变量一般都在数据段或附加段中使用数据定义伪指令DB、DW和DD来进行定义。定义变量就是给变量分配存储单元,且对这个存储单元赋予一个符号名――变量名,同时将这些存储单元预置初值。经过定义的变量具有以下3个属性:
①段属性:表示与该变量相对应的存储单元所在段的段基值;
②偏移量属性:表示该变量相对应的存储单元与段起始地址相距的字节数;
③类型属性:表示变量占用存储单元的字节数。这一属性是由数据定义伪指令DB、DW、DD来规定的。它们可以是单字节变量(或称字节变量)、双字节变量(或称字变量)、4字节变量(或称双字变量)。
(3)标号
标号是某条指令所在存储单元的符号地址,它指示指令在汇编语言程序中的位置,通常,标号用来作为汇编语言源程序中转移、调用以及循环等指令的操作数,即转移的目标地址。
标号和变量相似,也有3个属性:段、偏移量和距离,前两个属性和变量的同名属性完全相同,而标号的第三个属性"距离"可以是 NEAR(近距离)或FAR(远距离)。
NEAR(近距离):本标号只能被标号所在段的转移和调用指令所访问(即段内转移)。
FAR(远距离):本标号可被其他段(不是标号所在段)的转换和调用指令所访问(即段间转移)。
标号的基本定义方法是在指令的操作助记符前加上标识符和冒号,该标识符就是我们所要定义的标号。例如:
START:PUSH DS
标号还可以采用伪指令定义,如用LABEL伪指令和过程定义伪指令来定义,这将在后面叙述。
2.表达式
由常数、变量或标号和运算符连接而成的式子称为表达式,它是操作数的基本形式。表达式有数字表达式和地址表达式,汇编程序在汇编期间对表达进行计算,得到一个数值或一个地址。
8086/8088汇编语言中的操作运算符分为:算术运算符、逻辑运算符、关系运算符、数值返回运算符、属性修改运算符。
(1)算术运算符
算术运算符包括加(+)、减(-)、乘(*)、除(/)和模运算符MOD。MOD施于操作,得到的是除法的余数,例如,27MOD 4,其结果为3。
当算术运算为地址操作数时,应保证其结果是一个有意义的存储器地址,因而通常只使用+/-运算。
(2)逻辑运算符
逻辑运算符包括:非(NOT)、与(AND)、或(OR)和异或(XOR)。逻辑运算符的运算对象必须是数值型的操作数,并且是按位运算。应当注意逻辑运算符与逻辑运算指令之间的区别,逻辑运算符的功能是在汇编时由汇编程序完成,而逻辑运算指令的功能由CPU完成。
(3)关系运算符
关系运算符包括相等(EQ)、不等(NE)、小于(LT)、不大于(LE)、大于(GT)和不小于(GE)。关系运算符用于将两个操作数进行比较,若符合比较条件(即关系式成立),所得结果为全1;否则,所得结果为全0。
(4)数值返回运算符
数值返回运算符包括:段基值(SEG)、偏移量(OFFSET)、类型(TYPE)、长度(LENGTH)和字节总数(SIZE)。
数值返回运算符用来把存储器操作数(变量或标号)分解为它的组成部分(段基值、偏移量、类型、元素个数总数和数据字节总数),并且返回一个表示结果的数值。这些运算符的格式如下:
运算符 变量或标号
①段基值SEG运算符
当运算符SEG加在一个变量名或标号的前面时,得到的运算结果是返回这个变量名或标号所在段的段基值。
②偏移量OFFSET运算符
当运算符OFFSET加在一个变量名或标号前面时,得到的运算结果是返回这个变量或标号在它段内的偏移量。例如:
MOV SI,OFFSET KX
设KX在它段内的偏移量是15H,那么这个指令就等效于:
MOV SI,15H
这个运算符十分有用。例如,现有以ARRAY为首址的字节数组,为了逐个字节进行某种操作,可以使用下面的部分程序:
在这段程序中,首先把数组变量的首字节偏移量送给SI,把寄存器SI作为数组的地址指针。这样在数组的逐个字节处理(即在LOP循环)中,用寄存器间接寻址方式,每处理完一个字节,就很方便地对地址指针SI进行修改,使它指向下一个字节。
③类型TYPE运算符
运算结果是返回反映变量或标号类型的一个数值。如果是变量,则数值为字节数,DB为1,DW为2,DD为4,DQ为8,DT为10;如果是标号,则数值为代表标号类型的数值,NEAR为-1,FAR为-2。
④长度LENGTH运算符
这个运算符仅加在变量的前面,返回的值是指数给变量的元素个数。如果变量是用重复数据操作符DUP说明的,则返回外层DUP给定的值;如果没有CUP说明,则返回的值总是1。
对于数组变量,可以用重复操作表达式表达式表示,其格式为:
重复次数DUP(操作数…操作数)
其中,重复次数为正整数,DUP是重复操作符,括号中的操作数是重复的内容,操作数类型可以是字节、字或双字等。
⑤字节总数SIZE运算符
SIZE运算符仅用于变量的前面,运算结果是返回数组变量所占的总字节数,也就是等于LENGTH和TYPE两个运算符返回值的乘积。
如数组变量ARRAY是用20HDUP(0)定义的,且数组元素的数据类型是字,则
MOV AL,SIZE ARRAY
等效为: MOV AL,40H
(5)属性运算符
属性运算符包括:类型修改(PTR)、短转移(SHORT)、类型指定(THIS)和段超越运算符(:)。这种运算符用来对变量、标号或某存储器操作数的类型属性进行修改。
①类型修改PTR运算符
PTR运算符格式如下:
类型 PTR 地址表达式
其中,类型可以是BYTE(字节)、WORD(字)、DWORD(双字)、NEAR(近距离)、FAR(远距离)。
运算结果是将地址表达式所指定的变量、标号或存储器操作数的类型属性,临时性地修改或指定为PTR运算中规定的类型。这种修改是临时性的,仅在有修改运算符的语句内有效。
②短转移SHORT运算符
当JMP指令的目标地址与JMP指令之间的距离在-128~+127字节的范围内,可以用SHORT操作符来告诉汇编程序,将JMP指令汇编成两个字节的代码(一个字节为操作码,后一个字节为相对位移量)。例如:
JMP SHORT NEAR_LABLE
其中,目标标号NEAR_LABLE与JMP指令间的相对位移量在-128~+127个字节的范围内。
③类型指定THIS运算符
THIS运算符的格式如下:
THIS 类型
其中,类型可以是BYTE、WORD、DWORD、NEAR或FAR。该操作符用来指定或补充说明变量或标号的类型。运算符THIS与LABEL伪指令有类似的效果,THIS运算符的应用举例,将在后面叙述。
④段超越运算符(跨段前缀)
段超越运算符用来临时给变量、标号或地址表达式指定一个段属性。
段超越运算符的格式为:
段名:地址表达式
或 段寄存器名:地址表达式
例如:INC ES:[BP+3]
ES:为跨段前缀,冒号":"前的ES段寄存器指明了操作数当前所在的段为附加数据段。如果没有跨段前缀"ES:",那么,由 [BP+3]地址表达式所表示的偏移地址将被系统默认为是在堆栈中。
3、运算符优先级(后期补充)
汇编语言程序设计(三)-基础语法知识
汇编语言的伪指令
汇编语言中有3种基本语句:指令语句、伪指令语句和宏指令语句。
指令语句是上一章介绍的指令,它们经过汇编之后产生可供计算机硬件执行的机器目标代码,所以这种语句又称为执行语句;伪指令语句是一种说明(指示)性语句,仅仅在汇编过程中告诉汇编程序应如何汇编,例如告诉汇编程序已写出的汇编评议程序有几个段,段的名称是什么?是否采用过程?汇编到某处是否需要留出存储空间?应留多大?是否要用到外部变量等。所以,伪指令语句是一种汇编程序在汇编时用来控制汇编过程以及向汇编程序提供汇编相关信息的批示性语句。与指令语句不同,伪指令语句其本身并不直接产生可供计算机硬件执行的机器目标代码,它仅是一种非执行语句。
宏指令语句用于替代源程序中一段有独立功能的程序,由汇编时产生相应的目标代码。宏指令语句是使用指令语句和伪指令语句,由用户自己定义的新指令。本教材对宏指令语句不作讨论,在这一节里只介绍几种常用的伪指令语句。
1.数据定义伪指令
该指令的功能是把数据项或项表的数值存入存储器连续的单元中,并把变量名与存储单元地址联系在一起。在程序中,用户可以用变量名来访问这些数据项。
数据定义伪指令的格式如下:
其中,变量名是任选项。
若用DB定义变量,则变量类型为BYTE,汇编时为每个操作数分配一个存储单元;
若用DW定义变量,则变量类型为WORD,汇编时为每个操作数分配2个存储单元,操作数的低字节在低地址,高字节在高地址;
若用DD定义变量,则变量类型为DWORD,汇编时为每个操作数分配4个存储单元,操作数的低字节在低地址,高字节在高地址。
2.符号定义伪指令
在编制源程序时,程序设计人员常把某些常数、表达式等用一特定符号表示,这样,为编写程序带来许多方便。为此,就要使用符号定义语句,这种语句有以下两种:
(1)赋值伪指令
赋值伪指令是为表达赋予一个符号名,其后指令中凡需要用到该表达式的地方均可以用此名字来代替。缩写程序时,通过使用赋值伪指令可以使汇编语言简明易懂,便于程序的调试和修改。赋值伪指令的格式如下:
符号名 EQU 表达式
符号名是必需项,赋值伪指令仅在汇编源程序时作为替代符号用,不产生任何目标代码,也不占用存储单元。因此,赋值伪指令左边的符号名没有段、偏移量和类型3个属性。同一符号名不能重复定义。表达式可以是常数表达式、地址表达式、变量名、标号名、过程名、寄存器名或指令名等。如果表达式包含有变量、标号或过程名,则应在EQU语句以前的某处定义过。
(2)等号伪指令
语句格式: 符号名=表达式
这种语句的含义和表达的内容都与赋值语句相同;但是等号语句可以重新定义符号。
3.类型定义伪指令
类型定义伪指令的格式如下:
变量名或标号名 LABEL 类型
LABEL伪指令为当前存储单元重新定义一个指定类型的变量或标号,该伪指令并不为指定的变量或标号分配存储单元。
例如:DA-BYTE LABEL BYTE
DA-WORD DW 20H DUP(0)
上面第二个语句是定义了20H个字单元,如要对这数组元素中某单元以字节访问它,则可以很方便地直接使用DA-BYTE变量名。DA-BYTE和DA-WORD有相同 段和偏移量属性。同样,也可以有:
JUMP-FAR LABEL FAR
JUMP-NEAR:MOV AL,30H
当从段内某指令来调用这程序段时,可以用标号JUMP-NEAR,如果从另一代码段来调用时,则可用JUMP-FAR标号。
运算符THIS和LABEL伪指令有类似的效果,上面两条LABEL伪指令可分别改为:
DA-BYTE EQU THIS BYTE
JUMP-FAR EQU THIS FAR
4.段定义伪指令
我们知道,8086/8088CPU的地址空间是分段结构的。因此,我们在编制任一源程序时,亦必须按段来构造程序。一个程序通常按用途划分成几个逻辑段(至少要定义一个段),如存放数据的段、作堆栈使用的段、存放主程序的段、存放子程序的段等等。那么,如何告诉汇编程序源程序中的哪些内容属于数据段、哪些内容属于代码段呢?这自然是由汇编系统中提供的伪指令来实现。
段定义伪指令的功能就是把源程序划分为逻辑段,便于汇编程序在相应段名下生成目标码,同时也便于连接程序组合、定位、生成可执行的目标程序。利用段定义伪指令可以定义一个逻辑段的名称和范围,并且指明段的定位类型、组合类型和类别名,其指令格式如下:
在源程序中,每一段都是以SEGMENT伪指令开始,以ENDS伪指令结束。其中:
(1)段名
由用户自己选定,通常使用与本段用途相关的名字。如第一数据段DATA1,第二数据段DATA2,堆栈段STACK,代码段CODE……一个段开始与结尾用的段名应一致。
(2)段参数
段参数有定位类型、组合类型和类别名,各之间必须用空格分隔。同时,它们必须按给定的顺序排定,它们都是任选项,它们决定了段与段之间联系的形态。
①定位类型
定位类型表示对该段起始边界的要求,可有4种选择:
·BYTE 起始地址=×××× ×××× ×××× ×××× ××××,即字节型,表示本段起始单元可以从任一地址开始。
·WORD 起始地址=×××× ×××× ×××× ×××× ×××0,即字型,表示本段起始地址可以是任何一个字的边界(偶地址)。
·PARA 起始地址=×××× ×××× ×××× ×××× 0000,即节型,表示本段起始地址必须从存储器的某一个节的边界开始(1节等于16个字节)。
·PAGE 起始地址=×××× ×××× ×××× 0000 0000,即页型,表示本段起始地址必须从存储器的某一个页的边界开始(1页等于256个字节)。
对于上述4种定位类型,它们20位边界地址分别可以被1、2、16、256除尽,分别称为以字节、字、节、页为边界。其中,PARA为隐含值,即如果省略"定位类型",则汇编程序按PARA处理。
②组合类型
组合类型指定段与段之间是怎样连接和定位的。它批示连接程序,如何将某段与其他段组合起来的关系。连接程序不但可以将不同模块的同名段进行组合,并可根据组合类型,将各段顺序地或重叠地连接在一起。其中有6种组合类型可供选择:
·NONE类型
表示该段与其他段在逻辑上不发生连接关系,这是隐含的组合类型,若省略"组合类型"项,即为NONE。
·PUBLIC类型
表连接时,应把不同模块中属于该类型的同名同类别的段相继顺序地连成一个逻辑运算时装入同一物理段中,使用同一段基址。连接顺序与LINK时用户所提供的各模块的顺序一致,应当注意,各模块中属于PUBLIC类型的同名同类别的各段的总长度不能超64KB。
·STACK类型
与PUBLIC类型同样处理,只是组合后的该段用作堆栈。当段定义中指明了STACK类型后,说明堆栈已经确定,系统自动对段寄存器SS初始化在这个连续段的首址,并初始化堆栈指针SP。用户程序中至少有一个段用STACK类型说明,否则需要用户程序自己初始化SS和SP。
·COMMON类型
表明连接时,应将不同模块中属于该类型的同名同类别的各段连接成一段,它们共用一个基地址,且互相覆盖(重叠地放在一起),连接后,段的长度取决于最长的COMMON段的长度,这样可以使不同模块的变量或标号使用同一存储区域,便于模块间的通信。
·AT表达式类型
表示本段可定位在表达式所指示的节边界上。如"AT 0930H",那么本段从绝对地址09300H开始。但是,它不能用来指定代码段。
·MEMORY类型
表明连接时应把本段装在被连接的其他所有段的最后(高地址端),若有几个段都指出了MEMORY纵使类型,则汇编程序认为所遇的第1个为MEMORY组合类型,其他段认为是COMMON类型。
③类别名
类别名必须用单引号括起来。类别名是由用户任选字符串组成,以表示该段的类别。在连接时,连接程序将各个程序模块中具有同样类别名的逻辑段集中在一起,形成一个统一的物理段。典型的类别名有"STACK"、"CODE"、"DATA1"、"DATA2"……
一个典型程序的段结构如下:
STACK SEGMENT PARA STACK 'STACK';堆栈段,定位类型为节起点,组
合为公用堆栈段,类别名为SATCK。
语句
…
语句
STACK ENDS
DATA SEGMENT PARA 'DATA';数据段,定位类型为节起点,不与其他段组
合,类别名为DATA。
语句
…
语句
DATA ENDS
CODE SEGMENT PARA MEMORY;代码段,定位类型为节起点,本段地址位于
高地址端。
语句
…
语句
CODE ENDS
5.设置起始地址伪指令
ORG伪指令用来指出其后的程序段或数据块存放的起始地址的偏移量。其指令格式为: ORG 表达式
汇编程序把语句中表达式之值作为起始地址,连续存放程序和数据,直到出现一个新的ORG指令。若省略ORG,则从本段起始地址开始连续存放。
6.汇编结束伪指令
标志着整个源程序的结束,它使汇编程序停止汇编操作。其指令格式为:
END 表达式(标号)
其中,表达式与源程序中的第一条可执行指令语句的标号相同。它提供了代码段寄存器CS与指令指示器IP的数值,作为程序执行时第一条要执行的指令的地址。
伪指令END必须是汇编语言源程序中的最后一条语句,而且每一个源程序只能有一条END伪指令。如果出现一第以上的END伪指令,则在第一条伪指令END以后的语句是无效的。
7.段寄存器设定伪指令
段寄存器设定伪指令ASSUME,一般出现在代码段中,它用来告诉汇编程序由SEGMENT/ENDS伪指令定义的段和段寄存器的对应关系,即设定已定义段各自属于哪个段寄存器。其指令格式为:
ASSUME 段寄存器名:段名[,段寄存器名:段名]
段寄存器名是CS、DS、SS或ES,段名必须是由SEGMENT/ENDS定义过的段名。
应当注意:使用ASSUME伪指令,仅仅告诉汇编程序,关于段寄存器与定义段之间的对应关系。但它并不意味着汇编后这些段地址已装入了相应的段寄存器中,这些段地址的真正装入,仍需要用程序来送入,且这4个段寄存器的关入略有不同。
8.过程定义伪指令
在程序设计中,我们常常把具有一定功能的程序段设计成一个子程序。汇编程序用"过程"(PROCEDURE)来构造子程序。过程定义伪指令格式如下:
一个过程是以PROC伪指令开始,以ENDP伪指令结束。
其中,过程名不能省略,且过程的开始(PROC)和结束(ENDP)应使用同一过程名。它就是这个子程序的程序名,也是过程调用指令CALL的目标操作数。它类同一个标号的作用,仍有3个属性――段、偏移量和距离类型。过程的距离类型可选择NEAR和FAR。在定义过程时,如没有选择距离类型,则隐含为NEAR。"过程"应在一个逻辑段内。
过程和段可以相互嵌套,即过程可以完全地包含某个段,而段也可以完全地包含某个过程,但它们不能交叉覆盖,即过程可以完全地某个段,而段也可以完全地包含某个过程,但它们不能交叉覆盖,例如,以下的序列是合法的:
每一个过程一定含有返回指令RET,它可以在过程中的任何位置,不一定放在一个过程的最后。如果一个过程有多个出口,它可能有多个返回指令;但是,一个过程执行的最后一第指令必定是返回指令RET。
9.程序开始伪指令
在程序的开始可以用NAME或TITLE伪指令,用来为程序取名。
(1)程序开始语句格式:NAME程序名
程序名是由用户任意选定的,如果源程序中缺少该语句,将用源文件名作为程序名。
(2)标题语句格式:TITLE文本
该伪指令用于给程序指定一个标题,以便在列表文件中每一页的第一行都会显示这个标题。它的文本可以是用户任意选用的名字或字符串,但是字符个数不得超过60个。
汇编语言的源程序中语句的结构由4部分组成,每个部分称为项,其语句格式如下:
[名字] 操作码 [操作数] [注释]
上述4部分中带方括号的项为任选项,操作码部分是必需项。各项之间常用冒号“:”、逗号“,”、分号“;”和空格作为分界符分隔开来。下面分别说明组成汇编语句的4个部分的含义。
1.名字
名字是用户为汇编语句所定义的具有特定意义的字符序列,它表示汇编语句的符号地址或符号名。
指令语句的名字称为标号,标号是指令的符号地址。用标号表示地址能方便程序的编写。尤其是,转移地址用标号表示时,程序员不必 计算地址值,减少了发生错误的可能性;加有标号的程序便于查询和修改。
标号是任选的,只在必要的地方才加标号。例如,子程序的第1条语句的地址、转移指令的转移地址等需用标号表示。
对于伪指令语句的名字,可以是常数名、变量名、过程名或段名等,它可以作为指令语句和伪指令语句的操作数。
汇编程序对名字语法格式有以下规定:
(1)以字母开头,由字母、数字、特殊字符(如“?”、“*”、“下划线”、“$”、“@”等)组成的字符串表示。名字的最大长度一般不超过31个字符。
(2)名字不能与保留字相同。汇编语言中的保留字通常包括:CPU寄存器名、指令助记符、伪指令助记符或运算符等。
(3)名字在汇编语句中是任选项,多数指令性语句并不出现名字,但多数伪指令语句出现名字。
(4)指令语句的名字是以冒号为分界符;伪指令语句的名字是以空格为分界符,这是两种语句的名字在格式上的区别。
2.操作码
操作码是汇编语句中惟一不可缺少的核心部分,它规定了所要执行的各种操作,一般由指令或伪指令的助记符组成。
3.操作数
操作数是参与操作的数据,或是参与操作的数据的地址。它与操作码一起确定指令所要执行的具体操作。
操作数是汇编语句中最复杂的部分,它可以是常数、字符串、寄存器名、变量、标号或表达式等。指令语句的操作数可以是双操作数、单操作数或无操作数;伪指令语句的操作数可以有多个操作数,当操作数有两个或两个以上时,操作数之间用逗号分开。操作数项在汇编期间,汇编程序对它进行处理,产生相应的数值或地址。
4.注释
语句行中分号“;”后面的字符串为注释部分。它用来简要说明该指令在程序中的作用,以提高程序的可读性。注释在语句中是任选项,且不对汇编产生任何影响
汇编语言学习之伪指令
段定义伪指令
段定义伪指令是表示一个段开始和结束的命令,80x86有两种段定义的方式:完整段定义和简化段定义,分别使用不同的段定义伪指令来表示各种段。
1 完整的段定义伪指令
完整段定义伪指令的格式如下:
段名 SEGMENT
.
.
.
段名 ENDS
段名由用户命名。对于数据段、附加段和堆栈段来说,段内一般是存储单元的定义、分配等伪指令语句;对于代码段中则主要是指令及伪指令语句。
定义了段还必须说明哪个段是代码段,哪个段是数据段。ASSUME伪指令就是建立段和段寄存器关系的伪指令,其格式为:
ASSUME 段寄存器名: 段名,…
段寄存器名必须是CS、DS、ES和SS中的一个,而段名必须是由SEGMENT定义的段名。
·定位类型:说明段的起始边界值(物理地址)。
·组合类型:说明程序连接时的段组合方法。
·类别:在单引号中给出连接时组成段组的类型名。连接程序可把相同类别的段的位置靠在一起。
例4.1
; * * * * * * * * * * * * * * * * * * * * * * *
data_seg1 segment ; 定义数据段
.
.
.
data_seg1 ends
; * * * * * * * * * * * * * * * * * * * * * * *
data_seg2 segment ; 定义附加段
.
.
.
data_seg2 ends
; * * * * * * * * * * * * * * * * * * * * * * *
code_seg segment ; 定义代码段
assume cs:code_seg, ds:data_seg1, es:data_seg2
start: ; 程序执行的起始地址
; set DS register to current data segment
mov ax, data_seg1 ; 数据段地址
mov ds, ax ; 存入DS寄存器
; set ES register to current extra segment
mov ax, data_seg2 ; 附加段地址
mov es, ax ; 存入ES寄存器
.
.
.
code_seg ends ; 代码段结束
; * * * * * * * * * * * * * * * * * * * * * * * * * *
end start
由于ASSUME伪指令只是指定某个段分配给哪一个段寄存器,它并不能把段地址装入段寄存器中,所以在代码段中,还必须把段地址装入相应的段寄存器中:
MOV AX,DATA_SEG1 ; 数据段地址
MOV DS,AX ; 存入DS寄存器
MOV AX,DATA_SEG2 ; 附加段地址
MOV ES,AX ; 存入ES寄存器
如果程序中还定义了堆栈段STACK_SEG,也需要把段地址装入SS中:
MOV AX,STACK_SEG ; 堆栈段地址
MOV SS,AX ; 存入ES寄存器
注意,在程序中不需要用指令装入代码段的段地址,因为在程序初始化时,装入程序已将代码段的段地址装入CS寄存器了。
为了对段定义作进一步地控制,SEGMENT伪指令还可以增加类型及属性的说明,其格式如下:
段名 SEGMENT [定位类型][组合类型]['类别']
.
.
.
段名 ENDS
[ ]中的内容是可选的,一般情况下,这些说明可以不用。但是,如果需要用连接程序把本程序与其他程序模块相连接时,就需要提供类型和属性的说明。
表 ·定位类型:说明段的起始边界值(物理地址)。
定位类型
说 明
BYTE
段可以从任何地址边界开始
WORD
段从字边界开始,即段的起始边界值为偶数
DWORD
段从双字的边界开始,即段的起始边界值为4的倍数
PARA
段从小段边界开始,即段的起始边界值为16 (或10H) 的倍数
PAGE
段从页边界开始,即段的起始边界值为256 (或100H) 的倍数
注意:
定位类型的缺省项是PARA,即在未指定定位类型的情况下,则连接程序默认为PARA。BYTE和WORD用于把其它段(通常是数据段)连入一个段时使用;DWORD一般用于运行在80386及后继机型上的程序。
表 ·组合类型:说明程序连接时的段组合方法。
组合类型
说 明
PRIVATE
该段为私有段,连接时将不与其它模块中的同名段合并
PUBLIC
该段连接时将与其它同名段连接在一起,连接次序由连接命令指定
COMMON
该段在连接时与其它同名段有相同的起始地址,所以会产生覆盖
AT 表达式
段地址=表达式的值,其值必为16位但AT不能用来指定代码段
MEMORY
与PUBLIC同义
STACK
将多个同名堆栈段连接在一起,SP设置在第一个堆栈段的开始
注意:组合类型的缺省项是PRIVATE。
例 在连接之前已定义两个目标模块如下:
模块1 SSEG SEGMENT PARA STACK
DSEG1 SEGMENT PARA PUBLIC 'Data'
DSEG2 SEGMENT PARA
CSEG SEGMENT PARA 'Code'
模块2 DSEG1 SEGMENT PARA PUBLIC 'Data'
DSEG2 SEGMENT PARA
CSEG SEGMENT PARA 'Code'
以上两个模块分别汇编后产生 .OBJ 文件,经连接程序连接后产生的 .EXE模块如下:
模块1 CSEG SEGMENT PARA 'Code'
模块2 CSEG SEGMENT PARA 'Code'
模块1+2 DSEG1 SEGMENT PARA PUBLIC 'Data'
模块1 DSEG2 SEGMENT PARA
模块2 DSEG2 SEGMENT PARA
模块1 SSEG SEGMENT PARA STACK 2 存储模型与简化段定义伪指令
较新版本的汇编程序(MASM5.0与MASM6.0)除支持完整段定义伪指令外,还提供了一种新的简单易用的存储模型和简化的段定义伪指令。
1.存储模型伪指令
存储模型的作用是什么呢?存储模型决定一个程序的规模,也确定进行子程序调用、指令转移和数据访问的缺省属性(NEAR或FAR)。当使用简化段定义的源程序格式时,在段定义语句之前必须有存储模型 .MODEL语句,说明在存储器中应如何安放各个段。
MODEL伪指令的常用格式如下:
. .MODEL 存储模型
2. 简化的段伪指令
简化的段定义语句书写简短,语句.CODE、.DATA和.STACK分别表示代码数据段和堆栈段的开始,一个段的开始自动结束前面一个段。采用简化段指令之前必须有存储模型语句.MODEL。
3.与简化段定义有关的预定义符号
汇编程序给出了与简化段定义有关的一组预定义符号,它们可在程序中出现,并由汇编程序识别使用。有关的预定义符号如下:
(1)@code 由.CODE 伪指令定义的段名或段组名。
(2)@data 由.DATA 伪指令定义的段名,或由 .DATA 、.DATA?、
.CONST和 .STACK所定义的段组名。
(3)@stack 堆栈段的段名或段组名。
4.简化段定义举例
1. 存储模型伪指令
表 MASM 5.0和MASM 6.0支持的存储模型:
存储模型
功 能
适用操作系统
Tiny (微型)
所有数据和代码都放在一个段内,其访问都为NEAR型,整个程序≤64K,并会产生.COM文件。
MS-DOS
Small (小型)
所有代码在一个64KB的段内,所有数据在另一个64KB的段内(包括数据段,堆栈段和附加段)。
MS-DOS
Windows
Medium (中型)
所有代码>64K时可放在多个代码段中,转移或调用可为FAR型。所有数据限在一个段内,DS可保持不变。
MS-DOS
Windows
Compact(紧凑型)
所有代码限在一个段内,转移或调用可为NEAR型。数据>64K时,可放在多个段中。
MS-DOS
Windows
Large (大型)
允许代码段和数据段都可超过64K,被放置在有多个段内,所以数据和代码都是远访问。
MS-DOS
Windows
Huge (巨型)
单个数据项可以超过64K,其它同Large模型
MS-DOS
Windows
Flat (平展型)
所有代码和数据放置在一个段中,但段地址是32位的,所以整个程序可为4GB。MASM 6.0支持该模型。
OS/2
WindowsNT
注意:Small 模型是一般应用程序最常用的一种模型,因为只有一个代码段和一个数据段,所以数据和代码都是近访问的。这种模型的数据段是指数据段、堆栈段和附加段的总和。
在DOS下用汇编语言编程时,可根据程序的不同特点选择前6种模型,一般可以选用SMALL模型。另外,TINY模型将产生COM程序,其他模型产生EXE程序。FLAT模型只能运行在32位x86 CPU上,DOS下不允许使用这种模型。当与高级语言混合编程时,两者的存储模型应当一致。
2. 简化的段伪指令
表 简化段伪指令的格式如下表:
简化段伪指令
功 能
注释
.CODE [段名]
创建一个代码段
段名为可选项,如不给出段名,则采用默认段名。对于多个代码段的模型,则应为每个代码段指定段名。
.DATA
创建一个数据段
段名是:_DATA
.DATA?
创建无初值变量的数据段
段名是:_BSS
.FARDATA [段名]
建立有初值的远调用数据段
可指定段名,如不指定,则将以FAR_DATA命名。
.FARDATA? [段名]
建立无初值的远调用数据段
可指定段名,如不指定,则将以FAR_BSS命名。
.CONST
建立只读的常量数据段
段名是:CONST
.STACK [大小]
创建一个堆栈段并指定堆栈段大小
段名是:stack。如不指定堆栈段大小,则缺省值为1KB
3.与简化段定义有关的预定义符号
下面的举例说明预定义符号的使用方法。在完整的段定义情况下,在程序的一开始,需要用段名装入数据段寄存器,如例4.1中的
mov ax,data_seg1
mov ds,ax
若用简化段定义,则数据段只用.data来定义,而并未给出段名,此时可用
mov ax,@data
mov ds,ax
这里预定义符号@data就给出了数据段的段名。
4.简化段定义举例
例 .MODEL SMALL
.STACK 100H ; 定义堆栈段及其大小
.DATA ; 定义数据段
.
.
.
.CODE ; 定义代码段
START: ; 起始执行地址标号
MOV AX, @DATA ; 数据段地址
MOV DS, AX ; 存入数据段寄存器
.
.
.
MOV AX, 4C00H
INT 21H
END START ; 程序结束
从例可以看出,简化段定义比完整的段定义简单得多。但由于完整的段定义可以全面地说明段的各种类型与属性,因此在很多情况下仍需使用它。 段组定义伪指令
段组定义伪指令能把多个同类段合并为一个64KB的物理段,并用一个段组名统一存取它。段组定义伪指令GROUP的格式如下:
段组名 GROUP 段名 [, 段名 …]
我们已经知道在各种存储模型中,汇编程序自动地把各数据段组成一个段组DGROUP,以便程序在访问各数据段时使用一个数据段寄存器DS,而GROUP伪指令允许用户自行指定段组。 例 将两个数据段DSEG1 和DSEG2合并在一个段组DATAGROUP中。
;----------------------------------------------------
DSEG1 SEGMENT WORD PUBLIC 'DATA'
.
.
.
DSEG1 ENDS
;---------------------------------------------------
DSEG2 SEGMENT WORD PUBLIC 'DATA'
.
.
.
DSEG2 ENDS
MOV AX, @DATA ; 数据段地址
MOV DS, AX ; 存入数据段寄存器
.
.
.
;---------------------------------------------------
DATAGROUP GROUP DSEG1, DSEG2 ;组合成段组
CSEG SEGMENT PARA PUBLIC 'CODE'
ASSUME CS : CSEG, DS : DATAGROUP
START: MOV AX, DATAGROUP
MOV DS, AX ;DS赋值为段组地址
.
.
.
MOV AX, 4C00H
INT 21H
CSEG ENDS
;-----------------------------------------------------
END START
利用GROUP伪指令定义段组后,段组内统一为一个段地址,各段定义的变量和标号都可以用同一个段寄存器进行访问。
Windows
Windows
Windows
Windows
Windows
WindowsNT
程序开始和结束伪指令
在程序的开始可以用NAME或TITLE作为模块的名字,其格式为:
NAME 模块名
TITLE 文件名
表示源程序结束的伪指令的格式为:
END [标号]
注意:NAME及TITLE伪指令并不是必需的,如果程序中既无NAME又无TITLE伪指令,则将用源文件名作为模块名。程序中经常使用TITLE,这样可以在列表文件中打印出标题来。
END伪指令中的"标号"指示程序开始执行的起始地址。如果多个程序模块相连接,则只有主程序的END要加上标号,其他子程序模块则只用END而不必指定标号。例4.1~4.3的最后使用了END START伪指令。汇编程序将在遇END时结束汇编,并且程序在运行时从START开始执行。
数据定义及存储器分配伪指令
80x86提供了各种数据及存储器分配伪指令,这些伪指令在汇编程序对源程序进行汇编期间,由汇编程序完成数据类型定义及存储器分配等功能。
数据定义及存储器分配伪指令的格式是:
[变量] 助记符 操作数[, …,操作数] [ ;注释]
下面介绍ORG伪指令以及常用的数据定义伪指令。
ORG(origin)
ORG伪指令用来表示起始的偏移地址,紧接着ORG的数值就是偏移地址的起始值。ORG伪操作常用在数据段指定数据的存储地址,有时也用来指定代码段的起始地址。
DB(define byte)
DB伪指令用来定义字节,对其后的每个数据都存储在一个字节中。DB能定义十进制数、二进制数、十六进制数和ASCII字符,二进制数和十六进制数要分别用"B"和"H"表示,ASCII字符用单引号(' ')括起来。DB还是唯一能定义字符串的伪操作,串中的每个字符占用一个字节。
DW(define word)
DW伪指令用来定义字,对其后的每个数据分配2个字节(1个字),数据的低8位存储在低字节地址中,高8位存储在高字节地址中,如下例中的变量DATA8的数据存储在0070字地址中,其中0070字节存储0BAH,0071字节存储03H。DW还可存储变量或标号的偏移地址。见左面DW伪指令的例子。
DD(define doubleword)
DD伪指令用来定义双字,对其后的每个数据分配4个字节(2个字)。该伪指令同样将数据转换为十六进制,并根据低地址存储低字节,高地址存储高字节的规则来存放数据。如下例DATA15的存储情况是:00A8:0F2H,00A9H:57H,00AAH:2AH,00ABH:5CH。
用DD存入地址时,第一个字为偏移地址,第二个字为段地址。
DQ(define quadword)
DQ伪指令用来定义4字,即64位字长的数据,DQ之后的每个数据占用8个字节(4个字)。
DT(define ten bytes)
DT伪指令用来为压缩的BCD数据分配存储单元,它虽然可以分配10个字节(5个字),但最多只能输入18个数字,要注意的是,数据后面不需要加"H"。左面是DQ和DT的例子。
DUP(duplicate)
DUP伪指令可以按照给定的次数来复制某个(某些)操作数,它可以避免多次键入同样一个数据。例如,把6个FFH存入相继字节中,可以用下面两种方法,显然用DUP的方法更简便些。
存入6字节的FFH
DATA20 DB 0FFH 0FFH 0FFH 0FFH 0FFH 0FFH;
DATA21 DB 6 DUP(0FFH)
DUP操作一般用来保留数据区,如用数据定义伪指令"DB 64 DUP(?)"可为堆栈段保留64个字节单元。DUP还可以嵌套,其用法见左例。
PTR属性操作符
PTR指定操作数的类型属性,它优先于隐含的类型属性。其格式为:
类型 PTR 变量[ ± 常数表达式]
其中类型可以是BYTE、WORD、DWORD、FWORD、QWORD或TBYTE,这样变量的类型就可以指定了。
LABEL伪指令
LABEL可以使同一个变量具有不同的类型属性。其格式为:
变量名 LABEL 类型
或 标号 LABEL 类型
其中变量的数据类型可以是BYTE,WORD,DWORD,标号的代码类型可以是NEAR或FAR。
数据定义及存储器分配伪指令格式中的"变量"是操作数的符号地址,它是可有可无的,它的作用与指令语句前的标号相同,区别是变量后面不加冒号。如果语句中有变量,那么汇编程序将操作数的第一个字节的偏移地址赋于这个变量。
"注释"字段用来说明该伪指令的功能,它也不是必须有的。
"助记符"字段说明所用伪指令的助记符。
DB(define byte)
请看下面数据定义的例子,注意DB定义的每个数据的存储情况,左边第一列是汇编程序为数据分配的字节地址,第二列是相应地址中存储的数据或ASCII字符(均用十六进制表示)。变量DATA7定义了3个数据和一个字符串,每个数据或串用","分开,它们分别存储在偏移地址002E开始的6个字节单元中。
表
; DB 例子的列表文件 0000 19 DATA1 DB 25 ; 十进制数 0001 89 DATA2 DB 10001001B ; 二进制数 0002 12 DATA3 DB 12H ; 十六进制数 0010 ORG 0010H ; 指定偏移地址为10h 0010 32 35 39 31 DATA4 DB '2591' ; ASCII码数 0018 ORG 0018H ; 指定偏移地址为18h 0018 00 DATA5 DB ? ; 保留一个字节 0020 ORG 0020H ; 指定偏移地址为20h 0020 4D 79 20 6E 61 6D DATA6 DB 'My name is Joe' ; ASCII码字符 65 20 69 73 20 4A 6F 65 002E 0A 10 02 31 30 42 DATA7 DB 10,10H,10B,'10B' ; 不同的数据类型 |
DW(define word)
表
; DW 伪指令例子的列表文件 0070 0RG 70H ;指定起始地址 0070 03BA DATA8 DW 954 ; 十进制数 0072 0954 DATA9 DW 100101010100B ; binary 0074 253F DATA10 DW 253FH ; 十六进制数 0076 FFFB DATA11 DW -5 ; 负数 0080 ORG 80H 0080 0009 FFFF 0007 000C DATA12 DW 9,-1,7,0CH,00100000B,100,'HI' 0020 0064 4849 ; 各种类型数据 |
DD(define doubleword)
表
; DD例子的列表文件 00A0 ORG 00A0H ; 指定起始地址 00A0 FF030000 DATA13 DD 1023 ; 十进制数 00A4 5C960800 DATA14 DD 10001001011001011100B ; 二进制数 00A8 F2572A5C DATA15 DD 5C2A57F2H ; 十六进制数 00AC 23000000 89470300 DATA16 DD 23H,34789H,65533 ; 各种数据 FDFF0000 |
DT(define ten bytes)
表
; DQ、DT例子的列表文件 00C0 ORG 00C0H 00C0 C223450000000000 DATA17 DQ 4523C2H ; 十六进制数 00C8 4948000000000000 DATA18 DQ 'HI' ; ASCII字符 00D0 0000000000000000 DATA19 DQ ? ; 分配8个字节单元 00E0 ORG 00E0H 00E0 2998564379860000 DATA20 DT 867943569829 ; 压缩的BCD数 0000 00EA 0000000000000000 DATA21 DT ? ; 分配10个字节单元 0000 |
DUP(duplicate)
表
; DUP例子的列表文件 0100 ORG 0100H ; 数据区的起始地址 0100 0020[ DATA22 DB 32 DUP(?) ; 保留32字节 ?? ] 0120 ORG 0120H 0120 0005[ DATA23 DB 5 DUP(2 DUP(99)); 存入10个字节的99 0002[ 63 ] ] 012A 0008[ ATA24 DW 8 DUP(?) ; 保留8个字节 ???? ] |
对数据定义伪指令前面的变量还要注意它的类型属性问题。变量表示该伪指令中的第一个数据项的偏移地址,此外,它还具有一个类型属性,用来表示该语句中的每一个数据项的长度(以字节为单位表示),因此DB伪指令的类型属性为1,DW为2,DD为4,DQ为8,DT为10。变量表达式的属性和变量是相同的。汇编程序可以用这种隐含的类型属性来确定某些指令是字指令还是字节指令。
下例中变量OPER1为字节类型属性,OPER2为字类型属性,所以第一条MOV指令应为字节指令,第二条MOV指令应为字指令。而第三条指令的变量表达式OPER1+1为字节类型属性,AX却为字寄存器,第四条指令的OPER2为字类型属性,AL为字节寄存器,因此,汇编程序将指示这两条MOV指令出错:"类型不匹配"。
OPER1 DB ?, ?
OPER2 DW ?, ?
.
.
.
MOV OPER1, 0 ;字节指令
MOV OPER2, 0 ;字指令
MOV AX, OPER1+1 ;错误指令:类型不匹配
MOV AL, OPER2 ;错误指令:类型不匹配
PTR属性操作符
下例中的两条MOV指令把OPER1+1的类型属性指定为字,把OPER2的类型属性指定为字节,这样指令中两个操作数的属性就一致了,汇编时就不会出错了。
OPER1 DB ?, ?
OPER2 DW ?, ?
.
.
.
MOV AX, WORD PTR OPER1+1
MOV AL, BYTE PTR OPER2
LABEL伪指令
例如:
BYTE_ARRAY LABEL BYTE
WORD_ARRAY DW 50 DUP (?)
在50个字数组中的第一个字节的地址赋予两个不同类型的变量名:字节类型的变量BYTE_ARRAY和字类型变量WORD_ARRAY。
在程序中访问数组单元时,要按指令类型来选择变量,如下面两条指令:
MOV WORD_ARRAY + 2,0 ; 字指令,
; 把该数组的第3个和第4个字节置0
MOV BYTE_ARRAY + 2,0 ; 字节指令,
; 把该数组的第3个字节置0
表达式赋值伪操作EQU
EQU是一个赋值伪操作(伪指令),它给一个数据标号赋于一个常数值,但这个常数不占用存储单元。当这个数据标号出现在程序中时,汇编程序即用它的常数值代替数据标号。EQU可以在数据段之外使用,甚至可用在代码段中间。
= 伪操作
赋值伪操作"="的作用与EQU类似。它们之间的区别是,EQU伪操作中的标号名是不允许重复定义的,而=伪操作是允许重复定义的。
使用EQU操作的优点可从下面的例子中看出:
COUNT EQU 25
COUNTER DB COUNT
MOV AL, COUNT
假定在数据段和代码段中要多次使用一个数据(如25),那么在编程时凡是用到25的地方都可用数据标号COUNT来表示。如果程序想修改这个数据,那么只需修改EQU的赋值,而无须修改程序中其它部分,如COUNTER和MOV语句就不必修改。
EQU还可给表达式赋予一个名字,EQU的用法举例如下:
DATA EQU HEIGHT + 12 ; 地址表达式赋以符号名
ALPHA EQU 7 ; 常数赋以符号名
BETA EQU ALPHA-2 ; 把7-2=5赋以符号名BETA
ADDR EQU VAR + BETA ; VAR+5赋以符号名ADDR。
B EQU [BP + 8] ; 变址引用赋以符号名 B
P8 EQU DS:[BP + 8] ; 加段前缀的变址引用赋以符号名P8
注意:在EQU语句的表达式中,如果有变量或标号的表达式,则在该语句前应该先给出它们的定义。如上例,ALPHA必须在BETA之前定义,否则汇编程序将指示出错。
例如, TMP EQU 5
TMP EQU TMP+1 则是错误语句,因为TMP已赋值为5,就不能再把它定义为其它数值。
而 TMP = 5
TMP = TMP+1 则是允许使用的,因为=伪操作允许重复定义。第一个语句TMP的值为5,第二个语句TMP的值就为6了。
地址计数器与对准伪指令
1.地址计数器$
在汇编程序对源程序汇编的过程中,使用地址计数器来保存当前正在汇编的指令的地址。地址计数器的值在汇编语言中可用$来表示。
当$用在伪指令的参数字段时,它所表示的是地址计数器的当前值
2.EVEN伪指令
EVEN伪指令使下一个变量或指令开始于偶数字节地址。
3. ALIGN伪指令
ALIGN伪指令使它后面的数据或指令从2的整数倍地址开始。其格式为:
ALIGN 2n (n为任意整数)
1.地址计数器$
汇编语言允许用户直接用$来引用地址计数器的值,例如指令:
JMP $+ 6
它的转向地址是JMP指令的首地址加上6。当$用在指令中时,它表示本条指令的第一个字节的地址。在这里,$+ 6必须是另一条指令的首地址。否则,汇编程序将指示出错信息。
当$用在伪指令的参数字段时,则和它用在指令中的情况不同,它所表示的是地址计数器的当前值。例如指令:
ARRAY DW 1, 2, $+ 4, 3, 4, $+ 4
假设汇编时ARRAY 分配的偏移地址为0074H,则汇编后,$+ 4所在的两个字单元:
(ARRAY+4)=0078+4=007CH
(ARRAY+0A)=007E+4=0082H
应当注意,ARRAY数组中的两个$+ 4得到的结果是不同的,这是由于$的值是在不断变化的缘故。当在指令中用到$时,它只代表该指令的首地址,而与$本身所在的字节无关。
2.EVEN伪指令
例如:
DATA_SEG SEGMENT
BYTE_DAT DB ?
EVEN
WORD_DAT DW 100 DUP (?)
DATA_SEG ENDS
一个字的地址最好从偶地址开始,所以对于字数组为了保证它从偶地址开始,可以在DW定义之前用EVEN伪指令来达到这一目的。
3. ALIGN伪指令
例如:
.
ALIGN 4
ARRAY DD 100 DUP (?)
ALIGN伪指令保证了双字数组ARRAY地址边界从4的倍数开始。
ALIGN伪指令是将当前偏移地址指针指向2的乘方的整数倍的地址,如果源地址指针以指向2的乘方的整数倍的地址,则不作调整;否则将指针加以一个数,使地址指针指向下一个2的乘方的整数倍的地址。
当然,ALIGN 2和EVEN是等价的。
基数控制伪指令
.RADIX伪指令
.RADIX可以把默认的基数改变为2~16范围内的任何基数。其格式如下:
.RADIX 基数值
其中基数值用十进制数来表示。
例如:
MOV BX, 0FFH ;16进制数标记为H
MOV BL, 10000101B ;二进制数标记为B
MOV BX, 178 ;10进制为默认的基数,可无标记
.RADIX 16 ;以下程序默认16进制数
MOV BX, 0FF ;16进制为默认的基数,可无标记
MOV BX, 178D ;10进制数应加标记D
应当注意,在用 .RADIX 16把基数定为十六进制后,十进制数后面都应跟字母D。在这种情况下,如果某个十六进制数的末字符为D,则应在其后跟字母H,以免与十进制数发生混淆。