開始學習匯編語言,對相關的所學知識做個總結,希望對自己可以有所提高。
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)
根據標志位跳轉的指令:
跳轉相關的標志位:
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定義的段名。
·定位類型:說明段的起始邊界值(物理地址)。
·組合類型:說明程序連接時的段組合方法。
·類別:在單引號中給出連接時組成段組的類型名。連接程序可把相同類別的段的位置靠在一起。
; * * * * * * * * * * * * * * * * * * * * * * *
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
較新版本的匯編程序(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.簡化段定義舉例
例
.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 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偽指令定義段組后,段組內統一為一個段地址,各段定義的變量和標號都可以用同一個段寄存器進行訪問。
程序開始和結束偽指令
在程序的開始可以用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,以免與十進制數發生混淆。