01 Makefile介绍

1. 程序的编译和链接

1.1 GCC编译器的介绍

​ GCC(GNU Compiler Collection,即 GNU 编译器套装),是一套由 GNU 开发的编程语言编译器。它是一套以 GPL 及 LGPL 许可证所发行的自由软件,也是 GNU 计划的关键部分,亦是自由的类 Unix 及苹果计算机 Mac OS X 操作系统的标准编译器。GCC(特别是其中的 C 语言编译器)也常被认为是跨平台编译器的事实标准。Linux 系统下的 GCC 编译器实际上是 GNU 编译工具链中的一款软件,可以用它来调用其他不同的工具进行诸如预处理、编译、汇编和链接这样的工作。GCC 不仅功能强大,性能优越,其执行效率比一般的编译器相比要高 20%~30%,而且由于其是 GNU 项目之一,是开源的软件,我们可以直接从网上免费地下载安装它。

1.2 编译过程简介

​ 程序之所以需要编译,是因为处理器不能直接理解由文本字符组成的源代码文件,比如编写一个程序如下:

sun@ubuntu:~$ cat hello.c -n
#include <stdio.h>
    
int mai(void)
{
	printf("hello world!\n");
	return 0;
}

这个程序是一个符合 C 语言语法的文本源程序,不能指望 CPU 能像我们这样“读懂”它,CPU 没长眼睛,它只能“读懂”像这样的二进制序列:10110101010001…,因此我们必须将写好的文本程序编译生成一个可以被处理器直接解释的二进制指令文件,GCC 可以帮助我们达到这个目的,具体方法如下:

sun@ubuntu:~$ gcc hello.c -o hello

上述命令的意思是:用 gcc 这个工具编译 hello.c,并且使之生成一个二进制文件 hello。其中 –o 的意义是 output,指明要生成的文件的名称,如果不写 –o hello 的话会生成默认的一个 a.out 文件。
实际上,上面那条语句完成了从 C 源程序到 ELF 格式(Linux 系统下的可执行文件的格式)的全部步骤,生成的文件 hello 实际上是一个可以直接运行的二进制文件,其转换过程是:

image-20221121134834091

从上图可以清晰地看到,从 hello.c 到 hello 并不是一步到位的,中间经历的若干阶段,分别是预处理、编译、汇编和链接。

1.2.1 预处理

​ GCC 在第一个阶段会调用预处理器 cpp 来对 C 源程序进行预处理,所谓的预处理就是解释源程序当中的所有的预处理指令,那些诸如#include、#define、#if 等以井号’#’开头的语句就是预处理指令,预处理指令实际上并不是 C 语言本身的组成部分,而是为了更好地组织程序所使用的一些“预先处理的”工作,这些工作用一种叫做与处理指令的语句来描述,然后用预处理器来解释,这些工作包括我们熟悉的诸如文件包含、宏定义、条件编译等等。

​ 预处理指令将会在预处理阶段被解释掉,比如会把文件包含语句所指定的文件拷贝进来,覆盖掉原来的#include 语句,所有的宏定义被展开(因此宏展开是不占用运行时间的),所有的条件编译语句将被执行等。另外,在这个预处理阶段,除了处理这些预处理指令之外,GCC 还会把程序当中的注释删除,另外添加必要的调试信息(比如你的程序在编译时报告说第 10 行有语法错误,那么“第 10 行”这个信息就是在这个阶段被添加进去的,显然这些信息在你的程序已经调试完毕,要进行发布时是冗余的,因此可以用实用工具 strip 来去掉这些信息,使得程序的尺寸变得更加紧凑)。

sun@ubuntu:~$ arm-linux-gnueabi-strip hostapd //裁减网卡配置hostapd

如果你想要获得 C 源程序经过预处理之后的文件,方法如下:

sun@ubuntu:~$ gcc hello.c -o hello.i -E
sun@ubuntu:~$ cat hello.i 
# 1 "hello.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 31 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 32 "<command-line>" 2
# 1 "hello.c"
...


