x86汇编快速入门

2021/8/27 20:36:53

本文主要是介绍x86汇编快速入门,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

序言

本教程描述了32位x86汇编语言编程的基础知识,涵盖了可用指令和汇编器指令的一小部分但很有用的子集。

有几种不同的汇编语言可用于生成x86机器码。在这里我们使用Microsoft Macro Assembler (MASM)作为示例。MASM使用标准的Intel语法编写x86汇编代码。完整的x86指令集十分庞大复杂(英特尔的x86指令集手册长达2900多页),我们在本教程中不会全部介绍。例如,x86指令集有一个16位的子集。使用16位编程模型可能相当复杂。它采用分段内存模型,对寄存器使用有更多限制,等等。

在本教程中,我们将把注意力集中在x86编程的更多现代方面上,并深入研究指令集,以便对x86编程有一个基本的了解。

寄存器

现代(即386或更高)x86处理器有8个32位通用寄存器,如图1所示。寄存器名称大多是历史名称。例如,EAX过去被称为累加器(accumulator),因为它被许多算术运算使用,而ECX被称为计数器(counter),因为它被用来保存循环索引。虽然大多数寄存器在现代指令集中已经失去了它们的特殊用途,但按照惯例,有两个寄存器被保留用于特殊用途——堆栈指针(ESP)和基指针(EBP)。

对于EAX、EBX、ECX和EDX寄存器,可以使用子部分。例如,EAX的最低有效2个字节可被视作称为AX的16位寄存器。AX的最低有效字节可用作称为AL的单个8位寄存器,而AX的最高有效字节可用作称为AH的单个8位寄存器。这些名称指的是同一物理寄存器。将两字节量放入DX时,更新会影响DH、DL和EDX的值。这些子寄存器主要是兼容较旧的16位指令集版本。但是,在处理小于32位的数据(例如,1字节ASCII字符)时,它们有时会很方便。

在汇编语言中引用寄存器时,名称不区分大小写。例如,名称EAX和eax引用相同的寄存器。

存储器和寻址模式

声明静态数据区域

为此,您可以使用特殊的汇编指令在x86汇编中声明静态数据区域(类似于全局变量)。数据声明前面应该有.DATA指令。在此指令之后,可以使用指令DB、DW和DD分别声明一个、两个和四个字节的数据位置。声明的位置可以用名称标记以供以后引用——这类似于按名称声明变量,但遵循一些较低级别的规则。例如,相邻定义的标签在内存中是连续存放的。

示例声明:

.DATA
var DB 64   ; 声明一个字节,位置为var,值为6
var2 DB ?   ; 声明一个未初始化的字节,位置为var2
DB 10       ; 声明一个没有标签的字节,值为10,位置是var2+1
X DW ?      ; 声明一个2字节的未初始化值,位置为X
Y DD 30000  ; 声明一个4字节值,位置为Y,初始化为30000

在高级语言中,数组可以有很多维,并且可以通过索引进行访问,而x86汇编语言中的数组则不同,它只是位于内存中的若干个连续的单元格。只需列出值即可声明数组,如下面的第一个示例所示。用于声明数据数组的另外两种常用方法是DUP指令和字符串常量的使用。DUP指令告诉汇编器将表达式复制给定的次数。例如,4 DUP(2)等于2,2,2,2

下面是一些示例:

Z DD 1, 2, 3        ; 声明三个4字节值,初始化为1、2和3。位置Z+8的值将为3
bytes DB 10 DUP(?)  ; 从位置bytes开始声明10个未初始化的字节
arr DD 100 DUP(0)   ; 声明100个从位置arr开始的4字节字,全部初始化为0
str DB 'hello',0    ; 从地址str开始声明6个字节,初始化为hello的ASCII字符值和空字节0。

内存寻址

现代x86兼容处理器能够寻址多达232字节的内存:内存地址是32位宽。在上面的示例中,我们使用标签来引用内存区域,这些标签实际上被汇编器替换为指定32位的内存地址。除了支持通过标签(比如常量值)引用内存区域之外,x86还提供了计算和引用内存地址的灵活方案:最多可以将两个32位寄存器和一个32位有符号常量相加来计算内存地址。其中一个寄存器可以任选地预乘2、4或8。

