1. 首页
  2. 技术小贴

Linux内核编程之Makefile

总述

技术小贴
技术小贴

Makefile是一个文本形式的数据库文件,其中包含一些规则来告诉make处理哪些文件以及如何处理这些文件。这些规则主要是描述目标文件是从哪些别的依赖文件中产生的,以及用什么命令来执行这个过程。

执行make命令时,需要一个 Makefile 文件,以告诉make命令需要怎么样的去编译和链接程序。

它会对磁盘上的文件进行检查,如果目标文件的生成或被改动时的时间至少比它的一个依赖文件还旧的话,make就执行相应的命令,以更新目标文件。

目标文件不一定是最后的可执行文件,可以是任何一个中间文件并可以作为其他目标文件的依赖文件。

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中使用“#”字符,可以用反斜框进行转义,如:“#”。

显式规则

每条规则包含以下内容:

  1. 一个目标(target),即make最终需要创建的文件,如可执行文件和目标文件;目标也可以是要执行的动作,如clean
  2. 一个或多个依赖文件(dependency)列表,通常是编译目标文件所需要的其他文件。
  3. 一系列命今(command),是make执行的动作,通常是把指定的相关文件编译成目标文件的编译命令,每个命令占一行,且每个命令行的起始字符必须为TAB字符。

预编译、汇编、编译、链接过程如下:

.c & .h-->.i-->.s--->.o--->elf

hello: hello.o
	gcc hello.o -o hello
hello.o:hello.s
	gcc -c hello.s -o hello.o
hello.s:hello.i
	gcc -S hello.i -o hello.s
hello.i:hello.c
	gcc -E hello.c -o hello.i

编译时,编译器需要的是语法的正确,函数与变量的声明的正确。一般来说,每个源文件都应该对应于一个中间目标文件(O文件或是OBJ文件)。

链接时,主要是链接函数和全局变量,所以,我们可以使用这些中间目标文件(O文件或是OBJ文件)来链接我们的应用程序。链接器并不管函数所在的源文件,只管函数的中间目标文件(Object File),在大多数时候,由于源文件太多,编译生成的中间目标文件太多,而在链接时需要明显地指出中间目标文件名,这对于编译很不方便,所以,我们要给中间目标文件打个包,在Windows下这种包叫“库文件”(Library File),也就是 .lib 文件,在UNIX下,是Archive File,也就是 .a 文件。

简单Makefile如下:

test:prog.o code.o
		gcc –o test prog.o code.o
prog.o:prog.c prog.h code.h
		gcc –c prog.c –o prog.o
code.o:code.c code.h
		gcc –c code.c –o code.o
clean:
		rm –f *.o

上面的Makefile文件中共定义了四个目标:test、prog.o、code.o和clean。

如果有与这个目标有依赖性的其他目标或文件,把它们列在冒号后面,并以空格隔开。

在Makefile中,可使用续行号\将一个单独的命令行延续成几行。但要注意在续行号\后面不能跟任何字符(包括空格和键)。

下面介绍如何调用make命令:

make target

target是Makefile文件中定义的目标之一,如果省略target,make就将生成Makefile文件中定义的第一个目标。对于上面Makefile的例子,单独的一个make命令等价于:
make test

第一条规则说明只要文件test的时间戳比文件prog.o或code.o中的任何一个旧,下一行的编译命令将会被执行。

但是,在检查文件prog.o和code.o的时间戳之前,make会在下面的行中寻找以prog.o和code.o为目标的规则,在第三行中找到了关于prog.o的规则,该文件的依赖文件是prog.c、prog.h和code.h。

同样,make会在后面的规则行中继续查找这些依赖文件的规则,如果找不到,则开始检查这些依赖文件的时间戳,如果这些文件中任何一个的时间戳比prog.o的新,make将执行gcc –c prog.c –o prog.o命令,更新prog.o文件。

以同样的方法,接下来对文件code.o做类似 的检查,依赖文件是code.c和code.h。当make执行完所有这些套嵌的规则后,make将处理最顶层的test规则,这是因为prog.o和code.c的时间戳比test新。

在上面Makefile的例子中,还定义了一个目标clean,它是Makefile中常用的一种专用目标,即删除执行文件和所有的中间目标文件,以便重编译

