为什么对 运行 个 C 程序使用内存布局

Why using memory layout for running a C program

据我所知,当一个 C 程序 运行ning 时,virtual memory 是用 stack segmentlocal variable 创建的,heap 为动态分配, text segment 用于代码,data segment 用于 static variableglobal variable。我不明白为什么我们要把内存分成stackheapdata segmenttext segment? 哪个创建virtual memory,操作系统还是编译器? 根据我的理解,当我们 运行 一个裸机嵌入式程序时,我们将 运行 在物理内存上,所以我们不会在这里有 stackheapdata segment ,这样对吗?

我有点不同意 WeatherVane 链接的问题的公认答案。这里真正重要的是:

堆栈、堆、数据段、文本段——所有这些都是实现细节,C语言本身没有声明。

我同意 P__J__,此时你根本不应该理会它们,在深入研究特定于平台的细节之前先学习通用语言。


就 C 语言而言,有一种叫做 "automatic storage duration". This applies e.g. to variables declared at block scope 的东西,它们的存储在声明时分配,并在声明它们的块离开时释放。

这通常是通过堆栈实现的,但标准没有说明实现。具有大量 CPU 寄存器的平台可以将具有自动存储持续时间的变量放入这些寄存器中。


然后就是"dynamically allocated memory"(通过malloc()等得到的内存,用free()释放),通常是实现 由某种堆,但标准再次没有说明实现。这可以直接映射到所有重要的永久存储。


“数据段”(通常保存值初始化数据)和“文本段”(通常保存可执行代码)同样是实现细节,在本例中是可执行文件文件格式(PE、ELF、...)您显然需要将可执行代码放在某处,并且如果您将变量设置为文字值,该值也必须“存在”某处,但就 C 语言而言,这是编译器/可执行加载器/平台需要担心的问题。

嵌入式平台可能将这些硬编码在 ROM 中,在这种情况下您甚至可能 没有 数据段/文本段...


最重要的是,此时不要为这些烦恼。首先了解该语言,然后 然后 了解其通用原则(如自动存储持续时间和动态分配的内存)如何应用于给定平台。后者通常涉及深入研究编译器/加载器的细节,如果没有牢牢掌握现成的语言,你不应该去那里。

堆栈、堆、数据和文本位于物理内存中,与它没有区别。内存是为不同的目的分配的,在范围和持久性方面具有不同的行为,并有助于链接器为不同的目的分段(或划分)内存。

在许多嵌入式系统中,代码(文本段)和常量数据驻留在 ROM 中,在物理上 不同于 RAM。链接器需要知道 ROM space 在内存映射中的位置。

堆栈是临时的space,用于本地数据存储、函数参数和return call/function 地址。随着函数的调用和变量的进出范围,它会不断地被使用和重用。

堆用于通过malloc() / free()等函数进行动态内存分配。它是从 在 运行 时间 分配的内存,而不是在堆栈上静态分配或自动分配的内存。堆分配一直持续到它们被显式 returned 到堆而不是具有“作用域”并被自动实例化和销毁。

数据段是静态分配的数据所在的地方。这是静态和全局数据所在的位置。此内存中的对象在程序启动时实例化,并在代码执行期间持续存在。

在实践中,静态数据通常有两个段,databssdata 用于显式非零 初始化数据 。它们存在于 read/write 内存中,但此内存的初始化值在 text 中。程序启动时,运行sbeforemain()的启动代码将初始值复制到分配的RAM段。 bss 段被简单地初始化为零——静态数据的默认初始值。

所以:

  • bssdata 必须是不同的 space 以促进有效的初始化。
  • text 必须是不同的,因为它在 ROM 中就地定位和执行,或者在它加载到 RAM 中的系统中,通过复制连续的块来最有效地完成运行时间位置的代码。
  • heap是一个运行时间的内存池。将堆分布在非连续的内存中当然是可能的,但在简单的情况下,它通常是一个连续的块。
  • 堆栈概念是(大多数)微处理器如何在机器级别工作的产物,因此它是编译语言的自然模型。 stack 段本身是 main() 线程中使用的 call/data 堆栈。一些处理器切换到一个单独的堆栈来处理中断(有些则没有)。如果使用多线程,通常每个线程都有自己的堆栈。例如,这些线程堆栈可以从堆中动态实例化或在 bss 中静态分配。

