X86彙編快速入門

劍西樓發表於2017-02-08

本文翻譯自:http://www.cs.virginia.edu/~evans/cs216/guides/x86.html

本文描述基本的32位X86組合語言的一個子集,其中涉及組合語言的最核心部分,包括暫存器結構,資料表示,基本的操作指令(包括資料傳送指令、邏輯計算指令、算數運算指令),以及函式的呼叫規則。個人認為:在理解了本文後,基本可以無障礙地閱讀絕大部分標準X86彙編程式。當然,更復雜的指令請參閱Intel相關文件。

1 暫存器.

主要暫存器如下圖所示:


X86處理器中有8個32位的通用暫存器。由於歷史的原因,EAX通常用於計算,ECX通常用於迴圈變數計數。ESP和EBP有專門用途,ESP指示棧指標(用於指示棧頂位置),而EBP則是基址指標(用於指示子程式或函式呼叫的基址指標)。如圖中所示,EAX、EBX、ECX和EDX的前兩個高位位元組和後兩個低位位元組可以獨立使用,其中兩位低位元組又被獨立分為H和L部分,這樣做的原因主要是考慮相容16位的程式,具體相容匹配細節請查閱相關文獻。

應用暫存器時,其名稱大小寫是不敏感的,如EAX和eax沒有區別。

2 記憶體和定址模式

2.1宣告靜態資料區

可以在X86組合語言中用匯編指令.DATA宣告靜態資料區(類似於全域性變數),資料以單位元組、雙位元組、或雙字(4位元組)的方式存放,分別用DB,DW, DD指令表示宣告記憶體的長度。在組合語言中,相鄰定義的標籤在記憶體中是連續存放的。

.DATA      
var DB 64   ;宣告一個位元組,並將數值64放入此位元組中
var2 DB ? ; 宣告一個為初始化的位元組.
  DB 10 ; 宣告一個沒有label的位元組,其值為10.
X DW ? 宣告一個雙位元組,未初始化.
Y DD 30000     ; 宣告一個4位元組,其值為30000.

還可以宣告連續的資料和陣列,宣告陣列時使用DUP關鍵字

Z DD 1, 2, 3 ; Declare three 4-byte values, initialized to 1, 2, and 3. The value of location Z + 8 will be 3.
bytes   DB 10 DUP(?) ; Declare 10 uninitialized bytes starting at location bytes.
arr DD 100 DUP(0)     ; Declare 100 4-byte words starting at location arr, all initialized to 0
str DB 'hello',0 ; Declare 6 bytes starting at the address str, initialized to the ASCII character values for hello and the null (0) byte.

2.2 定址模式

現代X86處理器具有232位元組的定址空間。在上面的例子中,我們用標籤(label)表示記憶體區域,這些標籤在實際彙編時,均被32位的實際地址代替。除了支援這種直接的記憶體區域描述,X86還提供了一種靈活的記憶體定址方式,即利用最多兩個32位的暫存器和一個32位的有符號常數相加計算一個記憶體地址,其中一個暫存器可以左移1、2或3位以表述更大的空間。下面例子是彙編程式中常見的方式

mov eax, [ebx] ; 將ebx值指示的記憶體地址中的4個位元組傳送到eax中
mov [var], ebx ebx的內容傳送到var的值指示的記憶體地址中.
mov eax, [esi-4] ; 將esi-4值指示的記憶體地址中的4個位元組傳送到eax中
mov [esi+eax], cl ; 將cl的值傳送到esi+eax的值指示的記憶體地址中
mov edx, [esi+4*ebx]     ; 將esi+4*ebx值指示的記憶體中的4個位元組傳送到edx

下面是違反規則的例子:

mov eax, [ebx-ecx] ; 只能用加法
mov [eax+esi+edi], ebx     ; 最多隻能有兩個暫存器參與運算

2.3 長度規定

在宣告記憶體大小時,在組合語言中,一般用DB,DW,DD均可宣告的記憶體空間大小,這種現實宣告能夠很好地指導彙編器分配記憶體空間,但是,對於

