谁清理用于存储堆栈的ram?

时间:2017-10-13 09:54:39

标签: assembly operating-system

据我所知,我创建的每个变量都存储在内存中(ram或pagefile idc)。

因此,当我将变量存储在特定的虚拟地址中时,它实际上会存储在实际内存中的某个位置。 根据我的理解,应用程序不会直接清理堆栈 - 比如转到那些地址并将所有内容设置为零,它只会递增/递减堆栈指针,而另一个函数使用的内存可能会在以后重新使用不同的功能。这就是我们创建局部变量时需要初始化它的原因。

所以应用程序本身并没有转到 ram 中的那些地址并再次归零,所以我的问题是谁做了?所以下一个过程将能够使用这些确切的ram地址。

4 个答案:

答案 0 :(得分:8)

在同一个程序中,通常没人会清理。堆上新分配的内存和堆栈上的新局部变量可以包含旧数据(如果没有以其他方式初始化)。如果你不小心初始化它可能会导致间歇性的错误,或被黑客用来揭示“秘密”数据。

当您启动新程序时,操作系统应负责清除内存。这通常内置于分页系统中:当您请求页面时,您应该将其归零。但是操作系统之间的细节差异很大。

答案 1 :(得分:1)

在评论中进行了冗长的讨论后,我认为一些总结性答案对这个问题实际上是有意义的(我认为问题并不是那么糟糕 - 值得投票,毕竟你问的是特定的编程概念,只是误解了它可能会让人烦恼,但对我而言,这似乎是关于编程的问题。)

首先,操作系统在其内部结构中跟踪已用/可用内存,存储指针/地址范围等内容,使用"页面"内存而不是单个字节。因此,如果操作系统内部数据中的物理地址范围为0x10000-0x1FFFF的内存被跟踪,那么实际的内存内容就不会对操作系统感兴趣,因为它是免费的。#34;免费。字节的内容并不重要。如果某个进程声明了该存储区域,则操作系统会在其内部数据中对其进行跟踪,因此在该进程终止时,它会将该区域标记为“#34; free"”,即使该进程明确没有进行“处理”。设法在终止前释放它。

实际上由于性能原因,操作系统通常不会在分配请求时清除内存(虽然我猜*某些安全加固的操作系统实际上可能会在每个终止进程后清除RAM,以确保将来不会泄漏任何恶意或敏感内容下一个进程重用相同的物理内存)。如果应用程序是用语言编写的,以确保清除新分配的内存,则该语言运行库负责提供该内存。

例如,C和C ++不保证归零内存(再次出现性能原因,清除需要时间),但它们在libc运行时代码中有堆内存管理器代码,添加到从C源编译的每个应用程序并使用默认库和运行时。堆管理器从较大的块中分配OS中的可用内存,然后根据用户代码对其进行微管理,支持new/delete/malloc/free,实际上不直接转到OS内存管理器,这是当内部C运行时耗尽其当前的可用内存池时,它将执行什么操作。

因此,不需要为操作系统回收内存的零值,它必须为"零"只有关于RAM的哪些部分正在使用以及由哪个进程使用的内部数据。

