Win32反汇编(四)栈的工作原理与堆栈平衡,函数方法参数的调用约定

前言

作者:浪子花梦,一个有趣的程序员 ~
此系列文章都是一些基础的文章,每篇文章都通过几个小例子快速的了解 Win32反汇编与OD的使用,在此作个笔记
如若对您有帮助,记得三连哟 ~


前文链接

Win32反汇编(一) 初步探索Win32反汇编 与 Ollydbg的简单使用
Win32反汇编(二)几种常见的指令反汇编详解:EAX、MOVSX与MOVZX、LEA、SUB、CMP与转移指令
Win32反汇编(三)深层次的了解各种转移指令:IF语句有符号与无符号跳转


文章目录

  • 堆栈平衡
  • 调用约定

此文讲解非常重要的概念:栈、堆栈平衡 . . .


堆栈平衡

首先,我们先对一些基本的知识作一个了解,然后再通过写程序来反汇编调试观察 . . .

CALL 框架
EBP 寄存器 栈底指针
ESP 寄存器 栈顶指针

  1. EBP 栈底指针
    EBP 是一个特殊的寄存器,通过 EBP+偏移量 可以访问CALL里边的局部变量
    它的低 16位叫 BP。 // EAX 和AX 的关系
  2. ESP 栈顶指针
    ESP栈顶指针与EBP栈底指针构成一段空间大小,一般就是本CALL局部变量的空间大小总和
    ESP指针配合EBP使用。// SP

每个CALL会分配一个独立的栈段空间,供局部变量使用 . . .

下面我们通过一个程序来跟踪一个CALL,cpp代码如下所示:

#include 
 
void fun() {
	int a = 1;
	int b = 2;
	char c = '1';

	printf("go go go!\n");
} 

int main() {
	printf("Start\n");
 
	fun(); 
	  
	printf("end\n");
	 
	return 0;
}

在main中调用了一个方法,其中定义了三个局部变量,下面我们通过 OD来调试一下,看看这个在调用这个方法时发生了什么事 . . .

Win32反汇编(四)栈的工作原理与堆栈平衡,函数方法参数的调用约定_第1张图片
首先,在调用这个 CALL时,会先把CALL的下一个EIP 先进行压栈,用于在CALL时返回 . . . 如下所示:
Win32反汇编(四)栈的工作原理与堆栈平衡,函数方法参数的调用约定_第2张图片
子函数块在执行之前,它会计算栈(CALL的新栈)的大小,一般就是局部变量大小,如下所示:

int a = 1;
int b = 2;
char c = ‘1’;

这三个变量的大小之和为 9个字节,但编译器会进行一个速度计算上的优化,字节对齐,所以实际这三个变量所占大小为 12个字节,也就是说用空间换取了时间 . . .

现在我们可以来了解一下什么是堆栈平衡了 . . .

每调用一个CALL,就会开辟一个新的栈空间,如果没有栈平衡功能的实现,那么很容易会破坏其它栈里的数据,导致程序崩溃,所以堆栈平衡很好的解决了这个问题,CALL的子程序如下所示:
Win32反汇编(四)栈的工作原理与堆栈平衡,函数方法参数的调用约定_第3张图片
现在我们来分析一下在这个新栈中发生了什么事情 . . .

首先,push ebp 这条指令将 main中的栈底指针压入栈(保存),然后通过 mov ebp,esp 设置新的栈底指针,为什么将 esp 转移为 ebp呢? 我们看看如下的图所示:
Win32反汇编(四)栈的工作原理与堆栈平衡,函数方法参数的调用约定_第4张图片

我们通过push ebp 这条指令使 esp的值变化了(向上移动了),如下所示:
Win32反汇编(四)栈的工作原理与堆栈平衡,函数方法参数的调用约定_第5张图片
所以我们可以很清晰的知道,这些所谓的栈是一段线性的存储空间,所以mov ebp,esp 设置新的栈的底部指针为 esp,之前我们所说的为那三个变量分配了 12个字节的内存,所以这个栈的内存大小为 12个字节,通过 sub esp,0c来实现,如下所示:
Win32反汇编(四)栈的工作原理与堆栈平衡,函数方法参数的调用约定_第6张图片

然后对三个内存单元进行赋值(变量的初始化),然后对一个字符串进行压栈,调用 printf函数进行打印,printf函数结束后 使用 add esp,4 指令进行堆栈平衡(很重要的操作) 最后通过 mov esp, ebp 还原到原来的 栈顶指针位置,如下所示:
Win32反汇编(四)栈的工作原理与堆栈平衡,函数方法参数的调用约定_第7张图片
栈顶指针恢复了,那么main 的栈底指针也需要恢复,使用 pop ebp实现,如下所示:
Win32反汇编(四)栈的工作原理与堆栈平衡,函数方法参数的调用约定_第8张图片
执行完最后 retn 指令,EIP 会从栈中获取地址,如下所示:
Win32反汇编(四)栈的工作原理与堆栈平衡,函数方法参数的调用约定_第9张图片