总结一下make的执行流程

首先make会在当前目录找寻文件:GNUmakefile、makefile、Makefile,最好使用Makefile这个文件名,然后按顺序读取makefile中的规则,然后检查该规则中的依赖文件与目标文件的时间戳哪个更新,如果目标文件的时问戳比依赖文件还早,就按规则中定义的命令更新目标文件。

如果该规则中的依赖文件又是其他规则中的目标文件,那么依照规则链不断执行这个过程,直到Makefile文件的结束,至少可以找到一个不是规则生成的最终依赖文件,获得此文件的时间戳,然后从下到上依照规则链执行目标文件的时间戳比此文件时间戳旧的规则,直到最顶层的规则。

当然,你可以使用别的文件名来书写Makefile,如:“Make.Linux”,“Make.Solaris”,“Make.AIX”等,如果要指定特定的Makefile,你可以使用make的“-f”和“–file”参数,如:make -f Make.Linux或make –file Make.AIX。


变量定义

在Makefile中我们要定义一系列的变量,变量一般都是字符串,对大小写敏感,一般使用大写宇母。类似C语言中的宏,当Makefile被执行时,其中的变量都会被扩展到相应的引用位置上。

变量的作用:

1)保存文件名列表。

作为依赖文件的一些目标文件名出现在可执行文件的规则中,而在这个规则的命令行里同样包含这些文件并传递给gcc做为命令参数。

如果使用一个变量来保存所有的目标文件名,则可以方便地加入新的目标文件而且不易出错。

2) 保存可执行命令名,如编译器。

在不同的Linux系统中存在着很多相似的编译器系统,这些系统在某些地方会有细微的差别,如果项目被用在一个非gcc的系统里,则必须将所有出现编译器名的地方改成用新的编译器名,比如编译器的版本不同,arm-linux 3.4.1、4.3.2等。

如果使用一个变量来代替编译器名,那么只需要改变该变量的值。其他所有地方的命令名就都改变了。

3)保存编译器的参数。

在很多源代码编译时,gcc需要很长的参数选项,在很多情况下,所有的编译命令使用一组相同的选项,如果把这组选项使用一个变量代表,那么可以把这个变量放在所有引用编译器的地方。

变量的定义:

VARNAME=string     #变量赋值,定义
VARNAME1 := string #变量赋值,一般定义
VARNAME2 += string #变量追加

使用时,把变量用括号括起来,并在前面加上$符号,就可以引用变量的值:

${VARNAME}

变量一般都在Makefile的头部定义。按照惯例,所有的Makefile变量都应该是大写。如果变量的值发生变化,就只需要在一个地方修改,从而简化了Makefile的维护。

现在利用变量把前面的Makefile重写一遍:

OBJS = prog.o code.o
CC = GCC
test: ${OBJS}
	${CC} -o test ${OBJS}
prog.o:prog.c prog.h code.h
	${CC} -c prog.c -o prog.o
code.o: code.c code.h
	${CC} -c code.c -o code.o
clean:
	rm -f *.o

自动变量:

命令格式 含义
$* 不包含扩展名的目标文件名称
$+ 所有的依赖文件,以空格分开,可能重复包含
$< 第一个依赖文件的名称
$? 任何时间戳比目标文件晚的依赖文件,以空格隔开

通配符:

格式 含义
% 匹配任意字符,用于规则描述,如%.o:%.c
* 匹配任意字符,用于命令中,如“*.c”表示所以后缀为c的文件。
? 代替任意一个字符
~ 指定家目录,~/test表示当前用户家目录下,~hchen/test表示schen用户

隐式规则

由于make有自动推导的功能,所以隐式规则可以让我们比较粗糙地简略地书写Makefile,这是由make支持的。

在上面的例子中,几个产生目标文件的命令都是从“.c”的C语言源文件和相关文件通过编译产生“.o”目标文件,这也是一般的步骤。

实际上,make可以使工作更加自动化,也就是说,make知道一些默认的动作,它有一些称作隐含规则的内置的规则,这些规则告诉make当用户没有完整地给出某些命令的时候,应该怎样执行,它可以自动推导文件以及文件依赖关系后面的命令,于是我们就没必要去在每一个[.o]文件后都写上类似的命令,因为,我们的make会自动识别,并自己推导命令。

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

