《转》linux内存管理

news/2024/7/10 3:28:02 标签: linux, 语言, layout, windows, 数据结构, firefox

内存管理模块是操作系统的心脏;它对应用程序和系统管理非常重要。今后的几篇文章中,我将着眼于实际的内存问题,但也不避讳其中的技术内幕。由于不少概念是通用的,所以文中大部分例子取自32位x86平台的Linux和Windows系统。本系列第一篇文章讲述应用程序的内存布局。

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


这并不意味着内核使用了那么多的物理内存,仅表示它可支配这么大的地址空间,可根据内核需要,将其映射到物理内存。内核空间在页表中拥有较高的特权级(ring 2或以下),因此只要用户态的程序试图访问这些页,就会导致一个页错误(page fault)。在Linux中,内核空间是持续存在的,并且在所有进程中都映射到同样的物理内存。内核代码和数据总是可寻址的,随时准备处理中断和系统调用。与此相反,用户模式地址空间的映射随进程切换的发生而不断变化:




色区域表示映射到物理内存的虚拟地址,而白色区域表示未映射的部分。在上面的例子中,Firefox使用了相当多的虚拟地址空间,因为它是传说中的吃内存大户。地址空间中的各个条带对应于不同的内存段(memory segment),如:堆、栈之类的。记住,这些段只是简单的内存地址范围,与Intel处理器的段没有关系。

       不管怎样,下面是一个Linux进程的标准的内存段布局


        当计算机开心、安全、可爱、正常的运转时,几乎每一个进程的各个段的起始虚拟地址都与上图完全一致,这也给远程发掘程序安全漏洞打开了方便之门。一个发掘过程往往需要引用绝对内存地址:栈地址,库函数地址等。远程攻击者必须依赖地址空间布局的一致性,摸索着选择这些地址。如果让他们猜个正着,有人就会被整了。因此,地址空间的随机排布方式逐渐流行起来。Linux 通过对内存映射段的起始地址加上随机的偏移量来打乱布局。不幸的是,32 位地址空间相当紧凑,给随机化所留下的空当不大,削弱了这种技巧的效果

    进程地址空间中最顶部的段是栈,大多数编程语言将之用于存储局部变量和函数参数。调用一个局部变量或函数会将一个新的栈桢(stack frame)压入栈中。栈桢在函数返回时被清理。也许是因为数据严格的遵从LIFO的顺序,这个简单的设计意味着不必使用复杂的数据结构来追踪栈的内容,只需要一个简单的指针指向栈的顶端即可。因此压栈(pushing)和退栈(popping)过程非常迅速、准确。另外,持续的重用栈空间有助于使活跃的栈内存保持在CPU缓存中,从而加速访问。进程中的每一个线程都有属于自己的栈

      通过不断向栈中压入的数据,超出其容量就有会耗尽栈所对应的内存区域。这将触发一个页故障(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 ,可以通过mallopt()调整。

     说到堆,它是接下来的一块地址空间。与栈一样,堆用于运行时内存分配;但不同点是,堆用于存储那些生存期与函数调用无关的数据。大部分语言都提供了堆管理功能。因此,满足内存请求就成了语言运行时库及内核共同的任务。在 C 语言中,堆分配的接口是malloc()系列函数,而在具有垃圾收集功能的语言(如 C# )中,此接口是 new 关键字。

     如果堆中有足够的空间来满足内存请求,它就可以被语言运行时库处理而不需要内核参与。否则,堆会被扩大,通过brk()系统调用(实现)来分配请求所需的内存块。堆管理是很复杂的,需要精细的算法,应付我们程序中杂乱的分配模式,优化速度和内存使用效率。处理一个堆请求所需的时间会大幅度的变动。实时系统通过
特殊目的分配器来解决这个问题。堆也可能会变得零零碎碎,如下图所示:

    最后,我们来看看最底部的内存段:BSS,数据段,代码段。在C语言中,BSS和数据段保存的都是静态(全局)变量的内容区别在于BSS保存的是未被初始化的静态变量内容,它们的值不是直接在程序的源代码中设定的BSS内存区域是匿名的:它不映射到任何文件。如果你写static int cntActiveUsers,则cntActiveUsers的内容就会保存在BSS中



    另一方面,数据段保存在源代码中已经初始化了的静态变量内容。这个内存区域不是匿名的。它映射了一部分的程序二进制镜像,也就是源代码中指定了初始值的静态变量。所以,如果你写static int cntWorkerBees = 10,则cntWorkerBees的内容就保存在数据段中了,而且初始值为10。尽管数据段映射了一个文件,但它是一个私有内存映射,这意味着更改此处的内存不会影响到被映射的文件。也必须如此,否则给全局变量赋值将会改动你硬盘上的二进制镜像,这是不可想象的。



    下图中数据段的例子更加复杂,因为它用了一个指针。在此情况下,指针gonzo(4字节内存地址)本身的值保存在数据段中。而它所指向的实际字符串则不在这里。这个字符串保存在代码段中,代码段是只读的,保存了你全部的代码外加零零碎碎的东西,比如字符串字面值。代码段将你的二进制文件也映射到了内存中,但对此区域的写操作都会使你的程序收到段错误。这有助于防范指针错误,虽然不像在C语言编程时就注意防范来得那么有效。下图展示了这些段以及我们例子中的变量:

你可以通过阅读文件/proc/pid_of_process/maps来检验一个Linux进程中的内存区域。记住一个段可能包含许多区域。比如,每个内存映射文件在mmap段中都有属于自己的区域,动态库拥有类似BSS和数据段的额外区域。下一篇文章讲说明这些“区域”(area)的真正含义。有时人们提到“数据段”,指的就是全部的数据段 + BSS + 堆。

你可以通过nmobjdump命令来察看二进制镜像,打印其中的符号,它们的地址,段等信息。最后需要指出的是,前文描述的虚拟地址布局在Linux 中是一种“灵活布局”(flexible layout),而且以此作为默认方式已经有些年头了。它假设我们有值 RLIMIT_STACK。

     当情况不是这样时, Linux 退回使用“经典布局”(classic layout),如下图所示:

对虚拟地址空间的布局就讲这些吧。下一篇文章将讨论内核是如何跟踪这些内存区域的。我们会分析内存映射,看看文件的读写操作是如何与之关联的,以及内存使用概况的含义。



默认情况下,栈和 mmap 映射区域并不是从一个固定地址开始,并且每次的值都不一样,这是程序在启动时随机改变这些值的设置,使得使用缓冲区溢出进行攻击更加困难。在以前较老的内核中,这个地址是固定的,所以比较容易利用这点来进行缓冲区溢出攻击。但是也可以改变这个默认的特性(地址不固定),方法是将 /proc/sys/kernel/randomize_va_space 的值改为 0,它默认为 1。 修改的方法是使用 sysctl 命令:
sysctl -w kernel/randomize_va_space=0

如果希望该值在系统启动时也为 0,那么需要将  randomize_va_space=0 这个值写往 /etc/sysctl.conf 文件中。从安全角度考虑,一般是不建议这么做的,除非你运行在某些不同架构的 CPU 上,randomize_va_space=1 可能会造成系统系能低下。


http://www.niftyadmin.cn/n/1262340.html

相关文章

Windows Phone SDK for Windows 8

首先安装Games for Windows – LIVE Redistributable即可。 前两天刚好装了Windows 8 消费者预览版,用来学习Metro style Apps 开发。本来呢,也还正在学习Windows Phone 7开发。双系统,继续学习WP7的话,还得重启切换回Win7&#x…

《转》linux内核如何管理内存

在仔细审视了进程的虚拟地址布局之后,让我们把目光转向内核以及其管理用户内存的机制。再次从gonzo图示开始: Linux进程在内核中是由task_struct的实例来表示的,即进程描述符。task_struct的mm字段指向内存描述符(memory descript…

Qt Creator中的snippets

最近公司的一个项目使用Qt/QML进行开发。 项目有点小大,团队人员比较多。想规范大家的文件头、函数注释等。以前一直用VC的,刚开始使用Qt Creator,还在到处找类似VC助手的插件。 后来发现在Qt Creator本身就提供了snippets的功能,…

震撼人生的短文

一、女浴室起火,里面人乱作一团,赤身往外跑,只见大街上白花花一大群,一老者大喊“快捂住”,众裸女突然醒悟,但身上要紧部位有三处,手忙脚乱捂不过来,不知所措。这时老者又大喊&#…

哲文

1.别为你自己和别人下定论,你所看到听到的可能只是一面,为这个失去可能的朋友,很不值。 2.你可以有喝醉的时候,我们可以接受,但是你要明白和真正的朋友一醉才能让伤心事方休,否则,你只会是别人的…

PHP截取字符串

function GetBetween($content,$start,$end){$r explode($start, $content);if (isset($r[1])){$r explode($end, $r[1]);return $r[0];}return ; }echo GetBetween("abcdefghijklmnopqrstuvw","def","t"); //输出:ghijklmnopqrs转…

《转》《七年之前与七年之后》

同样是高中的时候在《意林》看到的一篇文章!如今高考,拿出来共勉,共勉我们这些为高考付出了汗水的人! 原文: 1993年夏季对我们小镇中学来说异乎寻常,史无前例,两位女孩儿分摘中考冠、亚军&#…

linux设置ip.dns.gateway

静态方法注意:所有操作均使用root用户 #ifconfig eth0 192.168.0.1 netmask 255.255.255.0 up 说明: eth0是第一个网卡,其他依次为eth1,eth* 192.168.0.1是给网卡配置的第一个网卡配置的ip地址 …