Makefile 笔记(1)— GNU make 基础
admin 于 2021年11月12日 发表在 嵌入式linux开发

写在前面:

最近抽空阅读了《GNU make 中文手册》。由于 Makefie 在嵌入式编译中广泛应用,以及技术细节又比较多;因此,笔者将 Makefile 中的相关规则进行了整理,方便后续需要时查找。

根据《GNU make 中文手册》的内容,最终将书籍内容总结为:GNU make 基础、使用变量、GNU make 指示符、GNU make 控制函数等四大章节。

1. make 概述

make 是一个命令工具,它解释 Makefile 中的规则。因此,make 命令执行前,需要一个 Makefile 文件,以告诉 make 命令如何去编译和链接程序。

所要完成的 Makefile 文件描述了整个工程的编译、连接等规则,包括:工程中哪些源文件需要编译以及如何编译、需要创建哪些库文件以及如何创建这些库文件、如何最后产生我们想要的可执行文件。

Makefile 有自己的书写格式、关键字、函数。像C语言有自己的格式、关键字和函数一样。而且在 Makefile 中可以使用系统 shell 所提供的任何命令来完成想要的工作。Makefile 在绝大多数 IDE 开发环境中都在使用,已经成为一种工程的编译方法。

以《GNU make 使用手册》中的示例说明 Makefile 的书写规则,如下:

  1. 如果这个工程没有被编译过,那么所有的c文件都要编译并链接。

  2. 如果这个工程的某几个c文件被修改,那么我们只编译被修改的c文件,并链接目标程序。

  3. 如果这个工程的头文件被改变了,那么我们需要编译引用这几个头文件的c文件,并链接目标程序。

只要 Makfile 写的好,所有这一切,我们只用一个 make 命令就可以完成,make 命令会自动智能地根据当前的文件修改情况来确定自己编译所需的文件和链接目标程序。

2. Makefile 规则

在此,先简单介绍下 Makefile 的规则,方便理解 make 工具,之后章节会对Makefile 的语法详细展开。

一条简单的 Makefile 规则,如下:

target ... : prerequistites ...
    command
    ...

target  规则的目标。通常是最后需要生成的文件名或者为了实现这个目的而必需的中间过程文件名。可以是.o文件、也可以是最后的可执行程序的文件名等。另外,目标也可以是一个make执行的动作的名称,如目标" clean",我们称这样的目标是"伪目标"

prerequistites 规则的依赖。生成规则目标所需要的文件名列表。通常一个目标依赖于一个或者多个文件。

command 规则的命令行。是规则所要执行的动作(任意的 shell 命令或者是可在shell 下执行的程序)。它限定了 make 执行这条规则时所需要的动作。

这是一个文件的依赖关系,也就是说,target 这一个或多个目标文件依赖于 prerequisites 中的文件,其生成规则定义在 command 中。

如果一个工程有3个 .h 文件和8个 .c 文件,为了完成前面规则的目标,编写 Makfile 内容如下:

CC=gcc

edit : main.o kbd.o command.o display.o \
		insert.o search.o files.o utils.o
    # 注意 : 使用空格进行缩进注释,不能使用<tab>缩进		
    # 注释 : 如果后面这些.o文件比edit可执行文件新,那么才会去执行下面这句命令
	$(CC) -o edit main.o kbd.o command.o display.o \
		insert.o search.o files.o utils.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
display.o : display.c defs.h buffer.h
	$(CC) -c display.c
insert.o : insert.c defs.h buffer.h
	$(CC) -c insert.c
search.o : search.c defs.h buffer.h
	$(CC) -c search.c
files.o : files.c defs.h buffer.h command.h
	$(CC) -c files.c
utils.o : utils.c defs.h
	$(CC) -c utils.c
clean :
	rm edit main.o kbd.o command.o display.o \
		insert.o search.o files.o utils.o

我们可以把这个内容保存在名字为 "makefile" 或 "Makefile" 。

然后在该目录下使用命令 "make" 就可以生成可执行文件 edit ,如下:

执行make执行make.png

如果要删除执行文件和所有中间目标文件,只要简单执行下 "make clean" 即可,如下:

make_cleanmake_clean.png

在 Makefile 文件中,目标文件(target)包含:执行文件 edit 和中间目标文件(*.o),依赖文件(prerequisites)就是冒号后面的那些 .c 文件和 .h 文件。每一个 .o 文件都有一组依赖文件,而这些 .o 文件又是可执行文件 edit 的依赖文件。依赖关系的实质就是说明目标文件是由哪些文件生成的,换言之,目标文件是哪些文件更新的。

在描述依赖关系行之下通常就是规则的命令行(存在一些些规则没有命令行),命令行定义了规则的动作(如何根据依赖文件来更新目标文件)。

命令行必需以 [Tab] 键开始,以和 Makefile 其他行区别。就是说所有的命令行必需以 [Tab] 字符开始,但并不是所有的以 [Tab] 键出现行都是命令行。 但make程序会把出现在第一条规则之后的所有以[Tab]字符开始的行都作为命令行来处理。

make 会比较 targets 文件和 prerequisites 文件的修改日期,如果 prerequisites 文件的日期比 targets 文件的日期要新,或者 target 不存在的话,那么,make 就会执行后续定义的命令。

