Makefile笔记

本文最后更新于 2024年5月17日 晚上

参考资料:陈皓makefile

  • Makefile作为一种自动化编译工具,理论上任何语言的编译,或者说任意工程的构建都是可以用make来完成的。但其实现如今主要还是作为C/C++的编译工具存在,其运行逻辑也和C/C++的编译流程十分契合。
  • 因此本文首先会讲讲C/C++的编译过程,如何使用gcc, g++, ar工具等。然后再讲诉make的依赖关系定义以及命令的书写。

一、C/C++编译

1.1 编译可重定位目标文件

  • C/C++的编译过程中,首先都是针对每个.c/.cc/.cpp文件编译成相同文件名的.o文件,这个过程即为生成可重定位目标文件,每个文件的编译过程都是独立的,我们是需要针对每一个.c/.cc/.cpp文件分别调用gcc/g++编译器的。
  • 另外这部分的编译相互之间是独立的,只要保证能找到引用的各种.h文件就能成功编译。但是注意,如果我们修改了其引用的某个.h文件,那么这个.o文件是需要重新编译的(这也是makefile.o依赖关系需要同时存在.c/.cc/.cpp.h的原因)
  • 编译方法gcc -c main.c -o main.o注意这条编译命令中并没有出现应用的.h文件,这是因为会到默认的搜索路径下去找需要用到的头文件,如果存在额外的头文件文件夹是需要我们使用-I./include/这样的参数添加头文件搜索路径的。

1.2 编译静态库文件

  • 静态库其实是对许多.o文件的归档Archive,因此打包静态库的命令其实就是ar cr libxxx.a xx1.o xx2.o
  • 打包出来的.a静态库文件其实就是一堆.o文件集合,是可以对单个目标文件进行替换操作的,即当某一个.o文件发生变化时我们可以在原来的.a文件上执行这一个.o文件的替换。(这个性质非常重要,会被应用到makefile的构建中。)
  • 参数r即表示在库中插入模块(替换)。当插入的模块名已经在库中存在,则替换同名的模块。默认的情况下,新的成员增加在库的结尾处,可以使用其他任选项来改变增加的位置。
  • 参数c表示创建一个库。不管库是否存在,都将创建。

1.3 编译动态库文件

  • 动态库的构建则涉及到链接过程,不仅仅是简单地将目标文件打包。通常需要使用链接器(如g++ld)来创建一个包含所有必要符号的共享对象文件。构建动态库的命令为gcc -shared -fPIC -o libtest.so test.o

  • 在动态库(共享库)的构建过程中,一旦生成了.so这样的共享对象文件,它就包含了所有输入的.o目标文件的编译结果以及它们之间的链接信息。这意味着,即使你修改了其中一个目标文件,也不能简单地将其替换到已有的动态库中,因为动态库的完整性和链接信息是基于原始构建时的所有目标文件的。

  • 另外,动态库的构建因为涉及链接过程,如果代码中引用了别的库是需要在构建的时候使用-L./path/ -l[lib name]进行引入的。另外动态库的链接尽量使用动态库的方式。

1.4 编译可执行文件

  • 编译过程和动态库是相似的,命令是gcc -o test test.o -lpthread libcertain.a注意动态库和静态库的链接方式的不同。

二、Makefile

2.1 基本运行方式

1
2
target: prerequisites
[Tab]command
  • 这是Makefile文件的基本规则:target也就是一个目标文件,可以是Object File,也可以是执行文件,还可以是一个标签Labelprerequisites就是要生成那个target所需要的文件或是依赖; command也就是make需要执行的命令(任意的Shell命令)。

  • 这里定义的其实是一种依赖关系,即target需要依赖于prerequisites这些对象,而使用这些依赖生成target的方式是下面给出的command指令。需要注意,不一定需要真的生成target文件;也不一定需要真的使用所有prerequisites对象;甚至command也不一定需要,某些依赖是不需要执行指令的,比如all:test1 test2 test2这样的一条依赖。

  • 一个Makefile文件中可能会定义多个依赖关系,make会自动帮我们理清目标之间的相互依赖关系,然后帮我们按照正确的顺序得到最终的目标文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
