当前位置: 首页 > news >正文

《汇编语言:基于X86处理器》第8章 高级过程(1)

8.1 引言

本章将介绍子程序调用的底层结构,重点集中于运行时堆栈。本章的内容对 C和 C++程序员也是有价值的,因为在调试运行于操作系统或设备驱动程序层的底层子程序时,他们也经常必须检查运行时堆栈的内容。

大多数现代编程语言在调用子程序之前都会把参数压人堆栈。反过来,子程序也常常把它们的局部变量压入堆栈。本章学习的详细内容与 C++和 Java 知识相关,将展示如何以数值或引用的形式来传递参数;如何定义和撤销局部变量;以及如何实现递归。在本章结束时,将解释 MASM使用的不同的内存模式和语言标识符。参数既可以用寄存器传递也可以用堆栈传递。这就是64 位模式下的情况,Microsoft发布的Microsoftx64调用规范中就是这样规定的。编程语言用不同的术语来指代子程序。例如,在C和 C++中,子程序被称为函数(functions)。在Java中,被称为方法(methods)。在MASM中,则被称为过程(procedures)。本章目的是说明典型子程序调用的底层实现,就像它们在 C 和 C++中展现的那样。在本章开始提到一般原则时,将使用泛称:子程序。而在提到具体汇编语言代码示例时,通常会使用术语过程来指代子程序。

调用程序向子程序传递的数值被称为实际参数(arguments)。而被调用的子程序要接收的数值被称为形式参数(parameters)。

8.2 堆栈帧

8.2.1 堆栈参数

在之前的章节中,子程序接收的是寄存器参数。比如在 Irvine32 链接库中就是如此。本章将展示子程序如何用堆栈接收参数。在 32 位模式下,堆栈参数总是由 Windows API 函数使用。然而在 64 位模式下,Windows 函数可以同时接收寄存器参数和堆栈参数。

堆栈帧(stackframe)(或活动记录(activationrecord))是一块堆栈保留区域,用于存放被传递的实际参数、子程序的返回值、局部变量以及被保存的寄存器。堆栈帧的创建步骤如下所示:

1)被传递的实际参数。如果有,则压入堆栈。

2)当子程序被调用时,使该子程序的返回值压人堆栈。

3)子程序开始执行时,EBP 被压入堆栈。

4)设置EBP 等于ESP。从这时开始,EBP就变成了该子程序所有参数的引用基址。

5)如果有局部变量,修改 ESP 以便在堆栈中为这些变量预留空间。

6)如果需要保存寄存器,就将它们压入堆栈。

argument 通常理解为实际参数,parameter 理解为形式参数。本书翻译中,在不影响理解的情况下,都译作参数

程序内存模式和对参数传递规则的选择直接影响到堆栈帧的结构。

学习用堆栈传递参数有个好理由:几乎所有的高级语言都会用到它们。比如,如果想要在32位 Windows应用程序接口(API)中调用函数,就必须用堆栈传递参数。而 64 位程序可以使用另一种不同的参数传递规则,该规则将在第 11 章讨论。

8.2.2寄存器参数的缺点

多年以来,Microsoft在32位程序中包含了一种参数传递规则,称为 fastcall。如同这个名字所暗示的,只需简单地在调用子程序之前把参数送入寄存器,就可以将运行效率提高一些。相反,如果把参数压人堆栈,则执行速度就要更慢一点。典型用于参数的寄存器包括EAX、EBX、ECX 和 EDX,少数情况下,还会使用 EDI 和 ESI。可惜的是,这些寄存器在用于存放数据的同时,还用于存放循环计数值以及参与计算的操作数。因此,在过程调用之前,任何存放参数的寄存器须首先入栈,然后向其分配过程参数,在过程返回后再恢复其原始值。例如,如下代码从Irvine32链接库中调用了DumpMem:

push ebx					;保存寄存器值
push ecx
push esi
mov esi, OFFSET array		;初始 OFESET
mov ecx, LENGTHOF array	    ;大小,按元素个数计
mov ebx, TYPE array			;双字格式
call Dumpmem				;显示内存
pop esi						;恢复寄存器值
pop ecx
pop ebx

这些额外的人栈和出栈操作不仅会让代码混乱,还有可能消除性能优势,而这些优势正是通过使用寄存器参数所期望获得的!此外,程序员还要非常仔细地将 PUSH 与相应的POP 进行匹配,即使代码存在着多个执行路径。例如,在下面的代码中,第8行的EAX 如果等于1,那么过程在第17行就无法返回其调用者,原因就是有三个寄存器的值留在运行时堆栈里。

