Makefile语法详细总结及示例解析(快速掌握)

发布时间 2023-10-23 14:14:32作者: zxddesk

 

 

一、简介

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

targets: prerequisites
  command
targets:规则的目标,可以是 Object File(一般称它为中间文件),也可以是可执行文件,还可以是一个标签;
prerequisites:是我们的依赖文件,要生成 targets 需要的文件或者是目标。可以是多个,也可以是没有;
command:make 需要执行的命令(任意的 shell 命令)。可以有多条命令,每一条命令占一行。
 

 

具体流程可参考下图:
在这里插入图片描述Makefile执行的具体过程如下图:
在这里插入图片描述Makfile程序的编写首先要了解程序的编译过程,可参考下面的链接:

gcc编译流程、参数实例总结

二、常用规则介绍

2.1 递归扩展变量

变量解析
= 是最基本的赋值
:= 是覆盖之前的值
?= 是如果没有被赋值过就赋予等号后面的值
+= 是添加等号后面的值
obj-y := 编译进内核的文件(夹)列表
obj-m := 编译成外部可加载模块的列表
lib-y := 和 lib-m := 编译成库文件
always := 总是需要被编译的模块
targets := 编译目标
subdir-y := 和 subdir-m := 表示需要递归进入的子目录

内核中常用的Makefile例子:

//例如: fs/ext2/Makefile
obj-$(CONFIG_EXT2_FS) += ext2.o
ext2-y := balloc.o dir.o file.o ialloc.o inode.o ioctl.o namei.o super.o symlink.o
ext2-$(CONFIG_EXT2_FS_XATTR) += xattr.o xattr_user.o xattr_trusted.o

CONFIG_EXT2_FS在/fs/ext2/Kconfig中,可以通过make menuconfig设置CONFIG_EXT2_FS=y
config EXT2_FS
        tristate "Second extended fs support"
        help
          Ext2 is a standard Linux file system for hard disks.
 

在这个例子中,如果$(CONFIG_EXT2_FS_XATTR)表示’y’,则xattr.o xattr_user.o和xattr_trusted.o都将是复合对象ext2.o的一部分.

注意: 当然,当你将编译目标文件到内核时,以上语法同样有效.因此,如果CONFIG_EXT2_FS=y,Kbuild将建立一个ext2.o来输出各个部分,然后将其链接到 built-in.o中,正如您期望的那样。

2.2 常见的自动化变量解析

变量解析
$0 当前脚本的文件名。
$n(n≥1) 传递给脚本或函数的参数。n 是一个数字,表示第几个参数。例如,第一个参数是 $1,第二个参数是 $2>。
$# 传递给脚本或函数的参数个数。
$* 传递给脚本或函数的所有参数。
$@ 表示目标文件。
$? 上个命令的退出状态,或函数的返回值。
$$ 当前 Shell 进程 ID。对于 Shell 脚本,就是这些脚本所在的进程 ID。
$^ 表示所有的依赖文件
$< 表示第一个依赖文件
# Makefile内容
.PHONY:all
all:first second third
	@echo "\$$@ = $@"
	@echo "\$$^ = $^"
	@echo "\$$< = $<"

[root@localhost /]# make
first second third:
$@ = all
$^ = first second third
$< = first
 
# Makefile内容
%.o:%.c
    gcc -o $@ $^
	
OBJ=$(wildcard *.c)
test:$(OBJ)
    gcc -o $@ $^
当用通配符赋值给变量时需要用wildcard通配符函数,通过它可以得到我们所需的文件,这个函数类似我们在 Windows 或Linux命令行中的“*
 

 

2.3 常用的编译器宏定义

宏定义含义
AR 归档维护程序的名称,默认值为 ar。
ARFLAGS 归档维护程序的选项。
AS 汇编程序的名称,默认值为 as。
ASFLAGS 汇编程序的选项。
CC C 编译器的名称,默认值为 cc。
CCFLAGS C 编译器的选项。
CPP C 预编译器的名称,默认值为 $(CC) -E。
CPPFLAGS C 预编译的选项。
CXX C++ 编译器的名称,默认值为 g++。
CXXFLAGS C++ 编译器的选项。
FC FORTRAN编译器的名称,默认值为 f77。
FFLAGS FORTRAN编译器的选项。

