这部分是计算机系统相关的知识,碍于本人才疏学浅,如本文存在疏漏或者错误,还望大佬能帮忙指出,感激不尽。
内存分段
从狭义上讲内存的分段可以分为堆、栈、数据段以及代码段(内存映射区比较复杂,暂不涉及),大致内容可以参考下图:
但其实细说的话其实并没有这么简单。在编译阶段,编译器生成的目标文件(.o或.obj)中至少包括编译后的机器指令代码、数据,以及链接时所需要的的一些信息(比如符号表、调试信息、字符串等)。其中,目标文件将这些信息按不同的属性以“ 段 ”(segment)的形式存储。如下是一些我们经常会提到的段:
这里我们主要分析表格中常见的段信息,虽然还有其他段,例如注释信息段( .comment )等,但它并不是我们关注的重点。
下图是一个目标文件的图例分析,主要分析代码和数据分别存放在哪些段中。图中并没有给出堆栈的信息,因为堆栈只有在程序被装载运行时才会分配内存,而在目标文件这个阶段还没有分配栈段的内存。而且堆栈相对来说比较熟悉,所以也就无伤大雅了。
各段说明
堆和栈我们经常使用,这里就不多说了。堆区就是相当于直接在内存中进行操作,但实际上是在虚拟内存中操作的,并不会直接干扰物理内存。而栈则是程序运行时创建的栈帧。下面我们来介绍剩下数据段和代码段。
代码段:
程序的源代码(包括函数等)编译后的机器指令就会放在代码段,即 .text 段中。在经典的x86体系结构中,代码段通常包含了程序的函数、方法和一些固定的指令集等信息。
数据段:
数据段包括 .data 、 .bss 、 .rodata 三个部分,暂且认为程序中的全局变量和局部变量就是数据段。那么为什么数据段要分成 .data 、 .bss 、 .rodata 三个部分呢?
其主要因素有两个:是否占用内存空间、读写权限如何。
已初始化的全局变量和局部静态变量保存在 .data 段,未初始化的全局变量和局部静态变量一般放在 .bss 段。理论上讲,未初始化的全局变量也是应该放在.data段的,但由于它们都是0,所以为它们在.data段分配空间并且存放数据0也就没有必要了。其中,
.bss
段并不会占用磁盘空间,只会在程序加载到内存时分配相应的空间。这种懒加载的特性可以节省可执行文件的大小,特别是当程序中包含大量未初始化的全局或静态变量时。
.data段和 .bss 段中的都是可读写的数据,而 .rodata 存放的是只读数据,主要是一些const变量、字符串常量等。单独设立 .radata 段的好处是:在程序加载的时候可以将 .rodata 段的属性映射成只读,这样对这个段的任何修改操作都作为非法操作处理。另外在某些平台还可以将 .rodata 段存放在只读存储器,例如ROM,通过硬件保证只读。
那么为什么要把 “代码段” 和 “数据段” 分开存放呢?
当程序被装载后,数据和指令分别被映射到两个虚拟内存区域。数据段对进程来讲是可读写的,而代码段对进程来说是只读的,所以这两个虚拟内存区域的权限可以被分别设置为可读写和只读,防止程序的指令被有意和无意地改写。
现代CPU的缓存一般被设计成数据缓存和指令缓存分离,程序的指令和数据被分开存放对CPU的缓存命中率提高有好处。
当系统中运行着多个该程序的副本时,例如多个线程同时都运行同一个程序,它们的代码段指令都是一样的,所以内存中只需要保存一份该程序的代码段,然后将每个副本进程的数据段区域分来,这样可以节省大量空间。
分段与分区的对应
所以其实之前将可执行程序分为堆区、栈区、静态全局区、文字常量区、代码区这大分区是有迹可循的。代码区对应的是text段。文字常量区对应的就是rodata段。静态全局区就是对应的data段和bss段,因为这两个分段是挨在一起的,所以统称为静态全局区是没毛病的。