内存

程序的内存布局

栈与调用惯例

什么是栈

栈会保存一个函数调用所需要的维护信息(又名堆栈帧或活动记录),它包含:

  • 函数的返回地址和参数
  • 临时变量
  • 保存的上下文(应该是自己的上下文,而不是调用自己的函数的上下文吧)

活动记录由ebp和esp这两个寄存器来划分范围。

感觉上图ebp的位置有一点问题,应该指向old ebp才对,看下面的i386函数体开头

之所以活动记录是这样的,是因为函数调用本身就是这么设计的:

  • i386下的函数调用:

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

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

    • 跳转到函数体执行。

  • i386下的函数体开头

    • push ebp:把 ebp 压入栈中(称为 old ebp)。
    • mov ebp, esp:ebp = esp(这时 ebp 指向栈顶,而此时栈顶就是 old ebp)。
    • 【可选】sub esp, XXX:在栈上分配 XXX 字节的临时空间。
    • 【可选】push XXX:如有必要,保存名为 XXX 寄存器(可重复多个)。
  • 函数返回的标准结尾

    • 【可选】pop XXX:如有必要,恢复保存过的寄存器(可重复多个)。
    • mov esp, ebp:恢复 ESP 同时回收局部变量空间。
    • pop ebp:从栈中恢复保存的 ebp 的值。
    • ret:从栈中取得返回地址,并跳转到该位置。

编译器可能在将分配出来的栈空间初始化为0xCC,这样就可以通过判断栈变量是否为0xCC来判断用户是否初始化了。

调用惯例

函数调用方和被调用方都对如何调用有一致的理解。一般调用惯例包括:(这些应该都属于ABI吧)

  • 函数参数的传递顺序及方式
  • 栈的维护方式
  • 名字修饰的策略

感觉,编译技术是不是讲过

可以发现,最后返回到main函数时,直接是使用sp+8,而不是pop

存在不同的调用规范,对于类成员函数的调用

  • 在VC中会将this指针存放到ecx寄存器
  • 在gcc、thiscall和cdecl都一致,只将this看成是函数的第一个参数
函数返回值的传递
  • 小于4字节函数,将返回值存放到eax寄存器。

  • 5-8字节,eax存低4字节,edx存高4字节

  • 更多,现在main函数中建立临时对象,然后向被调用的函数的参数中添加一个参数项,用来指定main函数中的那个临时对象,被调用的函数返回时,会将要返回的对象存放到参数指定的临时对象地址上,返回到main后,在将该临时对象复制到变量上。(下图的地址怪怪的,感觉不是按照从高到低的顺序呢)

C++中也是类似,对象模型中似乎也提到了这一点:blog3.2

RVO,返回一个构造函数时,会直接在临时对象上构造对象。

堆和内存管理

什么是堆

如何实现malloc?

  • 如果是交给内核去做,那么申请释放堆都需要通过系统调用(即都需要通过软中断,都需要切换处理器模式,都需要切换上下文),开销很大
  • 交给运行库去管理,从系统批发大量的堆,再零售给程序使用。

LInux进程堆管理

操作系统两种堆的申请方式:

  • brk系统调用,用来设置数据段的结束位置,扩大的那部分可以用来当堆
  • mmap系统调用,申请一段虚拟地址空间映射到文件,如果不映射到文件时,那么这段地址可以拿来当堆

glibc的malloc,如果申请小于128K,则在现有的堆中分配,如果申请大于128K,则使用mmap系统调用来为他分配

堆分配算法

空闲链表

之前看到过一篇文章,也提了这中链表分配方式

位图
对象池

假设每次申请的空间都一样,那么就把所有堆空间都分成等大小的块,这样查找速度很快。

在实际应用中,不是使用单一的分配算法。如glibc,

  • < 64字节, 使用对象池
  • > 512字节,采用最佳适配算法
  • > 64 && < 512字节, 采用最佳折中策略
  • >128k字节,使用mmap