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指的是同一个寄存器。

在这里插入图片描述

内存和寻址模式

2.1声明静态数据区域
您可以使用特殊的汇编指令在x86汇编中声明静态数据区域(类似于全局变量)。数据声明应该在. Data指令之前。在此指令之后,可以使用指令DB、DW和DD分别声明一个字节、两个字节和四个字节的数据位置。声明的位置可以用名称标记,以便以后引用——这类似于按名称声明变量,但遵循一些较低级别的规则。例如,按顺序声明的位置将一个挨着一个地位于内存中。
示例声明:
在这里插入图片描述
在高级语言中,数组可以有多个维度,并通过索引进行访问,而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

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

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:

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.

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Walker_CodeGou

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值