这个操作系统内存管理器代码可能并不简单(我从不打扰检查实际的实现,但是如果你真的很喜欢它,那么请阅读一些关于操作系统架构的书,你也可以研究当前操作系统的来源),但是我在启动时原则上猜测它会映射可用的物理内存,将其切换到不同的区域(有些区域是用户代码的禁止区域,有些区域是内存映射的I / O设备,所以它们可能会限制在除了设备的特定驱动程序,通常最大的块是用户应用程序的免费内存,并保留可用内存列表和#34;页面"或者操作系统想要管理它的任何粒度。

那么谁清理RAM(和其他资源) - 操作系统,在终止某个进程时,应该以这种方式设计好的操作系统,它将能够通过该终止进程检测所有被阻塞的资源,并回收它们返回(没有来自流程代码本身的合作)。对于较旧的操作系统,这部分有点缺陷并且操作系统随着时间的推移不断耗尽某些类型的资源,需要定期重新启动,但是任何可靠的操作系统(如大多数UNIX系列操作系统)都可以运行几年没有干预或泄漏任何东西。

为什么我们有垃圾收集器和其他内存管理方法:

因为作为应用程序的程序员,您决定应用程序将使用多少资源。如果您的应用程序将在后台持续运行并始终分配新资源,而不释放它们,最终将耗尽操作系统的可用资源,从而影响整个机器的性能。

但通常当你编写应用程序时,你不想微内存管理,所以如果你在一个地方分配100个字节,而在另一个地方分配另外100个字节,那么你不需要它们更多,但你需要200个字节,你可能不想编写复杂的代码来重用以前分配的已停止的100 + 100个字节,在大多数编程语言中,让他们的内存管理器更早收集它们更简单分配(例如:在C / C ++中由free/delete,除非您使用自己的内存分配器或垃圾收集器,在Java中您只需删除对实例的所有已知引用,GC将确定代码不需要内存并回收它,并分配全新的200字节块。

因此,内存管理器和GC对程序员来说非常方便,可以更简单地编写通用应用程序,这些应用程序只需要分配合理数量的内存并及时将其释放回来。

一旦你开始研究一些复杂的软件,例如电脑游戏,那么你需要更多的技能,计划和关怀,因为那时性能很重要,而且只需要根据需要分配一小块内存就会变得非常糟糕。

例如,假设粒子系统为每个粒子分配/释放内存,而游戏每分钟发出数千个内存,并且它们只活几秒钟,这将导致非常分散的内存管理器状态,这可能导致其崩溃时该应用程序将突然要求大量的内存(或者它将要求操作系统另一个,随着时间的推移缓慢增加内存使用,然后游戏将在几小时的播放后崩溃,因为操作系统的可用内存耗尽)。在这种情况下,程序员必须深入研究其内存的微观管理,例如,为游戏过程的总生命周期仅分配一次10k粒子的大缓冲区,并跟踪自己使用哪些槽并释放哪些槽,以及处理应用程序同时请求10 + k粒子时,情况优雅。

另一层隐藏的复杂性(来自程序员)是能够“交换”的操作系统。内存到磁盘。应用程序代码不知道特定的虚拟内存地址导致不存在的物理内存,这是由操作系统捕获的,操作系统知道该内存实际存储在磁盘上,所以它确实找到了一些其他的空闲内存页(或交换机)从其他页面读取内容,从磁盘读取内容,将该虚拟地址重新映射到新的物理内存地址,并将控制返回到过程代码,该代码试图访问这些值(现在可用)。如果这听起来像是一个非常缓慢的过程,那就是为什么PC上的所有东西都会爬行"当你耗尽空闲内存并且操作系统开始将内存交换到磁盘时。

而且,如果你要编写一些用敏感值操作的东西, *你* 应该在你自己之后清除未使用的内存,所以它不会泄漏到将来会收到的一些进程在您的进程释放(或终止)后,操作系统将使用相同的物理内存(在这种情况下,通过使用随机值将其删除,可以更好地清除"内存,因为有时甚至可以使用零值向攻击者泄漏一些小的提示,比如加密密钥有多长,而随机内容是随机的=无信息,只要RNG有足够的熵)。了解特定语言内存分配器的详细信息也很好,因此您可以在此类应用程序中使用例如特殊分配器来保证敏感数据的内存不能交换到磁盘(因为那时敏感数据存储在磁盘上)如果是交换),或者例如在Java中,你不要使用String来处理敏感数据,因为你知道Java中的字符串有自己的内存池,而且它们是不可变的,所以如果有人设法检查你的VM的字符串内存池的内容,它基本上可以读取你在运行的应用程序中使用过的所有字符串(我认为在字符串池中也可以使用某些GC,如果你用尽它,但它&# 39;没有普通的GC完成,普通的GC只回收对象实例,而不是String数据)。所以在这种情况下,你应该让程序员尽可能地确保你在内存中实际销毁这些值,当你不再需要它们时,只是释放内存是不够的。

  

操作系统何时为示例启动数据 - 创建变量或进程何时开始运行?

您是否意识到代码本身需要内存?操作系统确实将可执行文件从磁盘加载到内存中,首先是存储元数据的一些固定部分,告诉操作系统可以进一步加载多少可执行文件(代码+预先初始化的数据),各个部分的大小(即为数据分配多少内存+ bss部分),以及建议的堆栈大小。二进制数据从文件加载到内存中,因此它有效地设置了内存值。然后操作系统为进程准备运行时环境,即创建一些虚拟地址空间,设置各个部分,设置访问权限(如代码部分是只读的,数据是无执行的,如果操作系统是这样设计的话) ),同时它将所有这些信息保存在其内部结构中,以便以后可以随意终止进程。最后,当运行时环境准备就绪时,它会跳转到用户模式下的代码入口点。

如果它是一些带有默认标准库的C / C ++应用程序,它将进一步调整环境以使C运行时初始化,即它可能会立即从OS分配一些基本堆内存并设置C内存分配器,准备stdin / stdout / stderr流并根据需要连接到其他OS服务,最后调用main(...)并传递argc / argv。但是全局变量int x = 123;之类的东西已经是二进制文件的一部分,并且由操作系统加载,只有在启动应用程序时libc才会初始化更多动态内容。

因此操作系统确实为代码+数据分配了例如8MiB的RAM,并设置了虚拟空间。从那以后它就不知道应用程序的代码是什么(只要它不会触发一些监护人,比如访问无效的内存,或者不会调用某些OS服务)。操作系统不知道应用程序是否创建了一些变量,或者是否在堆栈上分配了一些局部变量(最多它会通过捕获无效的内存访问时注意到堆栈在原始分配的空间之外增长,此时它可能要么崩溃应用程序,要么它可以将更多的物理内存重新映射到堆栈正在增长的虚拟地址区域,并使应用程序继续使用新的堆栈内存。)

所有变量初始化/ etc都发生在加载二进制文件时(通过操作系统),或者它们完全受应用程序代码的控制。

如果在C ++中调用new,它将调用C运行时(该代码在构建可执行文件时附加到您的应用程序),这将从已设置的内存池中为您提供内存,或者它确实耗尽了备用内存,它将为一些大块调用操作系统堆分配,然后由clib的C内存分配器再次管理。并非每个new/delete都调用操作系统,只有非常少的操作系统,微操作系统由C运行时库完成,存储在可执行文件中(或者根据需要通过OS服务从.DLL / .so文件动态加载)加载动态代码。)

就像JVM是实际的应用程序代码一样,也可以执行各种内务处理,同时实现GC代码等等...... JVM必须清除已分配的内存,然后再将其传递给Java new .class代码,因为它是如何定义Java语言的。操作系统再次不知道发生了什么,它甚至不知道该应用程序是虚拟机从.class文件解释一些java字节码,它只是一些启动和运行的过程(并询问操作系统)服务,因为它。)

