程序员的自我修养-第十章
内存
程序的内存布局
栈与调用惯例
什么是栈
栈会保存一个函数调用所需要的维护信息(又名堆栈帧或活动记录),它包含:
- 函数的返回地址和参数
- 临时变量
- 保存的上下文(应该是自己的上下文,而不是调用自己的函数的上下文吧)
活动记录由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