目标 "clean" 不是一个文件,它仅仅代表执行一个动作的标识。正常情况下,不需要执行这个规则所定义的动作,因此目标 "clean" 没有出现在其它任何规则的依赖列表中。因此在执行 make 时,它所指定的动作不会被执行。除非在执行 make 时明确地指定它;而且目标 "clean" 没有任何依赖文件,它只有一个目的,就是通过这个目标名来执行它所定义的命令。

Makefile 中把那些没有任何依赖只有执行动作的目标称为"伪目标"(phony targets)。

3. make 执行过程

GNU的make工作时执行步骤如下:

  1. 依次读取变量 "MakefileS" 定义的 Makefile 文件列表。

  2. 读取工作目录下的 Makefile 文件。根据命名的查找顺序 "GNUMakefile","Makefile"," makefile",首先找到哪个就读取哪个。

  3. 依次读取工作目录 Makefile 文件中使用指示符 "include" 包含的文件。

  4. 查找重建所有已读取的 Makefile 文件的规则。如果存在一个目标是读取某一个 Makefile 文件,则执行此规则重建此 Makefile 文件,完成以后从第一步开始重新执行。

  5. 初始化变量值并展开那些需要立即展开的变量和函数,并根据预设条件确定执行分支。

  6. 根据"终极目标"以及其他目标的依赖关系建立依赖关系链表。

  7. 执行除"终极目标"以外的所有的目标的规则。规则中如果依赖文件中任一个文件的时间戳比目标文件新,则使用规则所定义的命令重建目标文件。

  8. 执行"终极目标"所列的规则。

4. 指定变量

如上例程,edit 的定义如下:

edit : main.o kbd.o command.o display.o \
		insert.o search.o files.o utils.o
	cc -o edit main.o kbd.o command.o display.o \
		insert.o search.o files.o utils.o

可以看到 [.o] 文件的字符被重复了两次,如果工程需要加入一个新的 [.o] 文件,则需要在两个地方添加。为了 Makefile 的易维护,在 Makefile 中我们可以使用变量。Makefile 变量就是一个字符串,理解成C语言中的宏可能更好。

比如,我们声明任意一个变量名,叫 "objects"、"OBJECTS"、"objs"、"OBJS"、"obj" 或 "OBJ",只要能够表示 obj 文件即可。

objects = main.o kbd.o command.o display.o \
		insert.o search.o files.o utils.o

对原始 Makefile 进行改良后的内容,如下:

CC=gcc

objects = main.o kbd.o command.o display.o \
		insert.o search.o files.o utils.o

edit : $(objects)
    # 注意 : 使用空格进行缩进注释,不能使用<tab>缩进		
    # 注释 : 如果后面这些.o文件比edit可执行文件新,那么才会去执行下面这句命令
	$(CC) -o edit $(objects)
	
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
display.o : display.c defs.h buffer.h
	$(CC) -c display.c
insert.o : insert.c defs.h buffer.h
	$(CC) -c insert.c
search.o : search.c defs.h buffer.h
	$(CC) -c search.c
files.o : files.c defs.h buffer.h command.h
	$(CC) -c files.c
utils.o : utils.c defs.h
	$(CC) -c utils.c
clean :
	rm edit $(objects)

5. 自动推导

GNU 的 make 很强大,它可以自动推导文件以及文件依赖关系后面的命令,于是我们没必要去再每一个 [.o] 文件后写上类似的命令,因为,我们的make会自动识别,并进行推导命令。

只要 make 看到一个 [.o] 文件,它就会自动的把 [.c] 文件加入依赖关系中,如果make找到一个 whatever.o,那么 whatever.c 就会是 whatever.o的依赖文件。并且 cc - c whatever.c 也会被推导出来。

对原始 Makefile 内容进行简化,如下:

CC=gcc

objects = main.o kbd.o command.o display.o \
		insert.o search.o files.o utils.o

edit : $(objects)
    # 注意 : 使用空格进行缩进注释,不能使用<tab>缩进		
    # 注释 : 如果后面这些.o文件比edit可执行文件新,那么才会去执行下面这句命令
	$(CC) -o edit $(objects)
	
main.o : defs.h
kbd.o : defs.h command.h
command.o : defs.h command.h
display.o : defs.h buffer.h
insert.o : defs.h buffer.h
search.o : defs.h buffer.h
files.o : defs.h buffer.h command.h
utils.o : defs.h

.PHONY : clean
clean :
	rm edit $(objects)

这种方法,就是make的"隐含规则"。

6. 清空目标文件

每个 Makefile 中都应该写一个清空目标文件(.o和执行文件)的规则,这不仅便于重编译,也很利于保持文件的清洁。

clean :
	rm edit $(objects)

更为稳健的做法是:

.PHONY : clean
clean :
	-rm edit $(objects)

通过 ".PHONY" 特殊目标将"clean"目标声明为伪目标,避免当磁盘上存在一个名为"clean"文件时,目标"clean"所在的规则的命令无法执行。

在命令行之前使用 "-",意思是,忽略命令 "rm" 的执行错误。当然,clean 的规则不要放在文件的开头,不然,这就变成make的默认目标。

不成文的规则是:clean 从来都是放在文件的最后。

下一篇:《Makefile 笔记(2)— 使用变量》

注意:本站所有文章除特别说明外,均为原创,转载请务必以超链接方式并注明作者出处。 标签:makefile,gnumake,make基础