执行最后一个 printf 方法 也需要压栈,然后进行堆栈平衡,如下所示:
在这里插入图片描述
所以这个程序分析就是这样,下面我们来看一下完整的分析过程,动图如下所示:

.
.


调用约定

调用约定针对于函数的参数读取顺序而言,有的是从左到右,有的是从右到左,下面我们来介绍一下三个不同的调用约定 . . .

cdecl 调用约定

__cdecl 是 C Declaration的缩写,所以参数从右到左依次入栈,这些参数由调用者清除,称为手动清栈。

VC++ 默认约定 __cdecl

cpp 代码如下所示:

#include 

int __cdecl sum(int m, int n) {
	return m + n;
}

int main() {
	printf("Start\n");

	int sum = (20, 10);

	printf("end\n");

	return 0;
}

然后我们 od 来调试这个程序来观察,如下所示:
Win32反汇编(四)栈的工作原理与堆栈平衡,函数方法参数的调用约定_第10张图片
首先,在调用 add方法之前,会将两个参数进行压栈,如下所示:
在这里插入图片描述
我们发现这两个数据压栈的顺序发生了变化,从右到左入栈,这就是 __cdecl调用约定 . . .

之所以进行压栈,是为了在 call中访问这些数据而已,如下所示:
Win32反汇编(四)栈的工作原理与堆栈平衡,函数方法参数的调用约定_第11张图片
框起来的就是从栈中获取的数据(通过地址获取),这里我们发现数据的地址是通过 ebp + ?来获取的,而在 call中的局部变量是通过 ebp - ? 获取的,这是为什么呢? 如下所示:
Win32反汇编(四)栈的工作原理与堆栈平衡,函数方法参数的调用约定_第12张图片
新的 ebp 上面是新的栈空间,要想获取原来栈中的数据肯定是通过 ebp 加上一个偏移值才能访问到参数 . . .

这个call执行过后,我们需要手动清栈(其实是编译器帮我们做的事情),如下所示:
Win32反汇编(四)栈的工作原理与堆栈平衡,函数方法参数的调用约定_第13张图片
将栈顶的指针向下移动 8个字节就行了,因为是两个int数据,所以是 8 . . .

这样做的目的是为了 堆栈平衡 . . .

.

stdcall 调用约定

API 函数调用约定 __stdcall

__stdcall 是 standardcall 的缩写,是 C++ 的标准调用方式:所有参数从右到左依次入栈,如果是调用类成员的话,最后一个入栈的是 this指针 . . .

这些堆栈中的参数由被调用的函数在返回后清除,使用的指令是 retn X,X表示参数占用的字节数(内存空间大小),CPU 在 ret 之后自动弹出 X个字节的栈空间,称为自动清栈 . . .

cpp 代码如下所示:

#include 

int __stdcall add(int m, int n) {
	return m + n;
}

int main() {
	printf("Start\n");

	int sum = add(20, 10);

	printf("end\n");

	return 0;
}

使用 od来调试如下所示:
Win32反汇编(四)栈的工作原理与堆栈平衡,函数方法参数的调用约定_第14张图片
因为在 call 中就将栈给清空了,所以我们不需要手动堆栈平衡 . . .

.

fastcall 调用约定

__fastcall 是编译器指定的快速调用方式

fastcall 通常规定将前两个(或者若干个)参数由寄存器传递,其余参数还是通过堆栈传递,不同编译器编译的程序规定的寄存器不同返回方式和 stdcall相同

cpp 代码如下所示:

#include 

int __fastcall add(int a, int b, int c, int d, int e) {
	return a + b + c + d + e;
}

int main() {
	printf("Start\n");

	int sum = add(1, 2, 3, 4, 5);

	printf("end\n");

	return 0;
}

使用OD调试如下所示:
Win32反汇编(四)栈的工作原理与堆栈平衡,函数方法参数的调用约定_第15张图片
我们发现前两个参数用寄存器读取,后三个参数进行压栈,call中的指令如下所示:
Win32反汇编(四)栈的工作原理与堆栈平衡,函数方法参数的调用约定_第16张图片
通过寄存器访问,是将作局部变量来使用,所以栈顶指针向上移动了 8个字节 . . .

因为有两个是通过寄存器获取的数据,只有3个参数压栈,所以清栈的时候,大小为 0C,寄存器调用约定一般是对效率非常高的程序的要求,所以我们一般不使用 .
.
.


你可能感兴趣的:(Win32反汇编)