push ebx					;保存寄存器值
push ecx
push esi
mov esi, OFFSET array		;初始 OFESET
mov ecx, LENGTHOF array	    ;大小,按元素个数计
mov ebx, TYPE array			;双字格式
call Dumpmem				;显示内存
cmp eax, 1					;设置错误标志?
je 	error_exit			    ;设置标志后退出pop esi						;恢复寄存器值
pop ecx
pop ebx
ret
error_exit:
mov edx, OFFSET error_msg
ret

不得不说,像这样的错误是不容易发现的,除非是花了相当多的时间来检查代码。

堆栈参数提供了一种不同于寄存器参数的灵活方法:只需要在调用子程序之前,将参数压入堆栈即可。比如,如果 DumpMem 使用了堆栈参数,就可以通过如下代码对其进行调用:

push TYPE array
push LENGTHOF array
push OFFSET array
call DumpMem

子程序调用时,有两种常见类型的参数会人栈:

●值参数(变量和常量的值)。

●引用参数(变量的地址)

值传递 当一个参数通过数值传递时,该值的副本会被压入堆栈。假设调用一个名为AddTwo 的子程序,向其传递两个 32 位整数:

.data
val1 DWORD 5
val2 DWORD 6
.code
push val2
push val1
call AddTwo

执行 CALL 指令前,堆栈如下图所示:

用C++编写相同的功能调用则为

int sum = AddTwo(val1, val2);

观察发现参数入栈的顺序是相反的,这是C和C++语言的规范。

引用传递通过引用来传递的参数包含的是对象的地址(偏移量)。下面的语句调用了Swap,并传递了两个引用参数:

push OFFSET val2
push OFFSET val1
call Swap

调用Swap 之前,堆栈如下图所示:

在 C/C++中,同样的函数调用将传递 va11和 val2 参数的地址:

Swap(&val1, &val2);

传递数组 高级语言总是通过引用向子程序传递数组。也就是说,它们把数组的地址压人堆栈。然后,子程序从堆栈获得该地址,并用其访问数组。不愿意用值来传递数组的原因是显而易见的,因为这样就会要求将每个数组元素分别压入堆栈。这种操作不仅速度很慢,而且会耗尽宝贵的堆栈空间。下面的语句用正确的方法向子程序 ArrayFill传递了数组的偏移量:

.data
array DWORD 50 DUP(?)
.code
push OFFSET array
call ArrayFill

8.2.3访问堆栈参数

高级语言有多种方式来对函数调用的参数进行初始化和访问。以 C 和 C++语言为例,它们以保存 EBP 寄存器并使该寄存器指向栈顶的语句为开始(prologue)。然后,根据实际情况,它们可以把某些寄存器人栈,以便在函数返回时恢复这些寄存器的值。在函数结尾(epilogue)部分,恢复 EBP 寄存器,并用 RET 指令返回调用者。

AddTwo 示例 下面是用C编写的AddTwo 函数,它接收了两个值传递的整数,然后返回这两个数之和:

int AddTwo(int x, int y)
{return x+y;
}

现在用汇编语言实现同样的功能。在函数开始的时候,AddTwo 将 EBP 人栈,以保存其当前值:

AddTwo PROCpush ebp

接下来,EBP 的值被设置为等于ESP,这样EBP 就成为AddTwo堆栈帧的基址指针:

AddTwo PROCpush ebpmov ebp, esp

执行了上面两条指令后,堆栈帧的内容如下图所示。而形如 AddTwo(5,6)的函数调用会先把第一个参数入栈,再把第二个参数人栈:

AddTwo在其他寄存器入栈时,不用通过EBP来修改堆栈参数的偏移量。数值会改变的是ESP,而EBP则不会。

基址-偏移量寻址 可以使用基址-偏移量寻址(base-offset addressing)方式来访问堆栈参数。其中,EBP 是基址寄存器,偏移量是常数。通常,EAX为32 位返回值。AddTwo的实现如下所示,参数相加后,EAX 返回它们的和数:

AddTwo PROCpush ebpmov  ebp, esp			;堆栈帧的基址mov eax, [ebp+12]		;第二个参数add eax, [ebp+8]		;第一个参数pop ebpret
AddTwo ENDP

1.显式的堆栈参数

