跳转至

GNU Make

约 1950 个字 13 行代码 预计阅读时间 7 分钟

Abstract

GNU Make是一个用于自动化编译的工具,它可以根据文件的依赖关系自动执行编译任务.本文将介绍GNU Make的基本使用方法.

为啥要学Make?

属于是为了这盘醋包了顿饺子,由于数据结构基础这门课的大作业或多或少需要用到多文件编译,我干脆就希望使用Make进行自动化编译,而且在系统课上接触了Makefile的编写,所以就有了这篇文章.

Part 1 别急,先看看 gcc 编译!

gcc的编译过程可以简要分为四个阶段:预处理编译汇编链接.

gcc编译工具链是以gcc编译器为核心的一整套工具,主要包含以下三部分内容:

  • gcc-core:亦即gcc编译器,用于完成预处理过程和编译过程,将C代码转换成汇编代码;
  • Binutils:包含了除了gcc编译器以外的一系列小工具,比如汇编器as、连接器ld、目标文件格式查看器readelf等;
  • glibc:GNU C Library,是GNU组织为了GNU系统以及Linux系统编写的C语言标准库.

1.1 预处理/Pre-Processing

预处理阶段的主要任务是处理源文件以#开头的预处理指令,比如#include#define等.这里主要是将#include的一些头文件宏定义进行展开,生成一个.i文件.

预处理过程输入的是C的源文件,输出的是一个中间/预加载文件,这个文件还是C代码.此阶段使用gcc参数-E,同时参数-o指定了最后输出文件的名字,下面的例子就将main.c文件经过预处理生成main.i文件

gcc -E main.c -o main.i

1.2 编译/Compiling

编译过程使用gcc编译器将预处理后的.i文件通过编译转换为汇编语言,生成一个.s文件.这是gcc编译器完成的工作,在这部分过程之中,gcc编译器会检查各个源文件的语法,即使我们调用了一个没有定义的函数,也不会报错,

编译过程输入的是一个中间/预加载文件,输出的是一个汇编文件,当然,直接以C文件作为输入进行编译也是可以的.此阶段使用gcc参数-S,具体例子如下:

gcc -S main.i -o main.s
gcc -S main.c -o main.s

1.3 汇编/Assembling

汇编阶段的主要任务是将汇编语言文件经过汇编,生成目标文件.o文件,每一个源文件都对应一个目标文件.即把汇编语言的代码转换成机器码,这是as汇编器完成的工作.

汇编过程输入的是汇编文件,输出.o后缀的目标文件,gcc的参数-c表示只编译源文件但不链接,当然,我们也可以直接输入C源文件,就直接包含了前面两个过程.

gcc -c main.s -o main.o
gcc -c main.c -o main.o

Linux下生成的.o目标文件、.so动态库文件以及下一小节链接阶段生成最终的可执行文件都是elf格式的, 可以使用 readelf 工具来查看它们的内容.

从 readelf 的工具输出的信息,可以了解到目标文件包含ELF头、程序头、节等内容,对于.o目标文件或.so库文件,编译器在链接阶段利用这些信息把多个文件组织起来,对于可执行文件,系统在运行时根据这些信息加载程序运行.

1.4 链接/Linking

最后将每个源文件对应的.o文件链接起来,就生成了一个可执行程序文件,这是这是链接xx器ld完成的工作.

例如一个工程里包含了A和B两个代码文件,在链接阶段,链接过程需要把A和B之间的函数调用关系理顺,也就是说要告诉A在哪里能够调用到fun函数,建立映射关系,所以称之为链接.若链接过程中找不到fun函数的具体定义,则会链接报错.

链接分为两种:

  • 动态链接:gcc编译时的默认选项.动态是指在应用程序运行时才去加载外部的代码库,不同的程序可以共用代码库.所以动态链接生成的程序比较小,占用较少的内存.
  • 静态链接:链接时使用选项--static,它在编译阶段就会把所有用到的库打包到自己的可执行程序中.所以静态链接的优点是具有较好的兼容性,不依赖外部环境,但是生成的程序比较大.
gcc main.o -o main
gcc main.c -o main --static

Part 2 Makefile

2.1 书写规则

Makefile的规则包括两个部分:一个是依赖关系/prerequisites,另一个是生成目标的方法/command.在Makefile中,规则的顺序是很重要的,Makeflie中有且仅有一个最终目标,其他目标都是这个目标连带出来的,一般来说,定义在第一条规则的第一个目标就是最终目标,make完成的就是这个目标.

但是我们经常会遇见make a.o这样的命令,make后可以跟着一个或多个target,a.o作为make的参数,指定了执行的内容,优先级比Makefile里边的定义要高.换句话说,倘若make后边有目标,这个目标就是最终目标,make后边没目标,默认执行Makefile的第一个目标.

规则的语法是这样的:

1
2
3
targets : prerequisites
    recipe
    ...

或者这样的:

1
2
3
targets : prerequisites ; command
    recipe
    ...

targets是文件名,可以使用通配符,基本来说,我们的目标基本上是一个文件,可以是一个目标文件,可以是一个可执行文件,还可以是一个标签,是多个文件也是有可能的.

prerequisites是生成该target所依赖的文件或者target.

recipe是命令行,可以是任意的shell命令,如果其不与target:prerequisites在一行,那么,必须以 Tab 键开头,如果和prerequisites在一行,那么可以用分号做为分隔.如果命令太长,我们可以使用反斜杠\来作为换行符.

规则告诉make两件事:一个是文件的依赖关系,target依赖于prerequisites中的文件;另一个时就会如何生成目标文件,生成规则定义在recipe中,makefile最核心的内容是:

prerequisites中如果有一个以上的文件比target文件要新的话,recipe所定义的命令就会被执行.

2.2 使用变量

Makefile中的变量就像是C语言的宏一样,代表着一个文本字符串,在执行的时候会自动展开在所使用的地方,不过,我们可以在Makefile中改变其值.变量可以使用在目标,规则,依赖目标或者其他部分之中.

在声明变量的时候,我们需要给予其初值,使用的时候需要在变量名前面加上$符号,最好还用小括号括起来(),变量的名字可以包含字符,数字,下划线,甚至还可以数字开头,但是不能含有:#=或者空字符.

我们可以使用其他变量来构造变量的值,比如foo = $(bar),这里的bar不一定非要是已经定义好的值,我们可以使用后面定义的值,这就很好了嘛,我们可以把变量的真实值退到后面去定义,但是无法避免递归定义,虽然make有能力检测这样的定义。

为了避免这个问题,我们使用:=操作符,对于VAR := value,右边的value会在定义的时候就被展开.