mov [ebx], 2

如果沒有特殊的標識,則不確定常數2是單位元組、雙位元組,還是雙字。對於這種情況,X86提供了三個指示規則標記,分別為BYTE PTRWORD PTR, and DWORD PTR,如上面例子寫成:mov BYTE PTR [ebx], 2, mov WORD PTR [ebx], 2, mov DWORD PTR [ebx], 2,則意思非常清晰。

3 彙編指令

彙編指令通常可以分為資料傳送指令、邏輯計算指令和控制流指令。本節將講述其中最重要的指令,以下標記分別表示暫存器、記憶體和常數。

<reg32>     32位暫存器 (EAXEBXECXEDXESIEDIESP, or EBP)
<reg16> 16位暫存器 (AXBXCX, or DX)
<reg8> 8位暫存器(AHBHCHDHALBLCL, or DL)
<reg> 任何暫存器
   
<mem> 記憶體地址 (e.g., [eax][var + 4], or dword ptr [eax+ebx])
<con32> 32為常數
<con16> 16位常數
<con8> 8位常數
<con> 任何8位、16位或32位常數

3.1 資料傳送指令

mov — Move (Opcodes: 88, 89, 8A, 8B, 8C, 8E, ...)

mov指令將第二個運算元(可以是暫存器的內容、記憶體中的內容或值)複製到第一個運算元(暫存器或記憶體)。mov不能用於直接從記憶體複製到記憶體,其語法如下所示:

mov <reg>,<reg>
mov <reg>,<mem>
mov <mem>,<reg>
mov <reg>,<const>
mov <mem>,<const>

Examples
mov eax, ebx — 將ebx的值拷貝到eax
mov byte ptr [var], 5 — 將5儲存找var指示記憶體中的一個位元組中

push— Push stack (Opcodes: FF, 89, 8A, 8B, 8C, 8E, ...)

push指令將運算元壓入記憶體的棧中,棧是程式設計中一種非常重要的資料結構,其主要用於函式呼叫過程中,其中ESP只是棧頂。在壓棧前,首先將ESP值減4(X86棧增長方向與記憶體地址編號增長方向相反),然後將運算元內容壓入ESP指示的位置。其語法如下所示:

push <reg32>
push <mem>
push <con32>

Examples
push eax — 將eax內容壓棧
push [var] — 將var指示的4直接內容壓棧

pop— Pop stack

pop指令與push指令相反,它執行的是出棧的工作。它首先將ESP指示的地址中的內容出棧,然後將ESP值加4. 其語法如下所示:
pop <reg32>
pop <mem>

Examples
pop edi — pop the top element of the stack into EDI.
pop [ebx] — pop the top element of the stack into memory at the four bytes starting at location EBX.

lea— Load effective address

 lea實際上是一個載入有效地址指令,將第二個運算元表示的地址載入到第一個運算元(暫存器)中。其語法如下所示:

Syntax
lea <reg32>,<mem>

Examples
lea eax, [var] — var指示的地址載入eax中.
lea edi, [ebx+4*esi] — ebx+4*esi表示的地址載入到edi中,這實際是上面所說的定址模式的一種表示方式.

3.2 算術和邏輯指令

add— Integer Addition

add指令將兩個運算元相加,且將相加後的結果儲存到第一個運算元中。其語法如下所示:

add <reg>,<reg>
add <reg>,<mem>
add <mem>,<reg>
add <reg>,<con>
add <mem>,<con>
Examples
add eax, 10 — EAX ← EAX + 10
add BYTE PTR [var], 10 — 10與var指示的記憶體中的一個byte的值相加,並將結果儲存在var指示的記憶體中

sub— Integer Subtraction

sub指令指示第一個運算元減去第二個運算元,並將相減後的值儲存在第一個運算元,其語法如下所示:

sub <reg>,<reg>
sub <reg>,<mem>
sub <mem>,<reg>
sub <reg>,<con>
sub <mem>,<con>
Examples
sub al, ah — AL ← AL - AH
sub eax, 216 — eax中的值減26,並將計算值儲存在eax中

inc, dec— Increment, Decrement

inc,dec分別表示將運算元自加1,自減1,其語法如下所示:

inc <reg>
inc <mem>
dec <reg>
dec <mem>

Examples
dec eax — eax中的值自減1.
inc DWORD PTR [var] — var指示記憶體中的一個4-byte值自加1

imul— Integer Multiplication

整數相乘指令,它有兩種指令格式,一種為兩個運算元,將兩個運算元的值相乘,並將結果儲存在第一個運算元中,第一個運算元必須為暫存器;第二種格式為三個運算元,其語義為:將第二個和第三個運算元相乘,並將結果儲存在第一個運算元中,第一個運算元必須為暫存器。其語法如下所示:

imul <reg32>,<reg32>
imul <reg32>,<mem>
imul <reg32>,<reg32>,<con>
imul <reg32>,<mem>,<con>

Examples

imul eax, [var] — eax→ eax * [var]
imul esi, edi, 25 — ESI → EDI * 25

idiv— Integer Division

idiv指令完成整數除法操作,idiv只有一個運算元,此運算元為除數,而被除數則為EDX:EAX中的內容(一個64位的整數),操作的結果有兩部分:商和餘數,其中商放在eax暫存器中,而餘數則放在edx暫存器中。其語法如下所示:

Syntax
idiv <reg32>
idiv <mem>

Examples

idiv ebx
idiv DWORD PTR [var]
 
and, or, xor— Bitwise logical and, or and exclusive or
邏輯與、邏輯或、邏輯異或操作指令,用於運算元的位操作,操作結果放在第一個運算元中。其語法如下所示:
and <reg>,<reg>
and <reg>,<mem>
and <mem>,<reg>
and <reg>,<con>
and <mem>,<con>

or <reg>,<reg>
or <reg>,<mem>
or <mem>,<reg>
or <reg>,<con>
or <mem>,<con>

xor <reg>,<reg>
xor <reg>,<mem>
xor <mem>,<reg>
xor <reg>,<con>
xor <mem>,<con>

Examples
and eax, 0fH — 將eax中的錢28位全部置為0,最後4位保持不變.
xor edx, edx — 設定edx中的內容為0.

not— Bitwise Logical Not

位翻轉指令,將運算元中的每一位翻轉,即0->1, 1->0。其語法如下所示:

not <reg>
not <mem>

Example
not BYTE PTR [var] — 將var指示的一個位元組中的所有位翻轉.

neg— Negate

取負指令。語法為:

neg <reg>
neg <mem>

Example
neg eax — EAX → - EAX

shl, shr— Shift Left, Shift Right

位移指令,有兩個運算元,第一個運算元表示被運算元,第二個運算元指示位移的數量。其語法如下所示:

shl <reg>,<con8>
shl <mem>,<con8>
shl <reg>,<cl>
shl <mem>,<cl>

shr <reg>,<con8>
shr <mem>,<con8>
shr <reg>,<cl>
shr <mem>,<cl>

Examples

shl eax, 1 — Multiply the value of EAX by 2 (if the most significant bit is 0),左移1位,相當於乘以2
shr ebx, cl — Store in EBX the floor of result of dividing the value of EBX by 2n where n is the value in CL.
 
3.3 控制轉移指令
X86處理器維持著一個指示當前執行指令的指令指標(IP),當一條指令執行後,此指標自動指向下一條指令。IP暫存器不能直接操作,但是可以用控制流指令更新。
一般用標籤(label)指示程式中的指令地址,在X86彙編程式碼中,可以在任何指令前加入標籤。如:
       mov esi, [ebp+8]
begin: xor ecx, ecx
       mov eax, [esi]

如第二條指令用begin指示,這種標籤的方法在某種程度上簡化了彙編程式設計,控制流指令通過標籤實現程式指令跳轉。

jmp — Jump

