1. 程式人生 > 其它 >彙編相關運算子和指令

彙編相關運算子和指令

技術標籤:彙編指令彙編操作符

組合語言直接偏移量運算元

變數名加上一個位移就形成了一個直接 - 偏移量運算元。這樣可以訪問那些沒有顯式標記的記憶體位置。假設現有一個位元組陣列 arrayB:

arrayB BYTE 10h,20h,30h,40h,50h

用該陣列作為 MOV 指令的源運算元,則自動傳送陣列的第一個位元組:

mov al,arrayB ;AL = 10h

通過在 arrayB 偏移量上加 1 就可以訪問該陣列的第二個位元組:

mov al,[arrayB+1] ;AL = 20h

如果加 2 就可以訪問該陣列的第三個位元組:

mov al, [arrayB+2] ;AL = 30h

形如 arrayB+1 一樣的表示式通過在變數偏移量上加常數來形成所謂的有效地址。有效地址外面的括號表明,通過解析這個表示式就可以得到該記憶體地址指示的內容彙編器並不要求在地址表示式之外加括號,但為了清晰明瞭,建議使用括號。

MASM 沒有內建的有效地址範圍檢查。在下面的例子中,假設陣列 arrayB 有 5 個位元組,而指令訪問的是該陣列範圍之外的一個記憶體位元組。其結果是一種難以發現的邏輯錯誤,因此,在檢查陣列引用時要非常小心:

mov al, [arrayB+20] ; AL = ??

字和雙字陣列

在 16 位的字陣列中,每個陣列元素的偏移量比前一個多 2 個位元組。這就是為什麼在下面的例子中,陣列 ArrayW 加 2 才能指向該陣列的第二個元素:

.data
arrayW WORD 100h,200h,300h
.code
mov ax, arrayW ;AX = 100h
mov ax,[arrayW+2] ;AX = 200h

同樣,如果是雙字陣列,則第一個元素偏移量加 4 才能指向第二個元素:

.data
arrayD DWORD l0000h,20000h
.code
mov eax, arrayD ;EAX = 10000h
mov eax,[arrayD+4] ;EAX = 20000h

組合語言OFFSET運算子:返回資料標號的偏移量

OFFSET 運算子返回資料標號的偏移量。這個偏移量按位元組計算,表示的是該資料標號距離資料段起始地址的距離。如下圖所示為資料段內名為 myByte 的變數。

名為myBye的變數

OFFSET 示例

在下面的例子中,將用到如下三種類型的變數:

.data
bVal BYTE ?
wVal WORD ?
dVal DWORD ?
dVal2 DWORD ?

假設 bVal 在偏移量為 0040 4000(十六進位制)的位置,則 OFFSET 運算子返回值如下:

mov esi,OFFSET bVal ; ESI = 00404000h
mov esi,OFFSET wVal ; ESI = 00404001h
mov esi,OFFSET dVal ; ESI = 00404003h
mov esi,OFFSET dVal2 ; ESI = 00404007h

OFFSET 也可以應用於直接 - 偏移量運算元。設 myArray 包含 5 個 16 位的字。下面的 MOV 指令首先得到 myArray 的偏移量,然後加 4,再將形成的結果地址直接傳送給 ESI。因此,現在可以說 ESI 指向陣列中的第 3 個整數。

.data
myArray WORD 1,2,3,4,5
.code
mov esi,OFFSET myArray + 4

還可以用一個變數的偏移量來初始化另一個雙字變數,從而有效地建立一個指標。如下例所示,pArray 就指向 bigArray 的起始地址:

.data
bigArray DWORD 500 DUP (?)
pArray DWORD bigArray

下面的指令把該指標的值載入到 ESI 中,因此,這個 ESI 暫存器就可以指向陣列的起始地址:

mov esi,pArray

組合語言ALIGN偽指令:對齊一個變數