如前面的例子中所定义的那样,make使用变量CC来定义编译器,并且传递变量CFLAGS(编译器参数)、CPPFLAGS(C语言预处理器参数)、TARGET_ARCH(目标机器的结构定义)给编译器,然后加上参数-c,后面跟变量$<(第一个依赖文件名),然后是参数-o加变量$@(目标文件名)。

综上所述,一个C编译的具体命令将会是:

$ {CC} $ {CFLAGS} $ {CPPFLAGS} $ {TARGET_ARCH} –c $< -o $@

上面的文件利用隐式规则可以简化为:

OBJS=prog.o code.o
CC=gcc
test:${ OBJS }
	${ CC } –o $@ $^
prog.o:prog.c prog.h code.h #让make自动推导
code.o:code.c code.h
clean:
	rm –f *.o

文件引用

其包括了三个部分,一个是在一个Makefile中引用另一个Makefile,就像C语言中的include一样;

另一个是指根据某些情况指定Makefile中的有效部分,就像C语言中的预编译#if一样。

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

include的语法是:include <filename>

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

Make支持三种通配符: "*"、"?"、"[...]"

bar = e.mk f.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. 如果目录/include(一般是:/usr/local/bin或/usr/include)存在的话,make也会去找。

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

-include <filename>

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


文件搜寻

在一些大的工程中,有大量的源文件,我们通常的做法是把这许多的源文件分类,并存放在不同的目录中。所以,当make需要去找寻文件的依赖关系时,你可以在文件前加上路径,但最好的方法是把一个路径告诉make,让make自动去找。

Makefile文件中的特殊变量“VPATH”就是完成这个功能的,如果没有指明这个变量,make只会在当前的目录中去找寻依赖文件和目标文件。如果定义了这个变量,那么,make就会在当当前目录找不到的情况下,到所指定的目录中去找寻文件了。

VPATH = src:../headers

上面的的定义指定两个目录,“src”和“../headers”,make会按照这个顺序进行搜索。目录由“冒号”分隔。(当然,当前目录永远是最高优先搜索的地方)

另一个设置文件搜索路径的方法是使用make的“vpath”关键字(注意,它是全小写的),这不是变量,这是一个make的关键字,这和上面提到的那个VPATH变量很类似,但是它更为灵活。它可以指定不同的文件在不同的搜索目录中。这是一个很灵活的功能。它的使用方法有三种:

方法 描述
vpath < pattern> < directories> 为符合模式< pattern>的文件指定搜索目录<directories>
vpath < pattern> 清除符合模式< pattern>的文件的搜索目录
vpath 清除所有已被设置好了的文件搜索目录

vapth使用方法中的< pattern>需要包含“%”字符。“%”的意思是匹配零或若干字符,例如,“%.h”表示所有以“.h”结尾的文件。< pattern>指定了要搜索的文件集,而< directories>则指定了的文件集的搜索的目录。例如:

vpath %.h ../headers

该语句表示,要求make在“../headers”目录下搜索所有以“.h”结尾的文件。(如果某文件在当前目录没有找到的话)

我们可以连续地使用vpath语句,以指定不同搜索策略。如果连续的vpath语句中出现了相同的< pattern>,或是被重复了的< pattern>,那么,make会按照vpath语句的先后顺序来执行搜索。如:

 vpath %.c foo:bar
vpath %   blish

上面的语句则表示“.c”结尾的文件,先在“foo”目录,然后是“bar”目录,最后才是“blish”目录。


环境变量

如果你的当前环境中定义了环境变量MAKEFILES,那么,make会把这个变量中的值做一个类似于include的动作。

这个变量中的值是其它的Makefile,用空格分隔。只是,它和include不同的是,从这个环境变中引入的Makefile的“目标”不会起作用,如果环境变量中定义的文件发现错误,make也会不理。

建议不要使用这个环境变量,因为只要这个变量一被定义,那么当你使用make时,所有的Makefile都会受到它的影响,这绝不是你想看到的。


伪目标

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

一般的风格都是:

clean:
	rm edit $(objects)

