程序与内存的关系,不可分割,内存是承载程序运行的介质,也是程序进行各种运算和表达的场所,了解内存的状况,对于理解程序有着非常重要的作用
下图为一个linux下一个进程里典型的内存布局。
一个程序的内存空间,和其系统的位数有关,一般32为系统,其内存大小为4GB,(2的32次方)。但是对于4GB的内存空间,windows系统会默认将高地址的2GB空间分配给内核,linux默认将高地址1GB空间分配给内核。剩下的空间为用户空间。如上图,是32为Linux系统x86体系下的虚拟地址空间。
- 保留区
- 代码段和只读数据段
- 数据段
- 堆区
- 动态链接库区
- 栈区
- 命令行参数
- 环境变量
以上为一个进程的虚拟地址空间的内容。
- 保留区
保留区,在大多数的操作系统里,极小的地址是不允许访问的,这也是c语言中将无效指针赋值为0的一个原因,上图中128M的内存空间不可访问,访问会发生段错误。 - 代码段和只读数据段
代码段和只读数据段都是:可读不可写可执行,即内容不容更改。其起始地址为0x08040800。代码段存放的为指令,只读数据段通常存储的是常量字符串。 - 数据段
数据段分为.data和.bss段。数据段可读可写不可执行。.data段中存储的数据类型为初始化过的全局变量和静态变量,(且初始化值不为0)。.bss段中存储的数据类型为未初始化的全局变量和静态变量(或初始化后值为0)。 - 堆区
堆区的数据是通过malloc函数进行分配,通过free进行释放。它的数据增长方式是从低地址向高地址增长,这与栈完全相反。 - 动态链接库区
这个区域用于映射装在的动态链接库,在linux下,如果可执行文件依赖其他共享库,那么系统会从0x40000000开始的地址分配相应的空间,并将共享库载入到该空间。 - 栈区
栈是程序中最为重要的概念之一,没有栈就没有函数,就没有局部变量。另栈具有先进后出的特性,可以将栈想象成一个水杯,先进去的要最后才能出来。栈是一个动态的内存区域,随着函数的消亡而变大或变小,压栈使栈增大,出栈使栈减小。
并且在经典操作系统中,栈总是向下增长。在i386下,栈顶由esp的寄存器表示,栈底由ebp的寄存器表示。这两个寄存器的位置划定了当前函数的活动范围。压栈使栈顶的地址减小,出栈使栈顶地址增大。 - 命令行参数
- 环境变量
命令行参数和环境变量是存储在栈的后边,其存储地址高于栈的起始地址。
对一个小程序做一个简单的入栈出栈介绍
int add(int a,int b)
{
int temp;
temp=a+b;
return temp;
}
void main()
{
int a=10;
int b=20;
add(a,b);
}
如图所示:首先确定ebp和esp,共占有12个字节,故开辟出三个单位。
在调用add函数时,做了下面这几件事情。
这里总结调用函数时所做的事:
1.将所有或一部分参数压入栈中,如果有其他参数没有入栈,那么使用某些特定的寄存器传递。注意,不论函数的参数是否有默认参,都是需要将参数入栈操作的。还有一点就是进行参数传递时,如果参数为立即数,那么将该立即数直接入栈,省去了从内存中调用数值的步骤,比参数为变量高效一些。
2.把当前指令的下一条指令的地址压入栈中。在图中,就是将call指令的下一条指令压入栈中。
3.跳转到函数体执行。
其中2、3步由call一起执行。此时ebp为图中0x100的位置,esp在ebp的基础上增加了add函数所占内存空间的大小。
另外,如果需要寄存器变量,则在图中所示第五步之后,将寄存器变量入栈。
进入到add函数之后,做了一下几件事情:
这里,完成add函数体内的内容(即三句程序)。
注意:在函数体内部返回已知值时,当这个值所占空间小于4字节,则用eax寄存器返回,如果在4-8字节之间,则用eax 和 edx 寄存器一起返回,大于8字节,则系统产生一个临时量将其返回。
关于这个临时量的问题,暂且先放在这里,以后再来解释一下。
接着,做下面几个事:
这样就回到了main函数中,然后接着下面的指令继续进行。要知道main函数也是一个函数。
同时,将程序解剖到这一步,我也生成了一些疑问,比如:返回到main函数后,如果没有变量接着返回值,会进行怎样的编译,有变量接着返回值又是怎样的编译,希望可以在后续的工作,再认真看一下。