若堆栈参数的引用表达式形如[ebp+8],则称它们为显式的堆栈参数(explicit stack parameters)。这个名称的含义是:汇编代码显式地说明了参数的偏移量是一个常数。有些程序员定义符号常量来表示显式的堆栈参数,以使其代码更具易读性:

y_param EQU [ebp+12]
x_param EQU [ebp+8]
AddTwo PROCpush ebpmov ebp, espmov eax, y_paramadd eax, x_parampop ebp
AddTwo ENDP

2.清除堆栈

子程序返回时,必须将参数从堆栈中删除。否则将导致内存泄露,堆栈就会被破坏。例如,设如下语句在main 中调用AddTwo:

push 6
push 5
call AddTwo

假设 AddTwo有两个参数留着堆栈中,下图所示为调用返回后的堆栈:

main部分试图忽略这个问题,并希望程序能正常结束。但是,如果循环调用 AddTwo,堆栈就会溢出。因为每次调用都会占用12字节的堆栈空间--每个参数需要4个字节,再加4个字节留给CALL指令的返回地址。如果在main中调用Example1,而它又要调用AddTwo就会导致更加严重的问题:

main PROCcall Example1exit
main ENDP
Example1 PROCpush 6push 5call AddTworet							;堆栈被破坏了!
Example1 ENDP

当Example1 的 RET 指令将要执行时,ESP 指向整数5而不是能将其带回 main 的返回地址:

RET指令把整数5加载到指令指针寄存器,尝试将控制转移到内存地址为5的位置。假设这个地址在程序代码边界之外,那么处理器将给出运行时异常,通知 OS 终止程序。

完整代码测试笔记

;8.2.3.asm   8.2.3   访问堆栈参数INCLUDE Irvine32.inc.data
binArray BYTE 32 DUP(?), 0.code
main PROC;调用测试call Example1			;call一次会把push EIP call Example1	INVOKE ExitProcess,0
main ENDP
Example1 PROCpush 6push 5call AddTwo				;push 下一条指令的EIPadd esp, 8				;修复堆栈被破坏ret						;堆栈被破坏了!
Example1 ENDP;基址——偏移寻址, 两数相加过程 
AddTwo PROCpush ebpmov ebp, esp			;堆栈帧基址mov eax,[ebp+12]		;第二个参数add eax,[ebp+8]			;第一个参数pop ebpret
AddTwo ENDPEND main

运行调试:

8.2.4 32位调用规范

本节将给出 Windows 环境中两种最常用的 32 位编程调用规范。首先是 C 语言发布的C调用规范,该语言用于Unix和Windows。然后是STDCALL调用规范,它描述了调用Windows API函数的协议。这两种规范都很重要,因为在C和 C++程序中会调用汇编函数,同时汇编语言程序也会调用大量的Windows API函数。

C调用规范C调用规范用于C和C++语言。子程序的参数按逆序入栈,因此,C 程序在调用如下函数时,先将 B 入栈,再将 A 入栈:

AddTWO( A,B)

C调用规范用一种简单的方法解决了清除运行时堆栈的问题:程序调用子程序时,在CALL 指令的后面紧跟一条语句使堆栈指针(ESP)加上一个数,该数的值即为子程序参数所占堆栈空间的总和。下面的例子在执行 CALL 指令之前,将两个参数(5 和 6)入栈:

Example1 PROCpush 6push 5call AddTwoadd esp,8				;从堆栈移除参数ret
Example1 ENDP

因此,用C/C++编写的程序在从子程序返回后,总是能把参数从堆栈中删除。

STDCALL调用规范 另一种从堆栈删除参数的常用方法是使用名为STDCALL的规范。如下所示的AddTwo过程给RET指令添加了一个整数参数,这使得程序在返回到调用过程时,ESP 会加上数值8。这个添加的整数必须与被调用过程参数占用的堆栈空间字节数相等:

AddTwo PROCpush ebpmov ebp, esp			;堆栈帧基址mov eax, [ebp+12]		;第二个参数add eax, [ebp+8]		;第一个参数pop ebpret 8					;清除堆栈
AddTwo ENDP

要说明的是,STDCALL与C相似,参数是按逆序人栈的。通过在 RET 指令中添加参数,STDCALL不仅减少了子程序调用产生的代码量(减少了一条指令),还保证了调用程序永远不会忘记清除堆栈。另一方面,C 调用规范则允许子程序声明不同数量的参数,主调程序可以决定传递多少个参数。C语言的printf函数就是一个例子,它的参数数量取决于初始字符串参数中的格式说明符的个数:

int x = 5;
float y = 3.2;
char z = 'z';
printf("Printing values: %d, %f, %c", x, y, z);

C 编译器按逆序将参数入栈,被调用的函数负责确定要传递的实际参数的个数,然后依次访问参数。这种函数实现没有像给RET指令添加一个常数那样简便的方法来清除堆栈,因此,这个责任就留给了主调程序。

调用32位WindowsAPI函数时,Irvine32链接库使用的是STDCALL调用规范。Irvine64 链接库使用的是x64 调用规范。

从这里开始,本书假设所有过程示例使用的都是STDCALL,除非明确说明使用了其他规范。

保存和恢复寄存器

通常,子程序在修改寄存器之前要将它们的当前值保存到堆栈。这是一个很好的做法,因为可以在子程序返回之前恢复寄存器的原始值。理想情况下,相关寄存器人栈应在设置EBP 等于 ESP 之后,在为局部变量保留空间之前。这有利于避免修改当前堆栈参数的偏移量。例如,假设如下过程MySub有一个堆栈参数。在EBP被设置为堆栈帧基址后,ECX和EDX 入栈,然后堆栈参数加载到EAX:

MySub PROCpush ebp					;保存基址指针mov ebp, esp				;堆栈帧基址push ecx		push edx					;保存EDXmov eax, [ebp+8]			;取堆栈参数.pop edx					;恢复被保存的寄存器pop ecx							pop ebp					;恢复基址指针ret						;清除堆栈
MySub ENDP

EBP被初始化后,在整个过程期间它的值将保持不变ECX和EDX的人栈不会影响到已人栈参数与EBP之间的位移量,因为堆栈的增长位于EBP的下方(如图8-1所示)。

完整代码测试笔记

;8.2..asm   8.2.4   32位调用规范
;STDCALL调用规范INCLUDE Irvine32.inc.data
binArray BYTE 32 DUP(?), 0.code
main PROC;调用测试call Example1			;call一次会把push EIP call Example1	INVOKE ExitProcess,0
main ENDP
Example1 PROCpush 6push 5call AddTwo				;push 下一条指令的EIPret						
Example1 ENDP;基址——偏移寻址, 两数相加过程 
AddTwo PROCpush ebpmov ebp, esp			;堆栈帧基址mov eax,[ebp+12]		;第二个参数add eax,[ebp+8]			;第一个参数pop ebpret	8					;清除堆栈 先取值,再把esp+8
AddTwo ENDPEND main

运行调试:

8.2.5 局部变量

高级语言中,在单一子程序内新建、使用和撤销的变量被称为局部变量(localvariable)。局部变量创建于运行时堆栈,通常位于基址指针(EBP)之下。尽管不能在汇编时给它们分配默认值,但是能在运行时初始化它们。可以使用与C和C++相同的方法在汇编语言中新建局部变量。

示例 下面的C++函数声明了局部变量X和Y:

void MySub()
{int x = 10;int y = 20;
}

如果这段代码被编译为机器语言,就能看出局部变量是如何分配的。每个栈项都默认为32位,因此,每个变量的存储大小都要向上取整保存为4的倍数。两个局部变量一共要保留8个字节:

变量

字节数

堆栈偏移量

X

4

EBP-4

Y

4

EBP-8

MySub函数(在调试器中)的反汇编展示了C++程序如何创建局部变量,以及如何从堆栈中删除它们。该例使用了C调用规则:

MySub PROCpush ebxmov ebp, espsub esp, 8						;创建局部变量mov DWORD PTR [ebp-4], 10		;Xmov DWORD PTR [ebp-8], 80		;Ymov esp, ebp					;从堆栈中删除局部变量pop ebpret
MySub ENDP

局部变量初始化后,函数的堆栈帧如图8-2所示

在结束前,函数通过将EBP的值赋给堆栈指针完成对其的重置,该操作的效果是把局部变量从堆栈中删除:

mov esp, ebp ;从堆栈中删除局部变量

如果省略这一步,那么POPEBP指令将会把EBP设置为20,而RET指令就会分支到内存地址10的位置,从而导致程序因出现处理器异常而终止。下面的MySub代码就是这种情况:

MySub PROCpush ebxmov ebp, espsub esp, 8						;创建局部变量mov DWORD PTR [ebp-4], 10		;Xmov DWORD PTR [ebp-8], 80		;Ypop ebpret
MySub ENDP

局部变量符号 为了使程序更加易读,可以为每个局部变量的偏移量定义一个符号,然后在代码中使用这些符号:

