Makefile——高效的自动编译工具

预备知识

在Linux系统中,gcc/g++是一款非常常用的编译器。它可以将C/C++的源代码编译、汇编、链接,生成可执行文件或库文件。

在编译过程中,一个代码文件需要经过预处理、编译、汇编、连接等步骤才能转化为可执行的程序。

  1. 预处理:主要进行宏替换、文件包含、条件编译、去注释等操作。预处理指令以#号开头。

  2. 编译:在这个阶段中,gcc/g++ 首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作,然后将代码翻译成汇编语言

  3. 汇编:汇编阶段是将编译阶段生成的“.s”文件转成目标文件

  4. 链接:在成功编译之后,就进入了链接阶段,将目标文件链接成可执行文件或库文件。
    可能有人很早就有疑惑:在我们的C程序中,并没有定义“printf”的函数实现,且在预编译中包含的“stdio.h”中也只有该函数的声明,而没有定义函数的实现,那么到底printf是在哪里实现的?
    系统把这些函数实现都被写到名为 libc.so.6 的库文件中,在没有特别指定时,gcc会到系统默认的搜索路径“/usr/lib”下进行查找,也就是链接到 libc.so.6 库函数中去,这样就能实现函数“printf”了,而这也就是链接的作用。

函数库一般分为静态库和动态库两种。

  1. 静态库是指编译链接时,把库文件的代码全部加入到可执行文件中,因此生成的文件比较大,但在运行时也
    就不再需要库文件了。其后缀名一般为“.a”
  2. 动态库与之相反,在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时由运行时 链接文件加载库,这样可以节省系统的开销。动态库一般后缀名为“.so”,如前面所述的 libc.so.6 就是动态
    库。gcc 在编译时默认使用动态库。完成了链接之后,gcc 就可以生成可执行文件。

二者可以看作是网吧的电脑(动)和自己家的电脑(静),想玩电脑时,可以选择玩家里的或者去网吧上网,一旦网吧停业,就会有大批家里没有电脑的人无法上网,但自己买一台电脑价格也是高昂的。
总的来说,动态库虽然有效的节约了资源(不用自己买电脑),但一旦缺失,几乎各个程序都会无法运行(都上不了网)。而静态库虽然可以让程序独立的运行,但体积大比较消耗资源(单独买电脑价格高昂)是他的弊病。

这么说了一大串可能对初次接触的同学有些困难,可以结合图片来看一下。

可以看到从我们写出来的源文件到变成一个可执行的exe文件,大概经过了什么步骤。

在linux中,我们使用这样的指令:

1
gcc/g++的使用格式为:gcc [选项] 要编译的文件 [选项] [目标文件]

预处理:使用选项“-E”,该选项的作用是让 gcc 在预处理结束后停止编译过程。例如,要将hello.c文件预处理成hello.i文件,可以使用如下命令:

1
gcc –E hello.c –o hello.i

编译:使用选项“-S”,该选项只进行编译而不进行汇编,生成汇编代码。例如,要将hello.i文件编译成hello.s文件,可以使用如下命令:

1
gcc –S hello.i –o hello.s

汇编:使用选项“-c”,将汇编代码转化为“.o”的二进制目标代码。例如,要将hello.s文件汇编成hello.o文件,可以使用如下命令:

1
gcc –c hello.s –o hello.o

连接:将目标文件链接成可执行文件或库文件。例如,要将hello.o文件链接成hello可执行文件,可以使用如下命令:

1
gcc hello.o –o hello

什么是make/makefile?

Make是一个在软件开发中所使用的构建工具,用于自动化建构软件。 它通过一个名为 Makefile 的文本文件来描述源代码文件之间的依赖关系和构建规则。 Make 会根据这些规则和依赖关系,判断哪些文件需要重新编译,并执行相应的编译命令,以确保最终生成可执行文件或其他目标文件(这些目标被称为“target”)。 大多数情况下,它被用来编译源代码,生成结果代码,然后把结果代码连接起来生成可执行文件或者库文件。(以上摘自维基百科)

一个特别大的项目,一般来说会有很多的源文件,被分门别类的放在不同的目录中,有时候也会在一个目录里存放了多个程序的源代码。这时,如何对这些代码的编译就成了个问题。Makefile就是为这个问题而生的,它定义了一套规则,决定了哪些文件要先编译,哪些文件后编译,哪些文件要重新编译

简单地说,Make通过读取Makefile的文件中所定义的规则来执行一个项目的构建过程。

目标文件(Target):Makefile中定义的要生成的文件或执行的操作。目标可以是可执行文件、对象文件(.o)、库文件或任何其他类型的文件。
依赖文件(Dependencies):生成目标文件所需的其他文件。如果依赖文件发生了变化,那么目标文件就需要重新生成。
要执行的命令(Commands):用于生成目标文件的命令,这些命令会在目标文件或其依赖文件发生变化时被执行。
其基本格式:

1
2
target: dependencies 
command...[other commands]

command为生成目标文件需要执行的命令,这些命令以Tab键开始,而不是空格(这点需要注意!)。