实例使用:

ccflags-y	:= -DDEBUG
ccflags-y	:= -DVERBOSE_DEBUG
opps-objs	:=oops_test.o
obj-make	:= opps.o
KBUILD_CFLAGS	+=-g
CC = gcc
CCFLAGS = -D_DEBUG -g -m486
test.o: test.c test.h
     $(CC) -c $(CCFLAGS) test.c
 

在上面的例子中,CC和 CCFLAGS 就是 make 的变量。GNU make通常称之为变量,而其他 UNIX 的 make工具称之为宏。

2.4 条件语法

当 make 看到条件语法时将⽴即对其进⾏分析,这包括 ifdef、ifeq、ifndef 和 ifneq 四种语句形式。

conditional-directive
	text-if-true
else
	text-if-false
endif
 
# Makefile内容
.PHONY: all
sharp = square
desk = square
table = circle

ifeq ($(sharp), $(desk))
	result1 = "desk == sharp"
endif
ifneq "$(table)" 'square'
	result2 = "table != square"
endif
all:
	@echo $(result1)
	@echo $(result2)
 
[root@localhost /]# make
desk == sharp
table != square
 

2.5 其他特殊变量

(1)VPATH变量

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

   VPATH = src:../headers
 

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

   vpath %.h ../headers
 

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

(2).PHONY变量

因为,我们并不生成“clean”这个文件。“伪目标”并不是一个文件,只是一个标签,由于“伪目标”不是文件,所以make无法生成它的依赖关系和决定它是否要执行。我们只有通过显示地指明这个“目标”才能让其生效。当然,“伪目标”的取名不能和文件名重名,不然其就失去了“伪目标”的意义了。
当然,为了避免和文件重名的这种情况,我们可以使用一个特殊的标记“.PHONY”来显示地指明一个目标是“伪目标”,向make说明,不管是否有这个文件,这个目标就是“伪目标”。

.PHONY: clean
clean:
	rm -rf *.o
# “.PHONY”表示,clean是个伪目标文件,当执行make clean时,直接执行clean下面的命令rm -rf *.o
 

(3)include变量

"include"指示符告诉 make 暂停读取当前的 Makefile,而转去读取"include"指定的一个或者多个文件,完成以后再继续当前 Makefile 的读取。
为什么要include其他文件呢?
对于一些通用的变量定义、通用规则,写在一个文件中,任意目录结构中的makefile想要使用这些通用的变量或规则时,include指定的文件就好了,而不用在每个makefile中又重写一遍。
对于源文件自动生成依赖文件(makefile之目录搜索&自动依赖)时,将这些个依赖关系保存成文件,在需要使用时include进来,这样少了人为的干预,同时也减少的错误的发生
我们就不希望mkdir出错而终止规则的运行。
为了做到这一点,忽略命令的出错,我们可以在Makefile的命令行前加一个减号“-”(在Tab键之后),标记为不管命令出不出错都认为是成功的。
“-include” 来代替 “include” 来忽略文件不存在或者是无法创建的错误提示,使用格式如下:-include

名称功能
.PHONY: 这个目标的所有依赖被作为伪目标。伪目标是这样一个目标:当使用make命令行指定此目标时,这个目标所在的规则定义的命令、无论目标文件是否存在都会被无条件执行。
.SUFFIXES: 这个目标的所有依赖指出了一系列在后缀规则中需要检查的后缀名
.DEFAULT: Makefile中,这个特殊目标所在规则定义的命令,被用在重建那些没有具体规则的目标,就是说一个文件作为某个规则的依赖,却不是另外一个规则的目标时,make 程序无法找到重建此文件的规则,这种情况就执行 “.DEFAULT” 所指定的命令。
.PRECIOUS: 这个特殊目标所在的依赖文件在make的过程中会被特殊处理:当命令执行的过程中断时,make不会删除它们。而且如果目标的依赖文件是中间过程文件,同样这些文件不会被删除。
.INTERMEDIATE: 这个特殊目标的依赖文件在 make 执行时被作为中间文件对待。没有任何依赖文件的这个目标没有意义。
.SECONDARY: 这个特殊目标的依赖文件被作为中过程的文件对待。但是这些文件不会被删除。这个目标没有任何依赖文件的含义是:将所有的文件视为中间文件。