X_local EQU DWORD PTR [ebp-4]
Y_local EQU DWORD PTR [ebp-8]
MySub PROCpush ebxmov ebp, espsub esp, 8						;创建局部变量mov DWORD PTR [ebp-4], 10		;Xmov DWORD PTR [ebp-8], 80		;Ymov esp, ebp					;从堆栈中删除局部变量pop ebpret
MySub ENDP

完整代码测试笔记

;8.2.5.asm   8.2.5   局部变量
;示例 下面的C++函数声明了局部变量X和Y:
;void MySub()
;{
;  int x = 10;
;  int y = 20;
;}
;用汇编语言实现上述函数INCLUDE Irvine32.inc.code
main PROC;调用测试call MySub			;call一次会push EIP call MySub	INVOKE ExitProcess,0
main ENDPMySub PROCpush ebxmov ebp, espsub esp, 8						;创建局部变量mov DWORD PTR [ebp-4], 10		;Xmov DWORD PTR [ebp-8], 80		;Ymov esp, ebp					;从堆栈中删除局部变量pop ebpret
MySub ENDPEND main

运行调试:

8.2.6 引用参数

引用参数通常是由过程用基址-偏移量寻址(从 EBP)方式进行访问。由于每个引用参数都是一个指针,因此,常常作为一个间接操作数放在寄存器中。例如,假设堆栈地址[ebp+12]存放了一个数组指针,则下述语句就把该指针复制到ESP中:

mov esi, [ebp+12] ;指向数组

ArrayFill 示例 下面将要展示的ArrayFill 过程用16位整数的伪随机序列来填充数组。它接收两个参数:数组指针和数组长度,第一个为引用传递,第二个为值传递。调用示例如下:

.data
count = 100
array WORD count DUP(?)
.code
push OFFSET array		;数组偏移量
push count				;数据长度
call ArrayFill			;push EIP

在ArrayFill中,下面的代码为其开始部分,对堆栈帧指针(EBP)进行初始化:

ArrayFill PROCpush ebp				mov ebp, esp

现在,堆栈帧中包含了数组偏移量、数组长度(count)、返回地址以及被保存的EBP:

ArrayFil1保存了通用寄存器,检索参数并填充数组:

完整代码测试笔记

;8.2.6.asm   8.2.6   引用参数
;ArrayFill 示例   下面将要展示的ArrayFill 过程用16位整数的伪随机序列来填充数组。
;它接收两个参数:数组指针和数组长度,第一个为引用传递,第二个为值传递。INCLUDE Irvine32.inc.data
count = 100
array WORD count DUP(?).code
main PROC;调用测试push OFFSET array		;数组偏移量push count				;数据长度call ArrayFill			;push EIPINVOKE ExitProcess,0
main ENDPArrayFill PROCpush ebpmov ebp, esppushad					;保存寄存器mov esi,[ebp+12]		;数组偏移量mov ecx,[ebp+8]			;数据长度cmp ecx, 0				;ECX==0?je L2					;是:跳过循环
L1:mov eax, 10000h			;随机范围0~FFFFhcall RandomRange		;从链接库生成随机数mov [esi], ax			;在数组中插入值add esi, TYPE WORD		;指向下一个元素loop L1
L2: popad					;恢复寄存器pop ebpret 8					;清除堆栈
ArrayFill ENDP
END main

运行调试:

填充后:

8.2.7 LEA 指令

LEA指令返回间接操作数的地址。由于间接操作数中包含一个或多个寄存器,因此会在运行时计算这些操作数的偏移量。为了演示如何使用LEA,现在来看下面的C++程序该程序声明了一个局部数组myString,并引用它来分配数组值:

void makeArray()
{char myString[30];for(int i = 0; i < 30; i++)myString[i] = '*';
}

与之等效的汇编代码在堆栈中为myString分配空间,并将地址--间接操作数--赋给ESI。虽然数组只有30个字节,但是ESP还是递减了32以对齐双字边界。注意如何使用LEA 把数组地址分配给 ESI:

makeArray PROCpush ebpmov ebp, espsub esp, 32				;myString位于EBP-30的位置lea esi, [ebp-30]		;加载myString的地址mov ecx, 30				;循环计数器
L1:	mov BYTE PTR [esi], '*'	;填充一个位置inc esi					;指向下一个元素loop L1					;从循环,直到ECX-0add esp, 32				;删除数组(恢复ESP)pop ebp		ret  
makeArray ENDP