# 最终目标文件--可执行文件
edit : main.o kbd.o command.o
cc -o edit main.o kbd.o command.o
# 中间目标文件
main.o : main.c defs.h
cc -c main.c
kbd.o : kbd.c defs.h command.h
cc -c kbd.c
command.o : command.c defs.h command.h
cc -c command.c
# 伪目标文件
clean :
rm edit main.o kbd.o command.o
  • 这里给出了一个构建依赖关系的例子,如果使用make进行编译,根据依赖关系会优先编译所有的.o文件,最后编译可执行文件。这就是简单按照步骤编译整个项目,并不是make的核心功能。
  • 我们知道一个比较大的C/C++项目在构建的时候是分成多个步骤的,存在很多中间文件。以上面这个例子,如果我们已经先编译过一次了,现在只修改了其中的main.c文件。如果我们再次编译项目make会发现main.o的日期早于main.c的日期,此时make会自动判定需要重新生成main.o文件。当然这会造成edit的日期又晚于main.o,也需要重新生成。
  • 在一个大项目的修改中,往往只会涉及较少的一部分文件,因此很多文件是不需要重新编译的。make自动帮我们解决了这个问题。
  • 对于伪目标而言,因为缺失依赖的对象,也就不会自动执行其后所定义的命令。要执行其后的命令,就要在make命令后明显得指出这个lable的名字。

2.2 .o目标依赖

2.2.1 隐式规则

  • 一般来说我们项目中的每一个.c/.cc/.cpp文件都是需要生成对应的.o文件的。如果我们按照例子中的方式对每一个.c/.cc/.cpp文件都手动写出依赖和构建方式,这个工作量将会是巨大的。make提供了一种隐式规则方式来自动生成他们的构建方式。
1
2
%.o: %.cc 
$(GXX) $(GXXFLAGS) -c $< -o $@
  • 在这个模式规则的定义下,当我们需要某个.o文件时,会按照模式给出的模版自动生成构建指令。
  • 这里的$<表示prerequisites中的第一个对象;$@表示target中的挨个值。

2.2.2 依赖关系

  • 但是我们发现这样的模式规则是没有包含.h文件的,虽然.h文件并不直接写入我们编译.o文件的命令。但是这个依赖关系是会影响makefile的编译过程的,即如果依赖关系仅仅是%.o: %.cc,那么当我们仅修改.h文件并进行编译时,make并不会重新编译引用该头文件的.cc
  • 但是一个源文件可能引用非常多的头文件,如果这个引用关心手动管理会非常繁琐,因此使用g++ -MM xxx.cc工具自动生成源文件和头文件的依赖文件。
  • 当然这个依赖文件的生成过程是可以通过makefile实现的,先生成依赖文件并将其引入当前makefile即可:
1
2
3
4
5
6
7
8
9
%.d: %.c
@set -e; rm -f $@; \
$(CC) -MM $(CPPFLAGS) $< > $@.; \
sed 's/\($*\)\.o[ :]*/\1.o $@ : /g' < $@.> $@; \
rm -f $@.

SRC=$(wildcard *.cc)
DEP=$(SRC:.cc=.d)
-include $(DEP)
  • 这里通过g++ -MM xxx.cc生成的内容是形如main.o : main.c defs.h的内容,后面跟了一条sed指令是在原内容中新增main.d的依赖关系,即改成main.o main.d : main.c defs.h。如此依赖根据依赖关系的定义,只要修改过相关的.h文件,就必须要重新编译依赖于它的源文件了。
  • 然后再将生成的所有.d依赖文件包含到当前makefile中,需要注意include命令之前增加符号-,避免第一次make时由于.d文件不存在报告错误信息。
  • 但是存在一个疑问,make是根据依赖关系来生成文件的,这里的关于.d的文件依赖存在哪里,如何触发这些文件的生成的呢?

2.3 静态库的依赖

  • 在第一节的内容中我们提到静态库文件其实就是一堆.o文件的归档,根据ar命令的使用规则我们可以很容易写出依赖关系:
1
2
libxxx.a : xx1.o xx2.o
ar cr $@ $^
  • $^的意思是所有的依赖目标的集合。以空格分隔,如果在依赖目标中有多个重复的,那个这个变量会去除重复的依赖目标,只保留一份。

  • 但是静态库的打包是可以仅仅新增或者更新一小部分的,即当这里的.o文件列表只有一部分发生更改的时候,并不需要完全重新打包一遍,所以应该使用$?,表示所有比目标新的依赖目标的集合,以空格分隔。个人认为这个自动变量是专门为ar的运行逻辑设计的。

1
2
libxxx.a : xx1.o xx2.o
ar cr $@ $?

2.4 伪目标

  • 我们定义依赖关系的时候,可以在依赖的下方定义用于生成目标文件的指令,也可以不定义而根据隐含规则生成指令。但是还存在一种可能是这就是一个伪目标,这常见于clean, all等命令或者标签。
  • 由于伪目标不是文件,所以make无法生成它的依赖关系和决定它是否要执行。我们只有通过显示地指明这个目标才能让其生效。
  • 当这些为目标存在和项目中的某个文件重名的风险,我们需要使用.PHONY: clean all指令来指明这是一个伪目标。

Makefile笔记
https://lluvialuo.github.io/2024/05/17/Makefile笔记/
作者
Lluvia Luo
发布于
2024年5月17日
许可协议