控制轉移到label所指示的地址,(從label中取出執行執行),如下所示:

jmp <label>

Example
jmp begin — Jump to the instruction labeled begin.

jcondition— Conditional Jump

條件轉移指令,條件轉移指令依據機器狀態字中的一些列條件狀態轉移。機器狀態字中包括指示最後一個算數運算結果是否為0,運算結果是否為負數等。機器狀態字具體解釋請見微機原理、計算機組成等課程。語法如下所示:

je <label> (jump when equal)
jne <label> (jump when not equal)
jz <label> (jump when last result was zero)
jg <label> (jump when greater than)
jge <label> (jump when greater than or equal to)
jl <label> (jump when less than)
jle <label>(jump when less than or equal to)

Example
cmp eax, ebx
jle done  , 如果eax中的值小於ebx中的值,跳轉到done指示的區域執行,否則,執行下一條指令。

cmp— Compare
cmp指令比較兩個運算元的值,並根據比較結果設定機器狀態字中的條件碼。此指令與sub指令類似,但是cmp不用將計算結果儲存在運算元中。其語法如下所示:
cmp <reg>,<reg>
cmp <reg>,<mem>
cmp <mem>,<reg>
cmp <reg>,<con>

Example
cmp DWORD PTR [var], 10
jeq loop, 

比較var指示的4位元組內容是否為10,如果不是,則繼續執行下一條指令,否則,跳轉到loop指示的指令開始執行
 
callret— Subroutine call and return
這兩條指令實現子程式(過程、函式等意思)的呼叫及返回。call指令首先將當前執行指令地址入棧,然後無條件轉移到由標籤指示的指令。與其它簡單的跳轉指令不同,call指令儲存呼叫之前的地址資訊(當call指令結束後,返回到呼叫之前的地址)。
ret指令實現子程式的返回機制,ret指令彈出棧中儲存的指令地址,然後無條件轉移到儲存的指令地址執行。
call,ret是函式呼叫中最關鍵的兩條指令。具體細節見下面一部分的講解。語法為:
call <label>
ret
 
4 呼叫規則
為了加強程式設計師之間的協作及簡化程式開發程式,設定一個函式呼叫規則非常必要,函式呼叫規則規定函式呼叫及返回的規則,只要遵照這種規則寫的程式均可以正確執行,從而程式設計師不必關心諸如引數如何傳遞等問題;另一方面,在組合語言中可以呼叫符合這種規則的高階語言所寫的函式,從而將組合語言程式與高階語言程式有機結合在一起。
呼叫規則分為兩個方面,及呼叫者規則和被呼叫者規則,如一個函式A呼叫一個函式B,則A被稱為呼叫者(Caller),B被稱為被呼叫者(Callee)。
下圖顯示一個呼叫過程中的記憶體中的棧佈局:

在X86中,棧增長方向與記憶體編號增長方向相反。

Caller Rules

呼叫者規則包括一系列操作,描述如下:

1)在呼叫子程式之前,呼叫者應該儲存一系列被設計為呼叫者儲存的暫存器的值。呼叫者儲存暫存器有eax,ecx,edx。由於被呼叫的子程式會修改這些暫存器,所以為了在呼叫子程式完成之後能正確執行,呼叫者必須在呼叫子程式之前將這些暫存器的值入棧。

2)在呼叫子程式之前,將引數入棧。引數入棧的順序應該是從最後一個引數開始,如上圖中parameter3先入棧。

3)利用call指令呼叫子程式。這條指令將返回地址放置在引數的上面,並進入子程式的指令執行。(子程式的執行將按照被呼叫者的規則執行)

當子程式返回時,呼叫者期望找到子程式儲存在eax中的返回地址。為了恢復呼叫子程式執行之前的狀態,呼叫者應該執行以下操作:

1)清除棧中的引數;

2)將棧中儲存的eax值、ecx值以及edx值出棧,恢復eax、ecx、edx的值(當然,如果其它暫存器在呼叫之前需要儲存,也需要完成類似入棧和出棧操作)