Makefile的基本规则如上,接下来用几个实例深入学习。

实例

简单文件编译

新建test.c与makefile文件。使用vim编辑test.c与makefile:

1
2
3
4
5
6
7
8
9
10
//test.c
#include<stdio.h>
int main(){
printf("hello world");
return 0;
}
//makefile
test:test.c
gcc test.c -o test

再输入make命令行,此时就会先检查test.c文件是否存在,若存在则执行gcc test.c -o test命令。

1
2
3
4
5
6
[root@hcss-ecs-3ad5 ~]# make  
gcc test.c -o test
[root@hcss-ecs-3ad5 ~]# ls -a
. .. .bash_history .bash_logout .bash_profile .bashrc .cache .cshrc .history makefile .ssh .tcshrc test test.c .viminfo
[root@hcss-ecs-3ad5 ~]# ./test
hello world[root@hcss-ecs-3ad5 ~]#

如果再次执行make,文件不会有任何变化。而当touch操作test.c后,再执行make就会重新编译。
也可以同时编译多个文件。

1
2
test:test1.c test2.c //用空格隔开即可
gcc test1.c test2.c -o test

这样就会出现一个问题,如果test1.c被修改,那么test2.c也会跟着重新编译。
所以更规范的makefile写法:

1
2
3
4
5
6
7
test:test1.o test2.o
gcc test1.o test2.o -o test
test1.o: test1.c
gcc -c test1.c
test2.o: test2.c
gcc -c test2.c

这样就先把两个test文件先编译成.o文件,最后再一起链接。

稍微提一提工作原理:
当运行make命令(不带任何目标)时:
Make会查找Makefile中的第一个目标,如果没有指定目标,它会尝试构建第一个定义的目标(在这个例子中是test)。如果指定如make test1.o ,那么就只会构建test1.o。
构建test目标:
Make看到test目标依赖于test1.o和test2.o。
它会检查这两个对象文件是否已经存在且是最新的(即它们的修改时间是否晚于它们的依赖源文件或任何其他相关的文件)。
如果test1.o或test2.o中的任何一个不存在或不是最新的,make会构建它们。
构建test1.o目标:
如果test1.o需要被构建(因为它不存在或不是最新的),make会查找test1.o的规则。
它看到test1.o依赖于test1.c。
然后,它会执行用于生成test1.o的命令:gcc -c test1.c。
构建test2.o目标:
与test1类似,如果test2.o需要被构建,make会查找test2.o的规则并执行相应的命令:gcc -c test2.c。
链接生成test:
一旦test1.o和test2.o都是最新的或已经被成功构建,make会执行用于生成test的命令:gcc test1.o test2.o -o test。
这将链接test1.o和test2.o生成最终的可执行文件test。

当执行一些清理临时文件或者打包等不需要生成文件的操作,就会用到伪目标来定义规则。伪目标就是不生成文件的操作。
例如:

1
2
clean:
`rm -f *.o test

当执行make clean时,就会删除所有.o文件与test文件。

要是有一个文件恰好叫clean呢?那执行make clean不就有两层意思:一层是构建clean文件,一层是执行伪目标clean。

为了避免此情况,会使用如下语法:

1
.PHONY: clean

此时再执行make clean就可以达到删除文件的预期效果。
再加入all:

1
2
3
4
5
6
7
8
9
.PHONY: clean all
all: hello chengzi

hello:test.c
gcc test.c -o hello
chengzi:test.c
gcc test.c -o chengzi
clean:
rm -f *.o hello

命令行如下:

1
2
3
4
5
6
7
8
9
[root@hcss-ecs-3ad5 ~]# make all  //构建hello和chengzi两个文件
gcc test.c -o hello
gcc test.c -o chengzi
[root@hcss-ecs-3ad5 ~]# ls -a //查看当前目录下文件
. .. .bash_history .bash_logout .bash_profile .bashrc .cache chengzi .cshrc hello .history makefile .ssh .tcshrc test test.c .viminfo
[root@hcss-ecs-3ad5 ~]# make clean //清除所有.o文件与hello文件
rm -f *.o hello
[root@hcss-ecs-3ad5 ~]# ls -a //再次查看目录下文件
. .. .bash_history .bash_logout .bash_profile .bashrc .cache chengzi .cshrc .history makefile .ssh .tcshrc test test.c .viminfo

变量

使用变量对hello,chengzi等文件进行替换,有利于整体的修改和维护。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.PHONY: clean all

# 定义编译器和编译选项
CC = gcc
CFLAGS =

# 定义目标文件
TARGETS = hello chengzi

# 定义源文件
SRC = test.c

# 默认目标
all: $(TARGETS) //使用变量的规则就是$加上(变量名)

# 规则:如何生成每个目标
$(TARGETS): $(SRC)
$(CC) $(CFLAGS) $(SRC) -o $@
# 清理规则
clean:
rm -f *.o $(TARGETS)

$@ 是一个自动变量,它代表当前规则的目标文件名。

1
2
hello: test.c
gcc test.c -o $@

$@ 会被替换成 hello,所以实际的命令会是:

1
gcc test.c -o hello