ALIGN 偽指令將一個變數對齊到位元組邊界、字邊界、雙字邊界或段落邊界

語法如下:

ALIGN bound

Bound 可取值有:1、2、4、8、16。當取值為 1 時,則下一個變數對齊於 1 位元組邊界(預設情況)。當取值為 2 時,則下一個變數對齊於偶數地址。當取值為 4 時,則下一個變數地址為 4 的倍數。當取值為 16 時,則下一個變數地址為 16 的倍數,即一個段落的邊界。

為了滿足對齊要求,彙編器會在變數前插入一個或多個空位元組。為什麼要對齊資料?因為,對於儲存於偶地址和奇地址的資料來說,CPU 處理偶地址資料的速度要快得多

下述例子中,bVal 處於任意位置,但其偏移量為 0040 4000。在 wVal 之前插入 ALIGN 2 偽指令,這使得 wVal 對齊於偶地址偏移量:

bVal BYTE ? ;00404000h
ALIGN 2
wVal WORD ? ;00404002h
bVal2 BYTE ? ;00404004h
ALIGN 4
dVal DWORD ? ;00404008h
dVal2 DWORD ? ;0040400Ch

請注意,dVal 的偏移量原本是 0040 4005,但是 ALIGN 4 偽指令使它的偏移量成為 0040 4008。

組合語言PTR運算子:重寫運算元的大小型別

PTR 運算子可以用來重寫一個已經被宣告過的運算元的大小型別。只要試圖用不同於彙編器設定的大小屬性來訪問運算元,那麼這個運算子就是必需的。

例如,假設想要將一個雙字變數 myDouble 的低 16 位傳送給 AX由於運算元大小不匹配,因此,彙編器不會允許這種操作:

.data
myDouble DWORD 12345678h
.code
mov ax,myDouble

但是,使用 WORD PTR 運算子就能將低位字(5678h)送入 AX:

mov ax,WORD PTR myDouble

為什麼送入 AX 的不是 1234h ?因為,x86 處理器採用的是小端儲存格式,即低位位元組存放於變數的起始地址。如下圖所示,用三種方式表示 myDouble 的記憶體佈局:第一列是一個雙字,第二列是兩個字(5678h、1234h),第三列是四個位元組(78h、56h、34h、12h)。

myDouble的記憶體分佈

不論該變數是如何定義的,都可以用三種方法中的任何一種來訪問記憶體。比如,如果 myDouble 的偏移量為 0000,則以這個偏移量為首地址存放的 16 位值是 5678h。同時也可以檢索到 1234h,其字地址為 myDouble+2,指令如下:

mov ax,WORD PTR [myDouble+2] ; 1234h

同樣,用 BYTE PTR 運算子能夠把 myDouble 的單個位元組傳送到 BL:

mov b1,BYTE PTR myDouble ; 78h

注意,PTR 必須與一個標準彙編資料型別一起使用,這些型別包括:BYTE、SEYTE、WORD、SWORD、DWORD、SDWORD、FWORD、QWORD 或 TBYTE。

將較小的值送入較大的目的運算元

程式可能需要將兩個較小的值送入一個較大的目的運算元。如下例所示,第一個字複製到 EAX 的低半部分,第二個字複製到高半部分。而 DWORD PTR 運算子能實現這種操作:

.data
wordList WORD 5678h,1234h
.code
mov eax, DWORD PTR wordList ; EAX = 12345678h

組合語言TYPE運算子:返回變數的大小

TYPE 運算子返回變數單個元素的大小,這個大小是以位元組為單位計算的。比如,TYPE 為位元組,返回值是 1;TYPE 為字,返回值是 2;TYPE 為雙字,返回值是 4;TYPE 為四字,返回值是 8。示例如下:

.data
var1 BYTE ?
var2 WORD ?
var3 DWORD ?
var4 QWORD ?

下表是每個 TYPE 表示式的值。

