程序运行之栈空间

一般来讲,应用程序使用的内存空间里有如下的默认区域:

1 栈:用于维护函数调用的上下文。栈通常在用户空间的最高地址出分配,通常有数兆字节的大小

2 堆:堆是用来容纳应用程序动态分配的内存区域。比如使用malloc和new分配内存就从堆里分配。

3 可执行文件镜像:这里存储着可执行文件在内存里的映射

首先来介绍栈:

在操作系统中,栈总是向下增长的,栈顶由称为esp的寄存器进行定位,压栈的操作使栈顶的地址减小,弹出的操作使栈顶的地址增大。栈保存了一个函数调用所需要维护的信息,这通常称为堆栈帧或活动记录。堆栈帧包括如下几个方面的内容:

1 函数返回地址和参数

2 临时变量:包括函数的非静态局部变量以及编译器自动生成的其他临时变量

3 保存的上下文:包括在函数调用前后需要保持不变的寄存器。

 

在i386中,一个函数的活动记录用ebp和esp这两个寄存器划定范围。esp寄存器始终指向栈的顶部,ebp寄存器指向了函数活动记录的一个固定位置,ebp寄存器又称为帧指针。常见的活动记录如下图所示:

在ebp之前首先是这个函数的返回地址,它的地址是ebp-4, 再往前是压入栈中的参数,它们的地址分别是ebp-8,ebp-12等等。ebp所直接指向的的数据是调用该函数前ebp的值,这样函数在返回的时候,ebp可以读取这个值恢复到调用前的值。所以一个i386的程序调用顺序如下:

1 把所有或者一部分参数压入栈中,如果有其他参数没有入栈,那么使用某些特定的寄存器传递

2 把当前指令的下一条指令的地址压入栈中

3 跳转到函数体执行

其中2,3由执行call一起执行。跳转到函数体之后就开始执行函数。I386函数体的标准开头过程如下:

1 push ebp: 把ebp压入栈中,也就是old ebp

2 move ebp,esp: ebp=esp(ebp指向栈顶,此时栈顶就是old ebp)

3 sub esp,xxx  在栈上分配xxx字节的临时空间

4 push xxx 保存名为xxx的寄存器

把ebp压入栈中,是为了在函数返回的时候便于恢复以前的ebp值,函数返回的时候过程正好相反。

1 pop xxx

2 mov esp,ebp 恢复esp同时收回局部变量空间

3 pop ebp:从栈中恢复保存的ebp的值

4 ret: 从栈中取得返回地址,并跳转到该位置。

我们用一个简单的函数调用然后查看汇编代码来看下这个过程

#include <stdio.h>

int foo()

{      

        return 123;

}      

int main()

{      

        int ret;

        ret=foo();

        return 1;

}  

objdump –d stack_test.o 可以看到如下结果

00000000000005fa <foo>:

 5fa:        55                           push   %ebp

 5fb:        48 89 e5             mov    %esp,%ebp

 5fe:        b8 7b 00 00 00         mov    $0x7b,%eax

 603:       5d                           pop    %ebp

 604:       c3                           retq  

 

在main中首先是将ebp进栈,然后是将esp赋值为ebp。同时将esp减去0x10,也就是开辟了0x10的栈空间。同样的过程在foo对应的汇编也可以看到。mov    $0x7b,%eax 这条指令是将返回值123赋值给eax,同时将ebp值出栈

0000000000000605 <main>:

 605:       55                           push   %ebp

 606:       48 89 e5             mov    %esp,%ebp

 609:       48 83 ec 10            sub    $0x10,%esp

 60d:       b8 00 00 00 00         mov    $0x0,%eax

 612:       e8 e3 ff ff ff               callq  5fa <foo>

 617:       89 45 fc               mov    %eax,-0x4(%ebp)

 61a:       b8 01 00 00 00         mov    $0x1,%eax

 61f:        c9                           leaveq

 620:       c3                           retq  

我们在把函数变更下使得foo函数带参数

#include <stdio.h>

int foo(int i, int j)

{      

        return 123;

}      

int main()

{      

        int ret;

        ret=foo(1,2);

        return 1;

}

再看下汇编代码:可以看到参数i和j的入栈过程,首先在main中将参数值分别赋给esi和edi寄存器。然后在foo中分别将edi和esi的值存入到ebp+0x04和ebp-0x08的地址中。

00000000000005fa <foo>:

 5fa:        55                           push   %ebp

 5fb:        48 89 e5             mov    %esp,%ebp

 5fe:        89 7d fc               mov    %edi,-0x4(%ebp)

 601:       89 75 f8               mov    %esi,-0x8(%ebp)

 604:       b8 7b 00 00 00         mov    $0x7b,%eax

 609:       5d                           pop    %ebp

 60a:       c3                           retq  

 

