前言
作者:
浪子花梦
,一个有趣的程序员 ~
此系列文章都是一些基础的文章,每篇文章都通过几个小例子快速的了解 Win32反汇编与OD的使用,在此作个笔记
如若对您有帮助,记得三连哟 ~
Win32反汇编(一) 初步探索Win32反汇编 与 Ollydbg的简单使用
Win32反汇编(二)几种常见的指令反汇编详解:EAX、MOVSX与MOVZX、LEA、SUB、CMP与转移指令
Win32反汇编(三)深层次的了解各种转移指令:IF语句有符号与无符号跳转
此文讲解非常重要的概念:栈、堆栈平衡 . . .
首先,我们先对一些基本的知识作一个了解,然后再通过写程序来反汇编调试观察 . . .
CALL 框架
EBP 寄存器 栈底指针
ESP 寄存器 栈顶指针
ESP栈顶指针与EBP栈底指针构成一段空间大小
,一般就是本CALL局部变量的空间大小总和每个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来调试一下,看看这个在调用这个方法时发生了什么事 . . .
首先,在调用这个 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 是 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 来调试这个程序来观察,如下所示:
首先,在调用 add方法之前,会将两个参数进行压栈,如下所示:
我们发现这两个数据压栈的顺序发生了变化,从右到左入栈,这就是 __cdecl调用约定 . . .
之所以进行压栈,是为了在 call中访问这些数据而已,如下所示:
框起来的就是从栈中获取的数据(通过地址获取),这里我们发现数据的地址是通过 ebp + ?来获取的,而在 call中的局部变量是通过 ebp - ? 获取的,这是为什么呢? 如下所示:
新的 ebp 上面是新的栈空间,要想获取原来栈中的数据肯定是通过 ebp 加上一个偏移值才能访问到参数 . . .
这个call执行过后,我们需要手动清栈(其实是编译器帮我们做的事情),如下所示:
将栈顶的指针向下移动 8个字节就行了,因为是两个int数据,所以是 8 . . .
这样做的目的是为了 堆栈平衡 . . .
.
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来调试如下所示:
因为在 call 中就将栈给清空了,所以我们不需要手动堆栈平衡 . . .
.
__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调试如下所示:
我们发现前两个参数用寄存器读取,后三个参数进行压栈,call中的指令如下所示:
通过寄存器访问,是将作局部变量来使用,所以栈顶指针向上移动了 8个字节 . . .
因为有两个是通过寄存器获取的数据,只有3个参数压栈,所以清栈的时候,大小为 0C,寄存器调用约定一般是对效率非常高的程序的要求,所以我们一般不使用 .
.
.