表示式表示式
TYPE var11TYPE var34
TYPE var22TYPE var48

組合語言LENGTHOF運算子:計算陣列中元素的個數

LENGTHOF 運算子計算陣列中元素的個數,元素個數是由陣列標號同一行出現的數值來定義的。示例如下:

.data
byte1 BYTE 10,20,30
array1 WORD 30 DUP (?),0,0
array2 WORD 5 DUP(3 DUP(?))
array3 DWORD 1,2,3,4
digitStr BYTE "12345678",0

如果陣列定義中出現了巢狀的 DUP 運算子,那麼 LENGTHOF 返回的是兩個數值的乘積。下表列出了每個 LENGTHOF 表示式返回的數值。

表示式表示式
LENGTHOF byte13LENGTHOF array34
LENGTHOF array130+2LENGTHOF digitStr9
LENGTHOF array25*3

如果陣列定義佔據了多個程式行,那麼 LENGTHOF 只針對第一行定義的資料。比如有如下資料,則 LENGTHOF myArray 返回值為 5 :

myArray BYTE 10,20,30,40,50
BYTE 60,70,80,90,100

另外,也可以在第一行結尾處用逗號,並在下一行繼續進行陣列初始化。若有如下資料定義, LENGTHOF myArray 返回值為 10:

myArray BYTE 10,20,30,40,50,
60,70,80,90,100

組合語言LABEL偽指令

LABEL 偽指令可以插入一個標號,並定義它的大小屬性,但是不為這個標號分配儲存空間。LABEL 中可以使用所有的標準大小屬性,如 BYTE、WORD、DWORD、QWORD 或 TBYTE。
LABEL 常見的用法是,為資料段中定義的下一個變數提供不同的名稱和大小屬性。如下例所示,在變數 val32 前定義了一個變數,名稱為 val16 屬性為 WORD:

.data
val16 LABEL WORD
val32 DWORD 12345678h
.code
mov ax,val16 ; AX = 5678h
mov dx,[val16+2] ; DX = 1234h

val16 與 val32 共享同一個記憶體位置。LABEL 偽指令自身不分配記憶體。
有時需要用兩個較小的整陣列成一個較大的整數,如下例所示,兩個 16 位變數組成一個 32 位變數並載入到 EAX 中:

.data
LongValue LABEL DWORD
val1 WORD 5678h
val2 WORD 1234h
.code
mov eax,LongValue ; EAX = 12345678h

組合語言間接定址

直接定址很少用於陣列處理,因為,用常數偏移量來定址多個數組元素時,直接定址不實用。反之,會用暫存器作為指標(稱為間接定址)並控制該暫存器的值。如果一個運算元使用的是間接定址,就稱之為間接運算元。

間接運算元

保護模式

任何一個 32 位通用暫存器(EAX、EBX、ECX、EDX、ESI、EDI、EBP 和 ESP)加上括號就能構成一個間接運算元。

暫存器中存放的是資料的地址。示例如下,ESI 存放的是 byteVal 的偏移量,MOV 指令使用間接運算元作為源運算元,解析 ESI 中的偏移量,並將一個位元組送入 AL:

.data
byteVal BYTE 10h
.code
mov esi,OFFSET byteVal
mov al,[esi] ; AL = 10h

如果目的運算元也是間接運算元,那麼新值將存入由暫存器提供地址的記憶體位置。在下面的例子中,BL 暫存器的內容複製到 ESI 定址的記憶體地址中:

mov [esi],bl

PTR 與間接運算元一起使用

一個運算元的大小可能無法從指令中直接看出來。下面的指令會導致彙編器產生“operand must have size(運算元必須有大小)”的錯誤資訊:

inc [esi] ;錯誤:operand must have size

彙編器不知道 ESI 指標的型別是位元組、字、雙字,還是其他的型別。而 PTR 運算子則可以確定運算元的大小型別:

inc BYTE PTR [esi]

陣列

