进程的虚拟空间划分
任何编程语言,都会产生两样东西,指令和数据。
.exe程序运行的时候会从磁盘被加载到内存中,但是不能直接加载到物理内存中。Linux会给当前进程分配一块空间,比如x86 32位linux环境下会给进程分配2^32(4G)大小的空间,这个空间被叫做【进程的虚拟地址空间】,进程的虚拟地址空间其实并不存在,从底层来看它不过是内核创建的一系列数据结构而已。
以x86 32位linux为例,讲解进程的虚拟地址空间:这部分空间默认首地址是0x00000000,尾地址是0xFFFFFFFF,以0xCCCCCCCC为界被划分为两块,0x00000000~0xCCCCCCCC是【用户空间】,0xCCCCCCCC~0xFFFFFFFF是【内核空间】。
用户空间被进一步划分,0x00000000~0x08048000是不允许访问的空间。往后就是.【text段】,用于存储程序的机器代码(即已编译的指令),其中还有一部分是,rodata段(readonly data),用于存放只读数据(比如char *p=“hello”,所以不能修改)。
再往下就是.data段(专门存放初始化并且初始化不为0的数据),和.bss段(存放未初始化和初始化为0的数据),.bss段会自动把放入的数据做0初始化。最终生成的可执行文件就已经包含了./data段和./bss段里的数据了。
再往下就是.head段,俗称堆内存。
再往下就是共享库空间,用于加载.dll,.so等文件。
再往下就是stack段,俗称栈空间。栈是从高地址往低地址增长,堆是从低地址往高地址增长。栈空间在运行到相应的指令的时候才会被使用。
最后存放命令行参数和环境变量。
内核空间被划分为ZONE_DMA(16M左右),ZONE_NORMAL(800M左右,存放了各种控制块,比如tcb),ZONE_HIGHMEM(地址映射用)
注意:每一个进程用户空间是私有的,内核空间是共享的!所以进程之间通信需要通过内核空间。
#include<stdio.h>
//可执行文件展开的时候
//这些数据都被直接放入相应的./data或./bss段
int gdata1 = 0; //.data
int gdata2 = 0;//.bss
int gdata3;//.bss
static int gdata4 = 1; //.ata
static int gdata5 = 0;//.bss
static int gdata6;//.bss
int main(){
//编译的时候产生的是指令,
//可执行问价展开的时候放在.text中,
//指令运行的时候才会再栈上开辟空间
int a =12;
int b =0;
int c;
//同.data或.bss
static int c =13;
static int f =0;
static int g;
return 0;
}
函数调用堆栈空间的过程
下面用一段很简单的代码说明栈堆调用过程。
#include <iostream>
using namespace std;
int sum(int a,int b)
{
int temp = 0;
temp = a + b;
return temp;
}
int main()
{
int a = 10;
int b = 20;
int ret = sum(a,b);
cout << ret <<endl;
return 0;
}
我们知道函数运行的时候需要在栈上开辟一块栈桢,.text里的汇编指令运行的时候,会往栈帧里写入相关数据。 比如int a = 10对应的指令运行的时候,会对main函数的栈帧进行压栈,从ebp(栈底)压入了a=10这个数据。
运行上述代码会依次发生以下事情:
1、ebp指针指向main栈帧的栈底,esp指针指向main栈帧的栈顶。esp和ebp指向的是当前调用的栈帧,能表示一块空间。
2、a、b、ret会依次从ebp压入main函数的栈帧中。
3、sum函数的实参会从右往左依次从esp(栈顶压入)。注意,栈帧的大小是会动态变化的,esp会保持位移一直指向栈帧的顶部。
4、把下一指令,也就是sum函数的指令的地址跟从实参地址一起从esp方向压入栈帧中。
5、把main函数调用的地址跟从sum函数调用的地址一起从esp方向压入栈帧中。
6、给sum函数开辟栈帧空间,ebp赋值成esp后,esp指向sum栈帧的顶部。sum函数栈帧可能会被初始化成0xCCCCCCCC。
7、给sum函数的栈帧空间从ebp压入temp。
8、计算a+b的值并赋值给temp空间。
9、回退栈帧,让ebp获取pop出的栈值,所以能重新指向main函数的栈底。