typedef unsigned char __u_char;
typedef unsigned short int __u_short;
typedef unsigned int __u_int;
typedef unsigned long int __u_long;
...

extern int ftrylockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) ;

extern void funlockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));
# 868 "/usr/include/stdio.h" 3 4

# 2 "hello.c" 2

# 3 "hello.c"
int main(int argc, char **argv)
{
    printf("hello world!\n");
    return 0;
}

加上一个编译选项-E就可以使得 GCC 在进行完第一阶段的预处理之后停下来,生成一个默认后缀名为.i 的文本文件。在我们编写一个包含复杂宏的 C 源程序的时候,如果需要查看宏展开之后的样子,可以用这个选项帮助我们得到这些信息。

1.2.2 编译

​ 过预处理之后生成的.i 文件依然是一个文本文件,不能被处理器直接解释,我们需要进一步的翻译。接下来的编译阶段是四个阶段中最为复杂的阶段,它包括词法和语法的分析,最终生成对应硬件平台的汇编语言(不同的处理器有不同的汇编格式),具体生成什么平台的汇编文件取决于所采用的编译器,如果用的是 GCC,那么将会生成 x86 格式的汇编文件,如果用的是针对 ARM 平台的交叉编译器,那么将会生成 ARM 格式的汇编文件。如果你想要获得 C 源程序经过预处理和编译之后的汇编程序,方法如下:

sun@ubuntu:~$ gcc hello.i -o hello.s -S
sun@ubuntu:~$ cat hello.s
	.file	"hello.c"
	.text
	.section	.rodata
.LC0:
	.string	"hello world!"
	.text
	.globl	main
	.type	main, @function
main:
.LFB0:
	.cfi_startproc
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	subq	$16, %rsp
	movl	%edi, -4(%rbp)
	movq	%rsi, -16(%rbp)
	leaq	.LC0(%rip), %rdi
	call	puts@PLT
	movl	$0, %eax
	leave
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE0:
	.size	main, .-main
	.ident	"GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0"
	.section	.note.GNU-stack,"",@progbits

加上一个编译选项 -S 就可以使得 gcc 在进行完第一和第二阶段之后停下来,生成一个默认后缀名为.s 的文本文件。打开此文件看一看,你会发现这是一个符合 x86 汇编语言的源程序文件。

1.2.3 汇编

下来的步骤相对而言比较简单,编译器 gcc 将会调用汇编器 as 将汇编源程序翻译成为可重定位文件。汇编指令跟处理器直接运行的二进制指令流之间基本是一一对应的关系,该阶段只需要将.s 文件里面的汇编翻译成指令即可。
要想得到这样的一个文件,方法如下:

sun@ubuntu:~$ gcc hello.s -o hello.o -c

在编译的时候加上一个编译选项-c,则会生成一个扩展名为.o 的文件,这个文件是一个 ELF 格式的可重定位(relocatable)文件,所谓的可重定位,指的是该文件虽然已经包含可以让处理器直接运行的指令流,但是程序中的所有的全局符号尚未定位,所谓的全局符号,就是指函数和全局变量,函数和全局变量默认情况下是可以被外部文件引用的,由于定义和调用可以出现在不同的文件当中,因此他们在编译的过程中需要确定其入口地址。

​ 比如 a.c 文件里面定义了一个函数 func()b.c 文件里面调用了该函数,那么在完成第三阶段汇编之后,b.o 文件里面的函数 func()的地址将是 0,显然这是不能运行的,必须要找到 a.c 文件里面函数 func() 的确切的入口地址,然后将 b.c 中的“全局符号” func重新定位为这个地址,程序才能正确运行。因此,接下来需要进行第四个阶段:链接。

1.2.4 链接

​ 如前面所述,经过汇编之后的可重定位文件不能直接运行,因为还有两个很重要的工作没完成,首先是重定位,其次是合并相同权限的段。
关于重定位的问题,上面已经给出了简单的描述。更一般的地,我们编译一个程序通常都需要链接系统的标准 C 库、gcc 内置库等基本库文件。因为 Linux 下任何一个程序编译都需要用到这些基本库的全局符号。