寻址模式可以与许多x86指令一起使用(我们将在下一节描述它们)。这里我们演示了一些使用MOV指令在寄存器和内存之间移动数据的示例。此指令有两个操作对象:第一个是目标,第二个指定源。

使用地址计算的MOV指令的一些示例如下:

mov eax, [ebx]	     ; 将EBX中的地址所指向的内存中的4个字节移动到EAX中
mov [var], ebx	     ; 将EBX的内容移到内存地址var的4个字节中(注意,不加中括号的var是一个32位地址常量,加中括号才是取地址指向的内容)
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个寄存器出现

字节大小

通常,数据项在给定内存地址的预期大小可以从引用它的汇编代码指令中推断出来。例如,在上述所有指令中,可以从寄存器操作对象的大小推断内存区域的大小。当我们加载一个32位寄存器时,汇编器可以推断出我们引用的内存区域是4字节宽。当我们将一个字节寄存器的值存储到内存中时,汇编程序可以推断出我们希望该地址引用内存中的一个字节。

然而,在某些情况下,引用的内存区域的大小是不明确的。考虑指令mov [ebx], 2。此指令是否应将值2移入地址EBX处的单字节空间中?也许它应该将32位整数表示的2移到地址EBX开始的4个字节中。由于这两种解释都是有效的可能解释,因此必须明确指示汇编程序哪种解释是正确的。大小指令BYTE PTRWORD PTRDWORD PTR用于此目的,分别表示1、2和4字节的大小。
例如:

mov BYTE PTR [ebx], 2   ; 将2移入EBX指向的内存地址的单字节
mov WORD PTR [ebx], 2   ; 将16位整数表示的2移动到从EBX指向的地址开始的2个字节中
mov DWORD PTR [ebx], 2  ; 将32位整数表示的2移动到从EBX指向的地址开始的4个字节中

常用指令

机器指令通常分为三类:数据移动、算术/逻辑和控制流。在本节中,我们将查看每个类别中的重要x86指令示例。本节不会详尽地列出所有x86指令,但它对于新手来说仍将会非常有用。有关完整列表,请参阅英特尔指令集参考。

我们使用以下符号:

<reg32> ; 任何32位寄存器 (EAX, EBX, ECX, EDX, ESI, EDI, ESP, or EBP)
<reg16>	; 任何16位寄存器 (AX, BX, CX, or DX)
<reg8>	; 任何8位寄存器 (AH, BH, CH, DH, AL, BL, CL, or DL)
<reg>	; 任何寄存器
<mem>	; 一个内存地址 (e.g., [eax], [var + 4], or dword ptr [eax+ebx])
<con32>	; 任何32位常量
<con16>	; 任何16位常量
<con8>	; 任何8位常量
<con>	; 任何8、16、32位常量

数据移动说明

mov — Move (操作码: 88, 89, 8A, 8B, 8C, 8E, ...)

MOV指令将其第二操作对象(即寄存器内容、内存内容或常量值)所引用的数据项复制到其第一操作对象(即寄存器或内存)所引用的位置。虽然寄存器到寄存器的移动是可能的,但是直接内存到内存的移动是不可能的。在需要内存传输的情况下,必须首先将源内存中的内容加载到寄存器中,然后才能将其存储到目标内存地址。

语法

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

示例

mov eax, ebx ; 将EBX中的值复制到EAX
mov byte ptr [var], 5 ; 将5存储到地址var的一个字节中

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

PUSH指令将其操作对象放在内存中硬件支持堆栈的顶部。具体地说,PUSH首先将ESP递减4,然后将其操作对象放入内存地址[ESP]处的32位大小的区域中。ESP(堆栈指针)通过push递减,因为x86堆栈向下增长——即堆栈从高位地址增长到低位地址。

Syntax

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

示例

push eax ; 将eax入栈
push [var] ; 将地址var处开始的4个字节入栈

pop — Pop stack

POP指令将4字节数据元素从硬件支持的堆栈顶部移至指定的操作对象(即寄存器或内存位置)。它首先将位于内存位置[SP]的4个字节移动到指定的寄存器或内存位置,然后将SP递增4。

语法

pop <reg32>
pop <mem>

示例

