本文最后更新于:2020年6月29日 晚上
内存是承载程序的介质,是程序进行运算和表达的场所。
未有特殊说明,则默认在32bit操作系统中。
1. 程序的内存布局
操作系统会将内存空间中的一部分分给内核使用,应用程序无法访问这段内存,这段内存被称为内核空间。Windows默认情况将高地址的2GB空间分配给内核,Linux默认情况将高地址的1GB空间分配给内核。
剩下的内存空间称为用户空间,用户空间中有许多默认区域。
栈:栈用于维护函数调用的上下文。栈通常在用户空间的最高地址处分配,一般大小位数兆字节
堆:堆用来容纳应用程序动态分配的内存区域。堆通常在栈的下方(低地址方向)。堆一般比栈大可以有几十至数百兆字节的容量
可执行文件映像:存储着可执行文件在内存里的映像。
保留区:保留区不是一个单一的内存区域,而是对内存中受到保护而禁止访问的内存区域的总称
栈向低地址增长,堆向高地址增长。当栈或堆现有的大小不够用时,它们将按照增长方向扩大,直到预留的的空间被用完为止
2. 栈(stack)
将数据压入栈中(入栈,push),将已经压入栈中的数据弹出(出栈,pop),即先入栈的数据后出栈(First In Last Out,FIFO)
压栈操作使栈增大,弹出操作使栈减小
栈总是向低地址增长的,栈顶由esp寄存器进行定位,栈底由ebp寄存器进行定位。压栈操作使栈顶地址减小,弹出操作使栈顶地址增大
栈保存了一个函数调用所需要的维护信息,其被称为栈帧(Stack Frame)或活动记录(Activate Record),栈帧包含如下内容:
函数的返回地址和参数
临时变量:包括函数的非静态局部变量、编译器自动生成的其他临时变量
保存的上下文: 包括在函数调用前后需要保持不变的寄存器
一个函数的活动范围由ebp和esp寄存器划定范围。esp寄存器始终指向栈的顶部即当前函数的活动记录的顶部,ebp寄存器指向函数活动记录的底部,ebp寄存器也被称为帧指针(Frame Pointer)
i386下的函数调用步骤如下
把所有或一部分参数压入栈中,如果有其他参数没有入栈,那么使用某些特定的寄存器传递
把当前指令的下一条指令的地址压入栈中
跳转到函数体执行
第二、三步由指令call一起执行
i386下的函数体“标准”开头如下
push ebp(保存本栈帧的ebp)
mov ebp, esp(将ebp移动到栈顶)
sub esp, XXX(开辟新的栈帧)
push XXX(保存寄存器)
i386下的函数体“标准”结束如下
pop XXX(恢复寄存器)
mov esp, ebp(恢复成调用者所在栈帧的栈顶)
pop ebp(恢复成调用者所在栈帧的栈基址)
ret(从栈顶取得下一条指令的地址,并跳转)
示例如下
测试代码
main函数反汇编
foo函数反汇编
foo函数return之前时寄存器
foo函数return之前时内存
foo函数反汇编代码解析
某些场合,编译器生成函数的进入和退出指令序列不按照标准的方式进行,比如C函数满足:
函数被声明为static(不可在此编译单元之外访问)
函数在本编译单元仅被直接调用,没有显示或隐式的取地址(即没有任何函数指针指向过这个函数)
编译器确信满足这两条的函数不会在其他编译单元内被调用,因此可以修改指令,达到优化目的
函数调用惯例(Calling Convention)
函数参数的传递顺序和方式:规定函数调用方将参数压入栈的顺序(从左到右、从右至左);规定函数参数的传递方式(通过栈传递,函数调用方将参数压入栈,自己在从栈中将参数取出、使用寄存器传递,提高性能)
栈的维护:函数体执行完后,之前压入栈中的参数需要弹出,可以由函数调用方完成,也可以由函数体本身完成
名字修饰(Name-mangling)策略:链接时区分调用惯例,不同的调用惯例有不同的名字修饰策略
C语言默认调用惯例是cdecl,任何一个没有显式指定调用惯例的函数都默认是cdecl,比如:
int _cdecl foo(int a, int b, int c)
函数返回值传递
- 当返回值小于等于4字节时,函数将返回值存储在eax,调用者读取eax
- 当返回值大于4字节,小于等于8字节时,函数使用eax和edx联合返回的方式。eax存储低4字节,edx高4字节
- 当返回值大于8字节时,函数会使用一个临时的栈上内存空间(临时对象)作为中转,返回值对象会被拷贝两次
- 当返回值大于8字节时,函数返回值传递示例如下:
- 函数返回值传递测试代码
- main函数返回值反汇代码
- return_test函数反汇编代码
- main函数中n的地址
- 临时对象给main函数中的n赋值
- 函数返回值传递测试代码
- 首先调用者在栈上将一部分空间作为传递返回值的临时对象
- 将临时对象的地址作为隐藏参数传递给函数
- 函数将数据拷贝给临时对象,并将临时对象的地址用eax传出
- 函数返回后,调用者将eax指向临时对象的内容拷贝给局部变量
- 返回值传递流程
- 需要注意的是返回对象的拷贝情况完全不具备可移植性,不同的编译器产生的结果可能不同。函数传递大尺寸的返回值所使用的方法不是可移植的,不同编译器、不同平台、不同调用惯例、不同编译参数可能采用不同的实现方法
- 在C++中要使用返回值优化技术(Return Value Optimization, RVO),直接将对象构造在临时对象上,减少一次从函数内局部变量对临时对象的拷贝构造步骤
cpp_obj return_test() { return cpp_obj(); }
* 码字好累。。。→_→ *
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!