谁清洁用于存储堆栈的 RAM



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

因此,当我将变量存储在特定的虚拟地址中时,它实际上将存储在实际内存中的某个位置。 据我了解,应用程序不会从字面上清理堆栈 - 就像转到这些地址并将所有内容设置为零一样,它只会增加/减少堆栈指针,并且另一个函数使用的内存可能会在以后被另一个函数重新使用。这就是为什么当我们创建一个局部变量时,我们需要初始化它。

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

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

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

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

在应用程序中,看到每个函数的清理不会像您指出的那样发生,这有点微不足道。

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

我认为你对计算机比较陌生。您缺少的是逻辑和虚拟内存转换的大主题。

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

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

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

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

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

AIX O/S 过去喜欢将数据清除为值 DEADBEAF。

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

在评论中进行了长时间的讨论之后,我认为一些总结性答案实际上对这个问题有意义(我也认为这个问题并没有那么糟糕 - 值得反对票,毕竟你问的是特定的编程概念,只是有点误解它,可能这让人们很恼火,但对我来说,这看起来像是关于编程的问题)。

首先,操作系统在其内部结构中跟踪已用/可用内存,存储诸如指针/地址范围之类的东西,使用内存的"页"而不是单个字节。因此,操作系统对内存的实际内容不感兴趣,如果物理地址范围为 0x10000-0x1FFFF 的内存在操作系统内部数据中被跟踪为"免费",则它是免费的。字节的内容无关紧要。如果某个进程声明了该内存区域,操作系统确实会在其内部数据中跟踪该区域,因此在该进程终止时,它会将该区域标记为"空闲",即使该进程在终止前明确没有设法释放它。

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

例如,C 和C++ 不保证将内存归零(同样是性能原因,清除需要时间),但它们在 libc 运行时代码中具有堆内存管理器代码,添加到从 C 源代码编译的每个应用程序中,并使用默认库和运行时。堆管理器以更大的块从操作系统中分配可用内存,然后它为用户代码进行微管理,支持new/delete/malloc/free,实际上不会直接进入操作系统内存管理器,这就是内部 C 运行时在耗尽其当前可用内存池时会做的事情。

因此,无需将值归零即可回收操作系统的内存,它只需"清零"其内部数据,即可了解正在使用RAM的哪些部分以及由哪个进程使用。

这个操作系统内存管理器代码可能不是微不足道的(我从来没有费心检查实际实现,但如果你真的喜欢它,请买一些关于操作系统架构的书,你也可以研究当前操作系统的来源),但我想原则上在启动时它会映射可用的物理内存,将其切入不同的区域(有些是用户代码的禁区, 某些范围是内存映射的 I/O 设备,因此除了设备的特定驱动程序之外,它们可能对所有人禁止,通常最大的块是用户应用程序的"可用"内存),并保留类似可用内存"页面"列表或操作系统想要管理它的任何粒度。

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

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

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

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

因此,内存管理器和GC对程序员来说很方便,使编写常见应用程序变得更加简单,这些应用程序只需要分配合理的内存量,并会及时释放它。

一旦你开始处理一些复杂的软件,比如电脑游戏,那么你需要更多的技能、计划和关怀,因为那样性能就很重要,而这种天真的方法只是根据需要分配小块内存会很糟糕。

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

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

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


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

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

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

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

所有变量初始化/等要么在加载二进制文件时发生(由操作系统),要么完全受应用程序代码控制。

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

就像JVM是实际的应用程序代码一样,也做各种内务管理,实现GC代码等......JVM必须在.class代码中将其传递给Java的new之前清除分配的内存,因为这是Java语言的定义方式。操作系统再次不知道发生了什么,它甚至不知道该应用程序是虚拟机从.class文件中解释一些java字节码,它只是一些启动并运行的进程(并根据需要询问操作系统服务)。

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

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

最新更新