虚拟地址空间与编译链接


概述

如果它存在,而且你能看见它,它是物理的(real)
如果它不存在,但你能看见它,它是虚拟的(virtual)
如果它存在,但你看不见它,它是透明的(transparent)

虚拟地址空间

虚拟地址空间和虚拟内存是两码子事,

虚拟地址空间是一种内存管理技术,它允许程序以一种抽象的方式访问内存资源,从而实现对物理内存的高效管理。虚拟地址空间将物理内存抽象为一个连续的地址空间,使得每个进程都拥有自己独立的虚拟地址空间。操作系统负责管理虚拟地址到物理地址的映射。

在Linux中,虚拟地址空间通常分为两个区域:用户区域和内核区域。

  • 用户区域: 用户区域包含了用户程序的代码、数据和堆栈等信息。具体包括以下部分:
    • 程序代码:存放编译后的可执行二进制代码,这部分内存区域是只读的,防止程序意外或恶意修改代码。
    • 数据段:存放程序的全局变量和静态变量。
    • 堆:用于存储程序运行时动态分配的内存。堆从低地址向高地址增长。
    • 文件映射区(mmap):存储映射到内存中的文件数据,如用于装载动态共享库、内存映射文件等。
    • 栈:存放程序的局部变量、函数调用的参数和返回地址等信息。栈从高地址向低地址增长。
    • 环境变量和命令行参数:存放传递给程序的环境变量和命令行参数。
  • 内核区域:内核区域是操作系统内核代码和数据的存储区域。用户进程无法直接访问这部分内存,只能通过系统调用与内核进行交互。内核空间包括以下部分:
    • 内核代码:操作系统内核的代码。
    • 内核数据:操作系统的全局数据结构,如进程表、文件系统缓存等。
    • 内核栈:为内核模式下的函数调用提供栈空间。
    • 物理内存映射:将物理内存映射到内核虚拟地址空间,便于内核访问。
    • 高速缓存:缓存一些常用的内核数据结构,提高访问速度。

用户空间和内核空间的主要区别在于访问权限和功能。用户空间主要用于存储用户程序的代码和数据,而内核空间则存放操作系统内核的代码和数据。用户空间的程序无法直接访问内核空间,只能通过系统调用与内核进行交互。这种设计有助于保护内核数据结构的完整性,避免用户程序意外或恶意破坏操作系统。

当计算机程序运行时,它需要访问一些内存,例如使用的变量或代码等。虚拟地址空间是一种使程序可以直接访问这些内存而不必真正了解这些内存在物理上所处的位置的技术。虚拟地址由处理器内存管理单元映射到实际物理地址。每个进程都有自己的独立虚拟地址空间,同时操作系统负责将每个进程的虚拟地址映射到主存中的物理内存地址。

在32位Linux系统中,虚拟地址空间大小为4GB,分为内核空间和用户空间。以下是一个简要概述:

  • 内核空间(Kernel Space):
    • 内核空间位于高地址段,通常在3GB到4GB之间。这个区域存放内核代码和数据结构,如内存管理、进程管理、设备驱动等。这部分内存只能由内核模式下的代码访问。
  • 用户空间(User Space):
    • 用户空间位于低地址段,通常在0到3GB之间。这个区域存放用户程序和相关数据结构。用户空间从高地址到低地址包含以下部分:
      • 环境变量和命令行参数:位于用户空间的高地址部分。
      • 栈空间(Stack):位于环境变量和命令行参数之下,用于存放函数调用栈、局部变量和返回地址等。
      • 动态链接库(Shared Libraries):位于栈空间下方,存放动态链接的共享库。内存映射文件也会被加载到这个区域。
      • 堆空间(Heap):位于动态链接库下方,用于存放动态分配的内存。堆空间在程序运行期间可以根据需要增长或缩小。
      • 未初始化数据段(BSS)、初始化数据段(Data)和代码段(Text):位于堆空间下方,存放程序的全局变量、静态变量和程序代码。

内存映射文件和动态链接库通常位于用户空间的共享库部分。其中应该熟悉的区有这几个:

BSS段

BSS(Block Started by Symbol)段中通常存放程序中以下符号:

  • 未初始化的全局变量和静态局部变量

  • 初始值为0的全局变量和静态局部变量(依赖于编译器实现)

  • 未定义且初值不为0的符号(该初值即common block的大小)

    C语言中,未显式初始化的静态分配变量被初始化为0(算术类型)或空指针(指针类型)。由于程序加载时,BSS会被操作系统清零,所以未赋初值或初值为0的全局变量都在BSS中。BSS段仅为未初始化的静态分配变量预留位置,在目标文件中并不占据空间,这样可减少目标文件体积。但程序运行时需为变量分配内存空间,故目标文件必须记录所有未初始化的静态分配变量大小总和(通过start_bss和end_bss地址写入机器代码)。当加载器(loader)加载程序时,将为BSS段分配的内存初始化为0。在嵌入式软件中,进入main()函数之前BSS段被C运行时系统映射到初始化为全零的内存(效率较高)。

    注意,尽管均放置于BSS段,但初值为0的全局变量是强符号,而未初始化的全局变量是弱符号。若其他地方已定义同名的强符号(初值可能非0),则弱符号与之链接时不会引起重定义错误,但运行时的初值可能并非期望值(会被强符号覆盖)。因此,定义全局变量时,若只有本文件使用,则尽量使用static关键字修饰;否则需要为全局变量定义赋初值(哪怕0值),保证该变量为强符号,以便链接时发现变量名冲突,而不是被未知值覆盖。

    某些编译器将未初始化的全局变量保存在common段,链接时再将其放入BSS段。在编译阶段可通过-fno-common选项来禁止将未初始化的全局变量放入common段。

    此外,由于目标文件不含BSS段,故程序烧入存储器(Flash)后BSS段地址空间内容未知。U-Boot启动过程中将U-Boot的Stage2代码(通常位于lib_xxxx/board.c文件)搬迁(拷贝)到SDRAM空间后必须人为添加清零BSS段的代码,而不可依赖于Stage2代码中变量定义时赋0值。

