标签搜索

C程序内存空间

anker
2021-06-26 / 0 评论 / 6 阅读 / 正在检测是否收录...

内存空间布局原理主要解决了我的这些疑问:

  1. 内存空间中分哪些大类
  2. 堆内存和栈内存地址谁大
  3. 多个有前后关系栈变量或者堆内存地址大小关系
  4. malloc是如何分配内存的
  5. 内存池的来由
  6. 内存越界写为什么往往难以定位,如何防范。

内存空间的组成部分

  1. 正文段(Text Segment)。 CPU执行的指令
  2. 数据段(DATA Segment)。这是指的是初始化数据段。比如明确赋值的全局变量。比如int maxcount = 99
  3. 非初始化数据段(BSS Segment)。也称为BSS(block started by symbol)段。在程序开始执行前,内核将此段中的数据初始化为0或空指针。比如long sum[100]
  4. 栈区。自动变量和函数调用时所需保存的信息,比如函数返回地址以及调用者环境信息、临时变量。这也是递归实现业务时需要考虑的代价。递归函数每次调用自身时,都会产生一个新的栈帧。一些脚本语言实现尾调用来优化这个情况。
  5. 堆区。动态分配的内存。

典型x86处理器Linux内存布局(存储器安排)

x86内存布局图
从上到下,分别是高地址到低地址。

  • Kernel space. 内核使用空间,有命令行参数和环境变量等。他的地址最大,大于0xC0000000
  • 栈区。栈底则在0xC0000000开始。栈分配从高地址向低地址方向增长。注意这和一个栈区数组地址加法来实现向前并不矛盾。栈变量指向的地址其实是这次分配段的末尾地址,于是地址的加法是没有问题的。一般一个栈帧大小是2MB。
  • 内存映射区。主要动态库文件和其他文件或者匿名内存映射使用空间。
  • 堆区。malloc、free是C标准的内存申请释放接口。在Linux下还是通过sbrk/mmap系统调用来实现的。注意malloc返回的指针一定是适当对齐的,使其可以用于任何数据对象。

    #include <stdio.h>
    #include <stdlib.h>// malloc是返回void*的,包含此文件可以避免强制类型转换
    int main(int argc, char **argv)
    {
      int a, b;
      int *c = malloc(sizeof(int));
      int *d = malloc(sizeof(int));
      printf("%p\n%p\n%p\n%p\n", &a, &b, c, d);
      free(c);
      free(d);
      return  0;
    }

    因为手头没有x86机器,即使是x64其地址布局也相似,以下是输出:

    [anker@ms ~]$ gcc -o test test.c
    [anker@ms ~]$ ./test 
    0x7ffd3f14b80c
    0x7ffd3f14b808
    0x1a172a0
    0x1a172c0

    可以留意到

    • 栈地址是减少方向。这里又可以联系到C语言参数入栈是从右往左。即书写在前面的参数越接近栈顶,也就是书写在前面的参数内存地址越低。所以32位下C语言处理变参va_arg就是指针地址加4.
    • 堆地址是增大的方向。同时我们也留意到再次malloc返回的地址差异大于两个int长度,这是因为分配的空间比要求的稍大一些。进一步的原因是,每次申请需要额外的空间来记录管理信息(分配块的长度,指向下一个分配块的指针等等)。也正是这个块管理记录所以越界写或者重复free,除了会修改另外一个块的程序内容,还可能严重的修改了其他块的管理信息。这种错误不会很快的暴露出来,导致了内存越界难以定位。目前在VS的调试模式下是有自动检测的,但Linux环境通过设置环境变量,支持附加调试功能。
    • 同时栈地址比堆地址大。

虽然sbrk可以扩充或缩小进程的堆空间,但是大多数malloc/free的实现都是不实时减小进程的存储空间,原因有:

  • 释放的空间可供以后再分配,通常他们保持在malloc池中而不返回给内核。
  • 如果频繁的申请和释放,每次都调用sbrk或者mmap都增加了系统调用次数,从而影响性能。
  • sbrk在高地址释放但中间有较低地址没有free时也无法收缩,这也是内存碎片产生原因。

因为glibc等的内部自己实现了池式结构,小于某个阈值(默认128KB)时使用brk/sbrk实现内存分配。大于这个阈值则使用mmap申请匿名空间。mmap方式可以整段释放不容易有内存碎片。另外malloc/free在多线程下,需要进行锁操
作。于是有了更高效tcmalloc和jemalloc实现。 tcmalloc是分线程和进程级别缓存的。使用glibc内存池可能会存在内存回收不及时,碎片过多问题,表面看起来和内存泄漏一样只增不减。

0

评论 (0)

取消