[翻译] 简谈编译、汇编、链接和加载 – part V
W.8 运行时数据结构
一个进程就是一个运行着的程序。这意味着操作系统已经把程序的可执行文件加载到内存中,安排好了程序对它的参数和环境变量的访问权限,开始执行它的代码。通常一个进程有5个内存区域,如下表所示:
Segment | 描述 |
代码 – text segment | 通常称为 text segment, 这里是程序的可执行代码指令区. 比如, 当一个程序同时运行多个实例时,Linux/Unix 会尽可能让他们共享text segment。 这个内存区的内容对应可执行文件的 text section. |
初始化的数据 – data segment | 非零值的静态分配的数据以及全局数据被加载到data segment. 一个程序有多个进程时,每个进程都有自己的data segment。这个内存区的内容对应可执行文件的 data section. |
未初始化数据 – bss segment | BSS 表示 ‘Block Started by Symbol’(符号开始的区域)。零值的静态分配的数据以及全局数据被加载到bss segment. 一个程序有多个进程时,每个进程都有自己的bss segment。运行时,bss里面的数据会被放入data segment。在可执行文件中,它们被保存在BSS section. 对Linux/Unix 的ELF格式,只有初始化为非零值的变量才会占用磁盘空间。 |
heap(堆) | heap部分是动态内存(通过 malloc(), calloc(), realloc() 以及C++中的 new)的来源. Heap中的所有内容都是匿名的,因此你只能通过指针访问它的一部分。Heap中的内存被分配以后,进程的地址空间相应增长。虽然将(不再使用的)内存还给系统是可能的并会让进程的地址空间缩小,但是事实上几乎没有程序会这么做,因为它还会被分配给其它的进程。通过free()和delete释放的内存会回到heap中,并会在heap上形成洞。Heap通常向上增长,这意味着后续添加到heap中的内容其内存地址 要大于前面的内容。Heap通常开始于BSS后面 。Heap的结束以一个名为break的指针标志。你不能引用break后面的内容,但你可以通过brk()或sbrk()系统调用将break指向一个新地址来增加heap的地址空间。 |
Stack(栈) | Stack是本地(自动)变量内存的来源。在C程序中,所有在函数体内定义的非静态变量都称为本地变量 。栈上数据的存储遵从后进先出(LIFO)原则。Stack保存了本地变量,临时信息,函数的参数,返回地址之类的信息。当一个函数被调用时,一个stack frame会被创建出来,并被PUSH到stack的顶端。这个stack frame包含的信息包括,调用此函数的地址,函数结束后应该返回的地址,函数的参数,本地变量以及其它函数需要的信息。信息的顺序每种系统和编译器不尽相同。当一个函数返回时,这个stack frame会被从stack上POP下去。stack的地址向下增长,意味着处于更深层的调用使用的内存地址更小。 |
可执行文件section (磁盘文件) | 地址空间 segment | 程序内存 segment |
.text | Text | Code |
.data | Data | Initialized data |
.bss | Data | BSS |
– | Data | Heap |
– | Stack | Stack |
当一个程序正在运行时,初始化的数据,BSS和heap区域通常被放在一个连续的内存区,称为data segment。
Stack segment和代码segment被data segment分隔开来。虽然理论上heap和stack可能会相遇,操作系统会阻止这种事件发生。不同的sections和segments的关系汇总成下表:
W.9 进程 (IMAGE)
下图展示了一个典型的C程序进程的内存布局。进程从它的基地址处开始加载segments(图中的text和data)。主stack(main stack)位于下方(译者:什么东西的下方?)并向下增长。任何其它的线程或函数调用都会有它们自己的stack,位于main stack下方。每个栈帧(stack frame)都会被一个保护页分离开来,用于侦测栈溢出(stack overflow)。
Heap位于进程的上方并向上增长。在进程地址空间的中间部分,有一个为共享对象保留的区域。当创建一个新进程时,进程管理器首先从可执行文件中把这两个(译者:哪两个?)segment映射到内存中。然后再解码程序的ELF头部信息。如果头部信息指明可执行文件使用了共享库来链接,进程管理器会提取动态解释器的名称。动态解释器指向一个包含运行时链接器代码的共享库。进程管理器会把这个共享库加载到内存然后把控制权交给这个库文件中的运行时链接器代码。
—待续