简介

当丰富多彩的应用程序在计算机上运行,为你每天的工作和生活带来便利时,你是否知道它们是如何在计算机中工作呢?本文用形象的图表与生动的解释,揭示了程序在计算机中运行的奥秘。

内存管理是操作系统的核心功能,无论对于开发者还是系统管理员内存管理的重要性都是不言而喻的。我会在接下来的几篇文章通过计算机的实际运行过程谈谈内存管理,当然在必要的时候我也会从底层原理去阐释这个问题。我们提到的概念是不局限于平台特性的通用概念,不过为了阐述这些概念我们选取的实例大多来源于Linux和基于x86架构的32位Windows操作系统。这篇文章,我们首先来看看程序是如何使用内存的。

多任务操作系统中,每一个进程都有它自己的内存“沙盒”。所谓“沙盒”,是指虚拟地址空间,在32位模式下,虚拟地址空间最多能表示4GB容量。通过页表机制,虚拟地址空间能够映射到物理内存。页表由操作系统内核来管理,并可被处理器访问。每个进程有着属于自己的页表,不过进程也不能随心所欲。因为虚拟地址一旦投入使用,所有在计算机中运行的软件都会占用虚拟地址空间,包括操作系统内核自身。也就是说,操作系统内核将保留一部分虚拟地址空间。

这并不意味着系统内核能够肆无忌惮的使用物理内存,系统内核只能使用其管辖的虚拟地址空间所对应的物理内存。系统内核所使用的内存空间通过特权码(privileged code,2级或者更低)来标记,以防止用户模式的程序访问到内核空间而发生页面错误。在Linux中,内核始终占用着一定空间,并且每个内核进程映射的物理内存地址是固定的。因此,内核代码与数据在内存中的地址总是能够被准确定位,从而为时刻处理中断以及系统调用做好了准备。与此相反,只要用户进程状态发生变化,其映射的地址空间也随即改变。

图中蓝色区域表示虚拟地址中映射到物理内存的部分,白色区域则是未映射。在这个例子中,Firefox惊人的内存需求让它使用的虚拟地址远远超过了其自身的地址空间。内存地址空间是由诸如堆、栈等段式内存管理方式进行管理的。需要指出的是,这里段的概念只不过是表示了一段内存地址,它和Intel段表机制(Intel-style segments)没有任何关系。总的来说,我们在这里讨论的是Linux系统进程标准的段式内存管理方法。

如果运行过程轻松愉快、准确无误,那么上图显示的段式虚拟地址管理启用过程对于计算机内几乎所有进程都完全一致。而这种机制为远程攻击带来了安全隐患。远程攻击往往需要参考绝对内存地址:诸如栈地址、库函数地址等等。而远程攻击者们知道了这些地址空间是固定的,他们闭着眼睛都能找到他们需要的位置。倘若真的如此,那么人们毫无疑问就会被黑客攻击了。正因为这样,随机地址空间已经成为流行的内存地址管理方式。Linux随机为栈(stack)、内存映射段(memorymapping segment)以及堆(heap )的起始地址添加偏移量。不幸的是,32位地址空间非常吃紧,限制了随机分配地址的范围和效率(hamperingits effectiveness)。

进程地址空间的首段地址便是栈,它储存了局部变量以及大多数编程语言的函数参数。当调用方法或者函数时,会有一个新的元素进栈。一旦函数返回了值,那么该元素就会被销毁。这种简单的设计,很有可能是考虑到数据操作都符合后进先出(LIFO )规则,这意味着访问栈的内容并不需要复杂的数据结构,一个简单的栈顶指针就能搞定一切。进栈和出栈的操作方便快捷,不需要过多判断。另外,栈的反复使用能够使栈驻留在CPU缓存(cpu caches)中,从而加快数据存取。每个进程中的每个线程都有属于自己的栈。

如果映射的栈地址空间被压入了超过栈容量的数据,那么栈便无法继续工作了。这种情况会导致一个由expand_stack()函数处理的页面错误,这个函数会调用acct_stack_growth() 函数去检查是否应该为这个栈增加容量。如果这个栈的容量低于RLIMIT_STACK (通常为 8MB)限定的值,那么栈的容量会正常增加,程序也会继续正常运行,并且程序不会知道刚刚发生了什么。当然,这是根据实际需要来调整栈大小的一般机制,如果栈的容量达到了最大值上限,那么栈就会溢出,程序也会收到一个段出错的信息。虽然在程序需要的时候映射的栈空间会增加,但是栈使用的空间减少时,栈却不会释放多余的空间。这就好像联邦政府预算,只可能越来越多。

