关于编译程序如何与操作系统交互的问题
Posted
技术标签:
【中文标题】关于编译程序如何与操作系统交互的问题【英文标题】:Questions on how compiled programs interact with the operating system 【发布时间】:2011-02-14 06:27:40 【问题描述】:已经有一段时间了,我一直在使用 C/C++ 进行编程,但有些领域仍然让我难以理解。也许我没有阅读写得很好的和权威的材料。
(1) 在 Linux/Unix 中,用户程序的大小是否有限制?程序可以拥有的最大堆栈大小?用户程序可以使用的堆中的最大内存量?
(2) 我了解 C 可执行文件具有数据部分、代码部分和堆栈部分。如果程序要进行许多递归调用,它将需要大量的堆栈。这个堆栈是预定义的大小还是会随着递归的增加而增长。在增长的情况下,程序的地址空间是否也必须动态增加?如果是这样,那会不会减慢程序的速度?
(3) 同样,当程序mallocs时,当堆内存分配给程序时,堆的那个区域是否需要添加到程序的地址空间?因此在这种情况下,也需要更新程序的页表。我的理解正确吗?
(4) 为什么 2 个文件(我打算将它们组合成一个可执行文件)不能有一个同名的全局变量。这将有助于了解目标文件的外观。
加法:
我正在阅读来自http://www.open-std.org/jtc1/sc22/wg...docs/n1256.pdf 的 ISO C99 标准。 它在第 42 页上说:
6.2.2 标识符的链接 1 一个标识符可以在不同的作用域或同一作用域中多次声明 通过称为链接的过程来引用相同的对象或函数。有 三种链接:外部、内部和无。
2 在构成整个程序的一组翻译单元和库中,每个 具有外部链接的特定标识符的声明表示相同的对象或 功能。在一个翻译单元内,每个标识符的声明都带有内部 链接表示相同的对象或函数。每个标识符的声明都没有 链接表示一个唯一的实体。
3 如果对象或函数的文件范围标识符的声明包含存储类说明符 static,则该标识符具有内部链接。
4 对于在一个范围内使用存储类说明符 extern 声明的标识符 该标识符的先前声明是可见的,如果先前声明指定了内部或外部链接,则后面声明的标识符的链接与先前声明中指定的链接相同。如果前面的声明不可见,或者前面的声明没有指定链接,则标识符具有外部链接。
5 如果函数的标识符声明没有存储类说明符,则其链接的确定与使用存储类说明符 extern 声明时完全相同。如果对象的标识符声明具有文件范围并且没有存储类说明符,它的链接是外部的。
读完后,如果我在 2 个源文件中声明一个变量,比如 say int a。然后根据规则 5 和 4 都具有外部链接。然后根据规则 2,两者都应该引用同一个对象。那么为什么编译器会产生问题。在标准中暗示我们不能在 2 个源文件中这样声明,这应该会引发编译错误。
谢谢。
【问题讨论】:
linuxhowtos.org/Tips%20and%20Tricks/ulimit.htm -1。四个问题应该作为四个问题发布,以便将来可以搜索。当前标题不可搜索 【参考方案1】:回答您的问题-
大多数操作系统使用虚拟内存让每个程序认为它拥有所有地址空间。这意味着通常程序大小的限制是系统中的物理内存量,减去通常为无效(认为是 NULL)指针和内核保留的少量内存。最大内存限制通常取决于平台,但在 32 位系统上,您的程序通常可以获得近 4GB 的内存,而在 64 位系统上则更多。当然,您还必须考虑磁盘的大小,这限制了您可以拥有多少虚拟内存。从理论上讲,您可以编写一个大到无法将其放入内存的程序,但除非您使用的是嵌入式设备(这确实是一个问题),否则我怀疑这永远不会发生。
在包括 C 和 C++ 在内的大多数编程语言中,堆栈大小在编译时并不固定,而是从小开始并随着程序运行而增长。然而,堆栈增长的方式通常使得这特别便宜 - 要获得更多空间,您只需稍微增加堆栈指针即可。如果这将您带入当前未为程序分配的内存,则操作系统通常会通过将页面与堆栈现在所在的虚拟地址相关联来为您分配内存,这比进行堆分配要快得多。从长远来看,这样做的成本通常可以忽略不计,因此不要气馁使用堆栈内存。有趣的是,一些较旧的编程语言,即 FORTRAN 的第一个版本,没有动态堆栈空间,因此不可能递归。几乎所有现代语言都消除了这些限制。
您是对的 - 当需要更多堆空间时,通常会调整页表以增加堆空间。许多内存分配器选择将大部分内存放入匿名内存映射文件中,以避免直接为此目的使用堆空间,但原理基本相同 - 更新页表以为新内存腾出空间。
如果您在不同的文件中有两个全局变量链接在一起,那么这两个目标文件都将包含符号链接,表示它们需要引用具有该名称的变量,并且两个目标文件都将包含定义说它们提供了这个名称的符号。当您尝试将它们链接在一起时,链接器会注意到已在两个位置定义了相同的符号名称,并会报告错误,因为它不确定应该将其中的哪一个用作该全局变量的“该”实例。为了解决这个问题,至少在 C 中,您可以标记全局变量 static
以赋予它们内部链接。这使得符号不会全局导出,因此生成的目标文件可以在内部解析引用或修改名称,使其不会与其他文件中的其他符号冲突。 C++ 允许这样做,以及匿名命名空间功能,以实现相同的效果。
希望这会有所帮助!如果有人在这里发现错误或歧义,请告诉我,我很乐意纠正它们。
【讨论】:
好的,感谢您提供的指针:“但是,堆栈的增长方式通常会特别便宜 - 要获得更多空间,您只需稍微增加堆栈指针即可。”【参考方案2】:是的,是的,是的。请参阅 bash 或 man getrlimit 中的“帮助 ulimit”。
堆栈大小在程序启动时设置,不能增加。地址空间不会随着您使用比以前使用的更多的堆栈而增长,但内存使用会增长。
当使用“拆分堆栈”时(例如在 Google 的 Go 中,但正在努力在 gcc 和其他语言的其他编译器中允许这样做),分配的额外内存不是“在堆栈上”和堆栈指针被调整。这是动态管理的,因为函数被调用并返回。
堆可能会根据需要增长。请参阅 man sbrk 以简要了解这是如何发生的,或查看 various malloc implementations。您似乎明白其中的要点。
因为,至少对于 C 和 C++,全局变量在整个程序中只能定义一次。两个翻译单元(您可以将 TU 视为 .o 文件)可以使用同名的全局变量,但它只能定义一次,并且必须在其他 TU 中声明(使用正确的类型)。我不认为了解目标文件的细节在这里会有所帮助,但了解 C++ 中所谓的单一定义规则 (ODR) 的细节,或者它在您使用的任何语言中的等价物,可能会很有用。
关于编辑,您可能在两个 TU 中定义一个 int:
int this_is_a_definition;
你不能这样做。您应该在标题中声明它:
extern int this_is_a_declaration;
然后在需要变量的地方包含该标头,并在一个 TU 中定义变量。当然,如果您不想在不同的 TU 中使用 same 变量,那么您可能需要一个“内部”名称,例如使用 namespace-scope static 或未命名的命名空间:
static int local_to_this_TU;
namespace
int another_local_to_this_TU;
【讨论】:
所以,我们对可增长的堆栈段有很大的分歧。需要一些证明链接。 @ulidtko:我应该说得更清楚些,但我认为您不同意我(希望)通过这次编辑澄清的内容。这是什么? 感谢您对 ulimit 的指点。从概念上讲,我确实理解虚拟内存概念允许程序与总虚拟内存一样大(这比物理内存还多),但是这个指针填补了实际理解中的空白,即如果系统管理员不想要该怎么办t 允许用户使系统对其他用户不可用。以上是关于关于编译程序如何与操作系统交互的问题的主要内容,如果未能解决你的问题,请参考以下文章
.NET 应用程序与 Google Chrome 扩展程序的交互