Loading

快乐Linux —— 6. 编译链接装载

参考:

《程序员自我修养》《深入理解计算机系统》

https://blog.csdn.net/baidu_41667019/category_8404116.html

https://www.tutorialspoint.com/compiler_design/compiler_design_overview.htm

关于链接详细可以看看这两个博文:

https://blog.csdn.net/baidu_41667019/article/details/84789564 符号和符号解析

https://blog.csdn.net/baidu_41667019/article/details/84872940 重定位

简述

​ 整个程序从写好到执行过程如下,下面简单介绍这个过程

预处理

  • 展开并删除所有#define

  • 处理所有条件编译

  • 将#include头文件插入

  • 删除所有的注释

  • 添加行号和文件名标识,以便产生调试用的行号信息和用于编译器产生编译错误信息

  • 保留#pragma ,因为编译器要使用

    !!!!.c .cpp 经过预处理器后生成 .i 文件

编译

在编译原理这本书里,编译过程最终输出有三种:

  1. 输出使用绝对地址的机器语言程序。
  2. 输出可重定位的机器语言程序。
  3. 输出汇编程序。

一般而言,常见编译器都是生成汇编代码,所以下文我们默认为第三种情况进行讨论。

在整个过程中符号表错误处理程序贯穿整个编译过程。

下面就表达式 array[index] = (index + 4) * (2 + 6) 做简要分析。

  1. 词法分析 有限状态机

    将源代码中的字符扫描为一个个记号(token),记号一般可分为以下几类: 关键字标识符字面量特殊符号(运算符号)。将分析到的符号放到合适的地方(标识符放到符号表,字面量放到文字表等)

  2. 语法分析 上下文无关语法

    将扫描到的表达式生成语法树,判断语法是否正确。

  3. 语义分析

    • 分析语义是否有意义(静态语义)例如 两个指针相乘语法是正确的但是没有意义的,会在这个阶段排查出来。

      静态语义指在编译期就可以确定的语义,包括声明和类型匹配,类型转换等。

      动态语义只有在运行期才能够确定。动态语义包括运行时发生除0错误等。

    • 对语法分析生成的语法树进行进一步更新。添加类型标识,添加类型转换节点等。

  4. 中间代码生成 与 优化

    语法树的顺序表示,有很多种类型,常见为三地址码。

    为什么要生成中间代码?

    • 为了优化代码,而原有的语法树不利于修改,所以将语法树顺序表示转化为中间代码。
    • 将编译器分为前端和后端,前端生成与机器无关的代码,后端将中间代码转换成目标机器代码。简单来说,就是现在有一个跨平台的编译器,那么它只需要开发一个前端编译器,再根据不同平台开发不同后端编译器。
  5. 目标代码生成 与 优化

    !!!到这一步生成的代码就与特定的运行环境和目标机器有关了。

    生成: 将中间代码转换成汇编代码,依赖于目标机器,因为不同机器有着不同字长,寄存器,整数数据类型等等。

    优化: 删除多余的指令,用位移代替乘法运算等。

汇编

将编译器生成的汇编代码翻译成相应的机器指令(01 指令序列)。

生成各个section。

根据编译过程中汇总的符号表 生成 链接过程的符号表

  • 编译时的符号表包括局部自动变量,因为要进行语法语义判断和代码优化等工作。
  • 而链接过程中符号表没有局部自动变量,因为这些变量是在运行时在栈上管理,跟链接所做的工作无关。

链接

到这块,建议先看看生成的可重定位文件内容: https://www.cnblogs.com/starrys/p/11911064.html

整个链接所作的工作可以简单概括下面几句:目标文件纯粹是字节块的集合,这些块中包含程序代码和程序数据,和引导链接器和加载器的数据结构。链接器将这些块链接起来,确定被连接块的运行时位置,并且修改代码和数据块中的各种位置。链接器对目标机器了解甚少。主要有两个工作 符号解析重定位

符号解析

符号解析的目的就是 关联符号的定义与引用。将每个引用的符号与可重定位文件的符号表中一个确定的符号定义关联起来。

对于第三种 局部符号由编译器确保它们只有一个定义。链接过程中主要处理第一种和第二种全局符号。

对于 定义在本模块被其他模块引用全局符号 和 定义在其他模块被本模块引用全局符号 有以下:

  • 在编译时,编译器向每个全局符号附加强 弱属性,经过汇编器把这个属性隐含在可重定位文件的符号表里。
    • 强符号 包括 函数 和 已初始化的全局变量。
    • 弱符号 包括 未初始化的全局变量。
  • 用以下规则处理多重定义的符号名:
    • 不允许多个同名强符号。
    • 如果有强符号和弱符号同名,那么选择强符号。
    • 当多个弱符号同名,则从其中任选一个。