这就是一个“伪目标”,需要说明的是“伪目标”并不是一个文件,只是一个标签,由于“伪目标”不是文件,所以make无法生成它的依赖关系和决定它是否要执行。我们只有通过显示地指明这个“目标”才能让其生效。当然,“伪目标”的取名不能和文件名重名,不然其就失去了“伪目标”的意义了。

当然,为了避免和文件重名的这种情况,我们可以使用一个特殊的标记“.PHONY”来显示地指明一个目标是“伪目标”,向make说明,不管是否有这个文件,这个目标就是“伪目标”。

于是上面的代码可以修改如下:

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

PHONY意思表示clean是一个“伪目标”,而在rm命令前面加了一个小减号的意思就是,也许某些文件出现问题,但不要管,继续做后面的事。

当然,clean的规则不要放在文件的开头,不然,这就会变成make的默认目标,相信谁也不愿意这样。不成文的规矩是——“clean从来都是放在文件的最后”。

 .PHONY: cleanall cleanobj cleandiff
  cleanall : cleanobj cleandiff
      rm program
 cleanobj :
      rm *.o
cleandiff :
      rm *.diff

从上面的例子我们可以看出,目标也可以成为依赖。所以,伪目标同样也可成为依赖。

“makeclean”将清除所有要被清除的文件。“cleanobj”和“cleandiff”这两个伪目标有点像“子程序”的意思。我们可以输入“makecleanall”和“make cleanobj”和“make cleandiff”命令来达到清除不同种类文件的目的。

多目标

Makefile的规则中的目标可以不止一个,其支持多目标,有可能我们的多个目标同时依赖于一个文件,并且其生成的命令大体类似。

于是我们就能把其合并起来。当然,多个目标的生成规则的执行命令是同一个,这可能会可我们带来麻烦,不过好在我们的可以使用一个自动化变量“$@”),这个变量表示着目前规则中所有的目标的集合。

 bigoutput littleoutput : text.g
 	generate text.g -$(subst output,,$@) > $@

上述规则等价于:

bigoutput : text.g
    generate text.g -big > bigoutput
littleoutput : text.g
    generate text.g -little > littleoutput

-$(subst output,,$@)中的“$”表示执行一个Makefile的函数,函数名为subst,后面的为参数。这个函数是截取字符串的意思,“$@”表示目标的集合,就像一个数组,“$@”依次取出目标,并执于命令。


静态模式

静态模式可以更加容易地定义多目标的规则,可以让我们的规则变得更加的有弹性和灵活。我们还是先来看一下语法:

<targets...>: <target-pattern>: <prereq-patterns ...>
   <commands>
...
  • targets定义了一系列的目标文件,可以有通配符,是目标的集合。
  • target-parrtern是指明了targets的模式,也就是的目标集模式。
  • prereq-parrterns是目标的依赖模式,它对target-parrtern形成的模式再进行一次依赖目标的定义。

如果我们的<target-parrtern>定义成“%.o”,意思是我们的集合中都是以“.o”结尾的,而如果我们的<prereq-parrterns>定义成“%.c”,意思是对<target-parrtern>所形成的目标集进行二次定义,其计算方法是,取<target-parrtern>模式中的“%”(也就是去掉了[.o]这个结尾),并为其加上[.c]这个结尾,形成的新集合。

所以,我们的“目标模式”或是“依赖模式”中都应该有“%”这个字符,如果你的文件名中有“%”那么你可以使用反斜杠“\”进行转义,来标明真实的“%”字符。

 objects = foo.o bar.o
 all: $(objects)
 $(objects): %.o: %.c
 	$(CC) -c $(CFLAGS) $< -o $@

上面的例子中,指明了我们的目标从$object中获取,“%.o”表明要所有以“.o”结尾的目标,也就是“foo.o bar.o”,也就是变量$object集合的模式,而依赖模式“%.c”则取模式“%.o”的“%”,也就是“foo bar”,并为其加下“.c”的后缀,于是,我们的依赖目标就是“foo.c bar.c”。

而命令中的“$<”和“$@”则是自动化变量,“$<”表示所有的依赖目标集(也就是“foo.c bar.c”),“$@”表示目标集(foo.o bar.o”)。

于是,上面的规则展开后等价于下面的规则:

foo.o : foo.c
           $(CC) -c $(CFLAGS) foo.c -o foo.o
