X86汇编快速入门

本文转自http://www.cnblogs.com/YukiJohnson/archive/2012/10/27/2741836.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 汇编指令

汇编指令通常可以分为数据传送指令、逻辑计算指令和控制流指令。本节将讲述其中最重要的指令,以下标记分别表示寄存器、内存和常数。

     32位寄存器 (EAXEBXECXEDXESIEDIESP, or EBP)
16位寄存器 (AXBXCX, or DX)
8位寄存器(AHBHCHDHALBLCL, or DL)
任何寄存器
   
内存地址 (e.g., [eax][var + 4], or dword ptr [eax+ebx])
32为常数
16位常数
8位常数
任何8位、16位或32位常数

3.1 数据传送指令

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

mov指令将第二个操作数(可以是寄存器的内容、内存中的内容或值)复制到第一个操作数(寄存器或内存)。mov不能用于直接从内存复制到内存,其语法如下所示:

mov ,
mov ,
mov ,
mov ,
mov ,

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
push
push

Examples
push eax — 将eax内容压栈
push [var] — 将var指示的4直接内容压栈

pop— Pop stack

pop指令与push指令相反,它执行的是出栈的工作。它首先将ESP指示的地址中的内容出栈,然后将ESP值加4. 其语法如下所示:
pop
pop

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 ,

Examples
lea eax, [var] — var指示的地址载入eax中.
lea edi, [ebx+4*esi] — ebx+4*esi表示的地址载入到edi中,这实际是上面所说的寻址模式的一种表示方式.

3.2 算术和逻辑指令

add— Integer Addition

add指令将两个操作数相加,且将相加后的结果保存到第一个操作数中。其语法如下所示:

add ,
add ,
add ,
add ,
add ,
Examples
add eax, 10 — EAX ← EAX + 10
add BYTE PTR [var], 10 — 10与var指示的内存中的一个byte的值相加,并将结果保存在var指示的内存中

sub— Integer Subtraction

sub指令指示第一个操作数减去第二个操作数,并将相减后的值保存在第一个操作数,其语法如下所示:

sub ,
sub ,
sub ,
sub ,
sub ,
Examples
sub al, ah — AL ← AL - AH
sub eax, 216 — eax中的值减26,并将计算值保存在eax中

inc, dec— Increment, Decrement

inc,dec分别表示将操作数自加1,自减1,其语法如下所示:

inc
inc
dec
dec

Examples
dec eax — eax中的值自减1.
inc DWORD PTR [var] — var指示内存中的一个4-byte值自加1

imul— Integer Multiplication

整数相乘指令,它有两种指令格式,一种为两个操作数,将两个操作数的值相乘,并将结果保存在第一个操作数中,第一个操作数必须为寄存器;第二种格式为三个操作数,其语义为:将第二个和第三个操作数相乘,并将结果保存在第一个操作数中,第一个操作数必须为寄存器。其语法如下所示:

imul ,
imul ,
imul ,,
imul ,,

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
idiv

Examples

idiv ebx
idiv DWORD PTR [var]
 
and, or, xor— Bitwise logical and, or and exclusive or
逻辑与、逻辑或、逻辑异或操作指令,用于操作数的位操作,操作结果放在第一个操作数中。其语法如下所示:
and ,
and ,
and ,
and ,
and ,

or ,
or ,
or ,
or ,
or ,

xor ,
xor ,
xor ,
xor ,
xor ,

Examples
and eax, 0fH — 将eax中的钱28位全部置为0,最后4位保持不变.
xor edx, edx — 设置edx中的内容为0.

not— Bitwise Logical Not

位翻转指令,将操作数中的每一位翻转,即0->1, 1->0。其语法如下所示:

not
not

Example
not BYTE PTR [var] — 将var指示的一个字节中的所有位翻转.

neg— Negate

取负指令。语法为:

neg
neg

Example
neg eax — EAX → - EAX

shl, shr— Shift Left, Shift Right

位移指令,有两个操作数,第一个操作数表示被操作数,第二个操作数指示位移的数量。其语法如下所示:

shl ,
shl ,
shl ,
shl ,

shr ,
shr ,
shr ,
shr ,

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 2 n 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

Example
jmp begin — Jump to the instruction labeled begin.

jcondition— Conditional Jump

条件转移指令,条件转移指令依据机器状态字中的一些列条件状态转移。机器状态字中包括指示最后一个算数运算结果是否为0,运算结果是否为负数等。机器状态字具体解释请见微机原理、计算机组成等课程。语法如下所示:

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

Example
cmp eax, ebx
jle done  , 如果eax中的值小于ebx中的值,跳转到done指示的区域执行,否则,执行下一条指令。

cmp— Compare
cmp指令比较两个操作数的值,并根据比较结果设置机器状态字中的条件码。此指令与sub指令类似,但是cmp不用将计算结果保存在操作数中。其语法如下所示:
cmp ,
cmp ,
cmp ,
cmp ,

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
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.

你可能感兴趣的:(编程语言)