程序存取上图所示的未映射区域,是唯一正常实现动态增加栈空间的情况,程序访问其他未映射内存访问将会出现页面错误最终导致段错误。有些映射区域是只读的,程序试图写入这些区域同样会导致这种错误。

说到堆,我们就不得不提它的内存使用机制。堆支持运行时内存分配,和栈不同,大多数语言都允许程序使用堆管理内存。满足内存需求是语言运行时和C语言核心之间的联结点,而堆的内存管理接口是通过malloc()及其友元函数来实现的,在C#这样支持垃圾回收机制的语言中,其接口是新定义的关键字。

当堆的空间能够满足程序的内存请求时,那么请求的处理过程就可以直接由语言运行时来负责,而不必有系统内核参与。但是如果堆的空间不能满足程序的内存申请,那么brk()函数会执行系统调用(implementation)来增加堆的内存空间以满足程序的请求。堆管理的实现过程十分复杂,,面对程序内存分配变化莫测的情况,堆管理需要成熟的算法去提升请求的响应速度与内存利用率。系统响应堆的内存请求花费的时间往往变化很大。实时操作系统解决这个问题的方法是采用专用内存分配器(special-purposeal locators)。堆在内存中的分布情况和其他内存管理机制一样充满了碎片,如下图所示:

最后,我们来聊聊刚才图中位置最下方的几个内存段:BSS段、数据段和程序段。在C语言中,BSS段和数据段存储的都是静态(全局)变量。这几个段的不同之处在于BSS段存储的静态变量没有初始化——程序员在源代码中没有为这些静态变量赋值。由于BSS段并没有映射任何文件,所以BSS段在内存中是以匿名形式存在的。举个例子,假设你定义了变量static int cntActiveUsers,那么cntActiveUsers 的数据就保存在BSS段中。

与BSS段不同的是,数据段储存了在源代码中经过了初始化的静态变量。因此,数据段的内存区域并不是匿名的。数据段映射了程序二进制映像中源代码给出静态变量初值的部分。所以,如果你定义了static int cntWorkerBees = 10,那么cntWorkerBees变量会赋以初值10并在数据段中保存下来。尽管数据段映射了文件,但这种内存映射是私有的,也就是说,数据段的内存更新不会在其映射的文件中生效。这样造成的结果就是,虽然全局变量的改变应用到了文件在内存中的二进制映像,但是文件本身却不能作出相应的变化!

下面图表中的示例由于使用了指针所以看起来不那么明了。在这个示例中,指针在数据段中占用了4个字节,但是指针所指向的字符串则不在数据段中。对于字符串,内存为它们准备了专门的文本段,文本段以只读的形式存储程序中诸如字符串类型等不会被直接执行的代码。文本段同样会将二进制文件映射到内存,但文件映射区域的写入操作只能以程序收到段错误而告终。这种机制能有效防止指针的错误指向而导致的误操作,不过也不得不承认这种做法显然没有直接在C语言代码中进行保护来的效率高。下面的图表显示了刚刚我们讨论到的段以及变量示例:

如果你想了解Linux中的进程是如何使用内存的,可以读读源代码文件/proc/pid_of_process/maps。值得一提的是,一个内存段往往由多个区域组成。例如,每个正确映射到内存的文件都有属于自己的段,动态库文件则拥有另外的段,这些段类似于BSS段与数据段。下一篇文章我们将进一步探讨“区域”的含义。另外,也会谈谈我个人对“数据段就是数据、BSS以及堆的总和”这种观点的看法。

使用nm 和objdump命令能够显示二进制映像的标识,映像的地址、段等等信息都可以查阅。最后要指出的是,上文讨论的Linux虚拟地址管理机制是“灵活”的,该机制在Linux中作为首选已经沿用了几年。使用这种机制要求程序为RLIMIT_STACK变量赋值,如果没有,那么Linux则退回到“传统”方式管理内存,如下图所示:

该图呈现了虚拟地址空间的管理方式。下一篇文章我们将讨论系统内核是如何跟踪这些内存区域的。进而我们会看看内存映射原理、与之相关的文件读写机制以及内存使用情况图表所揭示的含义。

同一文章的另一处翻译:剖析程序的内存布局

