内存中程序剖析

内存中程序剖析

1.引言

内存管理一直是操做系统的核心问题,它对于编程和系统管理都是异常重要。接下来会有一系列博文从实际角度给你们介绍内存管理的一系列内容,尽管这一律念比较宽泛,可是博文中列举的示例都是来自于Linux或Windows这些32位的x86系统。做为这系列的第一篇论文,首先简单描述一下程序如何在内存中布局。html

2.内存空间简要介绍

每一个多任务系统中的进程都运行在它本身的内存“沙盒”里,这个“沙盒”就是虚拟内存空间,这在32位模式下每每指4GB内存地址块。这些虚拟地址经过页表映射到物理内存,这些页表是由操做系统内核来维护而且被处理器(CPU)来查询。每一个进程都有它本身的页表,一旦虚拟地址启动,这些叶表就必须适用于机器上的全部软件程序,包括系统内核自己。所以,虚拟地址的一部分必须保存在内核中:linux

这不是意味着内核使用页表来匹配物理内存,只是内核须要使用那部分虚拟空间用来映射任意的物理空间。内核空间的页面在页表上被标记为特权级(环2或更低),所以,若是一个用户模式下的程序试图去访问一页特权级内存页,经常会致使一个页异常(page fault----编程中常见报错总结)。在Linux下,内核空间在全部用户进程中的物理内存空间映射都是一致的。任什么时候候,内存代码和数据,能够被中断处理程序和系统调用所寻址和使用。相比之下,映射为用户模式那部分的地址空间,随着一个任务切换的发生,也会映射到不一样的物理地址空间:web

上图中,蓝色区域表示映射到物理地址空间的虚拟地址空间,而白色则是还未映射的虚拟地址。地址空间中不一样的段对于不一样的内存段----堆、栈等等。记住,这些段仅仅表示一段内存地址范围,和实际的硬件架构中的段不一样。总之,下图就是Linux下一个进程的标准段布局:算法

当计算和程序上一切运行顺利,那么在机器上每一个进程的不一样段的布局基本一致,都如上图所示,这样有利于避免不少安全注入问题。一次注入攻击经常须要访问绝对内存位置:例如堆栈上的地址、函数库的地址等等。远程攻击者每每是盲目选择内存位置,主要是但愿每一个进程的地址空间都是一致的,若是是这样的话,那么我的的隐私就受到威胁;因此,现在线程地址空间的随机映射大行其道。Linux下,系统经过让基地址随机加上一个偏移地址,从而获得栈地址、堆地址以及其余的内存映射段地址。然而,现在的32位地址空间显得有点紧凑,这样就致使随机的空间很小,进而可能影响其安全性编程

3.Stack

在进程中最重要的段----堆栈段,不少编程语言下,它是用来存放局部变量和函数参数的。调用一个函数或者方法,就会在栈上压入一个新的栈帧,而且随着函数返回,所对应的栈帧也就被释放掉。这种简单的设计,主要的思想来源多是数据听从“先进先出”的规则,这也意味着不须要复杂的数据结构来追踪栈内容,一个简单的栈顶指针就能够实现内容查询----push和pop操做是很是迅速且稳定。同时,将那些被屡次重复使用的栈内容存放到“CPU缓存中(Cache)”,能够带来更快的访问速度。ubuntu

若是在内存空间中,放入过多的数据内容,会致使栈空间的耗尽。Linux下,这样所引发的页异常,能够经过expand_stack()来解决,这就会致使调用acct_stack_growth()来检查是否能够增加堆栈空间。若是栈空间尺度小于RLIMIT_STACK(通常是8MB),那么栈空间增加能够没有问题的执行下去。可是,若是栈空间已经达到上限,那么咱们进行上述操做,最终会收到一个段异常(segmentation fault)----也就是“栈溢出”。若是栈增加操做顺序运行,娜美操做完成后不回收缩栈大小。c#

动态堆栈增加是访问未映射内存区域的惟一正确方式。任何其余访问未映射内存区域,都会触发一个页异常,进而致使段异常。某些映射内存区域是只读内存区域,所以,试图写这些区域一样会致使异常。windows

在栈下面,就是内存映射段区域。这里,内存直接将文件内容映射到内存。任何应用能够经过调用Linux下的mmap()系统调用或者在Windows下的CreateFileMapping()/MapViewOfFile()来实现。内存映射对于文件I/O操做来讲是很是方便且高效的,因此它常常被用来加载动态库。也能够建立一个匿名内存映射,不对应任何文件,而专门用作程序数据。Linux下,若是程序经过malloc()需求一片大的内存块,C库将会建立一片匿名映射空间,这里的“大”是指大于MMAP_THRESHOLD字节,该字段的默认值是128KB,能够经过mallopt()动态修改。api

4.Heap

接下来,就是堆的介绍。堆----经常使用来提供运行时内存分配,这里点和栈比较相似;可是,堆也能够分配存放栈范围以外的数据变量,这一点区别于栈。大部分语言都提供堆管理程序。所以,知足内存请求是语言运行时和内核的共同事务。在C语言中,调用堆分配空间的接口是malloc(),以及在具备垃圾回收机制的C#语言中,对应的接口的new关键字。缓存

若是在堆上有足够的内存空间知足内存调用,那么仅仅经过语言运行时就能够知足相关操做,而不须要调用系统内核操做。不然的话,就须要经过调用brk()系统调用来分配更多的空间来知足需求。在咱们实际程序复杂的内存分配的模式下,堆的管理是很复杂的,须要精妙的算法来平衡速度和内存使用效率,于是,用于分配堆空间的时间消耗可能差别很大;实时系统主要经过special-purpose allocators来处理这类问题。堆在内存中示意图以下所示:

5.BSS、数据段和代码段

最后,咱们来分析最下面的一系列内存段----BSS、数据段和程序代码段。在C语言中,BSS、数据段都用来存储静态(static)变量。不一样之处是,BSS中存储的内容是未初始化的静态变量,这些变量的值是经过在程序代码中进行设置的。BSS内存区域是匿名的:它不会映射到任何文件。若是,你输入static int cntActiveUsers;语句,那么这个变量就存放在BSS段。

数据段,存放源码中已经被初始化的静态变量,因此,这段内存区域不是匿名的,它被映射到文件的二进制镜像的某个部分,而且包含这些静态变量在源码中被初始化的值。因此,当你输入static int cntWorkBees = 10;语句,则变量的内容存放于数据段且值为10。尽管数据段映射成一个文件,它还是私有内存映射,这也意味着对内存数据的更新不会同步到所映射的文件中。不然的话,对于静态变量的赋值,会致使磁盘上文件内容的变化。

下图中,数据段的示例比较复杂,由于使用了指针。下例中,指针gonzo内容----也就是一个4字节内存地址----存放在数据段。可是,它指向的实际字符串并不存放在数据段,而是在代码段,也就是一个只读段,也就是存放全部代码和全部字符串的区域。代码段一样映射内存中的二进制文件,可是要写代码段会致使一个段异常。这有助于阻止不少指针错误,下图是这些段的示意图:

6.总结

你能够经过阅读Linux源码文件中的/proc/pid_of_process/maps来进一步了解内存区域,请记住,一个段可能包含不少不一样的内存区域。例如,每一个内存映射文件一般在mmap段中都有本身的区域,而动态库具备相似于BSS和数据的额外区域。你能够借助Linux下的工具nmobjdump来查阅一个目标文件的符号、地址、段等等。最终的虚拟地址空间的布局在Linux中是一种灵活的方式,Linux下的经典内存布局以下:

相关文章
相关标签/搜索