sun@ubuntu:~$ gcc hello.o -o hello -lc -lgcc

标准 C 库和 gcc 内置库是基本的,因此 -lc-lgcc 是默认的,可以省略。链接的另外一个工作是合并相同权限的段(section)。我们知道,一个可执行镜像文件可以由多个可重定位文件链接而成,比如 a.o,b.o,c.o 这三个可重定位文件链接生成一个叫做 x 的可执行文件,这些文件不管是可重定位的,还是可执行的,它们都是 ELF 格式 的,ELF 格式是是符合一定规范的文件格式,里面包含很多段(section),比如我们上面所述的 hello.c 编译生成的 hello.o 有如下的格式:

image-20221121144249978

由此可见,ELF 格式的文件里面包含有很多不同的段(section),每个段都有自己的作用,比如 .text 段存放了运行代码,.data 段里面存放了已经初始化了的全局变量和静态局部变量,.rodata 段存放了程序中所有的常量等等,除了这些程序运行时需要用得到的代码和数据之外,还有一些是程序在从磁盘加载到内存时需要提供给加载器的辅助信息,比如提供代码重定位信息的.rel.text段,ELF 格式文件中的符号表 .symtab 段等,这些信息将会在程序加载完毕之后被丢弃,
而不会存在于程序运行的内存当中。

​ 将多个不同的可重定位 ELF 格式文件链接成一个可执行 ELF 格式文件的过程中,我们需要将他们不同的各个段按照所谓的“执行视图”合并起来,简单来讲,就是将具有相同权限的段合并到一起,比如各个文件中的具有只读权限的.text 段和 .rodata 段将会被合并到一起,按权限合并的理由是,如果该程序有多个执行实例(也就是多个进程),那么这些执行实例会共享一个只读段的副本而不需要多个相同的副本,以此可以节省大量的内存空间。

1.2.5 常用编译选项

选项作用示例
-o 指定输出文件名gcc a.c -o a
-E输出预处理后的代码文件gcc a.c -o a.i -E
-S输出编译后的汇编代码文件gcc a.c -o a.s -S
-c输出链接后的可重定位文件gcc a.c -o a.o -c
-g在编译结果中加入调试信息gcc a.c -o a -g
-I 指定头文件路径gcc a.c -o a -I./include
-L 指定库文件路径c a.c -o a -L./lib
-O指定优化等级[注 1]cc a.c -o a -O2
-static使用静态链接[注 2]gcc a.c -o a -lxxx -static
-Wall打开所有的警告gcc a.c -o a -Wall

注 1:可用的优化等级有 4 个,分别是 O0,O1,O2,O3。优化等级越高,编译速度越慢,相对而言程序运行速度越快,调试难度越大。其中 O0 表示关闭所有的优化项目。
注 2:链接库文件 xxx 时,如果系统中同时存在其对应的静态库和动态库,使用此选项可以使得程序链接静态库,使程序编译之后不依赖于该库文件。

2. Makefile 简介

​ 当我们要编译成千上万个源程序文件的时候,光靠手工地使用 GCC 工具来达到目的也许就会很没有效率,我们亟需一款能够帮助我们自动检查文件的更新情况,自动进行编译的软件,GNU make(工程管理器 make 在不同环境有很多版本分支,比如 Qt 下的 qmake,
Windows 下的 nmake 等,下面提到的 make 指的是 Linux 下的 GNU make)就是这样
的一款软件。
​ Makefile,是 make 的配置文件,用来配置运行 make 的时候的一些相关细节,比如指定编译选项,指定编译环境等等。一般而言,一个工程项目不管是简单还是复杂,每一个源代码子目录都会有一个 Makefile 来管理,然后一般有个所谓的顶层 Makefile 来统一管理所有的子目录 Makefile。

2.1 makefile的规则

在讲述这个makefile之前,还是让我们先来粗略地看一看makefile的规则。

