Linux进程地址空间 && 进程内存布局[转]

一 进程空间分布概述linux

 
    对于一个进程,其空间分布以下图所示:

                                     

 

 

 

程序段(Text):程序代码在内存中的映射,存放函数体的二进制代码。程序员

初始化过的数据(Data):在程序运行初已经对变量进行初始化的数据。算法

未初始化过的数据(BSS):在程序运行初未对变量进行初始化的数据。编程

栈 (Stack):存储局部、临时变量,函数调用时,存储函数的返回指针,用于控制函数的调用和返回。在程序块开始时自动分配内存,结束时自动释放内存,其操做方式相似于数据结构中的栈。安全

堆 (Heap):存储动态内存分配,须要程序员手工分配,手工释放.注意它与数据结构中的堆是两回事,分配方式相似于链表。数据结构

 

 

 
注:1.Text, BSS, Data段在编译时已经决定了进程将占用多少VM
        能够经过size,知道这些信息:
 
          2. 正常状况下,Linux进程不能对用来存放程序代码的内存区域执行写操做,即程序代码是以只读的方式加载到内存中,但它能够被多个进程安全的共享。
 
二  内核空间和用户空间
 
       Linux的虚拟地址空间范围为0~4G,Linux内核将这4G字节的空间分为两部分,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF)供内核使用,称为“内核空间”。而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF)供各个进程使用,称为“用户空间。由于每一个进程能够经过系统调用进入内核,所以,Linux内核由系统内的全部进程共享。因而,从具体进程的角度来看,每一个进程能够拥有4G字节的虚拟空间。

    Linux使用两级保护机制:0级供内核使用,3级供用户程序使用,每一个进程有各自的私有用户空间(0~3G),这个空间对系统中的其余进程是不可见的,最高的1GB字节虚拟内核空间则为全部进程以及内核所共享。
    内核空间中存放的是内核代码和数据,而进程的用户空间中存放的是用户程序的代码和数据。不论是内核空间仍是用户空间,它们都处于虚拟空间中。 虽然内核空间占据了每一个虚拟空间中的最高1GB字节,但映射到物理内存却老是从最低地址(0x00000000),另外,使用虚拟地址能够很好的保护内核空间被用户空间破坏,虚拟地址到物理地址转换过程有操做系统和CPU共同完成(操做系统为CPU设置好页表,CPU经过MMU单元进行地址转换)。多线程

      
        注:多任务操做系统中的每个进程都运行在一个属于它本身的内存沙盒中,这个沙盒就是虚拟地址空间(virtual address space),在32位模式下,它老是一个4GB的内存地址块。这些虚拟地址经过页表(page table)映射到物理内存,页表由操做系统维护并被处理器引用。每一个进程都拥有一套属于它本身的页表。
 
  进程内存空间分布以下图所示:
 
                           

 

     一般32位Linux内核地址空间划分0~3G为用户空间,3~4G为内核空间
 
     注: 1.这里是32位内核地址空间划分,64位内核地址空间划分是不一样的
          2.现代的操做系统都处于32位保护模式下。每一个进程通常都能寻址4G的物理空间。可是咱们的物理内存通常都是几百M,进程怎么能得到4G 的物理空间呢?这就是使用了虚拟地址的好处,一般咱们使用一种叫作虚拟内存的技术来实现,由于可使用硬盘中的一部分来看成内存使用 。
                                                
 
   
        Linux系统对自身进行了划分,一部分核心软件独立于普通应用程序,运行在较高的特权级别上,它们驻留在被保护的内存空间上,拥有访问硬件设备的全部权限,Linux将此称为内核空间。
        相对地,应用程序则是在“用户空间”中运行。运行在用户空间的应用程序只能看到容许它们使用的部分系统资源,而且不能使用某些特定的系统功能,也不能直接访问内核空间和硬件设备,以及其余一些具体的使用限制。
        将用户空间和内核空间置于这种非对称访问机制下有很好的安全性,能有效抵御恶意用户的窥探,也能防止质量低劣的用户程序的侵害,从而使系统运行得更稳定可靠。
            内核空间在页表中拥有较高的特权级(ring2或如下),所以只要用户态的程序试图访问这些页,就会致使一个页错误(page fault)。在Linux中,内核空间是持续存在的,而且在全部进程中都映射到一样的物理内存,内核代码和数据老是可寻址的,随时准备处理中断和系统调用。与之相反,用户模式地址空间的映射随着进程切换的发生而不断的变化,以下图所示:

                                           

      上图中蓝色区域表示映射到物理内存的虚拟地址,而白色区域表示未映射的部分。能够看出,Firefox使用了至关多的虚拟地址空间,由于它占用内存较多。app

 
 三  进程内存布局
 
       Linux进程标准的内存段布局,以下图所示,地址空间中的各个条带对应于不一样的内存段(memory segment),如:堆、栈之类的。
 
                                
 
                                                  q
 
      
          注:这些段只是简单的虚拟内存地址空间范围,与Intel处理器的段没有任何关系。
 
      几乎每一个进程的虚拟地址空间中各段的分布都与上图彻底一致,这就给远程发掘程序漏洞的人打开了方便之门。一个发掘过程每每须要引用绝对内存地址:栈地址,库函数地址等。远程攻击者必须依赖地址空间分布的一致性,来探索出这些地址。若是让他们猜个正着,那么有人就会被整了。所以,地址空间的随机排布方式便逐渐流行起来,Linux经过对栈、内存映射段、堆的起始地址加上随机的偏移量来打乱布局。但不幸的是,32位地址空间至关紧凑,这给随机化所留下的空间不大,削弱了这种技巧的效果。
 

     进程地址空间中最顶部的段是栈,大多数编程语言将之用于存储函数参数和局部变量。调用一个方法或函数会将一个新的栈帧(stack frame)压入到栈中,这个栈帧会在函数返回时被清理掉。因为栈中数据严格的遵照FIFO的顺序,这个简单的设计意味着没必要使用复杂的数据结构来追踪栈中的内容,只须要一个简单的指针指向栈的顶端便可,所以压栈(pushing)和退栈(popping)过程很是迅速、准确。进程中的每个线程都有属于本身的栈。编程语言

      经过不断向栈中压入数据,超出其容量就会耗尽栈所对应的内存区域,这将触发一个页故障(page fault),而被Linux的expand_stack()处理,它会调用acct_stack_growth()来检查是否还有合适的地方用于栈的增加。若是栈的大小低于RLIMIT_STACK(一般为8MB),那么通常状况下栈会被加长,程序继续执行,感受不到发生了什么事情。这是一种将栈扩展到所需大小的常规机制。然而,若是达到了最大栈空间的大小,就会栈溢出(stack overflow),程序收到一个段错误(segmentation fault)。函数

 
     注:动态栈增加是惟一一种访问未映射内存区域而被容许的情形,其余任何对未映射内存区域的访问都会触发页错误,从而致使段错误。一些被映射的区域是只读的,所以企图写这些区域也会致使段错误。
 
