内存布局
linux下一个进程的典型内存分布:
kernel space
____________________ 0xc0000000
stack
(向下拓展)
____________________
unused
____________________
dynamic libraries
____________________ 0x40000000
unused
____________________
heap
(向上拓展)
____________________
read/write sections
(.data, .bss)
____________________
readonly sections
(.init, .rodata, .text)
____________________ 0x0804800
reserved
____________________ 0
函数与栈
栈保存一个函数调用所需要的维护信息,这常常被称为堆栈帧 或 活动记录.
保存如下内容:
1. 函数的放回地址和参数
2. 临时变量: 包括函数的非静态局部变量 以及 比一起自动生成的其他临时变量.
3. 保存的上下文: 包括在函数调用前后需要保持不变的寄存器.
一个常见的活动记录如下:
参数
_____________
返回地址
_____________
old ebp <- ebp
_____________
保存的寄存器
_____________
局部变量
_____________
其他数据
_____________ <- esp
i386函数体的"标准"开头是这样的:
1. push ebp: 把ebp压入栈中
2. mov ebp, esp: ebp = esp (这时ebp指向栈顶, 而此时栈顶就是old ebp)
3. [可选]sub esp, XXX: 在栈上分配XXX字节的临时空间
4. [可选]push XXX: 如有必要,保存名为XXX的几个寄存器
那么"标准"结尾是这样的:
1. [可选]pop XXX: 如有必要, 恢复寄存器
2. mov esp, ebp: 恢复esp同时回收局部变量空间
3. pop: 从栈中恢复保存的ebp的值.
4. ret: 从栈中取得返回地址, 并跳转到该位置
反汇编一个函数:
int foo()
{
return 123;
}
如下:
004113A3 sub esp, 0C0h step two, 在栈上开辟一块空间
004113A9 push ebx step three,保存ebx,esi,edi寄存器
004113AA push esi
004113AB push edi
004113AC lea edi, [ebp-0C0h] step four,加入调试信息
004113B2 mov ecx, 30h
004113B7 mov eax, 0CCCCCCCh
004113BC rep stos dword ptr es[di]
004113BE mov eax, 7Bh step five,返回123(在这里返回值是通过eax寄存器传递的)
004113C3 pop edi step six,从栈上恢复寄存器
004113C4 pop esi
004113C5 pop ebx
004113C6 mov esp, ebp step seven,恢复进入函数前的esp,ebp
004113C8 pop ebp
004113C9 ret step eight,使用ret指令返回
补充:
函数的调用:
call 0x0EA23h
将ECS或EIP压栈;
转移到0x0EA23h。
ret函数的返回:
ret
弹栈得到EIP或ECS;
返回EIP所指的指令继续执行。
函数返回值传递方式
big_thing return_rest()
{
big_thing b;
b.buf[0] = 0;
return b;
}
int main()
{
big_thing n = return_rest();
return 0;
}
用伪代码表示如下:
int main()
{
big_thing temp;
big_thing n;
return_test(&temp);
memcpy(&n, eax, sizeof(big_thing));
}
步骤如下:
1. 首先main函数在栈上额外开辟了一片空间, 并将这块空间的一部分作为传递返回值的临时对象,temp.
2. 将temp对象的地址作为隐藏参数传递给return_test函数
3. return_test函数将数据拷贝给temp对象, 并将temp对象的地址用eax传出.
4. return_test返回之后, main函数将eax指向的temp对象的内容拷贝给n.
当c++对象返回时,
struct cpp_obj
{
cpp_obj()
{
cout << "ctor/n";
}
cpp_obj(const cpp_obj& c)
{
cout << "copy ctor/n";
}
cpp_obj& operator=(const cpp_obj& rhs)
{
cout << "operator=/n";
return *this;
}
~cpp_obj()
{
cout << "dtor/n";
}
};
cpp_obj return_test()
{
cpp_obj b;
cout << "before return/n";
return b;
}
int main()
{
cpp_obj n;
n = return_test();
return 0;
}
结果:
struct cpp_obj
{
cpp_obj()
{
cout << "ctor/n";
}
cpp_obj(const cpp_obj& c)
{
cout << "copy ctor/n";
}
cpp_obj& operator=(const cpp_obj& rhs)
{
cout << "operator=/n";
return *this;
}
~cpp_obj()
{
cout << "dtor/n";
}
};
cpp_obj return_test()
{
cout << "before return/n";
return cpp_obj();
}
int main()
{
cpp_obj n;
n = return_test();
return 0;
}
当将函数改成这样时, C++进行了返回值优化, 直接将对象构造在传出时使用的临时对象上, 结果如下:
堆
在windows中,利用VirtualAlloc取得的虚拟空间,类似于向操作系统"批发"空间.
而用HeapAlloc或者是malloc分配的空间实际上是堆空间的"零售".
这是因为如果内存管理由操作系统内核来进行,系统调用时的性能开销较大.而如果由程序自己管理时,有更高的效率.
堆分配算法有:
1. 空闲链表
2. 头/主体/空闲 位图
3. 对象池
在实际系统中, 是利用多种方法共同分配堆空间的.