linux进程中的内存分布

很多小伙伴在调试C代码的时候非常痛苦,C语言不像java那样可以给你指出具体的错误地方和错误原因,C语音因为指针的特殊性和C语言版本的兼容性的需要,很难直接定位到错误的地方。特别是各种段错误、溢出等。要想提高调试效率,了解和掌握进程内存布局还是很有必要的。了解了内存空间分配,有时候就可以通过指针或者地址的位置来确定是否是程序本身写错了等等。

 进程空间分布概述

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

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

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

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

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

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

 

注:1.Text, BSS, Data段在编译时已经决定了进程将占用多少VM
可以通过size,知道这些信息。

2. 正常情况下,Linux进程不能对用来存放程序代码的内存区域执行写操作,即程序代码是以只读的方式加载到内存中,但它可以被多个进程安全的共享。

内核空间和用户空间

Linux的虚拟地址空间范围为0~4G(intel x86架构32位),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使用了相当多的虚拟地址空间,因为它占用内存较多。

 进程内存布局

Linux进程标准的内存段布局,如下图所示,地址空间中的各个条带对应于不同的内存段(memory segment),如:堆、栈之类的。
 

 


:这些段只是简单的虚拟内存地址空间范围,与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退回到“经典布局”,如下图所示:

 

全局变量(初始化和未初始化)位于数据段

局部变量位于栈段 

const 常量位于栈段 

常字符串位于文本段 

动态申请内存位于堆段 

查看进程的内存布局

C语言程序实例分析如下所示:

#include <stdio.h>
#include <malloc.h>

void print(char *, int);
int g1=12;
long g2;
void main(){
  char *s1 = "abcde";
  char *s2 = "abcde";
  char s3[] = "abcd";
  long int *s4[100];
  char *s5 = "abcde";//常量字符串"abcde"在常量区,但是s1,s2,s5本身在stack上,但它们用有相同的地址
  int a = 5;
  int b = 6; //a和b在stack上,所以&a>&b
  const int c =10;
  sleep(60);
  printf("变量地址\n&s1=%p\n&s2=%p\n&s3=%p\n&s4=%p\n&s5=%p\ns1=%p\ns2=%p\ns3=%p\ns4=%p\ns5=%p\na=%p\nb=%p\n",&s1,&s2,&s3,&s4,&s5,s1,s2,s3,s4,s5,&a,&b);
  printf("&g1=%p\n &g2=%p\n&c=%p\n",&g1,&g2,&c);
  printf("变量地址在进程调用中");
  print("ddddddd",5);
  printf("main=%p, print=%p\n",main,print);
  while(1){}
}
void print(char *str, int p)
{
  char *s1 = "abcde";
  char *s2 = "abcde";
  char s3[] = "abcd";
  long int *s4[100];
  char *s5 = "abcde";//常量字符串"abcde"在常量区,但是s1,s2,s5本身在stack上,但它们用有相同的地址
  int a = 5;
  int b = 6; //a和b在stack上,所以&a>&b
  int c;
  int d;
  char *q = str;
  int m =p;
  char *r=(char*)malloc(1);
  char *w = (char*)malloc(1);
   printf("变量地址\n&s1=%p\n&s2=%p\n&s3=%p\n&s4=%p\n&s5=%p\ns1=%p\ns2=%p\ns3=%p\ns4=%p\ns5=%p\na=%p\nb=%p\n",&s1,&s2,&s3,&s4,&s5,s1,s2,s3,s4,s5,&a,&b);
   printf("str=%p\nq=%p\n&q=%p\n&p=%p\n&m=%p\nr=%p\nw=%p\n&r=%p\n&w=%p\n",&str,q,&q,&p,&m,r,w,&r,&w);
}

 

变量地址
&s1=0x7ffe949e0308
&s2=0x7ffe949e0310
&s3=0x7ffe949e0640
&s4=0x7ffe949e0320
&s5=0x7ffe949e0318
s1=0x400998
s2=0x400998//s1,s2,s5都指向同一个地址,该地址就是字符串常量保存的位置,位于text段
s3=0x7ffe949e0640
s4=0x7ffe949e0320
s5=0x400998
a=0x7ffe949e02fc
b=0x7ffe949e0300
&g1=0x601050 //位于bss段
 &g2=0x601060