从这点可以看出为什么在ELF可重定位文件中 未初始化的全局变量例如 x(弱符号) 会被分配到 COMMON块中,因为编译器没有办法预测链接器会使用x的哪个定义,所以把它分配到COMMON块中,把决定权交给链接器。而已初始化全局变量因为是强符号所以可以直接放到.data 或 .bss 。

重定位

重定位工作有以下:

  • 重定位节

    将可重定位文件中所有相同类型的section 合并成一个。

  • 重定位符号定义

    给符号和指令分配虚拟地址。使得程序中每条指令和全局变量都有唯一的虚拟内存地址。

  • 重定位符号引用

    修改代码和数据中对每个符号的引用,使它们指向程序的虚拟地址。这个过程需要可重定位目标文件中的 .rel section。

装载

为了节省内存,可以利用程序运行的局部性原理,把程序经常需要的信息驻留到内存,不太常用的存放在磁盘中。

有两种动态装载的方式:

  • 覆盖装入 已被淘汰

    简单来说,就是再写一个管理模块调用的代码,假如有A,B两个程序模块,且两模块间不会互相调用,当运行A模块时,从磁盘中加载A到内存上,当A没在运行并且要运行B时,把B覆盖到原来A所在内存上。相当于同一块内存由多个不同时刻独立模块共享。但这样做只能对模块间独立有效,并且,每次装载都要从磁盘读数据,用时间换空间的做法。把利用内存的任务交给了程序员。

  • 页映射 常用

    将内存和磁盘上的指令和数据按特定大小划分,将一个单位称为页,之后装载和操作的单位就是页。一般来说一个页的大小为4k。当需要哪个页的数据时,将其装载到内存上。当内存上可用页被占满后,根据FIFO 或者LUR 算法更换页。这块简单看看页映射的图。

当运行可执行文件时,发生了什么?

在 shell 终端输入 ./可执行文件 时,当键入回车键后,shell 知道我们已经输入完命令,检测到不是内置命令,所以将其当作一个可执行文件。然后通过调用加载器,将可执行文件中的代码和数据从磁盘复制到内存中,然后通过跳转到程序的第一条指令来运行该程序。

其中装载的过程:

  • 创建虚拟地址空间。

    虚拟空间的页映射,这个页映射机制其实就是一个映射函数,创建虚拟地址空间不是创建空间,而是创建这个映射函数所需要的数据结构。分配一个空的页目录就可以。

  • 读取可执行文件头,并建立虚拟空间与可执行文件的映射关系。

    这一步所做的是虚拟空间与可执行文件的映射关系。当发生页错误时,操作系统从可执行文件中读取页,并将它装载到物理内存上。

    若没有这一步,如果发生了页错误时,不知道要装载可执行文件中哪个页。

  • 将cpu的指令寄存器设置成可执行文件的入口地址,启动运行。

    这个入口地址就是可执行文件头部保存的 Entry point address

详细的装载过程中的指令请见 《程序员自我修养》 p173

页错误: 当程序开始执行时,发现某个页面是空页面,触发了异常中断,被操作系统捕捉执行错误处理程序,错误处理程序借助上面装载第二步建立的数据结构,找到了缺失的空白页在文件中的偏移,然后再在物理页上新开辟一页,建立该页与文件中的页映射关系,最后返回中断现场,继续执行。

当段的数目增多时,就会产生空间浪费的问题,因为ELF文件被映射时,每个section在映射时如果都是以页进行映射,并且section大小与页大小取模有剩余,那么多余出来的也会占一页。而一个ELF文件中有很多section,这就容易翻车。

由此引出了 segment ,还记得上一节 ELF 文件中的segment 吗?

当站在操作系统的角度看可执行文件的装载,发现它并不关心各个section的具体内容,而主要关心的是段的权限。于是乎,我们可以将多个权限相同的section 一起装载,减少内存碎片。这些section就称为 segment.

VMA , 页 , segment ,section 之间是什么关系?

  • 页就是内存和磁盘管理数据的单位。一般大小4k。

  • VMA (Virtual Memory Area)就是进程虚拟空间的一块区域,故名思意,就是一块区域。

  • section就是 ELF 文件中的 .data .text 等等。

  • segment 就是 在装载可执行文件时,将多个权限相同的 section 看作一个segment。

  • 一个segment 包含多个section,映射到虚拟内存上时可能要花费多个页。

    一个VMA根据大小,可能包含一个segment,也可能连一个section 都不够。

posted @ 2019-11-24 10:13  沉云  阅读(199)  评论(0编辑  收藏  举报