【扩展阅读】BSS历史

BSS(Block Started by Symbol,以符号开始的块)一词最初是UA-SAP汇编器(United Aircraft Symbolic Assembly Program)中的伪指令,用于为符号预留一块内存空间。该汇编器由美国联合航空公司于20世纪50年代中期为IBM 704大型机所开发。 后来该词被作为关键字引入到了IBM 709和7090/94机型上的标准汇编器FAP(Fortran Assembly Program),用于定义符号并且为该符号预留指定字数的未初始化空间块。 在采用段式内存管理的架构中(如Intel 80x86系统),BSS段通常指用来存放程序中未初始化全局变量的一块内存区域,该段变量只有名称和大小却没有值。程序开始时由系统初始化清零。 BSS段不包含数据,仅维护开始和结束地址,以便内存能在运行时被有效地清零。BSS所需的运行时空间由目标文件记录,但BSS并不占用目标文件内的实际空间,即BSS节段应用程序的二进制映象文件中并不存在。

Data段

数据段通常用于存放程序中已初始化且初值不为0的全局变量和静态局部变量。数据段属于静态内存分配(静态存储区),可读可写。

数据段保存在目标文件中(在嵌入式系统里一般固化在镜像文件中),其内容由程序初始化。例如,对于全局变量int gVar = 10,必须在目标文件数据段中保存10这个数据,然后在程序加载时复制到相应的内存。

数据段与BSS段的区别如下:

1) BSS段不占用物理文件尺寸,但占用内存空间;数据段占用物理文件,也占用内存空间。

对于大型数组如int ar0[10000] = {1, 2, 3, …}和int ar1[10000],ar1放在BSS段,只记录共有10000*4个字节需要初始化为0,而不是像ar0那样记录每个数据1、2、3…,此时BSS为目标文件所节省的磁盘空间相当可观。

2) 当程序读取数据段的数据时,系统会出发缺页故障,从而分配相应的物理内存;当程序读取BSS段的数据时,内核会将其转到一个全零页面,不会发生缺页故障,也不会为其分配相应的物理内存。

运行时数据段和BSS段的整个区段通常称为数据区。某些资料中“数据段”指代数据段 + BSS段 + 堆。

text段

代码段也称正文段或文本段,通常用于存放程序执行代码(即CPU执行的机器指令)。一般C语言执行语句都编译成机器代码保存在代码段。通常代码段是可共享的,因此频繁执行的程序只需要在内存中拥有一份拷贝即可。代码段通常属于只读,以防止其他程序意外地修改其指令(对该段的写操作将导致段错误)。某些架构也允许代码段为可写,即允许修改程序。

代码段指令根据程序设计流程依次执行,对于顺序指令,只会执行一次(每个进程);若有反复,则需使用跳转指令;若进行递归,则需要借助栈来实现。

代码段指令中包括操作码和操作对象(或对象地址引用)。若操作对象是立即数(具体数值),将直接包含在代码中;若是局部数据,将在栈区分配空间,然后引用该数据地址;若位于BSS段和数据段,同样引用该数据地址。

代码段最容易受优化措施影响。

段和节

段是程序执行的必要组成部分,在每一个段中,会有着代码或者数据被划分为不同的节。节头表是对这些节的位置和大小的描述,主要用于链接和调试。节头对于程序的执行来说不是必需的,没有节头表,程序仍可以正常执行,因为节头表没有对程序的内存布局进行描述,对程序内存布局的描述是程序头表的任务。节头是对程序头的补充。

如果二进制文件中缺少节头,并不意味着节不存在,只是没有办法通过节头来引用节,对于调试器或者反编译器程序来说,只是可以参考的信息变少了而已。

保留区的上边就是代码段和数据段,它们是从程序的二进制文件中直接加载进内存中的,BSS 段中的数据也存在于二进制文件中,因为内核知道这些数据是没有初值的,所以在二进制文件中只会记录 BSS 段的大小,在加载进内存时会生成一段 0 填充的内存空间。

堆空间的上边是一段待分配区域,用于扩展堆空间的使用。接下来就来到了文件映射与匿名映射区域。进程运行时所依赖的动态链接库中的代码段,数据段,BSS 段就加载在这里。还有我们调用 mmap 映射出来的一段虚拟内存空间也保存在这个区域。注意:在文件映射与匿名映射区的地址增长方向是从高地址向低地址增长

推荐阅读


评论
  目录