读书笔记 十月 29, 2016 chaossss No comments

《深入理解软件构造系统-原理与最佳实践》笔记

前言:最近想了解下Makefile相关的东西,就买了本这个看了看。里边的原理性的内容都看了一遍,实践性的都跳过了。后边看看《计算机程序的构造和解释》吧

构造系统概述

什么是构造系统

对于把某种形式的数据(输入)转译成另一种形式(即输出),构造系统可以管理这其中涉及的各种类型的活动。

编译型语言

源文件编译成为目标文件,将目标文件链接到程序库/可执行程序

解释型语言

不需要编译,直接将源文件汇集为发布包安装到目标机器(运行时)

Web应用

兼顾编译型和解释型

单元测试

不生成可在目标机器上安装的发布包,而是生成许多小型的单元测试包,每个包在目标机器上执行,通过产生通过和失败的结果来表示软件是否出现了预期行为。

静态分析

静态分析不生成目标文件和发布包,而是输出某种类型的缺陷报告文档

文档生成

构造过程和构造描述

构造工具为了表达这一工程,需要以文本格式编写的构造描述,如:

  • Make工具需要规则来指定文件间的依赖关系,这些规则保存在Makefile文件中
  • SCons工具则用Python语言的函数来描述编译步骤,相关信息存储在名为SConstruct的文件中

从软件开发人员的角度来说,文本格式的构造描述是构造过程的核心。

基于Make的构造系统

增量式构造:GNU Make不会盲目执行命令,而是事先做一些分析,判断某些文件是否真的需要编译,或这些文件是否已经存在。但如果没有问每一个目标文件创建单独的规则,无法实现增量式构造。

$(PROG): $(OBJS)
$(CC) $(CFLAGS) -o $@ $^
$@表示表达式的左半部
$^表示表达式的右半部

额外的构造任务

可以通过在makefile中添加对应的任务。

程序的运行时视图

可执行程序

可执行程序:一组指令序列。这些指令被载入内存,由CPU执行。

原生机器码

构造系统此时将可执行程序完全转换成CPU的原生机器码,CPU只需跳到程序的起始点,然后所有的执行都完全通过CPU硬件来完成。在执行时,程序偶尔会调用操作系统以访问文件或其他系统资源。

单体系统镜像

针对汽车、电视机、厨房设备内含的嵌入式系统,即小型操作系统。这种计算机一次只能运行一个程序。尽管有时使用解释器,但大多数嵌入式系统使用原生机器码的执行方式,程序占用了系统的全部内存。从构造系统的角度来看,最终的发布包事一个大型的文件,直接载入到内存中,即镜像。

程序完全解释执行

许多编程语言不编译成机器码,而是由运行时系统把整个源代码载入内存,并解释执行。(如:Basic、UNIX Shell)

但不需要将源代码便以为目标文件不意味着构造系统的工作量减少,它必须把源文件汇集到发布包中,以便在目标机器上安装。

解释型字节码

字节码和原生机器码很相似,区别是:CPU并不直接理解字节码。首先将字节码转译为原生机器码,或在程序执行时一边解释一边执行。因此,字节码环境需要额外的解释器或编译器,与程序一同载入内存。

程序库

静态链接

此时,程序库仅仅是若干目标文件的集合。在构造过程中,当链接工具判断当下需要某个函数时,它就从程序库中摘取合适的目标文件,并将其复制到可执行程序中。过程发生在构造过程中,因此最终得到的是单个可执行程序,可以随时载入到目标机器执行。(构造时)

动态链接

此时,标明为了顺利执行该程序,需要哪些程序库,当以后执行程序时,则将程序库作为单独实体载入刀内存中,通过动态链接器与主程序连接。

好处:

  • 无需重新创建可执行程序(更新)
  • 只需要载入一份程序库就可提供给多个程序使用,优化内存占用

子目标与构造变量

从源树映射到目标树的三种不同方式:

  1. 针对子目标进行构造
  2. 针对软件的不同版本进行构造
  3. 针对不同的系统架构进行构造

针对子目标进行构造

任何大型软件都可以划分为许多子组件,每个组件提供一部分程序功能,其开发过程在一定程度上与其他模块相互独立。但当程序包含许多源文件,即便只对修改过的文件进行重新编译,读取所有构造描述文件并判断哪些目标文件已过期也需要消耗许多时间。因此,可以考虑先知待构造的子组件数量,避免总是重新构造整个源树。

Make

GNU Make程序流程

  1. 解析makefile:首先读取makefile,建立依赖关系图;然后执行编译命令。
  2. 控制解析过程
  3. 执行规则

解析makefile

  1. 解析makefile:系统对makefile进行解析、验证,生成完整的依赖关系图。系统扫描所有规则,对全部变量进行赋值,并对所有变量和函数进行求值。
  2. 规则执行阶段:当整个依赖关系图都放入内存时,GNU Make就检查所有文件的时间戳,以确定是否有文件过期。如果发现过期文件,就执行适当的shell命令,更新过期文件。shell命令发生的任何问题,都在此阶段报告。

构造系统

为了做出一个全功能的构造系统,需要增加以下功能:

  • 用来定义目标文件、源文件和头文件之间依赖关系的GNU Make代码(使用自动依赖关系来分析)
  • 用来编译代码的规则(需要覆盖内置的C编译规则)
  • 用来把目标文件链接成静态程序库的代码
  • 用来链接形成最终可执行程序的代码(可能要编译不止一个程序)
  • 从子目录启动GNU Make进程的能力(目前只有一个makefile位于顶层src目录)
  • 对多种CPU架构的编译支持
  • C编译器标志参数可以用在文件级别,而不是目录级别(即每个文件都可以使用不同的C编译器标志参数)
  • 能够从上级目录向子目录继承编译器标志参数