target ... : prerequisites ...
    command
    ...
    ...
  • target

    可以是一个object file(目标文件),也可以是一个执行文件,还可以是一个标签(label)。对于标签这种特性,在后续的“伪目标”章节中会有叙述。

  • prerequisites

    生成该target所依赖的文件和/或target

  • command

    该target要执行的命令(任意的shell命令)

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

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

这就是makefile的规则,也就是makefile中最核心的内容。

说到底,makefile的东西就是这样一点,好像我的这篇文档也该结束了。呵呵。还不尽然,这是makefile 的主线和核心,但要写好一个makefile还不够,我会在后面一点一点地结合我的工作经验给你慢慢道来。内容还多着呢。😃

2.2 一个示例

​ 工程有3个头文件和8个c文件,为了完成前面所述的那三个规则,我们的makefile 应该是下面的这个样子的。

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

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”或“Makefile”的文件中,然后在该目录下直接输入命令 make 就可以生成执行文件edit。如果要删除执行文件和所有的中间目标文件,那么,只要简单地执行一下 make clean 就可以了。

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

在定义好依赖关系后,后续的那一行定义了如何生成目标文件的操作系统命令,一定要以一个 Tab 键作为开头。记住,make并不管命令是怎么工作的,他只管执行所定义的命令。make会比较targets文件和prerequisites文件的修改日期,如果prerequisites文件的日期要比targets文件的日期要新,或者target不存在的话,那么,make就会执行后续定义的命令。

这里要说明一点的是, clean 不是一个文件,它只不过是一个动作名字,有点像c语言中的label一样,其冒号后什么也没有,那么,make就不会自动去找它的依赖性,也就不会自动执行其后所定义的命令。要执行其后的命令,就要在make命令后明显得指出这个label的名字。这样的方法非常有用,我们可以在一个makefile中定义不用的编译或是和编译无关的命令,比如程序的打包,程序的备份,等等。

2.3 make是如何工作的

在默认的方式下,也就是我们只输入 make 命令。那么,

  1. make会在当前目录下找名字叫“Makefile”或“makefile”的文件。
  2. 如果找到,它会找文件中的第一个目标文件(target),在上面的例子中,他会找到 “edit” 这个文件,并把这个文件作为最终的目标文件。
  3. 如果edit文件不存在,或是edit所依赖的后面的 .o 文件的文件修改时间要比 edit 这个文件新,那么,他就会执行后面所定义的命令来生成 edit 这个文件。
  4. 如果 edit 所依赖的 .o 文件也不存在,那么make会在当前文件中找目标为 .o 文件的依赖性,如果找到则再根据那一个规则生成 .o 文件。(这有点像一个堆栈的过程)
  5. 当然,你的C文件和H文件是存在的啦,于是make会生成 .o 文件,然后再用 .o 文件生成make的终极任务,也就是执行文件 edit 了。

这就是整个make的依赖性,make会一层又一层地去找文件的依赖关系,直到最终编译出第一个目标文件。在找寻的过程中,如果出现错误,比如最后被依赖的文件找不到,那么make就会直接退出,并报错,而对于所定义的命令的错误,或是编译不成功,make根本不理。make只管文件的依赖性,即,如果在我找了依赖关系之后,冒号后面的文件还是不在,那么对不起,我就不工作啦。

通过上述分析,我们知道,像clean这种,没有被第一个目标文件直接或间接关联,那么它后面所定义的命令将不会被自动执行,不过,我们可以显示要make执行。即命令—— make clean ,以此来清除所有的目标文件,以便重编译。

于是在我们编程中,如果这个工程已被编译过了,当我们修改了其中一个源文件,比如 file.c ,那么根据我们的依赖性,我们的目标 file.o 会被重编译(也就是在这个依性关系后面所定义的命令),于是 file.o 的文件也是最新的啦,于是 file.o 的文件修改时间要比 edit 要新,所以 edit 也会被重新链接了(详见 edit 目标文件后定义的命令)。

而如果我们改变了 command.h ,那么, kdb.ocommand.ofiles.o 都会被重编译,并且, edit 会被重链接。