Example 

如下程式碼展示了一個呼叫子程式的呼叫者應該執行的操作。此彙編程式呼叫一個具有三個引數的函式_myFunc,其中第一個引數為eax,第二個引數為常數216,第三個引數為var指示的記憶體中的值。

push [var] ; Push last parameter first
push 216   ; Push the second parameter
push eax   ; Push first parameter last

call _myFunc ; Call the function (assume C naming)

add esp, 12

在呼叫返回時,呼叫者必須清除棧中的相應內容,在上例中,引數佔有12個位元組,為了消除這些引數,只需將ESP加12即可。

 _myFunc的值儲存在eax中,ecx和edx中的值也許已經被改變,呼叫者還必須在呼叫之前儲存在棧中,並在呼叫結束之後,出棧恢復ecx和edx的值。

被呼叫者規則

被呼叫者應該遵循如下規則:

1)將ebp入棧,並將esp中的值拷貝到ebp中,其彙編程式碼如下:

    push ebp
    mov  ebp, esp

上述程式碼的目的是儲存呼叫子程式之前的基址指標,基址指標用於尋找棧上的引數和區域性變數。當一個子程式開始執行時,基址指標儲存棧指標指示子程式的執行。為了在子程式完成之後呼叫者能正確定位呼叫者的引數和區域性變數,ebp的值需要返回。

2)在棧上為區域性變數分配空間。

3)儲存callee-saved暫存器的值,callee-saved暫存器包括ebx,edi和esi,將ebx,edi和esi壓棧。

4)在上述三個步驟完成之後,子程式開始執行,當子程式返回時,必須完成如下工作:

  4.1)將返回的執行結果儲存在eax中

  4.2)彈出棧中儲存的callee-saved暫存器值,恢復callee-saved暫存器的值(ESI和EDI)

  4.3)收回區域性變數的記憶體空間。實際處理時,通過改變EBP的值即可:mov esp, ebp。 

  4.4)通過彈出棧中儲存的ebp值恢復呼叫者的基址暫存器值。

  4.5)執行ret指令返回到呼叫者程式。

After these three actions are performed, the body of the subroutine may proceed. When the subroutine is returns, it must follow these steps:

  1. Leave the return value in EAX.

Example

.486
.MODEL FLAT
.CODE
PUBLIC _myFunc
_myFunc PROC
  ; Subroutine Prologue
  push ebp     ; Save the old base pointer value.
  mov ebp, esp ; Set the new base pointer value.
  sub esp, 4   ; Make room for one 4-byte local variable.
  push edi     ; Save the values of registers that the function
  push esi     ; will modify. This function uses EDI and ESI.
  ; (no need to save EBX, EBP, or ESP)

  ; Subroutine Body
  mov eax, [ebp+8]   ; Move value of parameter 1 into EAX
  mov esi, [ebp+12]  ; Move value of parameter 2 into ESI
  mov edi, [ebp+16]  ; Move value of parameter 3 into EDI

  mov [ebp-4], edi   ; Move EDI into the local variable
  add [ebp-4], esi   ; Add ESI into the local variable
  add eax, [ebp-4]   ; Add the contents of the local variable
                     ; into EAX (final result)

  ; Subroutine Epilogue 
  pop esi      ; Recover register values
  pop  edi
  mov esp, ebp ; Deallocate local variables
  pop ebp ; Restore the caller's base pointer value
  ret
_myFunc ENDP
END

子程式首先通過入棧的手段儲存ebp,分配區域性變數,儲存暫存器的值。

在子程式體中,引數和區域性變數均是通過ebp進行計算。由於引數傳遞在子程式被呼叫之前,所以引數總是在ebp指示的地址的下方(在棧中),因此,上例中的第一個引數的地址是ebp+8,第二個引數的地址是ebp+12,第三個引數的地址是ebp+16;而區域性變數在ebp指示的地址的上方,所有第一個區域性變數的地址是ebp-4,而第二個這是ebp-8.

相關文章