依赖关系

依赖关系图

依赖关系图是一种结构,它定义了源树和目标树之间的关系,图中的箭头表示某个文件的内容依赖于另一个文件的内容,即:目标文件依赖于C源文件。

增量式编译

从构造系统的角度来看,有意思的的是思考增量式编译如何实现。如果假定所有目标文件都是最新的,而开发人员又修改了C文件,那么构造工具的任务就是判断哪些目标文件收到影响。它必须调用正确的编译工具,把一切恢复为一致(全部都经过构造)状态。

当重新编译该树时,构造工具执行必要的编译命令,把一切回复为已更新状态。这些编译命令必须按特定顺序执行,以便任何使用其他文件作为输入的文件,都可以确保自己使用的是更新后的信息。

此外,依赖关系图不是循环的,也就意味着任何文件无法直接或间接地依赖于自身。避免构造树无法保持一致状态。

完全、增量式和子目标的构造

对于构造过程,有以下类型:

  1. 完全构造:本场景假定开发人员从未对构造树进行过编译。树中只有源文件,没有任何源文件被编译成目标文件。当开发人员对这种树进行初次构造时,必须执行所有编译命令,把整个树变成已更新状态。
  2. 增量式构造:本场景中,构造树已被完全构造过,包含所有必须的目标文件。但开发人员最近修改了其中若干文件,导致目标文件与源文件不再一致。因此需要对部分目标文件进行重新构造,以使它们恢复一致状态。在大规模增量式构造中,构造工具可能需要花几分钟时间来分析构造树,判断需要做哪些事情。当重新编译开始时,所花费的时间量级比完全构造要少的多。

子目标的构造:开发人员并非每次都生成最终可执行程序,而是可以选择对树中的部分内容进行构造。

依赖关系错误导致的问题

  1. 依赖关系缺失导致运行时错误
  2. 依赖关系缺失导致编译错误
  3. 多余的依赖关系导致大量不必要的重新构造
  4. 多余的依赖关系导致依赖关系分析失败
  5. 循环依赖关系
  6. 以隐式队列顺序替代依赖关系
  7. Clean什么也清除不了

计算依赖关系图

判断依赖关系图的第一步是理解每种编译工具使用哪些输入输出文件,这可以通过以下方法完成:

  1. 命令行参数
  2. 源代码指令
  3. 默认惯例

获取确切的依赖关系

在大多数情况下,构造工具和编译工具完全是两套程序,它们共享的信息并不多。构造工具要想有效运作,必须预测编译工具可能读写哪些文件。这一工作要在编译工具实际执行之前完成,而不是让它在编译过程中摸索依赖关系,否则可能导致编译命令按错误顺序执行。

硬编码的依赖关系

通过把依赖关系硬编码到构造描述文件,构造系统可以使用开发人员关于依赖关系的知识。此时,构造工具不需要做什么事,只要构造解析描述文件并更新依赖关系图即可。

这是指定依赖关系最简单的方法,但这种方法对大型程序效果不佳。在持续维护过程中可能引入错误,从而使这种方法不实用。但无论如何,当更自动化的方法难以实现时,常常使用这种方法。

从命令行衍生的依赖关系

由扫描器工具程序提供的依赖关系

扫描器可能导致不必要的重新编译。

由编译工具提供的依赖关系

由文件系统监控而得的依赖关系

优点:可以保证找到确切的依赖关系,如果编译工具永远只读写同一组文件,那么我们就永远不会有确实或过度依赖关系信息的烦恼。对于有大量难以预测依赖关系的系统的编译来说,上述特性是一大福音。

缺点:必须在计算机操作系统中嵌入额外的文件系统插件;此外,监控软件将记录下几乎每个文件的访问情况(除非你告诉它不要记录某些文件),但并非每个文件都有依赖关系。

缓存依赖关系图

缓存依赖关系图可以:

  • 减少依赖关系计算时间
  • 节省编译工具或扫描器运算得到依赖关系信息的时间

更新缓存的依赖关系图

判断哪些文件已过期

基于时间戳的方法

  1. 比较源文件和目的文件的时间戳
  2. 缓存文件的时间戳

基于校验和的方法

利用MD5或SHA等校验和技术可以获取整个文件内容摘要的数字指纹,但这些校验和方法并不保证文件内容摘要的唯一性,但如果两个文件的校验和相同,那么两个文件相同的概率很大。

但使用校验和方法要求构造工具启动时有能力计算并保存文件的校验和,因此可能损害构造工具的性能。

标志参数比较

构造工具需要有某种机制来保存每个目标文件的命令行选项。

其他高级方法

  • 版本控制工具
  • IDE
  • 文件系统

为编译步骤排定队列顺序

如何按正确顺序调用每个编译工具,以便使整个软件实体能够完全更新到最新状态是构造工具实际调用编译工具最重要的部分。对此一般的规则是:对于已更新的文件,如果它所依赖的任何文件被重新生成,那么它也要重新生成一次。

运用元数据进行构造

元数据的类型:

  1. 支持调试的元数据
  2. 支持性能分析的元数据
  3. 支持代码覆盖分析的元数据
  4. 用于源代码文档化的元数据
  5. 用于单元测试的元数据
  6. 用于静态分析的元数据

发表评论