编译器如何在内存中布局代码

Posted

技术标签:

【中文标题】编译器如何在内存中布局代码【英文标题】:How does compiler lay out code in memory 【发布时间】:2013-10-06 17:49:58 【问题描述】:

好的,我有一个菜鸟学生的问题。

所以我熟悉堆栈包含子程序调用,堆包含可变长度数据结构,全局静态变量分配到永久内存位置的事实。

但是,这一切在理论层面上是如何运作的呢?

编译器是否只是假设它拥有从地址 0 到地址无穷大的整个内存区域?然后就开始分配东西?

它在哪里布置指令、堆栈和堆?在内存区域的顶部,内存区域的末尾?

然后这如何与虚拟内存一起工作?虚拟内存对程序是透明的?

很抱歉有一个问题,但我正在学习编程语言结构,它一直提到这些区域,我想在更实际的层面上理解它们。

非常感谢!

【问题讨论】:

我可以要求快速跟进吗?程序编译时调用函数、创建局部变量并将它们添加到堆栈的底层机制是否包含在程序中?还是机器是操作系统的一部分? 【参考方案1】:

全面的解释可能超出了本论坛的范围。整篇文章都致力于这个主题。但是,从简单的角度来看,您可以这样看待它。

编译器不会在内存中布置代码。它确实假设它自己拥有整个内存区域。编译器生成目标文件,目标文件中的符号通常从偏移量 0 开始。

链接器负责将目标文件拉到一起,将符号链接到它们在链接目标中的新偏移位置,并生成可执行文件格式。

链接器也不在内存中布局代码。它将代码和数据打包到通常标记为.text 用于可执行代码指令和.data 用于全局变量和字符串常量之类的部分中。 (并且还有其他部分用于不同的目的)链接器可能会向操作系统加载器提供一个提示,将符号重定位到哪里,但加载器不必强制执行。

操作系统加载器解析可执行文件并决定代码和数据在内存中的布局位置。其位置完全取决于操作系统。通常堆栈位于比程序指令和数据更高的内存区域并向下增长。

每个程序的编译/链接都假设它拥有自己的整个地址空间。这就是虚拟内存的用武之地。它对程序完全透明,完全由操作系统管理。

虚拟内存的范围通常从地址 0 到平台支持的最大地址(不是无穷大)。这个虚拟地址空间被操作系统划分为内核可寻址空间和用户可寻址空间。假设在一个假设的 32 位操作系统上,0x80000000 上面的地址是为操作系统保留的,下面的地址是供程序使用的。如果程序试图访问此分区之上的内存,它将被中止。

操作系统可能决定堆栈从最高可寻址用户内存开始,并随着位于低得多的地址的程序代码向下增长。

堆的位置通常由您构建程序的运行时库管理。它可以从您的程序代码和数据之后的下一个可用地址开始。

【讨论】:

这很棒。正是在寻找。关于这方面的好网站/书籍有什么推荐吗?我什至不知道用谷歌搜索什么。我的意思不是解析树,而是更多关于在将解析树转换为代码时所有这些东西在低级别实际上如何工作。 一个不错的起点是谷歌搜索“Windows 内存布局”。这个链接出现在第一页,看起来信息量很大:Anatomy of a Program in Memory。从这里你可能会在谷歌上找到一些其他的概念。【参考方案2】:

这是一个涉及很多话题的开放性问题。

假设典型的编译器 -> 汇编器 -> 链接器工具链。编译器并不知道很多,它只是对堆栈相关的东西进行编码,不关心堆栈的大小或位置,那是堆栈的目的/美,不在乎。编译器生成汇编器,将汇编器组装成一个对象,然后链接器获取一些风格或命令行参数的信息链接器脚本,告诉它内存空间的详细信息,当你

gcc hello.c -o hello

您安装的 binutils 有一个针对您的目标(windows、mac、linux,无论您在什么平台上运行)定制的默认链接器脚本。该脚本包含有关程序空间从哪里开始的信息,然后它知道从哪里开始堆(在文本、数据和 bss 之后)。堆栈指针可能由该链接描述文件设置和/或操作系统以其他方式管理它。这定义了你的堆栈。

对于带有 mmu 的操作系统,这就是您的 windows 和 linux 以及 mac 和 bsd 笔记本电脑或台式电脑所拥有的,那么是的,每个程序都是假设它有自己的地址空间从 0x0000 开始编译的,这并不意味着程序链接到从 0x0000 开始运行,这取决于操作系统,操作系统规则是什么,例如,有些从 0x8000 开始。

对于类似桌面的应用程序,从程序的角度来看,它有点像单个线性地址空间,您可能首先拥有 .text,然后是 .data 或 .bss,然后在所有这些之后,堆将在某个时间点对齐那。然而,它设置的堆栈通常是向上的并且向下工作,但这可能是处理器和操作系统特定的。该堆栈通常位于世界的程序视图中其内存的顶部。

虚拟内存对所有这些都是不可见的,应用程序通常不知道或关心虚拟内存。如果以及当应用程序获取指令或进行数据传输时,它会通过操作系统配置的硬件并在虚拟和物理之间进行转换。如果 mmu 指示故障,意味着空间尚未映射到物理地址,这有时可能是故意的,然后适用术语“虚拟内存”的另一种用法。第二个定义,操作系统可以例如获取其他一些内存块,例如您的或其他人的,例如将其移动到硬盘上,将其他块标记为不存在,然后将您的块标记为具有一些内存然后让你执行时不知道你被一些你不知道你必须从别人那里拿走的公羊打断了。您的应用程序设计上不想知道这些,它只想运行,操作系统负责管理物理内存和为您提供虚拟(从零开始)地址空间的 mmu...

如果您要进行一些裸机编程,首先没有 mmu 的东西,然后使用微控制器、qemu、raspberry pi、beaglebone 等,您可以使用编译器、链接器脚本和配置来弄脏您的双手一个mmu。我会为此而不是 x86 使用 arm 或 mips,只是为了让您的生活更轻松,整体大图都直接跨目标转换。

【讨论】:

谢谢!让我失望的一件事是您必须声明一个地址才能在程序集中存储变量,所以我认为编译器/程序集生成器只是假设它有自己的完整地址空间。【参考方案3】:

视情况而定。

如果您正在编译必须从头开始的引导加载程序,您可以假设您已经为自己获得了整个内存。

另一方面,如果你正在编译一个应用程序,你可以假设你自己已经拥有了整个内存。

细微的区别在于,在第一种情况下,您拥有自己的所有物理内存。作为引导加载程序,RAM 中还没有其他内容。在第二种情况下,内存中有一个操作系统,但它会(通常)为您设置虚拟内存,以便您看起来拥有自己的整个地址空间。不过,通常您仍然需要向操作系统询问实际内存。

后者确实意味着操作系统强加了一些规则。例如。操作系统非常想知道程序的第一条指令在哪里。一个简单的规则可能是您的程序总是从地址 0 开始,因此 C 编译器可以将int main() 放在那里。操作系统通常想知道堆栈在哪里,但这已经是一个更灵活的规则。就“堆”而言,操作系统真的不在乎。

【讨论】:

谢谢!因此,可以肯定地假设您拥有我所看到的全部记忆。

以上是关于编译器如何在内存中布局代码的主要内容,如果未能解决你的问题,请参考以下文章

虚继承之单继承的内存布局(VC在编译时会把vfptr放到类的头部,这和Delphi完全一致)

继承类的内存布局

printf() var-arg 引用如何与堆栈内存布局交互?

C中的结构内存布局

C++类对象的内存布局

关于Class对象类加载机制虚拟机运行时的内存布局的全面解析和推测