間接運算元是步進遍歷陣列的理想工具。下例中,arrayB 有 3 個位元組,隨著 ESI 不斷加 1,它就能順序指向每一個位元組:

.data
arrayB BYTE 10h,20h,30h
.code
mov esi,OFFSET arrayB
mov alz [esi] ;AL = lOh
inc esi
mov al, [esi] ;AL = 20h
inc esi
mov al, [esi] ;AL = 30h

如果陣列是 16 位整數型別,則 ESI 加 2 就可以順序定址每個陣列元素:

.data
arrayW WORD 1000h,2000h,3000h
.code
mov esi,OFFSET arrayW
mov ax,[esi] ; AX = 1000h
add esi, 2
mov ax,[esi] ; AX = 2000h
add esi, 2
mov axz [esi] ; AX = 3000h

假設 arrayW 的偏移量為 10200h,下圖展示的是 ESI 初始值相對陣列資料的位置。

示例:32 位整數相加下面的程式碼示例實現的是 3 個雙字相加。由於雙字是 4 個位元組的,因此,ESI 要加 4 才能順序指向每個陣列數值:

.data
arrayD DWORD 10000h,20000h,30000h
.code
mov esi,OFFSET arrayD
mov eax, [esi] ;(第一個數)
add esi, 4
add eax, [esi] ;(第二個數)
add esi, 4
add eax, [esi] ;(第三個數)

假設 arrayD 的偏移量為 10200h。下圖展示的是 ESI 初始值相對陣列資料的位置:

變址運算元

變址運算元是指,在暫存器上加上常數產生一個有效地址。每個 32 位通用暫存器都可以用作變址暫存器。MASM 可以用不同的符號來表示變址運算元(括號是表示符號的一部分):

constant [reg]
[constant + reg]

第一種形式是變數名加上暫存器。變數名由彙編器轉換為常數,代表的是該變數的偏移量。下面給岀的是兩種符號形式的例子:

arrayB[esi][arrayB + esi]
arrayD[ebx][arrayD + ebx]

變址運算元非常適合於陣列處理。在訪問第一個陣列元素之前,變址暫存器需要初始化為 0:

.data
arrayB BYTE 10h,20h,30h
.code
mov esi, 0
mov al, arrayB[esi] ; AL = 10h

最後一條語句將 ESI 和 arrayB 的偏移量相加,表示式 [arrayB+ESI] 產生的地址被解析,並將相應記憶體位元組的內容複製到AL。
增加位移量變址定址的第二種形式是暫存器加上常數偏移量。變址暫存器儲存陣列或結構的基址,常數標識各個陣列元素的偏移量。下例展示了在一個 16 位字陣列中如何使用這種形式:

.data
arrayW WORD 1000h,2000h,3000h
.code
mov esi,OFFSET arrayW
mov ax, [esi] ;AX = 1000h
mov ax, [esi+2] ;AX = 2000h
mov ax, [esi+4] ;AX = 3000h

使用 16 位暫存器

在實地址模式中,一般用 16 位暫存器作為變址運算元。在這種情況下,能被使用的暫存器只有 SI、DI、BX 和 BP:

mov al,arrayB[si]
mov ax,arrayW[di]
mov eax,arrayD[bx]

如果有間接運算元,則要避免使用 BP 暫存器,除非是定址堆疊資料。

變址運算元中的比例因子

在計算偏移量時,變址運算元必須考慮每個陣列元素的大小。比如下例中的雙字陣列,下標(3 )要乘以 4(一個雙字的大小)才能生成內容為 400h 的陣列元素的偏移量:

.data
arrayD DWORD 100h, 200h, 300h, 400h
.code
mov esi , 3 * TYPE arrayD ; arrayD [ 3 ]的偏移量
mov eax,arrayD[esi] ; EAX = 400h