bar.o : bar.c
           $(CC) -c $(CFLAGS) bar.c -o bar.o

试想,如果我们的“%.o”有几百个,那种我们只要用这种很简单的“静态模式规则”就可以写完一堆规则,实在是太有效率了。“静态模式规则”的用法很灵活,如果用得好,那会一个很强大的功能。

再看一个例子:

  files = foo.elc bar.o lose.o
  $(filter %.o,$(files)): %.o: %.c
           $(CC) -c $(CFLAGS) $< -o $@
  $(filter %.elc,$(files)): %.elc: %.el
           emacs -f batch-byte-compile $<

$(filter%.o,$(files))表示调用Makefile的filter函数,过滤“$filter”集,只要其中模式为“%.o”的内容。

自动生成依赖性

在Makefile中,我们的依赖关系可能会需要包含一系列的头文件,比如,如果我们的main.c中有一句“#include “defs.h””,那么我们的依赖关系应该是:

  main.o : main.c defs.h

但是,如果是一个比较大型的工程,你必需清楚哪些C文件包含了哪些头文件,并且,你在加入或删除头文件时,也需要小心地修改Makefile,这是一个很没有维护性的工作。

为了避免这种繁重而又容易出错的事情,我们可以使用C/C++编译的一个功能。大多数的C/C++编译器都支持一个“-M”的选项,即自动找寻源文件中包含的头文件,并生成一个依赖关系。例如,如果我们执行下面的命令:

cc -M main.c

其输出是:

main.o : main.c defs.h

于是由编译器自动生成的依赖关系,这样一来,你就不必再手动书写若干文件的依赖关系,而由编译器自动生成了。

需要提醒一句的是,如果你使用GNU的C/C++编译器,你得用“-MM”参数,不然,“-M”参数会把一些标准库的头文件也包含进来。

GNU组织建议把编译器为每一个源文件的自动生成的依赖关系放到一个文件中,为每一个“name.c”的文件都生成一个“name.d”的Makefile文件,[.d]文件中就存放对应[.c]文件的依赖关系。

于是,我们可以写出[.c]文件和[.d]文件的依赖关系,并让make自动更新或自成[.d]文件,并把其包含在我们的主Makefile中,这样,我们就可以自动化地生成每个文件的依赖关系了。

这里,我们给出了一个模式规则来产生[.d]文件:

 %.d: %.c
     @set -e; rm -f $@; \
     $(CC) -M $(CPPFLAGS) $< > $@; \
     sed 's,$∗\.o[ :]*,\1.o $@ : ,g' < $@ > $@; \
     rm -f $@

这个规则的意思是,所有的[.d]文件依赖于[.c]文件,“rm-f $@”的意思是删除所有的目标,也就是[.d]文件,第二行的意思是,为每个依赖文件“$<”,也就是[.c]文件生成依赖文件,“$@”表示模式“%.d”文件,如果有一个C文件是name.c,那么“%”就是“name”,第二行生成的文件有可是“name.d.12345”,第三行使用sed命令做了一个替换,第四行就是删除临时文件。

总而言之,这个模式要做的事就是在编译器生成的依赖关系中加入[.d]文件的依赖,即把依赖关系:

main.o : main.c defs.h

转为:

 main.o main.d : main.c defs.h

于是,我们的[.d]文件也会自动更新了,并会自动生成了,当然,你还可以在这个[.d]文件中加入的不只是依赖关系,包括生成的命令也可一并加入,让每个[.d]文件都包含一个完赖的规则。

一旦我们完成这个工作,接下来,我们就要把这些自动生成的规则放进我们的主Makefile中。我们可以使用Makefile的“include”命令,来引入别的Makefile文件(前面讲过),例如:

sources = foo.c bar.c
include $(sources:.c=.d)

上述语句中的“$(sources:.c=.d)”中的“.c=.d”的意思是做一个替换,把变量$(sources)所有[.c]的字串都替换成[.d]。

书写命令

 

原创文章,作者:小嵘源码,如若转载,请注明出处:https://www.lcpttec.com/linux-makefile/

联系我们

176-888-72082

在线咨询:点击这里给我发消息

邮件:2668888288@qq.com

工作时间:周一至周五,9:00-18:00,节假日休息

QR code