2.4 makefile中使用变量

在上面的例子中,先让我们看看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 文件,那么我们需要在两个地方加(应该是三个地方,还有一个地方在clean中)。当然,我们的makefile并不复杂,所以在两个地方加也不累,但如果makefile变得复杂,那么我们就有可能会忘掉一个需要加入的地方,而导致编译失败。所以,为了makefile的易维护,在makefile中我们可以使用变量。makefile的变量也就是一个字符串,理解成C语言中的宏可能会更好。

比如,我们声明一个变量,叫 objectsOBJECTSobjsOBJSobj 或是 OBJ ,反正不管什么啦,只要能够表示obj文件就行了。我们在makefile一开始就这样定义:

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

于是,我们就可以很方便地在我们的makefile中以 $(objects) 的方式来使用这个变量了,于是我们的改良版makefile就变成下面这个样子:

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

edit : $(objects)
    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)

于是如果有新的 .o 文件加入,我们只需简单地修改一下 objects 变量就可以了。

2.5 make自动推导

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

只要make看到一个 .o 文件,它就会自动的把 .c 文件加在依赖关系中,如果make找到一个 whatever.o ,那么 whatever.c 就会是 whatever.o 的依赖文件。并且 cc -c whatever.c 也会被推导出来,于是,我们的makefile再也不用写得这么复杂。我们的新makefile又出炉了。

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

edit : $(objects)
    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的“隐晦规则”。上面文件内容中, .PHONY 表示 clean 是个伪目标文件。

实际项目开发中的makefile示例:

image-20221121161045083

2.6 另类风格的makefiles

既然我们的make可以自动推导命令,那么我看到那堆 .o.h 的依赖就有点不爽,那么多的重复的 .h ,能不能把其收拢起来,好吧,没有问题,这个对于make来说很容易,谁叫它提供了自动推导命令和文件的功能呢?来看看最新风格的makefile吧。

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

edit : $(objects)
    cc -o edit $(objects)

$(objects) : defs.h
kbd.o command.o files.o : command.h
display.o insert.o search.o files.o : buffer.h

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

这种风格,让我们的makefile变得很简单,但我们的文件依赖关系就显得有点凌乱了。鱼和熊掌不可兼得。还看你的喜好了。我是不喜欢这种风格的,一是文件的依赖关系看不清楚,二是如果文件一多,要加入几个新的 .o 文件,那就理不清楚了。

2.7 清空目标文件的规则

每个Makefile中都应该写一个清空目标文件( .o 和执行文件)的规则,这不仅便于重编译,也很利于保持文件的清洁。这是一个“修养”(呵呵,还记得我的《编程修养》吗)。一般的风格都是:

clean:
    rm edit $(objects)

更为稳健的做法是:

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

前面说过, .PHONY 表示 clean 是一个“伪目标”。而在 rm 命令前面加了一个小减号的意思就是,也许某些文件出现问题,但不要管,继续做后面的事。当然, clean 的规则不要放在文件的开头,不然,这就会变成make的默认目标,相信谁也不愿意这样。不成文的规矩是——“clean从来都是放在文件的最后”。

上面就是一个makefile的概貌,也是makefile的基础,下面还有很多makefile的相关细节,准备好了吗?准备好了就来。

2.8 Makefile里有什么?

Makefile里主要包含了五个东西:显式规则隐晦规则变量定义文件指示和注释

  1. 显式规则。显式规则说明了如何生成一个或多个目标文件。这是由Makefile的书写者明显指出要生成的文件、文件的依赖文件和生成的命令。
  2. 隐晦规则。由于我们的make有自动推导的功能,所以隐晦的规则可以让我们比较简略地书写 Makefile,这是由make所支持的。
  3. 变量的定义。在Makefile中我们要定义一系列的变量,变量一般都是字符串,这个有点像你C语言中的宏,当Makefile被执行时,其中的变量都会被扩展到相应的引用位置上。
  4. 文件指示。其包括了三个部分,一个是在一个Makefile中引用另一个Makefile,就像C语言中的include一样;另一个是指根据某些情况指定Makefile中的有效部分,就像C语言中的预编译#if一样;还有就是定义一个多行的命令。有关这一部分的内容,我会在后续的部分中讲述。
  5. 注释。Makefile中只有行注释,和UNIX的Shell脚本一样,其注释是用 # 字符,这个就像C/C++中的 // 一样。如果你要在你的Makefile中使用 # 字符,可以用反斜杠进行转义,如: \#

