学习交流加

  • 个人qq:
    1126137994
  • 个人微信:
    liu1126137994
  • 学习交流资源分享qq群:
    962535112

在我之前学习底层的知识的时候,也写过相关的内容。可以对比的学习:【软件开发底层知识修炼】二十 深入理解可执行程序的结构【软件开发底层知识修炼】二十三 ABI-应用程序二进制接口三之深入理解函数栈帧的形成与摧毁

学习本文的前提是了解进程的内存布局空间。可以看上面两篇博客进行巩固

1 程序中的栈内存结构

  • 栈在程序中用于维护函数调用的上下文
  • 函数中的参数和局部非静态变量存储在栈上
  • esp指针始终指向栈顶
  • ebp是函数栈帧,用于定位的,使用ebp可以查找到函数的参数,返回地址等信息。后面的函数调用分析会认识到这一点
  • 栈在整个进程内存空间中是从上往下扩展,也就是从高地址往低地址扩展。如下图是一个栈

push 操作相当于往栈中填数据,esp指针会向下走。pop操作相当于将栈顶数据弹出,esp指针会网上走。

那么栈对于程序而言,到底有什么用呢?

栈用于保存一个函数调用的时候所需要维护的信息,包括函数的参数、返回地址、局部变量、上下文信息等。它们在栈中的位置大致如下图所示:

1.1 函数的调用过程对应的栈的变化

每次函数调用都对应一个栈上的活动记录,被调用函数的活动记录位于调用函数的下面。比如有如下的几个函数调用:

那么在某一个时刻,栈中的内容大致是这样的:

注意:上面的栈中的内容并没有具体,只是说明各个函数的活动记录信息在哪个文职,具体里面的内容没有列出。看下面的分析:

从程序开始运行时分析:

  1. 从main函数开始运行

当main开始运行的时候,main函数的栈的信息是下面这样的:

  • 可以看到,首先入栈的是函数参数,然后是函数的返回地址
  • old_ebp代表调用main函数的ebp的位置。这个暂且不管
  • 可以看到,ebp向上偏移4字节(在X86 32位系统中,栈是以4字节为单位进行存储数据,所以一次偏移就是4字节)就能找到返回地址(这个返回地址是调用main函数的那个函数之前执行指令的地址)。ebp向下偏移4字节就是old_ebp。所以说ebp是函数栈帧,用于定位查找其他参数
  • esp始终指向栈顶
  1. 当main函数调用f()函数

当main函数执行到调用f() 函数的时候,main函数的活动记录(寄存器,返回地址等)需要保存入栈,f() 函数的参数信息需要入栈。如下图所示:


可以看到:

  • f函数的参数先入栈
  • 然后返回地址入栈(main函数调用f()函数那里的地址)
  • 然后main函数的ebp的地址入栈。用于定位上一个函数的ebp
  • 然后才是函数中的局部非静态变量信息入栈。这个参数的入栈顺序可以参考本文开头给出的两篇文章中的内容

f函数调用g函数就是一样的过程,这里就不再赘述。下面直接上当f执行完返回的过程是怎样的?

  1. 当从f()调用中返回到main函数的栈的变化

上述f返回后当前栈就只有白色空白部分,下面的深颜色(橘黄色???分不清)并不是当前栈的内容了

可以看出;

1.11 函数调用栈上的数据

函数调用栈上的数据,在函数返回时,将被释放,不再有效。所以对于以下代码,是错误的代码;

2 程序中(应该称为进程中才对)的堆结构

对于堆空间,本文只简单的讲述系统对堆空间的管理方式(也就是使用malloc时,系统是如何申请内存的,以及系统对内存的管理方式,当然本文也是简单描述,具体可以参考CSAPP书籍中相关章节)

操作系统对堆的管理方式主要有:

  • 空闲链表法、位图法、对象池法

下面主要简要说明空闲链表发的原理:

空闲链表就是操作系统将整个可用的堆内存空间分为一块一块的,对应的相同数量的指针指向各个内存块,然后内存块的末尾又是一个指针指向下一个内存块的头。

其实简单来说就是将多个内存块串联成一个链表形式。

如下图所示:

当使用malloc函数进行内存分配的时候,系统会遍历链表,找到能够满足申请大小的且没有被别人申请的空闲的链表的一个节点对应的内存块。比如上图,申请一个4字节的内存,最终遍历链表找到了一个大小为5字节的内存块,然后系统就为我们在该内存块上申请4字节的内存空间供指针p使用。

简单来说,空闲链表法就是上述的大致过程。

想要深入了解操作系统对堆空间的管理,可以阅读书籍《深入理解计算机系统》(csapp)。本文不再重复赘述。

3 程序中的静态存储区(其实就是数据区)

我们一般说进程,但这里说程序。有些术语描述真的是很模棱两可,但是只要自己明白就行

在进程的地址空间,栈,堆是程序运行时的效果。我们也知道这两个内存对应的数据到底是什么。

那么对于下面程序中的变量g_init_v,g_uninit_v,s_v1,s_v2以及字符串字面量,它们是存储在哪里呢?

我们已经看到了上图中,它们在可执行文件中都是存储在.data区,.bss区。这些区域。

当程序运行起来之后,它们还是会在进程的内存空间的.data与.bss段。与可执行文件的data与bss名字一样如下图


我们称之为静态存储区。为什么叫静态?并不是因为static,而是因为它们的值虽然有可能被改变,但是却一直在那个区域。不像栈区域的值最后会被销毁,堆上的值也需要free。

现在终于明白静态存储区实际就是.data与.bss区域了。

下面总结一下静态存储区的几点重要知识:

  1. 静态存储区是随着程序的运行而分配空间
  2. 静态存储区的生命周期直到程序运行而结束
  3. 在程序的编译期间,静态存储区的大小就已经确定
  4. 静态存储区主要用于保存全局变量静态局部变量以及类似于字符串字面量样式的字面量

4 总结

栈,堆与静态存储区是进程地址空间的基本数据区域。

  • 一定要注意区分可执行文件中的内容与程序运行起来后的进程地址空间中的内容的差别。
  • 本文所描述的就是进程地址空间中内容。可执行文件的内容可参考书籍《程序员的自我修养》