内存映射段     
     在栈的下方是内存映射段,内核将文件的内容直接映射到内存。任何应用程序均可以经过Linux的mmap()系统调用或者Windows的CreateFileMapping()/MapViewOfFile()请求这种映射。内存映射是一种方便高效的文件I/O方式,因此它被用来加载动态库。建立一个不对应于任何文件的匿名内存映射也是可能的,此方法用于存放程序的数据。在Linux中,若是你经过malloc()请求一大块内存,C运行库将会建立这样一个匿名映射而不是使用堆内存。“大块”意味着比MMAP_THRESHOLD还大,缺省128KB,能够经过mallocp()调整。
 
      与栈同样,堆用于运行时内存分配;但不一样的是,堆用于存储那些生存期与函数调用无关的数据。大部分语言都提供了堆管理功能。在C语言中,堆分配的接口是malloc()函数。若是堆中有足够的空间来知足内存请求,它就能够被语言运行时库处理而不须要内核参与,不然,堆会被扩大,经过brk()系统调用来分配请求所需的内存块。堆管理是很复杂的,须要精细的算法来应付咱们程序中杂乱的分配模式,优化速度和内存使用效率。处理一个堆请求所需的时间会大幅度的变更。实时系统经过特殊目的分配器来解决这个问题。堆在分配过程当中可能会变得零零碎碎,以下图所示:
                          

 

       通常由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式相似于链表。
         
 
