对一个程序在内存中的分析【转】

转自:http://blog.csdn.net/bullbat/article/details/7304404

bullbat  译

内存管理是操作系统的核心;它对于程序员和系统管理员都很关键。在接下来的几篇文章里面我将对内存的关键技术做谈论,但是不会远离其本质。然而概念很普通,例子多半来自32位X86系统的LINUX和Window操作系统。这第一篇文章谈论程序在内存中如何存放。

在多任务操作系统中的每一个进程运行在他自己的内存地址空间中。这个地址空间就是虚拟地址空间,虚拟地址空间在32位模式下总是4GB大小的内存地址。这些虚拟地址用页表方式映射物理内存,页表由操作系统内核维护,由处理器访问。每个进程有自己的页表集合,但这里有个难以理解的地方。一旦虚拟地址(作者的意思也就是分页机制)开启,它应用与所有正在运行与机器上的软件,包括内核自身。这样一部分虚拟地址空间必须保留用于内核:

这并不意味着内核使用那么多物理内存,只是他运用那部分可用的地址空间去映射他实际希望的物理内存大小。内核空间在页表中标志为特权级代码(等级小于等于2),这样当用户模式程序访问他时就会触发一个访页错误。在LINUX中,内核空间一直处于当前状态并且在所有进程中映射到相同的物理内存。内核代码和数据在任何时候总是为中断服务和系统调用做好寻址的准备。相反,用于映射用户模式的地址空间部分将会在进程切换的时候发生改变。

蓝色区域代表已经映射物理内存的虚拟地址,白色区域为没映射部分。在上面的例子中,Firefox由于他的巨大的内存需求,已经使用了他的大部分虚拟地址空间。地址空间中不同的带对应内存段如堆、栈等等。需要注意的是这些段就是简单的内存地址范围,他和Intel汇编中的”段”不相干。下面是在LINUX进程中标准的段视图:

当计算安全时,如上面的段所示,对于机器中的几乎每一个进程的开始虚拟地址都相同。这使得很容易远程利用安全漏洞。一个漏洞,往往需要引用绝对内存位置:栈上的地址,库函数的地址,等等。远程攻击必须盲目地选择这个内存位置,正指望这所有的地址空间都是一样的。如果真是这样,那么太容易被攻击了。故而地址空间的随机化就变得通用了。LINUX以在栈、内存映射段和堆的起始地址加上偏移的方式随机化他们。不幸的是,32位地址空间很紧缺,留下很少的空间用来做随机化从而牵制了他的有效性。

进程地址空间中最上面的段为栈,很多语言中栈用于存储本地变量和函数参数。调用一个方法或函数时压入栈一个新的栈帧。当函数返回时,这个压入的栈帧被释放。这个简单的设个,可能是因为数据遵循严格的FIFO次序,这意味着再复杂的数据结构都无需跟踪栈内容——一个简单的栈顶指针将会做跟踪作用。这样入栈和出栈非常快速和准确。进一步,堆栈地区不断重用,往往在CPU缓存中持有活跃的栈内存,加快存取。进程中的每个线程获得他自己的栈。

当压入操作他负载的数据时,耗尽栈的映射区域是有可能的。这将会触发一个缺页中断,在LINUX中由expand_stack()函数接手,该函数调用acct_stack_growth()来检查是否应当扩展栈。如果栈的大小在RLIMIT_STACK(通常是8MB),那么通常会扩展栈并且程序不会发觉刚才发生的这一切。这是正常的栈大小调节的机制。然而,如果栈的最大值已经达到,栈将会溢出并且程序会接手到一个段错误。当有扩展需求时,栈会扩大,而栈变小时他不会回缩。就像federal的budget,他只是扩展。

只有一种情况会发生动态栈扩展,那就是程序进入一个没有映射的内存区域,比如上面的白色区域,可能是无效的。任何其他进入没有映射内存区间触发与一个缺页中断会导致一个段错误。有些映射区域的只读的,所以,向这些区域写数据同样会导致段错误。

在栈下面是内存映射段。这里内核直接映射内存到文件内容。任何应用程序都可以通过mmap()系统调用(LINUX下)或CreateFileMapping()/MapViewOfFile()(windows下)获得这个映射。内存映射是文件I/O中一个方便高效的方法,所以他用于加载动态库。创建一个和任何文件不想干的匿名内存映射用于替代程序中的数据也是有可能的。在LINUX中,如果你通过malloc()申请一块很大的内存块,C标准库将创建一个匿名映射而不是用内存堆。’很大‘意味着大于MMAP_THRESHOLD个字节,默认是128KB,可以通过mallocpt()做调整。

