X86汇编快速入门

Tips:本文非原创,是学习过程中找到的资料,出处已忘,侵权联删~

本指南描述了32位x86汇编语言编程的基础,涵盖了可用指令和汇编指令的一个小但有用的子集。有几种不同的汇编语言用于生成x86机器码。我们将在CS216中使用的是Microsoft宏汇编器(MASM)汇编器。MASM使用标准的Intel语法来编写x86汇编代码。
完整的x86指令集又大又复杂(Intel的x86指令集手册包括超过2900页),我们在本指南中没有涵盖所有内容。例如,x86指令集有一个16位子集。使用16位编程模型可能相当复杂。它有一个分段内存模型,对寄存器使用有更多的限制,等等。在本指南中,我们将把注意力限制在x86编程的更现代的方面,只深入研究指令集,以便对x86编程有一个基本的感觉。
资源
这是一个关于在Visual Studio中构建和调试汇编代码的教程
Intel x86指令集参考
英特尔奔腾手册(全是血淋淋的细节)

寄存器

现代的x86处理器有8个32位通用寄存器,如图1所示。寄存器名称大多是历史名称。例如,EAX过去被称为累加器,因为它被用于许多算术操作,而ECX被称为计数器,因为它被用于保存循环索引。尽管大多数寄存器在现代指令集中已经失去了它们的特殊用途,但按照约定,有两个寄存器被保留用于特殊用途——堆栈指针(ESP)和基本指针(EBP)。
对于EAX、EBX、ECX和EDX寄存器,可以使用子部分。例如,EAX中最低有效的2个字节可以作为16位寄存器AX处理。AX的最低有效字节可以用作一个8位寄存器,称为AL,而AX的最高有效字节可以用作一个8位寄存器,称为AH。这些名称指的是相同的物理寄存器。当将一个双字节的数量放入DX中时,更新会影响DH、DL和EDX的值。这些子寄存器主要是从较旧的16位指令集版本保留下来的。然而,在处理小于32位的数据(例如1字节ASCII字符)时,它们有时很方便。
在汇编语言中引用寄存器时,名称不区分大小写。例如,名称EAX和EAX指的是同一个寄存器。

X86汇编快速入门_第1张图片

内存和寻址模式

2.1声明静态数据区域
您可以使用特殊的汇编指令在x86汇编中声明静态数据区域(类似于全局变量)。数据声明应该在. Data指令之前。在此指令之后,可以使用指令DB、DW和DD分别声明一个字节、两个字节和四个字节的数据位置。声明的位置可以用名称标记,以便以后引用——这类似于按名称声明变量,但遵循一些较低级别的规则。例如,按顺序声明的位置将一个挨着一个地位于内存中。
示例声明:
X86汇编快速入门_第2张图片
在高级语言中,数组可以有多个维度,并通过索引进行访问,而x86汇编语言中的数组仅仅是位于内存中连续的若干单元。可以通过列出值来声明数组,如下面的第一个示例所示。声明数据数组的另外两种常用方法是DUP指令和字符串文字的使用。DUP指令告诉汇编程序将一个表达式复制给定的次数。例如,4 DUP(2)等于2,2,2,2。
例子:
在这里插入图片描述
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+4ebx] ; 将esi+4ebx值指示的内存中的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+4esi] — ebx+4esi表示的地址载入到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 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

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指示的指令开始执行

call, ret— Subroutine call and return
这两条指令实现子程序(过程、函数等意思)的调用及返回。call指令首先将当前执行指令地址入栈,然后无条件转移到由标签指示的指令。与其它简单的跳转指令不同,call指令保存调用之前的地址信息(当call指令结束后,返回到调用之前的地址)。
ret指令实现子程序的返回机制,ret指令弹出栈中保存的指令地址,然后无条件转移到保存的指令地址执行。
call,ret是函数调用中最关键的两条指令。具体细节见下面一部分的讲解。语法为:
call
ret

4 调用规则

为了加强程序员之间的协作及简化程序开发进程,设定一个函数调用规则非常必要,函数调用规则规定函数调用及返回的规则,只要遵照这种规则写的程序均可以正确执行,从而程序员不必关心诸如参数如何传递等问题;另一方面,在汇编语言中可以调用符合这种规则的高级语言所写的函数,从而将汇编语言程序与高级语言程序有机结合在一起。
调用规则分为两个方面,及调用者规则和被调用者规则,如一个函数A调用一个函数B,则A被称为调用者(Caller),B被称为被调用者(Callee)。
下图显示一个调用过程中的内存中的栈布局:
X86汇编快速入门_第3张图片
在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:

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.

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