000000000000060b <main>:

 60b:       55                           push   %ebp

 60c:       48 89 e5             mov    %esp,%ebp

 60f:        48 83 ec 10            sub    $0x10,%esp

 613:       be 02 00 00 00         mov    $0x2,%esi

 618:       bf 01 00 00 00          mov    $0x1,%edi

 61d:       e8 d8 ff ff ff               callq  5fa <foo>

 622:       89 45 fc               mov    %eax,-0x4(%ebp)

 625:       b8 01 00 00 00         mov    $0x1,%eax

 62a:       c9                           leaveq

 62b:       c3                           retq  

 62c:       0f 1f 40 00             nopl   0x0(%rax)

以一个框图来表示调用关系

函数返回值传递

除了参数的传递外,函数与调用方的另一个交互就是返回值。前面可以看到eax寄存器是传递返回值的通道。但是eax本身只有4个字节,那么大于4字节的返回值是如何传递的呢。对于返回5-8字节的情况,需要联合eax和edx联合返回的方式进行。eax存储低4字节,edx存储高4字节。但是对于超过8个字节的情况,就比较复杂了。以下面的例子为例:

 

#include <stdio.h>

typedef struct big_thing

{

        char buf[128];

}big_thing;

 

big_thing return_test()

{

        big_thing b;

        b.buf[0]=0;

        return b;

}

 

int main()

{

        big_thing n=return_test();

}

 

对应的汇编代码: 将栈上的一个地址ebp-1D0h存储在eax中,然后将eax入栈,再调用return_test

lea eax,[ebp-1D0h]

push eax

call _return_test

这就相当于将eax的值作为了return_test的参数。但是实际上return_test是没有参数的,因此这个也被称为隐含参数。

下面这4行是一个整体,rep movs是一个复合指令,意思是重复movs指令知道ecx寄存器为0,于是rep movs a,b的意思就是将b指向位置的若干个双字节拷贝到a指向的位置上。相当于memcpy(ebp-88h,eax,0x20*4) ebp-88h就是n的地址

mov ecx ,20h

mov esi,eax

lea edi,[ebp-88h]

rep movs  dword ptr es:[edi],dword ptr [esi]

 

再来看下return_test的实现。

lea esi, [ebp-88h]

mov edi,dword ptr [ebp+8]

rep movs dword ptr es:[edi], dword ptr [esi]

 

ebp+8指向函数的参数,ebp-88h指向的是变量b的位置,因此rep movs dword ptr es:[edi], dword ptr [esi]相当于memcpy([ebp+8],&b,128)。也就是将变量b地址的内容拷贝到传入的参数也就是ebp-1D0h这个地址去。

那么整个流程如下:

1 main在栈中开辟一段空间,并将这块空间的一部分作为传递返回值的临时对象,称为temp

2 将temp对象的地址作为隐藏参数传递给return_test参数

3 return_test将数据拷贝到temp对象

4 return_test返回之后,main函数将eax指向的temp对象拷贝给n

 

转载于:https://www.cnblogs.com/zhanghongfeng/p/11083316.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
VS Code是一款功能强大的集成开发环境(IDE),它为开发者提供了广泛的开发资源和工具。VS Code的堆空间指的是可以在IDE中运行的操作系统所能使用的内存资源的大小。VS Code开大空间的好处主要有以下几点。 首先,开大空间可以提高程序的性能。是用于存储函数调用和局部变量的一种数据结构,较大的空间可以容纳更多的函数调用和变量,使程序能够更加高效地执行。特别是在处理大量数据或执行复杂计算时,有足够的空间可以避免内存溢出等问题,提高程序的执行效率。 其次,开大空间可以支持更复杂的程序和项目。随着项目的不断发展,代码规模和复杂度也会逐渐增大。一些大型项目可能需要同时运行多个函数调用和变量,这就需要足够的空间来支持更大规模的开发。VS Code开大空间可以满足这些需求,使得开发者能够更好地处理复杂的程序逻辑,并能够扩展项目的规模。 此外,开大空间可以提供更好的开发体验。在编写代码和调试过程中,开发者可能需要执行多次函数调用和变量的定义。如果空间较小,这些操作可能会导致溢出错误,影响代码的执行和调试。通过开大空间,开发者可以避免这些问题,更流畅地调试和开发代码。 总而言之,VS Code开大空间对于开发者来说具有重要的意义。它可以提高程序的性能,支持更复杂的项目和程序开发,并提供更好的开发体验。开发者可以根据项目的需求和代码复杂度来灵活配置空间的大小,以达到最佳的开发效果。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值