不能用 OFFSET 获得堆栈参数的地址,因为 OFFSET 只适用于编译时已知的地址。下面的语句无法汇编:

mov esi, OFFSET [ebp-30] ) ;错误

完整代码测试笔记

;8.2.7.asm   8.2.7   LEA指令
;用汇编语言实现下面C语言代码
;void makeArray()
;{
;  char myString[30];
;  for(int i = 0; i < 30; i++)
;    myString[i] = '*';
;}INCLUDE Irvine32.inc.code
main PROC;调用测试call makeArray			INVOKE ExitProcess,0
main ENDPmakeArray PROCpush ebpmov ebp, espsub esp, 32				;myString位于EBP-30的位置lea esi, [ebp-30]		;加载myString的地址mov ecx, 30				;循环计数器
L1:	mov BYTE PTR [esi], '*'	;填充一个位置inc esi					;指向下一个元素loop L1					;从循环,直到ECX-0add esp, 32				;删除数组(恢复ESP)pop ebp		ret  
makeArray ENDP
END main

运行调试:

填充后:

8.2.8 ENTER和LEAVE指令

ENTER指令为被调用过程自动创建堆栈帧。它为局部变量保留堆栈空间,把 EBP 入栈。具体来说,它执行三个操作:

●把EBP入栈(push ebp)

●把EBP设置为堆栈帧的基址(mov ebp,esp)

●为局部变量保留空间(sub esp,numbytes)

ENTER 有两个操作数:第一个是常数,定义为局部变量保存的堆栈空间字节数;第二个定义了过程的词法嵌套级。

ENTER numbyces, nestinglevel

这两个操作数都是立即数。Numbytes总是向上舍人为4的倍数,以便ESP对齐双字边界。Nestinglevel确定了从主调过程堆栈帧复制到当前帧的堆栈帧指针的个数。在示例程序中,nestinglevel 总是为0。Intel 手册解释了ENTER指令如何支持模块结构语言中的多级嵌套。

示例1 下面的例子声明了一个没有局部变量的过程:

MySub PROCenter 0, 0

它与如下指令等效:

MySub PROCpush ebpmov ebp, esp

示例2 ENTER指令为局部变量保留了8个字节的堆栈空间:

MySub PROCenter 8, 0

它与如下指令等效:

MySub PROCpush ebpmov ebp, espsub esp, 8

图 8-3 为执行 ENTER 指令前后的堆栈示意图。

如果要使用ENTER指令,那么本书强烈建议在同一个过程的结尾处同时使用LEAVE指令。否则,为局部变量保留的堆栈空间就可能无法释放。这将会导致RET指令从堆栈中弹出错误的返回地址。

LEAVE 指令 LEAVE指令结束一个过程的堆栈帧。它反转了之前的ENTER 指令操作恢复了过程被调用时ESP和EBP的值。再次以MySub过程为例,现在可以编码如下:

MySub PROCenter 8, 0..leaveret
MySub ENDP

下面是与之等效的指令序列,其功能是在堆栈中保存和删除8个字节的局部变量:

MySub PROCpush ebpmov ebp, espsub esp, 8..mov esp, ebppop ebpret
MySub ENDP

完整代码测试笔记

;8.2.8.asm   8.2.8   ENTER和LEAVE指令
;ENTER指令为被调用过程自动创建堆栈帧。它为局部变量保留堆栈空间,把 EBP 入栈。具体来说,它执行三个操作:
;●把EBP入栈(push ebp)
;●把EBP设置为堆栈帧的基址(mov ebp,esp)
;●为局部变量保留空间(sub esp,numbytes)
;LEAVE指令结束一个过程的堆栈帧。它反转了之前的ENTER 指令操作恢复了过程被调用时ESP和EBP的值。
;再次以MySub过程为例,现在可以编码如下:INCLUDE Irvine32.inc.code
main PROC;调用测试call MySub			;call一次会push EIP call MySub2	call makeArraycall makeArray2INVOKE ExitProcess,0
main ENDPMySub PROCenter 8, 0mov DWORD PTR [ebp-4], 10h		;Xmov DWORD PTR [ebp-8], 20h		;Yleaveret
MySub ENDPMySub2 PROCpush ebxmov ebp, espsub esp, 8						;创建局部变量mov DWORD PTR [ebp-4], 30h		;Xmov DWORD PTR [ebp-8], 40h		;Ymov esp, ebp					;从堆栈中删除局部变量pop ebpret
MySub2 ENDPmakeArray PROCenter 32, 0;myString位于EBP-30的位置lea esi, [ebp-30]		;加载myString的地址mov ecx, 30				;循环计数器
L1:	mov BYTE PTR [esi], '#'	;填充一个位置inc esi					;指向下一个元素loop L1					;从循环,直到ECX-0leave					;删除数组(恢复ESP)	ret  
makeArray ENDPmakeArray2 PROCpush ebpmov ebp, espsub esp, 32				;myString位于EBP-30的位置lea esi, [ebp-30]		;加载myString的地址mov ecx, 30				;循环计数器
L1:	mov BYTE PTR [esi], '*'	;填充一个位置inc esi					;指向下一个元素loop L1					;从循环,直到ECX-0add esp, 32				;删除数组(恢复ESP)pop ebp		ret  
makeArray2 ENDPEND main

