Makefile的使用

Linux中使用make命令来编译程序,特别是大程序;而make命令所执行的动作依赖于Makefile文件。

1. Makefile规则

Makefile文件规则如下:

目标(target)…: 依赖(prerequiries)<tab>命令(command)

**目标(target)**通常是要生成的文件的名称,可以是可执行文件或OBJ文件,也可以是一个执行的动作名称,诸如`clean’。

依赖是用来产生目标的材料(比如源文件),一个目标经常有几个依赖。

命令是生成目标时执行的动作,一个规则可以含有几个命令,每个命令占一行。

最简单的Makefile文件如下:

hello: hello.c
	gcc -o hello hello.c
clean:
	rm -f  hello

将上述4行存为Makefile文件(注意必须以Tab键缩进第2、4行,不能以空格键缩进),直接执行make命令即可编译程序,执行make clean即可清除编译出来的结果。

make命令根据文件更新的时间戳来决定哪些文件需要重新编译,这使得可以避免编译已经编译过的、没有变化的程序,可以大大提高编译效率。

2. Makefile语法

2.1 通配符

假如有a.cb.c两个程序。

//a.c
#include 

int main(){
	func_b();
	return 0;
}
//b.c
#include 

void func_b(){
	printf("This is B\n");
}

对应的Makefile如下:

test:a.o b.o
	gcc -o test a.o b.o
a.o:a.c
	gcc -c -o a.o a.c
b.o:b.c
	gcc -c -o b.o b.c

clean:
	rm *.o test

假如一个目标文件所依赖的依赖文件很多,那样岂不是我们要写很多规则,这显然是不合乎常理的。我们可以使用通配符,来解决这些问题。常用通配符如下:

  • %.o:表示所用的.o文件
  • %.c:表示所有的.c文件
  • $@:表示目标
  • $<:表示第一个依赖文件
  • $^:表示所有依赖文件

上面的Makefile修改如下:

test:a.o b.o
	gcc -o test $^
%.o:%.c
	gcc -c -o $@ $<
clean:
	rm *.o test

2.2 假想目标: .PHONY

如果想清除文件,在Makefile的结尾添加如下代码:

clean:
rm *.o test

然后在终端执行:make clean就可以了。

当执行 make clean 的时候,就会在 Makefile 里面找到 clean 这个目标,然后执行里面的命令,这个写法有些问题,原因是目录里面没有 clean 这个文件,这个规则执行的条件成立,他就会执行下面的命令来删除文件。

但是如果该目录下有名为clean文件时,执行make clean会有如下提示:

make: \`clean' is up to date.

即没有执行删除操作。

解决办法:需要把目标定义为假象目标,用关键字PHONY,用法:

# 把clean定义为假象目标。他就不会判断名为“clean”的文件是否存在
.PHONY: clean

改进2.2中的Makefile如下:

test:a.o b.o
	gcc -o test $^
%.o:%.c
	gcc -c -o $@ $<
clean:
	rm *.o test
.PHONY: clean

2.3 变量

在makefile中有两种变量:
(1)简单变量(即时变量)
对于即时变量使用 :=表示,它的值在定义的时候已经被确定了

# A的值即刻确定,在定义时即确定
A := xxx

(2)
对于延时变量使用=表示。它只有在使用到的时候才确定,在定义等于时并没有确定下来。

# B的值使用到时才确定
B = xxx 

常用的变量的定义如下:

:= # 即时变量
=  # 延时变量
?= # 延时变量, 如果是第1次定义才起效, 如果在前面该变量已定义则忽略这句
\+= # 附加, 它是即时变量还是延时变量取决于前面的定义
?=: # 如果这个变量在前面已经被定义了,这句话就会不会起效果,

实例:
Makefile如下:

# 想使用变量的时候使用 $ 来引用
A := $(C)
B = $(C)
C = abc

#D = 100ask
D ?= weishen

# 如果不想看到命令时,可以在命令的前面加上 @ 符号,就不会显示命令本身。
all:
	@echo A = $(A)
	@echo B = $(B)
	@echo D = $(D)

C += 123

执行make后,结果如下:

A =
B = abc 123
D = weishen

分析:

# A为即使变量,在定义时即确定,由于刚开始C的值为空,所以A的值也为空。
A =

# B为延时变量,只有使用到时它的值才确定,当执行make时,会解析Makefile里面的所用变量,所以先
# 解析C= abc,然后解析C += 123,此时,C = abc 123,当执行:@echo B = $(B) B的值为 abc 123。
B = abc 123

# D变量在前面没有定义,所以D的值为weishen,如果在前面添加D = 100ask,最后D的值为100ask。
D = weishen

3. Makefile函数

makefile里面可以包含很多函数,这些函数都是make本身实现的。函数调用的格式如下:

$(function arguments)

function是函数名,arguments是该函数的参数。参数和函数名之间是用空格或Tab隔开,如果有多个参数,它们之间用逗号隔开。这些空格和逗号不是参数值的一部分。

3.1 函数foreach

函数foreach语法如下:

$(foreach var,list,text)

简单地说,就是 for each var in list, change it to text。对list中的每一个元素,取出来赋给var,然后把var改为text所描述的形式。

实例:

A = a b c
B = $(foreach f, &(A), $(f).o)
all:
	@echo B = $(B)

结果:

B = a.o b.o c.o

3.2 函数filter/filter-out

函数filter/filter-out语法如下:

$(filter pattern...,text)     # 在text中取出符合patten格式的值
$(filter-out pattern...,text) # 在text中取出不符合patten格式的值

实例:

C = a b c d/

D = $(filter %/, $(C))
E = $(filter-out %/, $(C))

all:
	@echo D = $(D)
	@echo E = $(E)

结果:

D = d/
E = a b c

3.3 Wildcard函数

语法如下:

$(wildcard pattern) # pattern定义了文件名的格式, wildcard取出其中存在的文件。

函数 wildcard 会以 pattern 这个格式,去寻找存在的文件,返回存在文件的名字。
实例:
在该目录下创建三个文件:a.c b.c c.c

files = $(wildcard *.c)

all:
	@echo files = $(files)

结果:

files = a.c b.c c.c

也可以用wildcard函数来判断,真实存在的文件,实例:

files2 = a.c b.c c.c d.c e.c  abc
files3 = $(wildcard $(files2))

all:
	@echo files3 = $(files3)

结果:

files3 = a.c b.c c.c

3.4 patsubst函数

语法如下:

$(patsubst pattern,replacement,$(var))

patsubst 函数是从 var 变量里面取出每一个值,如果这个符合 pattern 格式,把它替换成 replacement 格式。
实例:

files2  = a.c b.c c.c d.c e.c abc

dep_files = $(patsubst %.c,%.d,$(files2))

all:
	@echo dep_files = $(dep_files)

结果:

dep_files = a.d b.d c.d d.d e.d

4. Makefile实例

使用到的程序如下:a.c b.c c.c c.h

//a.c
#include 

int main(){
	func_b();
	func_c();
	return 0;
}
//b.c
#include 

void func_b(){
	printf("This is B\n");
}
//c.c
#include 
#include ""c.h

void func_b(){
	printf("This is C = %d\n",C);
}
//c.h
#define C 1

如果按照上面的内容不难写出如下Makefile

test:a.o b.o c.o
	gcc -o test $^

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

clean:
	rm *.o test
.PHONY:clean

编译运行之后,结果为:

This is B
This is C =1

但是如果修改c.hC=2,重新编译运行,结果是不变的。说明编写的Makefile是有问题的。原因是该Makefile没有考虑头文件。

所以需要写出每个.c文件依赖了哪些.h文件。但是对于内核,有几万个文件,不可能为每个文件依次写出其头文件。
可以使用编译器的-M选项,自动获取源文件中包含的头文件,并生成一个依赖关系。

所以首先需要先了解gcc-M -MF等编译选项的用法。参考链接:
Linux Makefile 生成 *.d 依赖文件以及 gcc -M -MF -MP 等相关选项说明

总结如下:

gcc -M c.c // 打印出依赖

gcc -M -MF c.d c.c // 把依赖写入文件c.d

gcc -c -o c.o c.c -MD -MF c.d // 编译c.o, 把依赖写入文件c.d

下面给出改进:

objs = a.o b.o c.o

dep_files := $(patsubst %,.%.d, $(objs))
dep_files := $(wildcard $(dep_files))

test: $(objs)
	gcc -o test $^

# ifneq表示如果当前目录有$(dep_files)中的文件,则包含(添加)进来
ifneq ($(dep_files),)
include $(dep_files)
endif

%.o : %.c
	gcc -c -o $@ $< -MD -MF .$@.d

clean:
	rm *.o test

distclean:
	rm $(dep_files)
	
.PHONY: clean	

解释如下:
首先用obj变量将.o文件放在一块。利用前面讲到的函数,把obj里所有文件都变为.%.d格式,并用变量dep_files表示。利用前面介绍的wildcard函数,判断dep_files是否存在。然后是目标文件test依赖所有的.o文件。如果dep_files变量不为空,就将其包含进来。然后就是所有的.o文件都依赖.c文件,且通过-MD -MF生成.d依赖文件。清理所有的.o文件和目标文件,清理依赖.d文件。

这样,对头文件进行修改,再次编译运行即可看出改变。

还可以添加CFLAGS,即编译参数。例如

CFLAGS = -Werror -Iinclude
# -Werror选项是检查程序错误,即使是警告也会当成错误
# -Iinclude选项是指定头文件搜索路径为当前目录的 include 文件夹

5. 通用Makefile的使用

参考Linux内核的Makefile编写了一个通用的Makefile,它可以用来编译应用程序,有如下好处:

  • 支持多个目录、多层目录、多个文件;
  • 支持给所有文件设置编译选项;
  • 支持给某个目录设置编译选项;
  • 支持给某个文件单独设置编译选项;
  • 简单、好用。

5.1 通用Makefile展示

顶级目录结构如下:
Makefile的使用_第1张图片
知道了程序结构,这里就不再给出每个源程序的代码。

5.1.1 顶级目录的Makefile

顶级目录Makefile如下:

# CROSS_COMPILE是交叉编译器前缀。如果是使用gcc编译器,则CROSS_COMPILE不用设置。如果是给arm版编译使用,则需要设置使用哪个编译器
CROSS_COMPILE = 
AS		= $(CROSS_COMPILE)as
LD		= $(CROSS_COMPILE)ld
CC		= $(CROSS_COMPILE)gcc
CPP		= $(CC) -E
AR		= $(CROSS_COMPILE)ar
NM		= $(CROSS_COMPILE)nm

STRIP		= $(CROSS_COMPILE)strip
OBJCOPY		= $(CROSS_COMPILE)objcopy
OBJDUMP		= $(CROSS_COMPILE)objdump

# 使AS等变量在所有目录中都可见
export AS LD CC CPP AR NM
export STRIP OBJCOPY OBJDUMP

CFLAGS := -Wall -O2 -g
CFLAGS += -I $(shell pwd)/include

# 指定链接选项,比如指定使用哪个库等
LDFLAGS := 

export CFLAGS LDFLAGS

TOPDIR := $(shell pwd)
export TOPDIR

# 指定编译好的应用程序名字
TARGET := test

obj-y += main.o
obj-y += sub.o
obj-y += a/

# 开始递归编译
all : start_recursive_build $(TARGET)
	@echo $(TARGET) has been built!

# make -C ./ -f $(TOPDIR)/Makefile.build相当于make -C ./ -f ./Makefile.build,-C是指定目录为 ./   -f是指定文件为Makefile.build
start_recursive_build:
	make -C ./ -f $(TOPDIR)/Makefile.build	

$(TARGET) : built-in.o
	$(CC) -o $(TARGET) built-in.o $(LDFLAGS)

clean:
	rm -f $(shell find -name "*.o")
	rm -f $(TARGET)

distclean:
	rm -f $(shell find -name "*.o")
	rm -f $(shell find -name "*.d")
	rm -f $(TARGET)

5.1.2 顶级目录的Makefile.build

顶级目录下Makefile.build如下:

PHONY := __build
__build:

# 变量清零
obj-y :=
subdir-y :=
EXTRA_CFLAGS :=

# 包含Makefile,里面有obj-y += main.o sub.o a/
include Makefile

# 下边几行注释是把下面最近一行的命令拆分开讲解,目的就是从obj-y中获取 目录 而不是.o文件
# 而且obj-y := a.o b.o c/ d/只是一个假设,在本例子中,obj-y := main.o sub.o a/
# obj-y := a.o b.o c/ d/
# $(filter %/, $(obj-y))   : c/ d/
# __subdir-y  : c d
# subdir-y    : c d
# __subdir-y 中是顶级目录下的各个子目录的名字
__subdir-y	:= $(patsubst %/,%,$(filter %/, $(obj-y)))
subdir-y	+= $(__subdir-y)

# c/built-in.o d/built-in.o
# subdir_objs 是顶级目录下每个子目录中编译链接的 built-in.o文件的集合
subdir_objs := $(foreach f,$(subdir-y),$(f)/built-in.o)

# a.o b.o
# cur_objs是顶级目录下 .c文件 对应生成的 .o 文件的集合
cur_objs := $(filter-out %/, $(obj-y))

# .$(f)..a.o.d和.b.o.d,它们分别记录了顶级目录下每个 .c 文件所依赖的文件信息。dep_files是这些 .d 文件的集合
dep_files := $(foreach f,$(cur_objs),.$(f).d)

# 判断这些.d文件是否存在,把存在的保存在dep_files中
dep_files := $(wildcard $(dep_files))

# ifneq表示如果当前目录有$(dep_files)中的文件,则包含(添加)进来
ifneq ($(dep_files),)
  include $(dep_files)
endif


PHONY += $(subdir-y)

# 目标文件__build依赖于顶级目录下的built-in.o和每个子目录的built-in.o
# 这里解释一下:虽然$(subdir-y)是子目录的集合,但是由于Makefile.build是递归执行的,
# 子目录中返回的就是子目录生成的built-in.o
__build : $(subdir-y) built-in.o

# 子目录的built-in.o是通过顶级目录下的Makefile.build生成的
# make -C a/ -f Makefile.build
$(subdir-y):
	make -C $@ -f $(TOPDIR)/Makefile.build

# 顶级目录下的built-in.o依赖于顶级目录下的.o和每个子目录的built-in.o
built-in.o : $(cur_objs) $(subdir_objs)
	$(LD) -r -o $@ $^

dep_file = .$@.d

# .c生成.o
%.o : %.c
	$(CC) $(CFLAGS) $(EXTRA_CFLAGS) $(CFLAGS_$@) -Wp,-MD,$(dep_file) -c -o $@ $<

# 设为假象目标	
.PHONY : $(PHONY)

5.1.3 子目录的Makefile

子目录a下的Makefile如下:

EXTRA_CFLAGS := -D DEBUG
CFLAGS_sub3.o := -D DEBUG_SUB3

obj-y += sub2.o 
obj-y += sub3.o 

5.2 整个项目编译说明

对整个项目编译的说明:

本程序的Makefile分为3类:

  1. 顶层目录的Makefile
  2. 顶层目录的Makefile.build
  3. 各级子目录的Makefile

5.2.1 各级子目录的Makefile

它最简单,形式如下:

EXTRA_CFLAGS  := 
CFLAGS_file.o := 

obj-y += file.o
obj-y += subdir/

obj-y += file.o 表示把当前目录下的file.c编进程序里,
obj-y += subdir/ 表示要进入subdir这个子目录下去寻找文件来编进程序里,是哪些文件由subdir目录下的Makefile决定。
EXTRA_CFLAGS, 它给当前目录下的所有文件(不含其下的子目录)设置额外的编译选项, 可以不设置
CFLAGS_xxx.o, 它给当前目录下的xxx.c设置它自己的编译选项, 可以不设置

注意:

  1. subdir/中的斜杠/不可省略
  2. 顶层Makefile中的CFLAGS在编译任意一个.c文件时都会使用
  3. CFLAGS EXTRA_CFLAGS CFLAGS_xxx.o 三者组成xxx.c的编译选项

5.2.2 顶层目录的Makefile

顶层目录的Makefile除了定义obj-y来指定根目录下要编进程序去的文件、子目录外,主要是:

  • 定义工具链前缀CROSS_COMPILE,
  • 定义编译参数CFLAGS,
  • 定义链接参数LDFLAGS,

这些参数就是文件中用export导出的各变量。

5.2.3 顶层目录的Makefile.build

这是最复杂的部分,它的功能就是把某个目录及它的所有子目录中、需要编进程序去的文件都编译出来,打包为built-in.o

5.2.4 怎么使用这套Makefile

(1)把顶层Makefile, Makefile.build放入程序的顶层目录
在各自子目录创建一个空白的Makefile

(2)确定编译哪些源文件
修改顶层目录和各自子目录Makefileobj-y :
obj-y += xxx.o
obj-y += yyy/

这表示要编译当前目录下的xxx.c,要编译当前目录下的yyy子目录

(3)确定编译选项、链接选项
修改顶层目录MakefileCFLAGS,这是编译所有.c文件时都要用的编译选项;
修改顶层目录MakefileLDFLAGS,这是链接最后的应用程序时的链接选项;

修改各自子目录下的Makefile
EXTRA_CFLAGS,它给当前目录下的所有文件(不含其下的子目录)设置额外的编译选项, 可以不设置;
CFLAGS_xxx.o,它给当前目录下的xxx.c设置它自己的编译选项, 可以不设置

(4)使用哪个编译器?
修改顶层目录MakefileCROSS_COMPILE, 用来指定工具链的前缀(比如arm-linux-

(5)确定应用程序的名字:
修改顶层目录MakefileTARGET,这是用来指定编译出来的程序的名字

(6)执行make来编译,执行make clean来清除,执行make distclean来彻底清除

5.3 编译运行

如图:
Makefile的使用_第2张图片

6. 通用Makefile的解析

6.1 零星知识点

(1)make命令的使用
执行·make·命令时,它会去当前目录下查找名为Makefile的文件,并根据它的指示去执行操作,生成第一个目标。

可以使用-f选项指定文件,不再使用名为Makefile的文件,比如:

make  -f  Makefile.build 

可以使用-C选项指定目录,切换到其他目录里去,比如:

make -C  a/  -f  Makefile.build

可以指定目标,不再默认生成第一个目标:

make -C  a/  -f  Makefile.build   other_target

(2)变量的导出export
在编译程序时,我们会不断地使用make -C dir切换到其他目录,执行其他目录里的Makefile。如果想让某个变量的值在所有目录中都可见,要把它export出来。

比如CC = $(CROSS_COMPILE)gcc,这个CC变量表示编译器,在整个过程中都是一样的。定义它之后,要使用export CC把它导出来。

(3)Makefile中可以使用shell命令
比如:

TOPDIR := $(shell pwd)

这是个立即变量,TOPDIR等于shell命令pwd的结果。

(4)在Makefile中怎么放置第1个目标
执行make命令时如果不指定目标,那么它默认是去生成第1个目标。

所以“第1个目标”,位置很重要。有时候不太方便把第1个目标完整地放在文件前面,这时可以在文件的前面直接放置目标,在后面再完善它的依赖与命令。比如:

First_target:   // 这句话放在前面
....        // 其他代码,比如include其他文件得到后面的xxx变量
First_target : $(xxx)   $(yyy)   // 在文件的后面再来完善
	command

6.2 通用Makefile的设计思想

(1)在Makefile文件中确定要编译的文件、目录,比如:

obj-y += main.o
obj-y += a/

Makefile文件总是被Makefile.build包含的。

(2)在Makefile.build中设置编译规则,有3条编译规则:
1)怎么编译子目录? 进入子目录编译:

$(subdir-y):
	make -C $@ -f $(TOPDIR)/Makefile.build

2)怎么编译当前目录中的文件?

%.o : %.c
	$(CC) $(CFLAGS) $(EXTRA_CFLAGS) $(CFLAGS_$@) -Wp,-MD,$(dep_file) -c -o $@ $<

3)当前目录下的.o和子目录下的built-in.o要打包起来

built-in.o : $(cur_objs) $(subdir_objs)
	$(LD) -r -o $@ $^

(3)顶层Makefile中把顶层目录的built-in.o链接成APP

$(TARGET) : built-in.o
	$(CC) $(LDFLAGS) -o $(TARGET) built-in.o

6.3 情景演绎

如图:
Makefile的使用_第3张图片

你可能感兴趣的:(嵌入式相关,linux,make,makefile)