您必须了解在用户模式和内核模式之间CPU模式的上下文切换是非常昂贵的操作。因此,如果应用程序每次修改一些微小的内存时都会调用OS服务,性能会下降很多。由于现代计算机具有足够的RAM,因此可以更容易地为启动过程提供大约10-200MB的区域(基于来自可执行文件的元数据),并让它在需要更多动态时处理它们。但是任何合理的应用程序都会最小化OS服务调用,这就是为什么clib拥有它自己的内存管理器并且不为每个new使用OS堆分配器(OS分配器也可以工作)具有通用代码无法使用的粒度,例如允许仅在MiB块中分配内存等。)

在像C / Java这样的高级语言中,你有很大一部分由标准库提供的应用程序代码,所以如果你刚刚开始学习那种语言而你没有想到它在机器代码级别内部是如何工作的,您可以将某些功能视为理所当然,或由OS提供。事实并非如此,操作系统只提供非常基本和裸露的服务,其余的C / Java环境由代码提供,代码从标准库链接到您的应用程序。如果你创造了一些'#34; hello world"通常在C语言中,90%的二进制大小通常是C运行时,实际上只有少数字节来自那个hello world源。当你执行它时,在你的main(...)被调用之前,有数千条指令被执行(在你的进程中,从你的二进制文件,不包括操作系统加载器)。

答案 2 :(得分:1)

它将是控制一个应用程序与另一个应用程序之间的内存(技术上和期间)的实体。所以这是理想的操作系统。当然可以将其放在应用程序上,但您会遇到安全问题。

在应用程序中,根据您的指示,看到每个功能的清理没有发生,这有点微不足道。

我们习惯于使用处理器(尝试)保护一个应用程序与另一个应用程序彼此隔离的大型命名操作系统将为每个应用程序/线程创建一个堆栈,而不是每个都有一个大的通用堆栈空间分享。在启动时提供给该应用程序的整个内存可以在分支到应用程序之前初始化为某个值,不一定是0x00也不是0xFF。根据该语言/实现的规则初始化.text,.data,.bss和内存/空间部分的其他概念,其余部分可能是也可能不是。但是在应用程序不受信任的环境中,操作系统是无论如何加载和启动的操作系统,都是在应用程序启动或退出时执行此清除/清理的实体(或者实际上任何时候都是一块内存为该应用程序分配或取消分配,也可以是运行时。)

答案 3 :(得分:1)

我认为你对计算机比较陌生。你缺少的是逻辑和虚拟内存翻译的大话题。

您的进程内存被组织成逻辑页面。这些逻辑页面可能映射到物理页面框架。

一个进程正在使用的物理页面框架(假设它已完成)可能会被另一个进程重用。要重用,物理页面框架必须映射到逻辑页面。在映射期间可能会发生很多不同的事情,可能会改变该页面中内存的值。

两个进程可能想要共享页面。在这种情况下,内存根本不会改变。

页面框架可能会映射到已存储在页面文件中的逻辑页面。在这种情况下,逻辑页面将从页面文件中加载。

页面可能没有与之关联的数据,因此操作系统将在映射之前清除页面。清算可能为零或其他值。

AIX O / S曾经喜欢将数据清除为值DEADBEAF。

总之,您的问题的答案取决于操作系统如何将物理页面框架映射到流程中的逻辑页面。