说到堆,我们接下来跳到地址空间中的下一个。堆提供运行时内存分配,就像栈,而和栈不一样的是数据必须持久于分配函数。大多数程序语言都为程序提供了堆管理器。满足内存需求是一个语言运行库和内核之间的联合事务。在C中,堆分配器的接口是malloc()系列函数,然而在垃圾回收语言如C#的堆分配接口是new关键字。

如果堆里面有足够的空间满足内存需要,那么它可以完全由语言运行库处理而不需要内核环境。否则通过brk()系统调用扩展堆为请求块获得空间。堆管理器很复杂,在面对我们应用程序混乱的分配方式,需要复杂的算法来维持内存使用的速度和效率。为堆请求提供服务的时间可以有很大的差异。实时系统有特殊目地的分配器来处理这个问题。堆也变得有很多碎片,如下图所示:

最后,我们到达了内存中最下面的段:BSS段、数据段和代码段。BSS段和数据段在C语言中存储静态和全局变量。不同的是BSS段存放的是没有被初始化的静态变量,也就是所这些静态变量在源代码中没有被程序员设置初值。BSS内存区是匿名的:他不映射任何特定的文件。例如,static int cntActiveUsers,变量cntActiveUsers的内容存放在BSS段。

另一方面,数据段持有在源代码中已经初始化的静态变量。他的内存区域不是匿名的。他映射程序二进制镜像的一部分,这部分包含在源代码中已初始化的静态变量。例如,static int cntWorkerBees = 10, 那么cntWorkerBees存放在数据段中且初始数据为10。虽然数据段映射一个文件,但是他是一个私有内存映射,这意味着内存更新不会反映到底层文件。这一定是这样的,否则分配分配到的全局变量将改变你磁盘上的二进制镜像。后果难以想象!!

图中数据的例子是棘手的因为他使用了一个指针。在这种情况下,gonzo指针的内容——一个4字节的内存地址——存放在数据段中,而他指向的实际数据却不是。指向的实际内容存放在代码段中,代码段是只读的并且存放你的所有代码以及字符串文字花絮。代码段也在内存中映射你的二进制文件,但是写这个区域会发生段错误。这有助于防止指针错误,虽然没有有效的避免C语言放在首要位置。这里有个图,显示这些段和我们例子里面的变量:

在一个LINUX进程中你可以通过读/proc/pid_of_process/maps文件来检查内存区域。例如,每一个内存映射文件通常在映射段(mmap段)有他自己的区域,并且动态库有类似BSS段和数据段的额外区域。接下来的文章中会澄清什么“区域”(“area”)的真正含义。此外,有时人们说“数据段”意味着所有的数据+ bss +堆。

你可以检查二进制镜像,运用nm和objdump命令来显示符号、他们的地址、段等等。最后,上面所述的虚拟地址布局在LINUX中是“灵活”的,这几年一直是默认的。他假定我们有一个RLIMIT_STACK的值。当并非这种情况时,LINUX恢复到如下的“经典”布局:

这就是他的虚拟地址空间布局。接下来的文章中讨论的是内核如何保持跟踪这些内存区域。未来,我们将会看看内存映射、文件读写和这一切是怎么关联的以及内存使用计数意味这什么。

原文地址:http://duartes.org/gustavo/blog/

时间: 2024-10-16 12:06:56

对一个程序在内存中的分析【转】的相关文章

内存管理--程序在内存中的分布

在多任务操作系统中的每一个进程都运行在一个属于它自己的内存沙盘中.这个沙盘就是虚拟地址空间(virtual address space). 1 32位虚拟内存布局 在32位模式下虚拟地址空间总是一个4GB的内存地址块.这些虚拟地址通过页表(page table)映射到物理内存,页表由操作系统维护并被处理器引用.每一个进程拥有一套属于它自己的页表,但是还有一个隐情.只要虚拟地址被使用,那么它就会作用于这台机器上运行的所有软件,包括内核本身.因此一部分虚拟地址必须保留给内核使用: 图 1 这并不意味

程序在内存中的分配方式

