Linux系统编程(四)

写在前面

本文主要介绍makefile的编写

makefile

作用

Makefile 是和 make 命令一起配合使用的.
很多大型项目的编译都是通过 Makefile 来组织的, 如果没有 Makefile, 那很多项目中各种库和代码之间的依赖关系不知会多复杂.
Makefile的组织流程的能力如此之强, 不仅可以用来编译项目, 还可以用来组织我们平时的一些日常操作. 这个需要大家发挥自己的想象力.

引入

比如说,你有add.c, sub.c,div1.c,hello.c四个C语言文件,你要编译成一个a.out`,你会怎么做呢?你会执行这样的命令

gcc add.c sub.c div1.c hello.c -o a.out

如果此时我们无论对四个.c文件哪一个进行修改,都会影响最后生成的a.out,都需要重新将这四个文件进行编译。可编译在gcc运行的四个步骤中花费的系统资源最多,耗时最长,所以为了优化可以写成如下形式

gcc -c add.c -o add.o
gcc -c sub.c -o sub.o
gcc -c div1.c -o div1.o
gcc -c hello.c -o hello.o
gcc add.o sub.o div1.o hello.o

gcc的四个过程可知,使用-c命令使源文件经过了预处理,编译,汇编三个阶段生成.o文件,而最后通过.o文件进行链接编程可执行文件。例如当修改add.c时,通过这样的操作只需要重新编译一下add.c生成最新的add.o,在进行重新链接就可以跳过最耗时的编译过程。
可是这样写起来比较复杂,一个工程代码中可能有很多这样的代码,所以就有makefile来管理项目代码

介绍

make命令执行时,需要一个 makefile 文件,以告诉make命令如何去编译和链接程序。
首先,我们用一个示例来说明makefile的书写规则。以便给大家一个感性认识。这个示例来源于gnumake使用手册,在这个示例中,我们的工程有8个.c文件,和3个头文件,我们要写一个makefile来告诉make命令如何编译和链接这几个文件。我们的规则是:

  • 如果这个工程没有编译过,那么我们的所有c文件都要编译并被链接。
  • 如果这个工程的某几个c文件被修改,那么我们只编译被修改的c文件,并链接目标程序。
  • 如果这个工程的头文件被改变了,那么我们需要编译引用了这几个头文件的c文件,并链接目标程序。
    只要我们的makefile写得够好,所有的这一切,我们只用一个make命令就可以完成,make命令会自动智能地根据当前的文件修改的情况来确定哪些文件需要重编译,从而自己编译所需要的文件和链接目标程序。

命名

makefile 要想使用 make指令,那么当前目录下有且必须有Makefile或者makefile文件

一个规则

目标:依赖条件
	(一个tab缩进)命令

目标:要生成的文件
依赖条件:想生成目标文件需要那些东西,类似于前置技能?
命令:要想从依赖条件走到目标文件要执行的命令
例如,想要生成 add.o文件

add.o:add.c   #要想生成add.o,需要从add.c通过gcc编译得来
	gcc -c add.c -o add.o

所以以上程序通过makefile可以编写为

a.out:hello.o add.o sub.o div1.o mul.o
	gcc hello.o add.o sub.o div1.o mul.o -o a.out
hello.o:hello.c
	gcc -c hello.c -o hello.o
sub.o:sub.c
	gcc -c sub.c -o sub.o
div1.o:div1.c
	gcc -c div1.c -o div1.o
add.o:add.c
	gcc -c add.c -o add.o

注意上面代码的顺序可能与正常顺序不符,正常顺序应该是先把所有的.o文件准备好,然后再统一创造a.out,但是Makefile中的依赖定义构成了一个依赖链(树),比如上面这个Makefile中,a.out依赖于add.oa.out又依赖于sub.o~,所以,当你去满足a.out(这个目标)的依赖的时候,它首先去检查add.o的依赖,直到找到依赖树的叶子节点(add.c),然后进行时间比较。这个判断过程由make工具来完成,所以,和一般的脚本不一样。Makefile的执行过程不是基于语句顺序的,而是基于依赖链的顺序的。 此时我们可以添加一个ALL:来指定makefile`需要生成的最终文件。

两个函数