BBS和数据段
      在C语言中,BSS和数据段保存的都是静态(全局)变量的内容。区别在于BSS保存的是未被初始化的静态变量内容,他们的值不是直接在程序的源码中设定的。BSS内存区域是匿名的,它不映射到任何文件。若是你写static intcntActiveUsers,则cntActiveUsers的内容就会保存到BSS中去。
      数据段保存在源代码中已经初始化的静态变量的内容。数据段不是匿名的,它映射了一部分的程序二进制镜像,也就是源代码中指定了初始值的静态变量。因此,若是你写static int cntActiveUsers=10,则cntActiveUsers的内容就保存在了数据段中,并且初始值是10。尽管数据段映射了一个文件,但它是一个私有内存映射,这意味着更改此处的内存不会影响被映射的文件。

      你能够经过阅读文件/proc/pid_of_process/maps来检验一个Linux进程中的内存区域。记住:一个段可能包含许多区域。好比,每一个内存映射文件在mmap段中都有属于本身的区域,动态库拥有相似BSS和数据段的额外区域。有时人们提到“数据段”,指的是所有的数据段+BSS+堆。

     你还能够经过nm和objdump命令来察看二进制镜像,打印其中的符号,它们的地址,段等信息。最后须要指出的是,前文描述的虚拟地址布局在linux中是一种“灵活布局”,并且做为默认方式已经有些年头了,它假设咱们有值RLIMT_STACK。可是,当没有该值得限制时,Linux退回到“经典布局”,以下图所示:

                                   
 
 
 

进程内存分布

  以前一直在分析栈,栈这个东西的做用也介绍得差很少了,可是栈在哪儿尚未搞清楚,以及堆、代码、全局变量它们在哪儿,这都牵涉到进程的内存分布。

linux 0.01 的进程内存分布

  内存分布随着操做系统的更新换代,愈来愈科学合理,也愈来愈复杂,因此咱们仍是先了解一下早期操做系统的典型 linux 0.01 的进程的内存分布:

  linux 0.01 的一个进程固定拥有64MB的线性内存空间(ACM竞赛中单个程序的最大内存占用限制为64MB,这确定有猫腻O(∩_∩)O~),各个进程挨个放置在一张页目录表中,一个页目录表可管理4G的线性空间,所以 linux0.01 最多有 64个进程。每一个进程的内存分布以下:

  • .text 里存的是机器码序列
  • .rodata 里存的是源字符串等只读内容
  • .data 里存的是初始化的全局变量
  • .bss 上一篇介绍过了,存的是未初始化的全局变量
  • 堆、栈就不用介绍了吧!
  • 【注意】static 变量未初始化默认赋值为0或者空格。未初始化变量和初始化为0,都分配在.bss段。

  .text .rodata .data .bss 是常驻内存的,也就是说进程从开始运行到进程僵死它们一直蹲在那里,因此访问它们用的是常量地址;而栈是不断的加帧(函数调用)、减帧(函数返回)的,帧内的局部变量只能用相对于当前 esp(指向栈顶)或 ebp(指向当前帧)的相对地址来访问。

  栈被放置在高地址也是有缘由的: 调用函数(加帧)是减 esp 的,函数返回(减帧)是加 esp 的,调用在前,因此栈是向低地址扩展的,放在高地址再合适不过了。

现代操做系统的进程内存分布

  认识了 linux 0.01 的内存分布后,再看看现代操做系统的内存分布发生了什么变化:

  首先,linux 0.01 进程的64MB内存限制太过期了,如今的程序都有潜力使用到 2GB、3GB 的内存空间(每一个进程一张页目录表),固然,机器有硬伤的话也没办法,个人电脑就只有 2GB 的内存,想用 3GB 的内存是没期望了。但也不是有4GB内存就能够用4GB(32位),由于操做系统还要占个坑呢!现代 linux 中 0xC0000000 以上的 1GB 空间是操做系统专用的,而 linux 0.01 中第1个 64MB 是操做系统的坑,因此别的进程彻底占有它们的 64MB,也不用跟操做系统客气。

  其次,linux 0.01只有进程没有线程,可是现代 linux 有多线程了(linux 的线程实际上是个轻量级的进程),一个进程的多个线程之间共享全局变量、堆、打开的文件…… 但栈是不能共享的:栈中各层函数帧表明着一条执行线索,一个线程是一条执行线索,因此每一个线程独占一个栈,而这些栈又都必须在所属进程的内存空间中。

  根据以上两点,进程的内存分布就变成了下面这个样子:

  再者,若是把动态装载的动态连接库也考虑进去的话,上面的分布图将会更加"破碎"。

  若是咱们的程序没有采用多线程的话,通常能够简单地认为它的内存分布模型是 linux 0.01 的那种。

相关文章
相关标签/搜索