1 桟区(stack):由编译器自动分配和释放,存放函数的参数值,局部变量的值,其操作方式类似于数据结构中的栈. 若申请的内存小于系统所剩内存则成功,否则就会发生栈溢出错误. 栈的增长方向是向下的,通常栈的初始化指针指向内存的最高地址,它是连续的.系统自动分配,因此速度很快. 在函数调用时,第一个进栈的是主函数的下一条指令的地址,然后是各个函数的参数,参数从右向左进栈,然后是函数中的局部变量.静态变量是不入栈的. 2 堆区(heap):一般有程序员分配和释放,若不释放,最后由操作系统回收. 它与

如何保证一个类在内存中的对象唯一性?

我发现就算同一个答案,但提问的方式不同, 往往会对回答造成干扰或者影响, 貌似高考的时候老师也说过差不多的话, 如果这题目是问:请描述单例设计模式的实现步骤,那估计就能答出来了. 解决的问题:保证一个类在内存中的对象唯一性. 比如:多程序读取一个配置文件时,建议配置文件封装成对象. 会方便操作其中数据,又要保证多个程序读到的是同一个配置文件对象, 就需要该配置文件对象在内存中是唯一的.Runtime()方法就是单例设计模式进行设计的. 如何保证对象唯一性呢? 思想:1,不让其他程序创建该类对象.

程序在内存中的分布

转载,原文地址:http://blog.csdn.net/hackbuteer1/article/details/6786811 在现代的操作系统中,当我们说到内存,往往需要分两部分来讲:物理内存和虚拟内存.从硬件上讲,虚拟空间是CPU内部的寻址空间,位于MMU之前,物理空间是总线上的寻址空间,是经过MMU转换之后的空间. 一般我们所说的程序在内存中的分布指的就是程序在虚拟内存中的存储方式. 从低地址到高地址,可分为下面几段: 预留内存地址(操作系统维护的内存地址,不可访问) 程序代码区(只读,

作为一个程序员编程中经常碰到且觉得难的事是什么?

作为一个程序员编程中经常碰到且觉得难的事是什么?有人说,感觉最难的是 trade-off:也有人说,给函数和变量起一个不用写注释的名字:架构师说,预测需求的变化比较难:一线码农说,写出可被长期维护并持续产生价值的代码是最难的.你觉得编程中经常碰到且觉得难的事是什么? 比如用新技术做项目,编写到80%-90%左右,发现了更优的方案,然后在重构和继续之间取舍......变量命名难:编码进入超凡状态时被打断 ? 还是双方对接,涉及到加解密算法,对方又不给你提供明确说明的,比如RSA吧,虽然都叫RSA,

Java 程序的内存泄露问题分析

什么是内存泄露? 广义的Memory Leak:应用占用了内存,但是不再使用(包括不能使用)该部分内存 狭义的Memory Leak:应用分配了内存,但是不能再获取该部分内存的引用(对于Java,也不能被GC) 一个具体的例子: 应用创建了一个长时间运行的Thread 该Thread使用ClassLoader(可以是定制的也可以是默认的)加载了一个类 这个类有一个Static域,指向了一大块内存,然后该Thread的ThreadLocal变量保存了这个类的引用. 最后该Thread清理了对所有已

程序在内存中镜像

用户空间的内存模型 Linux操作系统在加载程序时,将程序所使用的内存分为5段:text(程序段).data(数据段).bss(bss数据段).heap(堆).stack(栈). text segment(程序段) text segment用于存放程序指令本身,Linux在执行程序时,要把这个程序的代码加载进内存,放入text segment.程序段内存位于整个程序所占内存的最上方,并且长度固定(因为代码需要多少内存给放进去,操作系统是清楚的). data segment(数据段) data s

实验一:初步认识程序在内存中运行

顾涛原创作品转载请注明出处<Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 如果我写的不好或者有误的地方请留言 实验要求: 题目自拟,内容围绕计算机是如何工作的进行: 博客中需要使用实验截图 博客内容中需要仔细分析汇编代码的工作过程中堆栈的变化 总结部分需要阐明自己对“计算机是如何工作的”理解 实验报告: 首先我们编译一个简单的c语言代码main.c int g(int x) { return x + 3; }

记一次对java对象在内存中的分析

java 对象 占内存大小 计算方式 及 常用类型的占用 HotSpot的对齐方式为8字节对齐 ----计算公式:(对象头 + 实例数据 + padding) % 8等于0且0 <= padding < 8 Hotspot 机 中 普通对象32位 对象头 占 8个字节 引用类型 占 4字节64位 对象头 占 16个字节 引用类型 占 8字节 64位中 空对象数组 对象头 占 24 增加一个长度 增加 一个引用类型的长度 64位中是 8空的基本数据类型数组 对象头 占 24 增加一个长度 增加一