&c=0x7ffe949e0304
变量地址在进程调用中变量地址
&s1=0x7ffe949dff80
&s2=0x7ffe949dff88
&s3=0x7ffe949e02d0
&s4=0x7ffe949dffb0
&s5=0x7ffe949dff90
s1=0x400998
s2=0x400998
s3=0x7ffe949e02d0
s4=0x7ffe949dffb0
s5=0x400998
a=0x7ffe949dff74
b=0x7ffe949dff78
str=0x7ffe949dff68
q=0x400a2f
&q=0x7ffe949dff98
&p=0x7ffe949dff64
&m=0x7ffe949dff7c
r=0x1d3c420
w=0x1d3c440
&r=0x7ffe949dffa0
&w=0x7ffe949dffa8
main=0x400626, print=0x40076f

使用/proc/进程id/maps文件查看进程的内存空间分布

查看进程的内存布局: 

00400000-00401000这一段可以读可以执行,是text段,也就是程序段;

00600000-00601000这一段可读,应该是data段

00601000-0060200这一段可读可写,应该是bss段

01d3c000-01d5d000这一段可读可写,属于heap区

7f44bbfd0000-7f44bc5c2000这一段应该是内存映射区(会增长)

7ffe949c0000-7ffe949e2000这一段属于stack区,长度为0x22000字节,

这里写图片描述

其中未分配的堆栈内存中一部分用于内存映射也就是mmap。

从上图可以看出,进程的内存空间从低地址到高地址内存布局依次是: 保留区 –> 文本段–>数据段—>堆—>共享库或mmap—>栈–>环境变量—>内核空间

查看该进程对应的内存布局,结果如下:

每一行依次对应的是: 地址范围、权限、偏移量、设备、文件inode、映射对象

64位地址时将0x0000,0000,0000,0000 – 0x0000,7fff,ffff,f000这128T地址用于用户空间。参见定义:

#define TASK_SIZE_MAX   ((1UL << 47) - PAGE_SIZE),注意这里还减去了一个页面的大小做为保护。

而0xffff,8000,0000,0000以上为系统空间地址。注意:该地址前4个都是f,这是因为目前实际上只用了64位地址中的48位(高16位是没有用的),而从地址0x0000,7fff,ffff,ffff到0xffff,8000,0000,0000中间是一个巨大的空洞,是为以后的扩展预留的。

从上述地址值来看,64位系统中应该有48根地址总线,低位:0~47位才是有效的可变地址,高位:48~63位全补0或全补1。一般高位全补0对应的地址空间是用户态。高位全补1对应的是内核态,如上面的第19行(vsyscall段)。这64位的地址空间并不能全部被使用(太多了),所以用户态和内核态之间会有未使用的空间(据说叫AMD64空洞)。

而真正的系统空间的起始地址,是从0xffff,8800,0000,0000开始的,参见:

#define __PAGE_OFFSET     _AC(0xffff,8800,0000,0000, UL)

而32位地址时系统空间的起始地址为0xC000,0000。

另外0xffff,8800,0000,0000 – 0xffff,c7ff,ffff,ffff这64T直接和物理内存进行映射,0xffff,c900,0000,0000 – 0xffff,e8ff,ffff,ffff这32T用于vmalloc/ioremap的地址空间。

至于用户空间的几个段的划分,不同的架构和编译选项貌似还不一样。暂时没有找到合适的解释。

查看程序各段的大小,使用size 命令:

    text       data        bss        dec        hex    filename
   2379        576          8       2963        b93    mmtest

使用 pmap 命令查看进程内存分布

pmap [参数] [进程pid]

参数:

  • -d 显示详细设备信息
  • -q 不显示首尾行信息

运行测试程序,用top或ps 命令查询进程pid:

属性 含义
Address 地址空间:起始地址~
Kbytes 大小
Mode 权限:r可读、w可写、x可执行、s共享内存、p私有内存
Offset 虚拟内存偏移量
Device 所在设备(主:次): 008:00008表示sda8
mapped 虚拟内存分配大小
shared 共享内存大小

 mmtest 是运行程序的名字 
.so 是使用的动态链接库 
stack 使用的栈空间 
anon 预分配的虚拟内存,还未有数据占用

附录:

栈与堆的区别:

参考:https://zhuanlan.zhihu.com/p/26857760

https://blog.csdn.net/chenyijun/article/details/79441166

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章