本文描述基本的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 PTR, WORD PTR, and DWORD PTR,如上面例子写成:mov BYTE PTR [ebx], 2, mov WORD PTR [ebx], 2, mov DWORD PTR [ebx], 2,则意思非常清晰。
3 汇编指令
汇编指令通常可以分为数据传送指令、逻辑计算指令和控制流指令。本节将讲述其中最重要的指令,以下标记分别表示寄存器、内存和常数。
32位寄存器 (EAX, EBX, ECX, EDX, ESI, EDI, ESP, or EBP) | |
16位寄存器 (AX, BX, CX, or DX) | |
8位寄存器(AH, BH, CH, DH, AL, BL, CL, 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
idiv— Integer Division
idiv指令完成整数除法操作,idiv只有一个操作数,此操作数为除数,而被除数则为EDX:EAX中的内容(一个64位的整数),操作的结果有两部分:商和余数,其中商放在eax寄存器中,而余数则放在edx寄存器中。其语法如下所示:
Syntax
idiv
idiv
Examples
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
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指示的区域执行,否则,执行下一条指令。
Example
cmp DWORD PTR [var], 10
jeq loop,
在X86中,栈增长方向与内存编号增长方向相反。
调用者规则包括一系列操作,描述如下:
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:
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.
原文地址:http://www.cnblogs.com/YukiJohnson/archive/2012/10/27/2741836.html