pop edi ; 将堆栈的顶部元素弹出到EDI中
pop [ebx] ; 将堆栈的顶部元素弹出到内存从EBX位置开始的四个字节中

lea — 加载有效地址

LEA指令将其第二个操作对象指定的地址放入其第一个操作对象指定的寄存器中。注意,内存位置的内容不会被加载,并且只有有效地址会被计算并放入寄存器中。这对于获取指向内存区域的指针非常有用。

语法

lea <reg32>,<mem>

示例

lea edi, [ebx+4*esi] ; 将地址EBX+4*ESI放入EDI
lea eax, [var] ; 将var中的值放在EAX中

算术和逻辑指令

add — 整数加法

ADD指令将其两个操作对象相加,将结果存储在其第一个操作对象中。注意,虽然两个操作对象都可以是寄存器,但最多只有一个操作对象可以是内存位置。

语法

add <reg>,<reg>
add <reg>,<mem>
add <mem>,<reg>
add <reg>,<con>
add <mem>,<con>

示例

add eax, 10 ; EAX ← EAX + 10
add BYTE PTR [var], 10 ; 将存储在内存地址var的单字节值加上10

sub — 整数减法

SUB指令在其第一个操作对象的值中存储从其第一个操作对象的值中减去其第二个操作对象的值的结果。与ADD一样。

语法

sub <reg>,<reg>
sub <reg>,<mem>
sub <mem>,<reg>
sub <reg>,<con>
sub <mem>,<con>

示例

sub al, ah ; AL ← AL - AH
sub eax, 216 ; 从存储在EAX中的值中减去216

inc, dec — 递增,递减

INC指令将其操作对象的内容加1。DEC指令将其操作对象的内容减1。

语法

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

示例

dec eax ; 从EAX的内容中减去1
inc DWORD PTR [var] ; 将存储在位置var的32位整数加1

imul — 整数乘法

IMUL指令有两种基本格式:两个操作对象和三个操作对象。

两个操作对象的形式将其两个操作对象相乘,并将结果存储在第一个操作对象中。结果(即第一个)操作对象必须是寄存器。

三个操作对象的形式将其第二个和第三个操作对象相乘,并将结果存储在其第一个操作对象中。同样,结果操作对象必须是寄存器。此外,第三个操作对象被限制为常量值。

语法

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

示例

imul eax, [var] ; 将EAX的内容乘以内存位置var的32位内容并将结果存储在EAX中
imul esi, edi, 25 ; ESI → EDI * 25

idiv — 整数除法

IDIV指令将64位整数EDX:EAX(通过将EDX视为最高有效四个字节,EAX视为最低有效四个字节)的内容除以指定的操作对象值。除法的商结果存储在EAX中,其余数的存储在EDX中。

语法

idiv <reg32>
idiv <mem>

示例

idiv ebx ; 将EDX:EAX的内容除以EBX的内容。把商放在EAX中,余放在EDX中
idiv DWORD PTR [var] ; 将EDX:EAX的内容除以存储在内存位置var的32位值。把商放在EAX中,余放在EDX中

and, or, xor — 按位与、或和异或

这些指令对其操作对象执行指定的位运算(分别为按位与、或和异或),并将结果放在第一个操作对象位置。

语法

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>

示例

and eax, 0fH ; 清除EAX的除最后4位以外的所有位
xor edx, edx ; 将EDX的内容设置为零

not — 按位取反

NOT 指令触发(翻转)操作对象中的所有位。其结果被称为反码。

语法

not <reg>
not <mem>

示例

not BYTE PTR [var] ; 取反内存位置var的字节中的所有位

neg — 求补

NEG是汇编指令中的求补指令,对操作对象执行求补运算:用零减去操作对象,然后结果返回操作对象。求补运算也可以表达成:将操作对象按位取反后加1。

语法

neg <reg>
neg <mem>

示例

neg eax ; EAX → - EAX

shl, shr — 左移,右移

这些指令将其第一个操作对象内容中的位左右移位,用零填充产生的空位位置。移位后的操作对象最多可以移位31位。要移位的位数由第二个操作对象指定,该操作对象可以是8位常量,也可以是寄存器CL。在任一情况下,以32为模执行大于31的移位计数。

语法

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

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

示例