运行调试

调用MySub2

调用makeArray

调用makeArray2

8.2.9 LOCAL 伪指令

不难想象,Microsoft 创建 LOCAL 伪指令是作为ENTER指令的高级替补。LOCAL 声明一个或多个变量名,并定义其大小属性。(另一方面,ENTER则只为局部变量保留一块未命名的堆栈空间。 如果要使用 LOCAL 伪指令,它必须紧跟在 PROC 伪指令的后面。其语法如下所示:

LOCAL varlist

varlist是变量定义列表,用逗号分隔表项,可选为跨越多行。每个变量定义采用如下格式:

label: type

其中,标号可以为任意有效标识符,类型既可以是标准类型(WORD、DWORD等),也可以是用户定义类型。(结构和其他用户定义类型将在第 10章进行说明。

示例 MySub过程包含一个局部变量varl,其类型为BYTE:

MySub PROCLOCAL var1:BYTE

BubbleSort 过程包含一个双字局部变量temp和一个类型为BYTE的SwapFlag 变量

BubbleSort PROCLOCAL temp:DWORD, SWapFlag:BYTE

Merge过程包含一个类型为PTRWORD的局部变量pArray,它是一个16位整数的指针:

Merge PROCLOCAL TempArray[10]:DWORD

局部变量 TempArray是一个数组,包含10个双字。请注意用方括号显示数组大小:

LOCAL TempArray[10]:DWORD

MASM 代码生成

使用LOCAL 伪指令时,查看 MASM 生成代码是有好处的。下面的过程 Example1 有一个双字局部变量:

Example1 PROCLOCAL temp:DWORDmov eax, tempret
Example1 ENDP

MASM为Examplel生成如下代码,展示了ESP怎样减去4,以便为双字变量预留空间:

push ebp
mov ebp, esp
add esp, OFFFFFFFCh		 ;ESP 加-4
leave
ret

Examplel的堆栈帧示意图如下所示:

完整代码测试笔记

;8.2.9.asm   8.2.9   LOCAL 伪指令
;Microsoft 创建 LOCAL 伪指令是作为ENTER指令的高级替补。LOCAL 声明一个或多个变量名,并定义其大小属性。
;(另一方面,ENTER则只为局部变量保留一块未命名的堆栈空间。
;如果要使用 LOCAL 伪指令,它必须紧跟在 PROC 伪指令的后面。其语法如下所示:
;LOCAL varlistINCLUDE Irvine32.inc.code
main PROC;调用测试call Eample1call Eample2call MySub			;call一次会push EIP call MySub2	INVOKE ExitProcess,0
main ENDPEample1 PROCLOCAL temp:DWORD, temp2:DWORDmov eax, tempmov temp, 10hmov temp2, 20hret
Eample1 ENDPEample2 PROCLOCAL tempArray[8]:DWORDmov eax, tempArray[0]mov ecx, 8mov edi, 0
L1:mov tempArray[edi], 55AAhadd edi, 4loop L1ret
Eample2 ENDPMySub PROCenter 8, 0mov DWORD PTR [ebp-4], 10h		;Xmov DWORD PTR [ebp-8], 20h		;Yleaveret
MySub ENDPMySub2 PROCpush ebxmov ebp, espsub esp, 8						;创建局部变量mov DWORD PTR [ebp-4], 30h		;Xmov DWORD PTR [ebp-8], 40h		;Ymov esp, ebp					;从堆栈中删除局部变量pop ebpret
MySub2 ENDPEND main

运行调试

调用Eample2填充数组

8.2.10 Microsoft x64 调用规范

Microsoft 遵循固定模式实现64位编程中的参数传递和子程序调用,该模式被称为Microsoft x64 调用规范(Microsoft x64 calling convention)。它既用于C 和 C++编译器,也用于 Windows API库。只有在要么调用Windows函数,要么调用C和C++函数时,才需要使用这个调用规范。它的特点和要求如下所示;

1)由于地址长为64 位,因此 CALL 指令把RSP(堆栈指针)寄存器的值减去8。

2)第一批传递给子程序的四个参数依次存放于寄存器 RCX、RDX、R8和 R9。因此,如果只传递一个参数,它就会被放入 RCX。如果还有第二个参数,它就会被放人 RDX,以此类推。其他参数按照从左到右的顺序人栈。

3)长度不足64 位的参数不进行零扩展,因此,其高位的值是不确定的。