程序在内存中运行的奥秘相关推荐

  1. Java程序在内存中运行详解

    Java语言是一门编译型语言,需要将编写的源代码(.java文件)编译之后(.class字节码文件),通过 jvm 才能正常的执行,下面的内容记录了一个程序从编写到执行整个过程在内存中是怎么一个变的. ...

  2. 在外部DDR3内存中运行的MicroBlaze程序的固化方法

    本文以基于米联客MA703FA开发板的MicroBlaze LWIP千兆以太网例程为例,详细介绍在外部DDR3内存中运行的MicroBlaze程序的固化方法. FPGA型号:XC7A35TFGG484 ...

  3. 组织c语言程序的是什么,C程序在内存中的组织方式

    对我本人来说,学习任何一种高级语言,总想知道高级语言是如何被编译并载入内存的,一个程序的运行,在计算机内存中是怎样组织的等等问题,总想打破砂锅问到底,有好多疑问,因此我就学习了汇编语言,汇编语言确实让 ...

  4. SQL Server 将在下一个版本实现内存中运行

    From:Microsoft adding in-memory capabilities to SQL Server 在本周召开的SQL Server专业组织峰会上,微软宣布,将在下一个SQL Ser ...

  5. C语言之程序在内存中的分布以及内存越界问题

    C语言程序在内存中的分布: bss段:该段用来存放没有被初始化或者初始化为0的全局变量,以及被static修饰的未初始化的局部变量.在程序运行的整个生命周期内都存在于内存中.这个段中的变量只占用程序运 ...

  6. 在内存中运行可执行程序

    在内存中运行可执行程序,好处是可以给程序加壳,加密源程序,静态反汇编无法获得PE输入节,但是因为运行后仍然是独立的进程,所以没办法防止远程线程注入,挂接API钩子. 转自:http://www.cpp ...

  7. 【转】程序在内存中的分布

    前些天学习到了程序在虚拟内存中分布的一些知识点,结合在网上查阅的一些资料,整理一下知识点.本博客参考博主 hackbuteer1的<程序在内存中的分布>这篇文章. v 在现代的操作系统中, ...

  8. 编译html成qch,在应用程序编译过程中运行qcollectiongenerator

    我一直在研究一个名为RoboJournal的程序很长一段时间.下一版本包含完整的文档;每当用户按F1或单击RoboJournal程序中的帮助项目时,帮助文件将显示在Qt助手中(比简单地打开浏览器窗口以 ...

  9. java程序在内存中的存储分配

    java程序在内存中的存储分配情况: 一. 堆区: 1.存储的全部是对象,每个对象都包含一个与之对应的class的信息.(class的目的是得到操作指令)  2.jvm只有一个堆区(heap)被所有线 ...

最新文章

  1. [原创] MAME架构、编译及配置浅析
  2. 算法练习day10——190328(根据指定值划分单链表、复制含有rand指针节点的链表、两个单链表相交)
  3. java servlet_Java Servlet的前100个问题
  4. caffe模型文件解析_深度学习 Caffe 初始化流程理解(数据流建立)
  5. 无法将类型int隐式转换为string_Scala implicit 隐式转换安全驾驶指南
  6. SAP License:SAP S/4HANA就是答案
  7. 大学四年只学java_大学四年废了怎么办,不如来学java
  8. asp.net写验证码
  9. python工资这么高为什么不学-为什么那么多人想学 Python?
  10. 约瑟夫环问题(动态链表操作)n个学生围成一圈,每m个出队,输出所有出队的序列
  11. ARM:ARM体系结构与编程、ARM指令流水线、ARM编程模型基础
  12. 热工检测系统设计知识点总结
  13. 新手试炼:C语言实现加密版2-4阶行列式运算!
  14. ICE C++ Hello World
  15. 计算机网络安全学校,网络空间安全学院
  16. IDEA代码和页面前进后退快捷键
  17. 电能计量芯片应用心得之选型篇
  18. Kanzi Shader入门
  19. 记一次微信小程序云函数操作Mysql数据库
  20. 基于心电的疲劳分析论文

热门文章

  1. c++:文件操作1 文件的打开
  2. Python Qt GUI设计:QCalendar日历类和QDateTimeEdit时间类(基础篇—20)
  3. Linux那些事儿 之 戏说USB(8)从这里开始
  4. Linux那些事儿 之 戏说USB(6)好戏开始了
  5. C++中extern使用
  6. 在Ubuntu 14.04和CentOS上安装boost1.55二进制包
  7. 深入理解ceph-disk prepare 源码逻辑
  8. SQL Server基础操作(此随笔仅作为本人学习进度记录七 !--存储过程)
  9. Python3模块Crypto改为pycryptodome
  10. Ubuntu 8.04嵌入式交叉编译环境arm-linux-gcc搭建过程图解