• 程序运行之栈空间


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

    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

  • 相关阅读:
    VMWare安装Solaris虚拟机的网络设置
    PeopleTools预警程序制作
    listener.ora增加监听端口
    用.Net Mage工具更新WPF ClickOnce应用程序部署清单
    基本测试方法用例场景
    Qt Vs msvc debug版本没有问题但release版本出现异常
    Qt 打包release发布问题
    Qt 鼠标悬浮按钮上出现浮窗效果
    Qt 样式对于QPushbutton 增加 hover press release效果
    阿里云ECS无法通过SSL远程链接问题。
  • 原文地址:https://www.cnblogs.com/zhanghongfeng/p/11083316.html
Copyright © 2020-2023  润新知