关键是C代码被编译为目标代码,然后链接形成最终的二进制可执行文件。链接器负责定位代码和数据,因此需要内存映射才能知道将什么放在哪里。堆栈必须是连续的,因为这是机器的工作方式,并且本地自动创建和销毁数据需要它。

让我们把事物和技术用语放在正确的上下文中。 stack , heap , text , ..etc 是进程结构或进程内存布局的一部分,而不是您提到的“内存布局”!现在很多人和工程师对过程和程序之间的区别感到困惑,我将在下面的回答中尝试解释。

现在什么是进程?

进程是正在执行的程序的一个实例。另一方面,程序是一个包含一系列信息的文件,这些信息描述了如何构建 运行 时间的进程。此信息包括以下内容:

二进制格式标识:每个程序文件包含元信息描述 可执行文件的格式。两种广泛使用的格式 UNIX 可执行文件是原始的 a.out(“汇编程序输出”)格式 以及后来更复杂的 COFF(通用对象文件格式)。

机器语言指令:这些对程序的算法进行编码。

程序入口点地址: 这标识了指令的位置 应该从哪个程序开始执行。

数据:程序文件包含用于初始化变量的值以及文字 程序使用的常量(例如字符串)。

其他信息:程序文件包含各种其他信息 描述了如何构建一个过程,包括(符号和重定位表共享库和动态链接信息等)。

进程是一个抽象的实体,由内核定义,给哪个系统 分配资源是为了执行程序。从内核的角度来看,一个进程由 user-space 内存组成,其中包含 程序代码和该代码使用的变量,以及一系列内核数据 维护有关进程状态信息的结构。该信息 记录在内核数据结构中的包括各种标识符号 (ID) 与进程、虚拟内存表等关联!

一个进程的内存布局

让我们从此处的进程内存布局图开始:

x-----------------------------------x
x  Kernel data (not accissible to   x
x  the program)                     x
x-----------------------------------x
x program environment variables     x
x-----------------------------------x
x          STACK                    x
x       grows downwards             x
x-----------------------------------x
x                                   x
x        Unallocated Memory         x    
x                                   x
x                                   x
x                                   x
x-----------------------------------x
x                                   x
x        ^                          x
x        ^       HEAP               x
x        | grows upwards            x
x-----------------------------------x
x               BSS                 x
x-----------------------------------x
x       Initialized data            x
x-----------------------------------x
x           Text                    x
x    (the C code in our case)       x 
x-----------------------------------x
x                                   x
x-----------------------------------x
             

分配给每个进程的内存由若干部分组成,通常 简称段。这些细分如下:

正文段:

包含程序的机器语言指令 运行 由过程。文本段被设为只读,以便进程 不会通过错误的指针值意外修改自己的指令。

初始化数据段

包含显式的全局变量和静态变量 初始化。这些变量的值是从可执行文件中读取的 当程序加载到内存中时。

未初始化数据段(BSS)

包含未显式初始化的全局变量和静态变量。在启动程序之前,系统将这个段中的所有内存初始化为0。这通常被称为BSS段。将已初始化的全局变量和静态变量与未初始化的变量放在单独的段中的主要原因是,当一个 程序存储在磁盘上,没有必要为未初始化的分配space 数据。相反,可执行文件只需要记录位置和大小 未初始化的数据段需要,而这个space是由 运行 时间的程序加载器。

堆栈

是一个包含栈的动态增长和收缩段 帧。为每个当前调用的函数分配一个堆栈帧。一种 frame 存储函数的局部变量(所谓的自动变量)、参数、 和 return 值。

是一个可以动态分配内存(用于变量)的区域 在 运行 时间。堆的顶端称为程序中断。此部分和分配由 malloc() 系列(系统调用)维护,仅在 运行 时间内执行。

进程的内存布局掩盖了布局在虚拟内存中的事实!而不是其他人之前所说的物理内存!

现在在大多数现代嵌入式系统中,都有一个实时操作系统 (RTOS),它创建和处理(通常)一个轻量级进程(线程)。在这些系统中,用户(工程师)在管理系统资源(如 malloc)方面具有更大的灵活性,并且由于这些系统中不存在虚拟内存,用户可以确定和处理上述所有部分 ram 映射。

进一步阅读:

一本很棒的书 linux 编程接口(我的大部分回答都来自那里),更多关于 RTOS 的信息请看这里 RTOS