• Welcome to the world's largest Chinese hacker forum

    Welcome to the world's largest Chinese hacker forum, our forum registration is open! You can now register for technical communication with us, this is a free and open to the world of the BBS, we founded the purpose for the study of network security, please don't release business of black/grey, or on the BBS posts, to seek help hacker if violations, we will permanently frozen your IP and account, thank you for your cooperation. Hacker attack and defense cracking or network Security

    business please click here: Creation Security  From CNHACKTEAM

一个函数推入和推出例子的详细分析


Recommended Posts

我们之前刚刚解释了函数的活动记录是什么样子的。相信大家对功能的详细调用过程的了解并不是太清楚。在本节中,我们将以VS2010调试模式为例进行深入分析。

请看下面的代码:

void func(int a,int b){

int p=12,q=345

}

int main(){

func(90,26);

返回0;

}

默认的调用约定cdecl用于函数,即参数从右向左堆叠,调用者负责卸载参数。该功能的推入和推出过程如下图所示:

bi3l21p1btz3378.jpg

函数进栈

步骤至是功能堆叠过程:

1) main()是主函数,也需要推入堆栈,如步骤所示。

2)在步骤中,执行语句func(90,26);先把参数90和26压入堆栈,再把返回地址压入堆栈,这些都是由main()函数(调用者)完成的。这个时候ebp的价值并没有改变,只是改变了esp的方向。

3)在步骤,将执行func()的函数体。首先将原ebp寄存器的值推入栈中(即图中的旧ebp),将esp的值赋给ebp,使ebp从main()函数的栈底指向func()函数的栈底,从而完成函数栈切换。此时因为esp和ebp的值相等,所以也指向相同的位置。

4)为局部变量、返回值等预留足够的内存。如步骤所示。由于在函数调用之前已经分配了堆栈内存,所以这里并没有真正分配内存,而是esp的值减去一个整数,比如esp-0XC0,就是预留0XC0字节的内存。

5)将ebp、esi、edi寄存器的值依次压入堆栈。

6)将局部变量的值放入保留内存。注意,第一个变量和旧ebp之间有4个字节的空白,变量之间也有几个字节的空白。

为什么要留这么多空白?难道不是浪费内存吗?这是因为我们使用调试模式生成程序,留下额外的内存方便添加调试信息;当程序在Release模式下生成时,内存会变得更加紧凑,空白会被消除。

至此,func()函数的活动记录被构建。可以发现,在函数的实际调用过程中,形参并不存在,也不占用内存空间,只有内存中的实参,在函数体代码执行前被调用者压入堆栈。

未初始化的局部变量的值为什么是垃圾值

在为局部变量分配内存时,只需从esp的值中减去一个整数,并预留足够的空白内存。不同的编译器在不同的模式下会以不同的方式处理这个空白内存,可能会也可能不会初始化为固定值。

例如,在VS2010调试模式下,保留内存将被初始化为0xcccccc,如果局部变量没有赋值,它们的内存将不会改变,输出结果将是0XCCCCCCCC。请看下面的代码:

#包含stdio.h

#包含stdlib.h

int main(){

int m,n;

printf('%#X,%#X\n ',m,n);

系统(“暂停”);

返回0;

}

运行结果:0XCCCCCCCC,0XCCCCCCCC

虽然编译器初始化了空白内存,但是这个值对我们来说一般是没有意义的,所以我们可以认为它是垃圾,是随机的。

函数出栈

步骤是func()函数弹出的过程:

7)函数func()执行后,开始堆栈。首先,堆叠edi、esi和ebx寄存器的值。

8)将局部变量、返回值等数据推出堆栈时,直接将ebp的值赋给esp,使ebp和esp指向同一个位置。

9)接下来,弹出旧ebp并将其分配给当前ebp。此时ebp指向func()调用前的位置,即main()活动记录的旧ebp位置,如步骤9所示。

这一步至关重要,它保证了函数被调用前的情况被恢复,这也是为什么每次函数被调用时,旧的ebp都必须被压入堆栈的原因。

最后,根据返回地址找到下一条指令的位置,返回地址和参数都从堆栈中取出。此时esp指向main()活动记录的栈顶,意味着func()完全从栈中取出,栈恢复到调用func()之前的情况。

遗留的错误认知

经过上面的分析可以发现,函数栈只是在增加esp寄存器的值,使其指向之前的数据,而不是破坏之前的数据。说函数运行后立即销毁局部变量,其实是不对的。这只是为了让大家更容易理解,对局部变量的范围有一个清晰的认识。

堆栈上的数据只有在后续函数继续进入堆栈时才能被覆盖,也就是说只要时机合适,在函数之外仍然可以获得局部变量的值。请看下面的代码:

#包含stdio.h

int * p;

void func(int m,int n){

int a=18,b=100

p=a;

}

int main(){

int n;

func(10,20);

n=* p;

printf('n=%d\n ',n);

返回0;

}

运行结果:n=18

在func()中,将局部变量A的地址赋给P,在main()函数中调用func()。函数刚刚被调用,没有其他函数放入堆栈,所以局部变量A所在的内存还没有被覆盖,所以由语句n=* p;能得到它的价值。

Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now