`
xinklabi
  • 浏览: 1560360 次
  • 性别: Icon_minigender_1
  • 来自: 吉林
文章分类
社区版块
存档分类
最新评论

程序在内存中运行的奥秘

阅读更多

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

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

                        

这并不意味着系统内核能够肆无忌惮的使用物理内存,系统内核只能使用其管辖的虚拟地址空间所对应的物理内存。系统内核所使用的内存空间通过特权码(privileged code2级或者更低)来标记,以防止用户模式的程序访问到内核空间而发生页面错误。在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则退回到“传统”方式管理内存,如下图所示:


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

分享到:
评论

相关推荐

    C++程序的生前和死后(课件笔记)

    如果你是一个对于各种基础知识內核原理感兴趣的人,对于各种隐身幕后的奥秘感兴趣的人,这门课能满足你的好奇心,让你完全通透 C++ 程序的整个运行过程。这门课带来的技术成长是无形于应用的,而无形,有时甚于有形...

    编语言:计算机底层的奥秘解析汇编语言:计算机底层的奥秘解析汇编语言:计算机底层的奥秘解析

    汇编语言作为计算机底层的编程语言,是计算机系统运行的核心组成部分。通过使用汇编语言,程序员可以直接操作计算机的硬件资源,并编写出高效和灵活的程序。本文将深入介绍汇编语言的基础概念、语法特点、应用场景...

    教学设计——4.1编制计算机程序解决问题.doc

    解释内存 " "新 "问2:生活中的计算机程序。 "中遇到的计"和外存的区别 " "课 "2、概念:(请一位同学大声读一遍)计算机"算机程序。",以及计算机 " "教 "的程序是有一系列的机器指令组成的,而指 "了解程序的"的...

    Python的变量内存

    嗯,最近打算从Java程序员转变为Python程序员,正所谓“人生苦短,我用Python”,突然间对Python对变量的管理感兴趣,于是就研究了一番,这里写篇文章记录一下Python变量的奥秘。 a = 1 b = 1 # id函数打印出内存...

    windows环境下32位汇编语言程序设计

    毕竟,不能为所欲为也可以有好的一面,我们可以不必再考虑一些老大难的问题,如程序运行时会面对什么样的显示卡,如何驱动不同的打印机,内存不够了如何用磁盘交换,等等。我们也可以在了解更少硬件知识的情况下就...

    江苏省二级C语言 tan hao qiang

     答题时,应先运行TC2.0应用程序,然后进入A盘操作。为防止意外损失,考生应该及时把做好的题目存入A盘。 注意:这里的A盘不是真正意义的软驱,是硬盘虚拟的,不用过多理会这个问题。 6. 监考人员在开考30分钟后...

    加密解密.技术内幕.chm

    code传奇8.3 VB编译奥秘8.4 VB与COM8.5 VB可执行程序结构研究8.6 VB程序事件解读8.7 VB程序图形界面(GUI)解读 8.8 VB程序执行代码研究8.9 我们的工具8.10 VB程序保护篇 附录A 在Visual C++中使用内联汇编附录B 在...

    软件加密技术内幕 chm

    软件加密技术内幕 要花时间看 第1章 PE文件格式深入研究 1.1 PE文件格式格式纵览 1.1.1 区块(Section) 1.1.2 相对虚拟地址(Relative Virtual Addresses) ...附录B 在Visual Basic中使用汇编

    软件加密技术内幕

    软件加密技术内幕chm文档及配套光盘 第1章 PE文件格式深入研究 1.1 PE文件格式格式纵览 1.1.1 区块(Section) 1.1.2 相对虚拟地址(Relative Virtual Addresses) ...附录B 在Visual Basic中使用汇编

    【软件加密技术内幕】

    <br/>8.8 VB程序执行代码研究 <br/>8.9 我们的工具 <br/>8.10 VB程序保护篇 <br/> <br/>附录A 在Visual C++中使用内联汇编 <br/>附录B 在Visual Basic中使用汇编

    Java编程艺术 PDF

    本书揭示了Java程序员如何创建高质量软件的奥秘。Herbert Schildt 和James Holmes两位编程大师通过将Java应用于某些有趣、实用的计算机问题和编程任务中,全面展示了Java语言的强大功能、敏捷性、多样性和艺术性。...

    java 编程艺术

    6.5 Web搜索程序的编译与运行 202 6.6 对Web搜索程序的思考 203 第7章 使用Java提交HTML 205 7.1 使用JEditorPane提交HTML 206 7.2 处理超链接事件 206 7.3 编写Mini Web浏览器 207 7.3.1 MiniBrowser类 208 ...

    性能测试从零开始:LoadRunner入门与提升

    8.3.3 在Java Vuser中开发SMTP发送mail脚本 258 8.3.4 参数化增强脚本 260 第9章 学以致用--一步一步做Web系统性能测试 264 9.1 软件系统背景及架构设计介绍 264 9.2 性能要求和性能指标分析(Goal阶段) 267 9.2.1 ...

Global site tag (gtag.js) - Google Analytics