4)如果返回值的长度小于或等于64 位,那么它必须放在RAX 寄存器中。

5)主调者要负责在堆栈中分配至少32字节的影子空间,以便被调用的子程序可以选择将寄存器保存在这个区域中。

6)调用子程序时,堆栈指针(RSP)必须对齐16字节边界。CALL指令将8字节的返回地址压人堆栈,因此,主调程序除了把堆栈指针减去32以便存放寄存器参数之外,还要减去8。

7)被调用子程序执行结束后,主调程序需负责从运行时堆栈中移除所有的参数和影子空间。

8)大于 64 位的返回值存放于运行时堆栈,由 RCX 指出其位置。

9)寄存器RAX、RCX、RDX、R8、R9、R10和R11常常被子程序修改,因此,如果主调程序想要保存它们的值,就应在调用子程序之前将它们人栈,之后再从堆栈弹出。

10)寄存器 RBX、RBP、RDI、RSI、R12、R13、R14和R15的值必须由子程序保存。

8.2.11 本节回顾

1.(真/假):子程序的堆栈帧总是包含主调程序的返回地址和子程序的局部变量。

答:真

2.(真/假):为了避免被复制到堆栈,数组通过引用来传递。

答:真

3.(真/假):子程序开始部分的代码总是将 EBP 人栈。

答:真

4.(真/假):堆栈指针加上一个正值即可创建局部变量。

答:假

5.(真/假):32位模式下,子程序调用中最后入栈的参数保存于EBP+8 的位置。

答:真

6.(真/假):引用传递意味着将参数的地址保存在运行时堆栈中。

答:真

7.堆栈参数有哪两种常见类型?

答:值参数和引用参数。

 

 

http://www.lryc.cn/news/588640.html

相关文章:

  • 基于UDP/IP网络游戏加速高级拥塞控制算法(示意:一)
  • 力扣-使用双指针的方法的题们(持续更新中。。。
  • 一、CV_图像分类简介
  • Typecho插件开发:优化文章摘要处理短代码问题
  • 基于redis的分布式锁 lua脚本解决原子性
  • 银河麒麟服务器版挂载镜像文件
  • sqli-labs靶场通关笔记:第18-19关 HTTP头部注入
  • exe直接传输会导致文件损坏
  • 【html常见页面布局】
  • AI应用服务
  • Axios 完整功能介绍和完整示例演示
  • 分布式全局唯一ID生成:雪花算法 vs Redis Increment,怎么选?
  • gRPC实战指南:像国际快递一样调用跨语言服务 —— 解密Protocol Buffer与HTTP/2的完美结合
  • TCP可靠性设计的核心机制与底层逻辑
  • Java基础(八):封装、继承、多态与关键字this、super详解
  • Java全栈工程师面试实录:从电商系统到AIGC的层层递进
  • 通用综合文字识别联动 MES 系统:OCR 是数据流通的核心
  • 在百亿流量面前,让“不存在”无处遁形——Redis 缓存穿透的极限攻防实录
  • 【Ubuntu22.04】repo安装方法
  • 1.2 vue2(组合式API)的语法结构以及外部暴露
  • 如何把手机ip地址切换到外省
  • 【深度学习优化算法】06:动量法
  • 从springcloud-gateway了解同步和异步,webflux webMvc、共享变量
  • iOS V2签名网站系统源码/IPA在线签名/全开源版本/亲测
  • iOS 抓包工具精选对比:不同调试需求下的工具适配策略
  • 项目总体框架(servlet+axios+Mybatis)
  • 【解决】联想电脑亮度调节
  • iOS高级开发工程师面试——多线程
  • Axios 和 Promise 区别对比
  • Supervisor 使用教程:进程守护的最佳实践指南