链接、装载与库——进程的栈

来源:互联网 发布:jsp企业网站源码 编辑:程序博客网 时间:2024/05/20 19:31

内存是承载程序的介质,是程序进行运算和表达的场所。

未有特殊说明,则默认在32bit操作系统中。

1. 程序的内存布局

操作系统会将内存空间中的一部分分给内核使用,应用程序无法访问这段内存,这段内存被称为内核空间。Windows默认情况将高地址的2GB空间分配给内核,Linux默认情况将高地址的1GB空间分配给内核。
剩下的内存空间称为用户空间,用户空间中有许多默认区域。

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

  • :堆用来容纳应用程序动态分配的内存区域。堆通常在栈的下方(低地址方向)。堆一般比栈大可以有几十至数百兆字节的容量

  • 可执行文件映像:存储着可执行文件在内存里的映像。

  • 保留区:保留区不是一个单一的内存区域,而是对内存中受到保护而禁止访问的内存区域的总称

  • Linux地址空间布局
  • 栈向低地址增长,堆向高地址增长。当栈或堆现有的大小不够用时,它们将按照增长方向扩大,直到预留的的空间被用完为止

2. 栈(stack)

  • 将数据压入栈中(入栈,push),将已经压入栈中的数据弹出(出栈,pop),即先入栈的数据后出栈(First In Last Out,FIFO)

  • 压栈操作使栈增大,弹出操作使栈减小

  • 栈总是向低地址增长的,栈顶由esp寄存器进行定位,栈底由ebp寄存器进行定位。压栈操作使栈顶地址减小,弹出操作使栈顶地址增大

  • 程序栈实例

  • 栈保存了一个函数调用所需要的维护信息,其被称为栈帧(Stack Frame)或活动记录(Activate Record),栈帧包含如下内容:

    • 函数的返回地址和参数

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

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

  • 一个函数的活动范围由ebp和esp寄存器划定范围。esp寄存器始终指向栈的顶部即当前函数的活动记录的顶部,ebp寄存器指向函数活动记录的底部,ebp寄存器也被称为帧指针(Frame Pointer)

  • 活动记录

  • i386下的函数调用步骤如下

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

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

    • 跳转到函数体执行

    • 第二、三步由指令call一起执行

  • i386下的函数体“标准”开头如下

    • push ebp(保存本栈帧的ebp)

    • mov ebp, esp(将ebp移动到栈顶)

    • sub esp, XXX(开辟新的栈帧)

    • push XXX(保存寄存器)

  • i386下的函数体“标准”结束如下

    • pop XXX(恢复寄存器)

    • mov esp, ebp(恢复成调用者所在栈帧的栈顶)

    • pop ebp(恢复成调用者所在栈帧的栈基址)

    • ret(从栈顶取得下一条指令的地址,并跳转)

  • 示例如下

    • 测试代码
      测试代码

    • main函数反汇编
      main反汇编

    • foo函数反汇编
      foo反汇编

    • foo函数return之前时寄存器
      foo函数返回之前时寄存器

    • foo函数return之前时内存
      foo函数返回之前时内存

    • foo函数反汇编代码解析
      foo函数反汇编代码解析

  • 某些场合,编译器生成函数的进入和退出指令序列不按照标准的方式进行,比如C函数满足:

    • 函数被声明为static(不可在此编译单元之外访问)

    • 函数在本编译单元仅被直接调用,没有显示或隐式的取地址(即没有任何函数指针指向过这个函数)

    • 编译器确信满足这两条的函数不会在其他编译单元内被调用,因此可以修改指令,达到优化目的

  • 函数调用惯例(Calling Convention)

    • 函数参数的传递顺序和方式:规定函数调用方将参数压入栈的顺序(从左到右、从右至左);规定函数参数的传递方式(通过栈传递,函数调用方将参数压入栈,自己在从栈中将参数取出、使用寄存器传递,提高性能)

    • 栈的维护:函数体执行完后,之前压入栈中的参数需要弹出,可以由函数调用方完成,也可以由函数体本身完成

    • 名字修饰(Name-mangling)策略:链接时区分调用惯例,不同的调用惯例有不同的名字修饰策略

    • C语言默认调用惯例是cdecl,任何一个没有显式指定调用惯例的函数都默认是cdecl,比如:int _cdecl foo(int a, int b, int c)

    • 函数调用惯例

  • 函数返回值传递

    • 当返回值小于等于4字节时,函数将返回值存储在eax,调用者读取eax
    • 当返回值大于4字节,小于等于8字节时,函数使用eax和edx联合返回的方式。eax存储低4字节,edx高4字节
    • 当返回值大于8字节时,函数会使用一个临时的栈上内存空间(临时对象)作为中转,返回值对象会被拷贝两次
    • 当返回值大于8字节时,函数返回值传递示例如下:
      • 函数返回值传递测试代码
        函数返回值传递测试代码
      • main函数返回值反汇代码
        main返回值反汇编
      • return_test函数反汇编代码
        return_test反汇编
      • main函数中n的地址
        main中n的地址
      • 临时对象给main函数中的n赋值
        临时对象给n赋值
    • 首先调用者在栈上将一部分空间作为传递返回值的临时对象
    • 将临时对象的地址作为隐藏参数传递给函数
    • 函数将数据拷贝给临时对象,并将临时对象的地址用eax传出
    • 函数返回后,调用者将eax指向临时对象的内容拷贝给局部变量
    • 返回值传递流程
      返回值传递流程
    • 需要注意的是返回对象的拷贝情况完全不具备可移植性,不同的编译器产生的结果可能不同。函数传递大尺寸的返回值所使用的方法不是可移植的,不同编译器、不同平台、不同调用惯例、不同编译参数可能采用不同的实现方法
    • 在C++中要使用返回值优化技术(Return Value Optimization, RVO),直接将对象构造在临时对象上,减少一次从函数内局部变量对临时对象的拷贝构造步骤
      cpp_obj return_test(){    return cpp_obj();}

    *码字好累。。。→_→*