先介绍一下程序的内存布局
现代的应用程序都运行在一个内存空间里,在32位的系统中,内存大小为4GB(2的32次方),整个内存是一个统一的地址空间,用户可以用一个32位的指针访问内存的任意位置。
但其实大多数操作系统会把4GB的内存空间中的一部分分给内核使用,被称为内核空间,应用程序无法直接访问。Windows下会默认把高2GB的空间分配给内核(可配置为1GB),Linux下默认将高地址的1GB空间分配给内核。剩下的空间称为用户地址空间。
用户地址空间中还有一个保留区和动态链接库映射区
栈
栈是一个容器 FIFO 向下增长
栈在程序运行中保存了一个函数调用所需要的维护信息,称为堆栈帧或活动记录,一般包括以下内容:
函数的参数和返回值;
保存的上下文; 包括函数调用前后需要保持不变的寄存器
临时变量。
一个活动的记录一班用ebp和esp两个寄存器维护
压参的时候把所有或者一部分压入栈中,没有压入栈的参数使用某些特定的寄存器传递。
开辟出的栈会被初始化为0XCCCCCCCCh,所以我们在栈上定义一个未初始化的变量调试的时候会看到“烫烫烫...”, 0XCCCC如果被当成文本就是这个字。
int foo() { return 123; }
上面这个函数的返回值会被放入exa寄存器中 调用方可以通过读取exa寄存器来获取返回值
调用惯例
有这样一个函数
int foo(int m,int n) { int a=0,b=0; ... } int main() { foo(3,4); }
函数的调用方和被调用方对于函数如何调用必须要有一个明确的约定,这样的约定成为调用惯例。
一般调用惯例会规定如下几个内容
函数参数的传递顺序和方式:参数的传递有很多种方式,最常见的是通过栈传递,函数的调用方将参数压入栈中,函数自己在把参数从栈中取出来。对于有多个参数的函数,调用惯例要规定参数的压栈顺序是从左还是从右压,有些惯例允许使用寄存器传值,以提高性能。
栈的维护方式:参数压栈后,函数体会被调用,此后 需要把压入栈中的参数全部弹出,以使得栈在函数调用前后保持一致
名字修饰的策略:为了链接的时候对调用管理进行区分,调用管理要对函数本身的名字进行修饰。不同的惯例有不同的修饰策略
在C语言中。存在着多个调用惯例,但默认是cdecl
函数返回值传递
exa是传递返回值的通道,函数将返回值存储在exa中,返回后函数的调用方再读取exa,但exa本身只有4个字节,对于返回5-8个字节的对象把低4个字节存储到exa中,高4个字节存储在edx中,联合返回。
大于8个字节的对象怎么存储呢?
typedef struct big_thing { char buf[128]; }bigthing; big_thing return_test() { big_thing b; b.buf[0]=0; return b; } int main() { big_thing n=return_test(); }
用伪码表示如下:
void return _test(void *tmp) { big_thing b; b.buf[0]=0; memcpy(temp,&b,sizeof(big_thing)); exa=temp; } int main() { big_thing temp; big_thing n; return_test(&temp); memcpy(&n,exa,sizeof(big_thing)); }
可看出如果返回值类型的尺寸太大,C语言字函数返回时会使用一个临时的栈上内存区域作为中转,结果返回值对象会被拷贝两次,及从被调用函数的栈上的变量b拷贝到temp,再从temp到n
——————————————————摘自程序员的自我修养