最后,还值得一提的是,在Makefile中的命令,必须要以 Tab 键开始。

2.9 引用其它的Makefile

在Makefile使用 include 关键字可以把别的Makefile包含进来,这很像C语言的 #include ,被包含的文件会原模原样的放在当前文件的包含位置。 include 的语法是:

include <filename>

filename 可以是当前操作系统Shell的文件模式(可以包含路径和通配符)。

include 前面可以有一些空字符,但是绝不能是 Tab 键开始。 include<filename> 可以用一个或多个空格隔开。举个例子,你有这样几个Makefile: a.mkb.mkc.mk ,还有一个文件叫 foo.make ,以及一个变量 $(bar) ,其包含了 e.mkf.mk ,那么,下面的语句:

include foo.make *.mk $(bar)

等价于:

include foo.make a.mk b.mk c.mk e.mk f.mk

make命令开始时,会找寻 include 所指出的其它Makefile,并把其内容安置在当前的位置。就好像C/C++的 #include 指令一样。如果文件都没有指定绝对路径或是相对路径的话,make会在当前目录下首先寻找,如果当前目录下没有找到,那么,make还会在下面的几个目录下找:

  1. 如果make执行时,有 -I--include-dir 参数,那么make就会在这个参数所指定的目录下去寻找。
  2. 如果目录 <prefix>/include (一般是: /usr/local/bin/usr/include )存在的话,make也会去找。

如果有文件没有找到的话,make会生成一条警告信息,但不会马上出现致命错误。它会继续载入其它的文件,一旦完成makefile的读取,make会再重试这些没有找到,或是不能读取的文件,如果还是不行,make才会出现一条致命信息。如果你想让make不理那些无法读取的文件,而继续执行,你可以在include前加一个减号“-”。如:

-include <filename>

其表示,无论include过程中出现什么错误,都不要报错继续执行。和其它版本make兼容的相关命令是sinclude,其作用和这一个是一样的。

2.10 环境变量MAKEFILES

如果你的当前环境中定义了环境变量 MAKEFILES ,那么,make会把这个变量中的值做一个类似于 include 的动作。这个变量中的值是其它的Makefile,用空格分隔。只是,它和 include 不同的是,从这个环境变量中引入的Makefile的“目标”不会起作用,如果环境变量中定义的文件发现错误,make也会不理。

但是在这里我还是建议不要使用这个环境变量,因为只要这个变量一被定义,那么当你使用make时,所有的Makefile都会受到它的影响,这绝不是你想看到的。在这里提这个事,只是为了告诉大家,也许有时候你的Makefile出现了怪事,那么你可以看看当前环境中有没有定义这个变量。

2.11 make的工作方式

GNU的make工作时的执行步骤如下:(想来其它的make也是类似)

  1. 读入所有的Makefile。
  2. 读入被include的其它Makefile。
  3. 初始化文件中的变量。
  4. 推导隐晦规则,并分析所有规则。
  5. 为所有的目标文件创建依赖关系链。
  6. 根据依赖关系,决定哪些目标要重新生成。
  7. 执行生成命令。

1-5步为第一个阶段,6-7为第二个阶段。第一个阶段中,如果定义的变量被使用了,那么,make会把其展开在使用的位置。但make并不会完全马上展开,make使用的是拖延战术,如果变量出现在依赖关系的规则中,那么仅当这条依赖被决定要使用了,变量才会在其内部展开。

参考: 跟我一起写Makefile

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Engineer-Jaylen_Sun

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值