Intel 設計師希望能讓編譯器編寫者的常用操作更容易,因此,他們提供了一種計算偏移量的方法,即使用比例因子。比例因子是陣列元素的大小(字 = 2,雙字 =4,四字 =8)。現在對剛才的例子進行修改,將陣列下標(3)送入 ESI,然後 ESI 乘以雙字的比例因子(4):

.data
arrayD DWORD 1,2,3,4
.code
mov esi, 3 ;下標
mov eax,arrayD[esi*4] ;EAX = 4

TYPE 運算子能讓變址更加靈活,它可以讓 arrayD 在以後重新定義為別的型別:

mov esi, 3 ;下標
mov eax,arrayD[esi*TYPE arrayD] ;EAX = 4

指標

如果一個變數包含另一個變數的地址,則該變數稱為指標。指標是控制陣列和資料結構的重要工具,因為,它包含的地址在執行時是可以修改的。比如,可以使用系統呼叫來分配(保留)一個記憶體塊,再把這個塊的地址儲存在一個變數中。

指標的大小受處理器當前模式(32位或64位)的影響。下例為 32 位的程式碼,ptrB 包含了 arrayB 的偏移量:

.data
arrayB byte 10h,20h,30h,40h
ptrB dword arrayB

還可以用 OFFSET 運算子來定義 ptrB,從而使得這種關係更加明確:

ptrB dword OFFSET arrayB

32 位模式程式使用的是近指標,因此,它們儲存在雙字變數中。這裡有兩個例子:ptrB 包含 arrayB 的偏移量,ptrW 包含 arrayW 的偏移量:

arrayB BYTE 10h,20h,30h,40h
arrayW WORD 1000h,2000h,3000h
ptrB DWORD arrayB
ptrW DWORD arrayW

同樣,也還可以用 OFFSET 運算子使這種關係更加明確:

ptrB DWORD OFFSET arrayB
ptrW DWORD OFFSET arrayW

高階語言刻意隱藏了指標的物理細節,這是因為機器結構不同,指標的實現也有差異。組合語言中,由於面對的是單一實現,因此是在物理層上檢查和使用指標。這樣有助於消除圍繞著指標的一些神祕感。

使用 TYPEDEF 運算子

TYPEDEF 運算子可以建立使用者定義型別,這些型別包含了定義變數時內建型別的所有狀態。它是建立指標變數的理想工具。比如,下面宣告建立的一個新資料型別 PBYTE 就是一個位元組指標:

PBYTE TYPEDEF PTR BYTE

這個宣告通常放在靠近程式開始的地方,在資料段之前。然後,變數就可以用 PBYTE 來定義:

.data
arrayB BYTE 10h,20h,30h,40h
ptr1 PBYTE ? ;未初始化
ptr2 PBYTE arrayB ;指向一個數組

示例程式:Pointers

下面的程式(pointers.asm)用 TYPEDEF 建立了 3 個指標型別(PBYTE、PWORD、PDWORD)。此外,程式還建立了幾個指標,分配了一些陣列偏移量,並解析了這些指標:


TITLE Pointers (Pointers.asm)
.386
.model flat,stdcall
.stack 4096
ExitProcess proto,dwExitCode:dword
;建立使用者定義型別
PBYTE TYPEDEF PTR BYTE ;位元組指標
PWORD TYPEDEF PTR WORD ;字指標
PDWORD TYPEDEF PTR DWORD ;雙字指標

.data
arrayB BYTE 10h,20h,30h
arrayW WORD 1,2,3
arrayD DWORD 4,5,6
;建立幾個指標變數
ptr1 PBYTE arrayB
ptr2 PWORD arrayW
ptr3 PDWORD arrayD

.code
main PROC
;使用指標訪問資料
mov esi,ptr1
mov al,[esi] ;10h
mov esi,ptr2
mov ax,[esi] ;1
mov esi,ptr3
mov eax,[esi] ;4
invoke ExitProcess,0
main ENDP
END main