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 <cstdio>
 
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来调试一下,看看这个在调用这个方法时发生了什么事 . . .

在这里插入图片描述
首先,在调用这个 CALL时,会先把CALL的下一个EIP 先进行压栈,用于在CALL时返回 . . . 如下所示:
在这里插入图片描述
子函数块在执行之前,它会计算栈(CALL的新栈)的大小,一般就是局部变量大小,如下所示:

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

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

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

每调用一个CALL,就会开辟一个新的栈空间,如果没有栈平衡功能的实现,那么很容易会破坏其它栈里的数据,导致程序崩溃,所以堆栈平衡很好的解决了这个问题,CALL的子程序如下所示:
在这里插入图片描述
现在我们来分析一下在这个新栈中发生了什么事情 . . .

首先,push ebp 这条指令将 main中的栈底指针压入栈(保存),然后通过 mov ebp,esp 设置新的栈底指针,为什么将 esp 转移为 ebp呢? 我们看看如下的图所示:
在这里插入图片描述

我们通过push ebp 这条指令使 esp的值变化了(向上移动了),如下所示:
在这里插入图片描述
所以我们可以很清晰的知道,这些所谓的栈是一段线性的存储空间,所以mov ebp,esp 设置新的栈的底部指针为 esp,之前我们所说的为那三个变量分配了 12个字节的内存,所以这个栈的内存大小为 12个字节,通过 sub esp,0c来实现,如下所示:
在这里插入图片描述

然后对三个内存单元进行赋值(变量的初始化),然后对一个字符串进行压栈,调用 printf函数进行打印,printf函数结束后 使用 add esp,4 指令进行堆栈平衡(很重要的操作) 最后通过 mov esp, ebp 还原到原来的 栈顶指针位置,如下所示:
在这里插入图片描述
栈顶指针恢复了,那么main 的栈底指针也需要恢复,使用 pop ebp实现,如下所示:
在这里插入图片描述
执行完最后 retn 指令,EIP 会从栈中获取地址,如下所示:
在这里插入图片描述

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


调用约定

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

cdecl 调用约定

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

VC++ 默认约定 __cdecl

cpp 代码如下所示:

#include <cstdio>

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 来调试这个程序来观察,如下所示:
在这里插入图片描述
首先,在调用 add方法之前,会将两个参数进行压栈,如下所示:
在这里插入图片描述
我们发现这两个数据压栈的顺序发生了变化,从右到左入栈,这就是 __cdecl调用约定 . . .

之所以进行压栈,是为了在 call中访问这些数据而已,如下所示:
在这里插入图片描述
框起来的就是从栈中获取的数据(通过地址获取),这里我们发现数据的地址是通过 ebp + ?来获取的,而在 call中的局部变量是通过 ebp - ? 获取的,这是为什么呢? 如下所示:
在这里插入图片描述
新的 ebp 上面是新的栈空间,要想获取原来栈中的数据肯定是通过 ebp 加上一个偏移值才能访问到参数 . . .

这个call执行过后,我们需要手动清栈(其实是编译器帮我们做的事情),如下所示:
在这里插入图片描述
将栈顶的指针向下移动 8个字节就行了,因为是两个int数据,所以是 8 . . .

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

.

stdcall 调用约定

API 函数调用约定 __stdcall

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

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

cpp 代码如下所示:

#include <cstdio>

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来调试如下所示:
在这里插入图片描述
因为在 call 中就将栈给清空了,所以我们不需要手动堆栈平衡 . . .

.

fastcall 调用约定

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

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

cpp 代码如下所示:

#include <cstdio>

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调试如下所示:
在这里插入图片描述
我们发现前两个参数用寄存器读取,后三个参数进行压栈,call中的指令如下所示:
在这里插入图片描述
通过寄存器访问,是将作局部变量来使用,所以栈顶指针向上移动了 8个字节 . . .

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


  • 5
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
由于无法直接获得程序的反汇编代码和调试信息,我将以伪代码的形式描述程序的大致行为和寄存器状态变化。 主程序代码: ``` int main() { int a = 10; int b = 20; int c = func1(a, b); return 0; } ``` func1函数代码: ``` int func1(int x, int y) { int z = x + y; return z; } ``` 调用func1函数时,程序将a和b的值分别压入,然后调用函数。具体的执行过程如下: 1. 在调用func1函数前,esp寄存器的值为初始顶指针,指向的顶部。 2. 将a的值(10)压入,此时esp减去4,指向新的顶。 3. 将b的值(20)压入,此时esp再次减去4,指向新的顶。 4. 调用func1函数,将当前函数的返回地址(即下一条指令的地址)压入,此时esp再次减去4,指向新的顶,同时将eip寄存器的值设置为func1函数的起始地址,开始执行func1函数。 5. 在func1函数内部,将x和y的值从弹出,此时esp加上8,指向原来的顶。 6. 将x和y的值相加,得到z的值(30)。 7. 将z的值压入,此时esp再次减去4,指向新的顶。 8. 将返回地址从弹出,此时esp加上4,指向原来的顶。 9. 将z的值从弹出,此时esp加上4,指向原来的顶。 10. 将返回值(即z的值)传递给调用func1函数的程序,通常使用eax寄存器进行传递。 根据上述信息,可以得出函数参数在机器语言程序的传递方式为通过传递,函数结果返回值的传递方式为通过寄存器eax传递。进入func1函数前后,顶指针esp的变化为减少了8个字节(即2个int类型的变量),在函数返回时再次增加8个字节

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值