shl eax, 1 ; 将EAX的值乘以2(如果最高有效位为0)
shr ebx, cl ; 将EBX的值除以2^n^的结果的下限存储在EBX中,其中n是CL中的值

控制流指令

x86处理器维护一个指令指针(IP)寄存器,它是一个32位值,指示当前指令在内存中的起始位置。通常,在执行一条指令后,它会递增以指向内存中的下一条指令的起始位置。IP寄存器不能直接操作,而是由提供的控制流指令隐式更新。

我们使用符号<LABEL>来表示代码中已标记的位置。通过输入标签名称后跟冒号,可以在x86汇编代码中的任意位置插入标签。例如:

       mov esi, [ebp+8]
begin: xor ecx, ecx
       mov eax, [esi]

此代码段中的第二条指令被标记为BEGIN。在代码的其他地方,我们可以使用更方便的符号名称BEGIN来引用此指令所在的内存中的位置。这个标签只是表示位置的一种方便方式,而不是它的32位值。

JMP — 跳转

将程序控制流转移到操作对象指示的内存位置上

语法

jmp <label>

示例

jmp begin ; 跳到标记为begin的指令位置

jcondition — 条件跳转

这些指令是基于一组条件码状态判断是否进行跳转,该条件码被存储在称为机器状态字的特殊寄存器中。机器状态字的内容包括有关上次执行的算术运算的信息。例如,此字的某一比特位表示最后结果是否为零,某另一个比特位指示上次结果是否为负数。基于这些条件码,可以执行多个条件跳转。例如,如果上次算术运算的结果为零,则JZ指令执行到指定操作对象标签的跳转。否则,控制按顺序前进到下一条指令。

许多条件分支的名字都是根据上一次执行的特殊比较指令cmp命名的(见下文)。例如,条件分支(如JLE和JNE)基于首先对所需操作对象执行CMP操作。

语法

je <label>  ; 相等时跳转
jne <label> ; 不相等时跳转
jz <label>  ; 最后结果为零时跳转
jg <label>  ; 大于时跳转
jge <label> ; 大于等于时跳转
jl <label>  ; 小于时跳转
jle <label> ; 小于等于时跳转

示例

cmp eax, ebx
jle done ; 如果EAX的中的值小于或等于EBX中的值,跳至标签done。否则,继续执行下一条指令

cmp — 比较

比较两个指定操作对象的值,适当设置机器状态字中的条件代码。此指令等同于SUB指令,不同之处在于将丢弃减法结果,而不是替换第一个操作对象。

语法

cmp <reg>,<reg>
cmp <reg>,<mem>
cmp <mem>,<reg>
cmp <reg>,<con>

示例

cmp DWORD PTR [var], 10
jeq loop ; 如果存储在var中的4个字节的值等于4字节整数常量10,则跳转到标记为loop的位置

call, ret — 子程序调用和返回

这些指令实现一个子程序调用和返回。CALL指令首先将当前代码位置压入到内存中硬件支持的堆栈中(有关详细信息,请参阅PUSH指令),然后无条件跳转到标签操作对象指示的代码位置。与简单的跳转指令不同,CALL指令保存当前位置,并在子程序完成时返回到此处。

RET指令实现子程序返回机制。此指令首先从硬件支持的内存堆栈中弹出代码位置(有关详细信息,请参阅POP指令),然后无条件跳转至该代码位置。

语法

call <label>
ret

调用约定

为了允许单独的程序员共享代码,开发供多个程序使用的库,并且为了简化子程序的使用,程序员通常采用共同的调用约定。调用约定是关于如何调用程序和从程序返回的协议。例如,给定一组调用约定规则,程序员不需要检查子程序的定义来确定应该如何将参数传递给该子程序。此外,给定一组调用约定规则,可以使高级语言编译器遵循这些规则,从而允许手工编码的汇编语言程序和高级语言程序相互调用。

在实践中,存在许多调用约定。我们采用广泛使用的C语言调用约定。遵循此约定将允许您编写可从C(和C++)代码安全调用的汇编语言子程序,还将使您能够从汇编语言代码调用C库函数。

C调用约定在很大程度上基于硬件支持的堆栈的使用。它基于PUSH、POP、CALL和RET指令。子程序参数通过堆栈传递。寄存器保存在堆栈上,子程序使用的局部变量放在堆栈的内存中。在大多数处理器上实现的绝大多数高级过程语言都使用了类似的调用约定。

