参考及转载自:
http://www.cnblogs.com/gtarcoder/p/6006023.html
http://www.cnblogs.com/smile267/archive/2012/10/21/2732099.html
进程地址空间
操作系统在管理内存时,每个进程都有一个独立的进程地址空间,这个进程地址空间的地址为虚拟地址,对于32位操作系统,该虚拟地址空间为2^32=4GB。进程在执行的时候,看到和使用的内存地址都是虚拟地址,而操作系统通过MMU部件将进程使用的虚拟地址转换为物理地址。
32位系统:数据线条数——一次能处理的数据量位数。
地址线条数——访问的地址空间大小。
虚拟内存地址
每个进程都先天设定了0-4G的虚拟内存地址(不是内存的真实地址,只是一个编号),虚拟内存地址开始时不对应任何的内存,直接使用会引发段错误。程序员所接触的都是虚拟内存地址。虚拟内存地址必须映射物理内存(或者硬盘上的文件)以后才能 存储数据。
虚拟内存地址中,0-3G 是用户空间,是用户层使用,3G-4G 是内核空间,是内核层使用。用户层不能直接访问内核层,可以通过 Unix的系统函数访问内核层。UC课程主要研究 Unix系统函数。
内存管理的最小单位是一个内存页,每页大小是4096字节(4K),虚拟内存地址连续时,物理内存地址可以不连续。两个内存页的物理地址可能不挨着。即使只申请一个字节的内存,也会映射至少一个内存页。
多次申请内存时,第一次映射物理内存,后面不会再映射物理内存,除非内存使用完毕。
ELF文件(可执行文件)
在代码在经过预处理、编译、汇编与链接之后会生成一个可执行文件,用术语来说就是ELF格式文件(Executable Linkable Format)。
ELF文件由ELF文件头与许多段(section)组成,ELF头中存放各个段的起始地址和长度以及其他的信息,各个段中存放不同属性内容。段中我们比较熟悉的有数据段、代码段等。
一般来说,C语言编译之后的可执行语句变成了可执行机器代码,保存在.text段;已经初始化的全局变量和局部静态变量都保存在.data段;未初始化的全局变量和局部静态变量都保存在.bss段。其中未初始化的全局变量和局部静态变量默认值都是0,也就是说.bss段中的值都是0。——从0x08048000(32位)开始
ELF(Executable Linkable Format)文件是linux下的可执行文件。ELF文件的结构如下所示:
其中C程序中的变量在ELF文件中的存储区域如下图所示:
//main.cpp
int a = 0; 全局初始化区
char *p1; 全局未初始化区
main()
{
int b; 栈
char s[] = "abc"; 栈
char *p2; 栈
char *p3 = "123456"; 123456/0在常量区,p3在栈上。
static int c =0; 全局(静态)初始化区
p1 = (char *)malloc(10); // 注意:p1、p2本身在栈区,申请的空间在堆区!!
p2 = (char *)malloc(20);
分配得来得10和20字节的区域就在堆区。
strcpy(p1, "123456"); 123456/0放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。
}
.bss段的大小可以通过ELF头中的信息得到,但是这只是一个“大小”数据,告诉程序中的非初始化的全局和静态变量会共占用多少内存,在文件中并不会有它的空间,只有当可执行文件装载运行时,才会被分配内存(并且位于data段内存块之后),并且初始化为0.
当可执行文件被装载入内存之后,各个段装载进的虚拟存储区域情况如下图所示(即进程虚拟空间图):
进程虚拟空间图
linux系统中进程的地址空间分布如下图所示,其中在32位系统中0-3GB为用户空间,3-4GB为内核空间:
- 公共的内核代码和数据:由于系统内核中有些代码、数据是所有进程所公用的,所以所有进程的进程地址空间中有一个专门的区域存放公共的内核代码和数据,该区域内的内容相同,且该虚拟内存映射到同一个物理内存区域。
- 进程相关的数据结构:进程在执行的时候,需要维护进程相关的数据结构,比如页表、task和mm结构、内核栈等,这些数据结构是进程独立的,各个进程之间可能不同。这些数据结构在进程虚拟地址空间中一个专门的区域中。
- 栈段(stack)——栈上内存分配:进程在进行函数调用的时候,需要使用栈,于是进程地址空间中存在一个专门的虚拟内存区域维护用户栈。 存储局部、临时变量,在程序块开始时自动分配内存,结束时自动释放内存,存储函数的返回指针。同时栈时一个严格遵循FILO的数据结构,因此很容易管理,只需要栈顶指针ESP(指向栈顶,向低地址(下)增长)即可。 - 由编译器自动分配、自动释放
- 堆段(heap)——动态内存分配:进程在进行动态内存分配的时候,需要使用堆,于是进程地址空间中存在一个专门的虚拟内存区域维护堆。 存储动态内存分配,需要程序员手工分配,手工释放。
- 数据段:全局区(静态区/static)——静态存储区域分配:全局变量和静态变量的存储是放在一块的:进程中初始化的全局变量和局部静态变量在 .data 段 ;进程中未初始化的全局变量和局部静态变量在 .bss 段 ,全部被初始化为0 - 程序结束后由系统释放。
- 文字常量段:常量字符串就是放在这里的。 - 程序结束后由系统释放
- 正文端(text、代码段):进程代码在 .text 段,存放函数体的二进制代码
- 共享库:进程执行的时候可能会调用共享库,在进程地址空间中有一个共享库的存储器映射区域,这个是进程独立的,因为每个进程可能调用不同的共享库。
#include<stdio.h>
#include<malloc.h>
int global_init_var = 123;
int global_uninit_var;
void func(int func_para)
{
static int static_init_var = 789;
static int static_uninit_var;
int local_uninit_func_var;
int local_init_func_var = 8;
//int local_uninit_func_var;
int* ptr_func=(int*)malloc(5*sizeof(int));
if(ptr_func==NULL)
printf("malloc error\n");
else{
for(int i=0;i!=5;i++)
{
*(ptr_func+i)=i*i;
printf("stack_func address:%p ",&(*(ptr_func+i)));
printf("stack_func[%d] value:%d\n",i,*(ptr_func+i));
}
}
printf("ptr_func address:%p\n",&ptr_func);
//printf("stack address:%p\n",&(*ptr_func));
printf("local_init_func_var address:%p local_init_func_var value:%d\n",&local_init_func_var, local_init_func_var);
printf("local_uninit_func_var address:%p local_uninit_func_var value:%d\n",&local_uninit_func_var, local_uninit_func_var);
printf("func_para address:%p func_para value:%d\n",&func_para, func_para);
printf("global_init_var address:%p global_init_var value:%d\n", &global_init_var, global_init_var);
printf("static_init_var address:%p static_init_var value:%d\n",&static_init_var, static_init_var);
printf("global_uninit_var address:%p global_uninit_var value:%d\n", &global_uninit_var , global_uninit_var);
printf("static_uninit_var address:%p static_uninit_var value:%d\n",&static_uninit_var, static_uninit_var);
free(ptr_func);
ptr_func=NULL;
}
int main(void)
{
int local_uninit_main_var;
int local_init_main_var = 4;
//int local_uninit_main_var;
int* ptr_main=(int*)malloc(5*sizeof(int));
if(ptr_main==NULL)
printf("malloc error\n");
else{
for(int i=0;i!=5;i++)
{
*(ptr_main+i)=i*i;
printf("stack address:%p ",&(*(ptr_main+i)));
printf("stack[%d] value:%d\n",i,*(ptr_main+i));
}
}
printf("ptr_main address:%p\n",&ptr_main);
//printf("stack address:%p\n",&(*ptr_main));
printf("local_init_main_var address:%p local_init_main_var value:%d\n",&local_init_main_var, local_init_main_var);
printf("local_uninit_main_var address:%p local_uninit_main_var value:%d\n",&local_uninit_main_var, local_uninit_main_var);
func(local_init_main_var);
printf("func() address:%p\n", func);//func函数名直接作为地址变量
printf("main() address:%p\n", main);
free(ptr_main);
ptr_main=NULL;
return 0;
}
- 无论是main函数还是别的函数,其局部变量都是在栈区内,具体分布估计和内核相关;
- 无论是main函数还是别的函数内分配的内存,也一样在堆区内;
- printf("func() address:%p\n", func); printf("main() address:%p\n", main);——可以看出函数都在在代码段;
程序启动
有了以上的进程地址空间分布和可执行文件的分布信息,就可以描述进程启动过程了。
程序启动时,操作系统会新建一个进程来执行该程序,主要分为三个步骤:
(1)操作系统分配一个独立的进程地址空间。
主要是在内存的内核区域中新建一个描述进程的结构体(linux中为task_struct),结构体中包含了进程的相关信息,比如进程运行状态,进程的寄存器,进程打开的资源,以及进程的内存管理结构(在linux中为mm_struct,进程的内存管理结构就描述了进程的虚拟地址空间的布局). 同时,为该进程创建一个页目录表。
(2)读取可执行文件头,建立可执行文件中各个段和进程虚拟地址空间中各个段之间的映射关系。
当程序运行时需要将可执行文件中的内容载入内存来执行,比如在进程访问某全局变量时,该全局变量还没有被载入内存,此时需要知道该全局变量对应在可执行文件的什么位置。于是我们就需要知道进程中虚拟地址到可执行文件中位置的对应。
(3)将PC指针指向进程的代码入口处,开始执行。
执行的时候会不断的发生缺页中断,发生缺页中断时(only this way)会将实际的可执行文件中的内容载入到物理内存中,然后建立虚拟内存页和物理内存页的映射关系。
系统对进程的管理
操作系统内核区域中存储了各个进程的结构体信息,linux中为task_struct,task_struct中包含了进程的相关信息,比如进程状态,寄存器,内核栈,状态字,内存分配mm_struct。针对单独一个进程,它在运行的时候使用并更新task_struct中的信息,比如使用mm_struct用于访问内存...
在进程切换的时候,系统将原进程的相关信息保存到它对应的task_struct中;然后选择另一个进程,将task_struct中的信息装载到机器的寄存器中去,然后新的进程就按照它的task_struct来指导运行....
函数调用
函数调用时入栈:第一个进栈的是主函数中后的下一条指令的地址(函数调用语句的下一条可执行语句),然后是函数的各个参数(在大多数的C编译器中,参数是由右往左入栈的 ),然后是函数中的局部变量。注意静态变量是不入栈的。
本次函数调用结束后出栈:局部变量先出栈,然后是参数,最后栈顶指针指向的返回地址,程序由该点继续运行。