(4)$(Q) 变量

本质就是 Makefile中的命令显示与否,命令前有@字符,则不显示命令本身而只显示命令的执行结果。

2.6 Makefile实例

# Makefile内容
.PHONY:all clean
MKDIR=mkdir
RM=rm
RMFLAGS=-fr
CC=gcc

DIR_OBJS=objs
DIR_EXE=exes
DIRS=$(DIR_OBJS) $(DIR_EXE)
EXE=test
EXE:=$(addprefix $(DIR_EXE)/, $(EXE))
SRCS=$(wildcard *.c)
OBJS=$(SRCS:.c=.o)
OBJS:=$(addprefix $(DIR_OBJS)/, $(OBJS))

all:$(DIRS) $(EXE)

$(DIRS):
        $(MKDIR) $@
$(EXE):$(OBJS)
        $(CC) -o $@ $^
$(DIR_OBJS)/%.o:%.c
        $(CC) -o $@ -c $^
clean:
        $(RM) $(RMFLAGS) $(DIRS)
 

2.7 添加打印信息

//直接添加方式
$(warning "this is test========================")
$(info $(KBUILD_CFLAGS))
$(error $(KBUILD_CFLAGS))

//使用echo命令打印
test:
	@echo $(KBUILD_CFLAGS)
执行make test 即可打印echo内容
 

具体链接可参考:C语言、Makefile和shell中添加打印调试信息总结

2.8 实例解析

objs := head.o init.o nand.o main.o
nand.bin : $(objs)   //冒号前面的是表示目标文件, 冒号后面的是依赖文件,这里是将所有*.o文件编译出nand.bin可执行文件
arm-linux-ld -Tnand.lds    -o nand_elf $^   //将*.o文件生成nand_elf链接文件
//-T:指向链接脚本, $^:指向所有依赖文件,

arm-linux-objcopy -O binary -S nand_elf $@ //将nand_elf链接文件生成nand.bin文件
//$@:指向目标文件:nand.bin
//-O :选项,其中binary就是表示生成的文件为.bin文件

arm-linux-objdump -D -m arm  nand_elf > nand.dis //将nand.bin文件反汇编出nand.dis文件
//-D :反汇编nand.bin里面所有的段, -m arm:指定反汇编文件的架构体系,这里arm架构
%.o:%.c
    arm-linux-gcc -Wall -c -O2 -o $@ $<

%.o:%.S
    arm-linux-gcc -Wall -c -O2 -o $@ $<
clean:
    rm -f  nand.dis nand.bin nand_elf *.o
 

其中 objs 是代表的一个变量,表示obj文件,也可以是objects, OBJECTS, objs, OBJS, obj, 或是 OBJ,后面就可以使用$(objs)来使用这个变量了。
$@  目标文件
$^  所有的依赖文件
$<   第一个依赖文件
例如:
arm-linux-ld -Tnand.lds -o nand_elf $^
<<—— 等价于 ——>>
arm-linux-ld -o nand_elf head.o init.o nand.o main.o
%.o:%.c  表示所有的.o文件,依赖于对应的.c文件
%.o:%.S  表示所有的.o文件,依赖于对应的.S文件

2.9 objs的用法

obj-y = main.o
main-objs := a.o  b.o  c.o
//将a.c b.c c.c三个文件编译后链接生成main.o
 

三、其他相关知识链接

1、gcc编译流程、参数实例总结

2、Linux下gcc交叉编译工具链总结

3、交叉编译linux内核实例讲解