调用约定分为两组规则:第一组规则由子程序的调用者使用,第二组规则由子程序的编写者(被调用者)遵守。应该强调的是,在实现这些约定时产生的马虎会导致致命的程序错误,因为堆栈将处于不一致的状态。因此,在您自己的子程序中实现调用约定时应该非常小心。

可视化调用约定操作的一个好方法是在子程序执行期间绘制堆栈附近区域的内容。上图描述了具有三个参数和三个局部变量的子程序执行期间的堆栈内容。堆栈中描述的单元是32位宽的内存空间,因此单元的内存地址相隔4字节。第一个参数位于距基指针8字节的偏移量处。调用指令在堆栈上的参数上方(基指针下方)放置返回地址,从而导致从基指针到第一个参数的额外4个字节的偏移量。当RET指令用于从子程序返回时,它将跳转到堆栈上存储的返回地址。

调用方规则

要进行子程序调用,调用方应:

  1. 在调用子程序之前,调用方应保存某些寄存器的内容,这些寄存器被称为caller-saved。调用方保存寄存器为EAX、ECX、EDX。由于允许被调用子程序修改这些寄存器,因此如果调用者在子程序返回后依赖于它们的值,则调用者必须将这些寄存器中的值压入堆栈(以便在子程序返回后恢复它们。

  2. 若要将参数传递给子程序,请在调用之前将它们压入堆栈。参数应按倒序压入(即最后一个参数先入栈)。由于堆栈向下生长,第一个参数将存储在最低地址(这种参数的倒序在历史上用于允许向函数传递可变数量的参数)。

  3. 要调用子程序,请使用CALL指令。此指令将返回地址放在堆栈上的参数之上,并跳转到子程序代码。这将调用子程序,该子程序应遵循下面的被调用者规则。

子程序返回后(紧跟在CALL指令之后),调用者可以期望在寄存器EAX中找到该子程序的返回值。要恢复机器状态,调用方应:

  1. 从堆栈中删除参数。这会将堆栈恢复到执行调用之前的状态。

  2. 通过从堆栈中弹出调用方保存寄存器(EAX、ECX、EDX)的内容来恢复这些寄存器的内容。调用者可以假设该子程序没有修改任何其他寄存器。

示例

下面的代码显示了遵循调用方规则的函数调用。调用方正在调用一个函数_myFunc,该函数接受三个整数参数。第一个参数在EAX中,第二个参数是常量216;第三个参数在内存位置var中。

push [var] ; 先压入最后一个参数
push 216   ; 再将倒数第二个参数入栈
push eax   ; 最后将第一个参数入栈

call _myFunc ; 调用函数_myFunc

add esp, 12

请注意,调用返回后,调用方使用Add指令清理堆栈。我们在堆栈上有12个字节(3个参数*每个参数大小4个字节),堆栈向下生长。因此,要去掉这些参数,我们只需在堆栈指针上加12即可。

_myFunc产生的结果现在可以在寄存器EAX中使用。调用方保存寄存器(ECX和EDX)的值可能已经被更改,如果调用方在调用之后想继续使用它们,则需要在调用之前将它们保存在堆栈中,并在调用之后恢复它们。

被调用方规则

子程序在初始化时应遵循以下规则:

  1. 将EBP的值压入堆栈,然后按照以下说明将ESP的值复制到EBP中:
push ebp
mov  ebp, esp
  1. 此初始操作维护基指针EBP。按照惯例,基指针用作查找堆栈上的参数和局部变量的参考点。当子程序执行时,基指针保存该子程序开始执行时的堆栈指针值的副本。参数和局部变量将始终位于距离基指针值的已知常量偏移量处。我们在子程序的开始处压入旧的基指针值,以便稍后当子程序返回时恢复调用方的基指针值。请记住,调用方并不期望子程序更改基指针值。然后,我们将堆栈指针移动到EBP所指示的内存地址,以获得访问参数和局部变量的参考点。

  2. 接下来,通过在堆栈上腾出空间来分配局部变量。回想一下,堆栈向下增长,因此为了在堆栈顶部腾出空间,堆栈指针应该递减。堆栈指针递减的数量取决于所需的局部变量的数量和大小。例如,如果需要3个整数局部变量(每个4字节),堆栈指针将需要减12,以便为这些局部变量腾出空间(即,sub esp,12)。与参数一样,局部变量将位于距基指针已知偏移量处。

  3. 接下来,保存函数将使用的被调用者保存寄存器(callee-saved)的值。要保存寄存器,请将它们压入堆栈。被调用者保存寄存器是EBX、EDI和ESI(ESP和EBP也将根据调用约定保留,但在此步骤中不需要推入堆栈)。

在执行这三个动作之后,子程序的主体可以继续。当子程序返回时,它必须遵循以下步骤:

  1. 将返回值保留为EAX。

  2. 恢复任何已修改的被调用方保存寄存器(EDI和ESI)的旧值,通过从堆栈中弹出寄存器内容来恢复它们,寄存器应该以与它们被推入相反的顺序弹出。

  3. 取消分配局部变量。最显而易见的方法可能是将堆栈指针加上相应的偏移量(因为空间是通过从堆栈指针中减去所需的量来分配的)。实际上,释放变量的一种不太容易出错的方法是将堆栈指针指向基指针值:mov esp,ebp。这是可行的,因为基指针在分配局部变量之前存入了堆栈指针的值。

  4. 在返回之前,通过从堆栈中弹出EBP来恢复调用方的基指针值。回想一下,我们在执行子程序时所做的第一件事就是压入基指针以保存其旧值。

  5. 最后,通过执行RET指令返回给调用方。此指令将从堆栈中查找适当的返回地址并删除它。

请注意,被调用者规则可以干净利落地分为两个部分,它们基本上是彼此的对称镜像。规则的前半部分应用于函数的开头,规则的后半部分应用于函数的末尾。

示例

以下是遵循被调用方规则的示例函数定义:

.486
.MODEL FLAT
.CODE
PUBLIC _myFunc
_myFunc PROC
  ; 子程序开头
  push ebp     ; 保存旧的基指针值
  mov ebp, esp ; 设置新的基指针值
  sub esp, 4   ; 为一个4字节的局部变量腾出空间
  push edi     ; 保存该函数将会修改的寄存器的值
  push esi     ; 此函数使用EDI和ESI(无需保存EBX、EBP或ESP)

  ; 子程序主体
  mov eax, [ebp+8]   ; 将参数1的值移动到EAX中
  mov esi, [ebp+12]  ; 将参数2的值移动到ESI中
  mov edi, [ebp+16]  ; 将参数3的值移动到EDI中

  mov [ebp-4], edi   ; 将EDI移入局部变量
  add [ebp-4], esi   ; 将ESI加到局部变量上
  add eax, [ebp-4]   ; 将局部变量的内容加到EAX上,(最终结果)

  ; 子程序结尾
  pop esi      ; 恢复寄存器的值
  pop  edi
  mov esp, ebp ; 销毁局部变量
  pop ebp ; 恢复调用方的基指针值
  ret
_myFunc ENDP
END

子程序开头执行以下标准操作:在EBP中保存堆栈指针的快照(基指针),通过递减堆栈指针来分配局部变量,以及在堆栈上保存寄存器值。

在子程序的主体中,我们可以看到基指针的使用。在子程序执行期间,参数和局部变量都位于基指针的常量偏移量上。特别地,我们注意到,由于参数是在调用子程序之前放到堆栈上的,所以它们总是位于堆栈上的基指针之下(即更高的地址)。子程序的第一个参数始终位于内存位置EBP+8,第二个参数位于EBP+12,第三个参数位于EBP+16。类似地,由于局部变量是在基指针设置之后分配的,因此它们始终位于堆栈的基指针上方(即较低的地址)。特别是,第一个局部变量始终位于EBP-4,第二个局部变量位于EBP-8,依此类推。基指针的这种常规用法允许我们快速识别函数体中局部变量和参数的使用。

函数结尾基本上是函数开头的镜像。从堆栈中恢复调用方的寄存器值、通过重置堆栈指针来释放局部变量、恢复调用方的基指针值、并使用RET指令返回到调用方代码的适当位置。



这篇关于x86汇编快速入门的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程