10.1 程序的内存布局
- 一个32bit 指针的寻址空间是 4GB, 其中一部分空间会被分配给内核空间, 剩余部分给用户
- 可以很容易看到栈是向低地址增长, 堆是向高地址增长
10.2 栈和调用惯例
10.2.1 什么是栈
- 经典的 i386 系统中, 栈总是向下增长的, 栈顶通过 esp 寄存器定位, 压栈操作使得栈顶的地址减少, 弹出操作使得栈顶地址增大
- 栈通常用来维护 一个函数调用所需要的信息, 称为 堆栈帧 或者 活动记录
- 函数返回地址 和 参数
- 临时变量
- 保存的上下文
- 在 i386 中一般通过 ebp 和 esp 寄存器划定函数范围, esp 指向栈的顶部, ebp 指向函数活动记录的一个固定位(被称为 帧指针)
- 栈通常用来维护 一个函数调用所需要的信息, 称为 堆栈帧 或者 活动记录
- 一个普通函数的反汇编
int foo(){
return 123;
}
- 这里的第四步, 实际上是加入调试信息, 编译器一般将栈空间数据初始化成 0xCC, 对应的汉子编码就是 烫,如果编译器使用 0xCD 作为未初始化标识, 相应的汉字编码就是 屯
- 另外, 反汇编代码中常常可以看到 nop , 这个是纯粹作为占位符来使用的, 可以很容易的被其他函数进行替换, ie, hook 技术
10.2.2 调用惯例
C 语言默认的调用惯例是使用 cdecl 方式调用
对于下面函数
int _cdecl foo(int n, int m)
,
他对应的调用栈:
还有其他形式的函数调用惯例
当然 C++ 也有自己的一种特殊的调用惯例, thiscall, 专门用于类成员函数的调用
- 对于MSVC, this 指针存放在 ecx 寄存器中, 其他从右向左压栈
- 对于gcc, thiscall 和 cdecl 完全一致, 只是将this 看做是函数的第一个参数
10.2.3 函数返回值传递
- 当返回值小于4byte 时候, 通过 eax 返回
- 当返回值 5~8 byte 时候, 通过eax 和 edx 联合返回
- 而对于 大于 8 byte时候, 编译器采用了一个trick, 传入一个隐含的临时空间的地址, 先将返回值得数据拷贝到临时空间中去, 然后再从临时空间拷贝到调用方的地址空间中
10.3 堆与内存管理
10.3.1 什么是堆
- 系统中管理堆空间分配的往往是程序的运行库
10.3.2 linux 进程堆管理
- linux 中采用两种方式:
- brk 系统调用
- 实际上就是设置进程数据段的结束位置, ie, 可以扩大或者缩小数据段
- mmap 调用
- 类似 windows 中的VirtualAlloc, 他用来向操作系统申请一段虚拟地址空间, 当不把地址空间映射到某个文件的时候, 我们又称这个空间为匿名, 可以用来作为堆空间
- brk 系统调用
- glibc 的malloc 对于 128 kb 的请求而言, 会在现有的堆空间里面, 按照堆空间分配算法为他分配一块空间并返回, 而对于128kb 的请求而言, 他会使用mmap 函数为他分配一段匿名空间, 然后再这段匿名空间中为用户分配空间。
- mmap 申请空间的大小不能超出空闲内存+空闲交换空间的总和
10.3.3 windows 进程堆管理
- windows 的进程空间比较凌乱。。。
- linux 中堆是向上增长的, 但是HeapCreate 系列函数并不完全按照这个规律增长
10.3.4 堆分配算法
- 空闲链表
- 实际上就是将堆中的各个空闲块, 按照链表的方式连接起来
- 位图
- 优点:
5. 速度快
6. 稳定性好 - 缺点:
- 分配内存容易产生碎片
- 如果堆很大, 而设定的块很小, 会导致位图非常大, 失去cache 命中率高的优势
- 优点:
- 对象池
- 对于某些情况下, 被分配对象是几个比较固定的值得时候, 可以设计一个更为高效的堆算法, ie, 对象池
- 思路:
- 按照每个申请的大小将空间分成大量的小块, 每次请求的时候只要找到其中的一个小块就可以了