src = $(wildcard ./*.c): 匹配当前工作目录下的所有`.c` 文件。将文件名组成列表,赋值给变量 `src`。  							 
	src = add.c sub.c div1.c
obj = $(patsubst %.c, %.o, $(src)): 将参数3中,包含参数1的部分,替换为参数2。 
	obj = add.o sub.o div1.o

通过这两个函数再次改写makefile文件,注意,每次在执行make命令之前都要去删除之前make生成的文件,因为只对makefile进行了修改而产生的终极目标并没有进行修改(文件修改时间),所以make指令不会去执行,为了简单加入clean
clean:(没有依赖)

  • -的作用:删除不存在文件时,不报错。顺序执行结束。
    使用方式:
  • make clean -n 通常先执行这一步看会执行什么命令,以防错删
  • make clean 即可
src = $(wildcard *.c)
obj = $(patsubst %.c, %.o, $(src))
ALL:a.out
a.out:$(obj)
	gcc $(obj) -o a.out
hello.o:hello.c
	gcc -c hello.c -o hello.o
sub.o:sub.c
	gcc -c sub.c -o sub.o
div1.o:div1.c
	gcc -c div1.c -o div1.o
add.o:add.c
	gcc -c add.c -o add.o
clean:
	-rm -rf $(obj) a.out

三个自动变量

$@: 在规则的命令中,表示规则中的目标。

$^: 在规则的命令中,表示所有依赖条件。

$<: 在规则的命令中,表示第一个依赖条件。如果将该变量应用在模式规则中,它可将依赖条件列表中的依赖依次取出,套用模式规则。

通过如上再次改写makefile

src = $(wildcard *.c)
	obj = $(patsubst %.c, %.o, $(src))
ALL:a.out
a.out:$(obj)
	gcc $^ -o $@
hello.o:hello.c
	gcc -c $< -o $@
sub.o:sub.c
	gcc -c $< -o $@
div1.o:div1.c
	gcc -c $< -o $@
mul.o:mul.c
	gcc -c $< -o $@
add.o:add.c
	gcc -c $< -o $@
clean:
	-rm -rf *.o a.out

模式规则

很容易发现上述代码有非常严重的重复都是通过gcc -c $< -o $@命令来实现,所以此时模式规则可以写为

%.o:%.c
	gcc -c $< -o $@

此时在去改写makefile文件

src = $(wildcard *.c)
obj = $(patsubst %.c, %.o, $(src))
ALL:a.out
a.out:$(obj)
	gcc $^ -o $@
%.o:%.c
	gcc -c $< -o $@
clean:
-rm -rf *.o a.out

静态模式规则在上述代码中可改写为$(obj):%.o:%.c,意思是$(obj)使用该模式规则。

扩展

伪目标

make命令执行的时候,后面跟一个目标(不带参数的话默认是第一个依赖的目标),然后以这个目标为根建立整个依赖树。依赖树的每个节点是一个文件,任何时候我们都可以通过比较每个依赖文件和被依赖文件的时间,以决定是否需要执行规则
但有时,我们希望某个规则“总是”被执行。这时,很自然地,我们会定义一下“永远都不会被满足”的依赖。比如clean
clean这个文件永远都不会被产生,所以,你只要执行这个依赖,rule是必然会被执行的。这种形式看起来很好用,但由于make工具默认认为你这是个文件,当它成为依赖链的一部分的时候,很容易造成各种误会和处理误差。

所以,简化起见,Makefile允许你显式地把一个依赖目标定义为“假的”(Phony):PHONY:clean,这样make工具就不用多想了,也不用检查clean这个文件的时间了,反正clean就是假的,如果有人依赖它,无条件执行就对了。
所以针对本文的栗子,最终章的makefile文件应该写成

src = $(wildcard *.c)
obj = $(patsubst %.c, %.o, $(src))
ALL:a.out
a.out:$(obj)
	gcc $^ -o $@
$(obj)%.o:%.c
	gcc -c $< -o $@
clean:
	-rm -rf *.o a.out
.PHONY:clean ALL

本文介绍的只是最最最基本的makefile,本人不接受任何对线,我暂时就到这了,你能写的更好欢迎评论我也学习学习。