GCC常用规则和Makefile通用规则解析
Makefile在编译场景中经常会用到,以前对Makefile是一知半解,只是知道怎么添加文件,怎样编译指定目标,内部是具体怎样运作的,基本上不了解,这篇文章来阐述一下Makefile的基本规则,主要是linux编译下面的Makefile规则。本文会先从GCC的简单使用过渡到借助Makefile进行直接make。的如过有有问题的地方,欢迎在评论区留言一起讨论。
GCC编译的基础知识
一个或者多个C/C++文件要经过预处理(preprocessing)、编译(compilation)、汇编(assembly)和链接(linking)等4步才能变成可执行文件。在linux编译工程中我们一般使用的是GCC交叉编译工具而不是直接使用GCC,交叉编译工具的安装也很简单,直接使用export
导出编译器所在的路径就行了,不建议直接更新在~/bashrc
。
1、GCC常用编译命令
arm-linux-gcc -o test test.c
命令会编译test.c文件,生成名称为test的可执行文件
该命令简单,不用添加各种各样的其他选项,甚至连-o
选项也可以不需要。文件数量少的时候可以直接使用,但是文件多了的时候就不方便了。
gcc -Wp,-MD,abc.dep -c -o main.o main.c
对main.o进行预处理、编译、汇编,但是不链接
同时生成一个main.c里面包含的所有的头文件的列表文件,命名为abc.dep
-Wp,-MD,abc.dep这个选项一般是在Makefile第一次编译的时候用来生成每个文件的头文件依赖列表,方便跟踪后面头文件有变化的时候重新编译涉及到的C文件。
2、GCC常用选项
选项 | 解释 |
---|---|
-c | 把预处理、编译、汇编都做了,但是不链接 |
-o | 指定输出文件名称 |
-I | 指定头文件目录 |
-L | 指定链接时库文件目录 |
-l | 指定链接哪一个库文件,-llibrary就是链接名为library的库文件 |
-D | 指定宏定义 |
-Wall | 打开警告选项 |
-g | 打开调试选项,生成可用于GDB调试的信息 |
-O0 -O1 -O2 -O3 | 优化选项 |
-static | 静态编译,在支持动态链接(dynamic linking)的系统上,阻止链接共享库 |
-Wp,-MD,abc.dep | 生成依赖的头文件,命令为abc.dep |
3、-Wp,-MD,abc.dep的二次说明
下面是一个简单的代码
#include <stdio.h>
int main(int argc, char* argv[])
{
printf("Main fun!\n");
return 0;
}
使用GCC进行编译gcc -Wp,-MD,abc.dep -o main main.c
,编译完成后会在当前目录生成main可执行文件,以及abc.dep头文件依赖列表文件,使用cat adb.dep
来查看一下文件内容
main.o: main.c /usr/include/stdc-predef.h /usr/include/stdio.h
/usr/include/x86_64-linux-gnu/bits/libc-header-start.h
/usr/include/features.h /usr/include/x86_64-linux-gnu/sys/cdefs.h
/usr/include/x86_64-linux-gnu/bits/wordsize.h
/usr/include/x86_64-linux-gnu/bits/long-double.h
/usr/include/x86_64-linux-gnu/gnu/stubs.h
/usr/include/x86_64-linux-gnu/gnu/stubs-64.h
/usr/lib/gcc/x86_64-linux-gnu/7/include/stddef.h
/usr/include/x86_64-linux-gnu/bits/types.h
/usr/include/x86_64-linux-gnu/bits/typesizes.h
/usr/include/x86_64-linux-gnu/bits/types/__FILE.h
/usr/include/x86_64-linux-gnu/bits/types/FILE.h
/usr/include/x86_64-linux-gnu/bits/libio.h
/usr/include/x86_64-linux-gnu/bits/_G_config.h
/usr/include/x86_64-linux-gnu/bits/types/__mbstate_t.h
/usr/lib/gcc/x86_64-linux-gnu/7/include/stdarg.h
/usr/include/x86_64-linux-gnu/bits/stdio_lim.h
/usr/include/x86_64-linux-gnu/bits/sys_errlist.h
GCC的基础知识到这里就可以了,如果你感兴趣的话,可以去百度GUN在线手册去了解,或者百度《GNU Make 使用手册》进行了解。
Makefile的相关知识
如果你去百度通用Makefile,会出现很多各种各样的Makefile程序,他们似乎大同小异,但是很多都是没有注释,看的让人一脸懵,有写地方都看不懂。Makefile的相关知识也可以参考《GNU Make 使用手册》进行了解。
Makefile的样式
目标(target)…: 依赖(prerequiries)…
<tab>命令(command)
命令被执行的2个条件:
- 依赖文件比目标文件
- 目标文件还没生成。
Makefile的常用函数
1、foreach
$(foreach var,list,text)
简单地说,就是对list中的每一个元素,取出来赋给var,然后把var改为text所描述的形式。
objs := a.o b.o
dep_files := $(foreach f, $(objs), .$(f).d)
// 最终 dep_files := .a.o.d .b.o.d
2、wildcard
$(wildcard pattern)
pattern所列出的文件是否存在,把存在的文件都列出来。
src_files := $( wildcard *.c)
// 最终 src_files中列出了当前目录下的所有.c文件
3、filter
$(filter pattern…,text)
把text中符合pattern格式的内容,filter(过滤)出来、留下来。
obj-y := a.o b.o c/ d/
DIR := $(filter %/, $(obj-y))
//结果为:c/ d/
4、filter-out
$(filter-out pattern…,text)
把text中符合pattern格式的内容,filter-out(过滤)出来、扔掉。
obj-y := a.o b.o c/ d/
DIR := $(filter-out %/, $(obj-y))
//结果为:a.o b.o
5、patsubst
$(patsubst pattern,replacement,text)
寻找text’中符合格式
pattern’的字,用replacement’替换它们。
pattern’和`replacement’中可以使用通配符。
subdir-y := c/ d/
subdir-y := $(patsubst %/, %, $(subdir-y))
// 结果为:c d
6、shell命令
TOPDIR := $(shell pwd)
这是个立即变量,TOPDIR等于shell命令pwd的结果。
rm -f $(shell find -name "*.o")
// 该命令会递归删除该目录下面所有的.o文件
7、指定Makefile文件命令
我们可以使用“-f”选项指定文件,不再使用名为“Makefile”的文件
make -f Makefile.build
我们可以使用“-C”选项指定目录,切换到其他目录里去
make -C a/ -f Makefile.build
// 用a目录下面的Makefile.build进行编译
我们可以指定目标,不再默认生成第一个目标:
make -C a/ -f Makefile.build other_target
Makefile中自动化变量
- $@ 目标
- $< 依赖目标中的第一个目标名字。如果依赖目标是以模式(即 % )定义的,那么 $< 将是符合模式的一系列的文件集。注意,其是一个一个取出来的
- $^ 所有的依赖目标的集合。以空格分隔。如果在依赖目标中有多个重复的,那个这个变量会去除重复的依赖目标,只保留一份。
Makefile中变量的导出
变量使用export
进行导出,导出了之后其他Makefile都可以获取到这个变量。
Makefile中的假想目标
我们的Makefile中有这样的目标:
clean:
rm -f $(shell find -name “*.o”)
rm -f $(TARGET)
如果当前目录下恰好有名为“clean”的文件,那么执行“make clean”时它就不会执行那些删除命令。
这时我们需要把“clean”这个目标,设置为“假想目标”,这样可以确保执行“make clean”时那些删除命令肯定可以得到执行。
使用下面的语句把“clean”设置为假想目标:
.PHONY : clean
Makefile变量的含义
A = xxx // 延时变量
B ?= xxx // 延时变量,只有第一次定义时赋值才成功;如果曾定义过,此赋值无效
C := xxx // 立即变量
D += yyy // 如果D在前面是延时变量,那么现在它还是延时变量
// 如果D在前面是立即变量,那么现在它还是立即变量
A = $@
test:
@echo $A #使用到的时候$@(目标)是test
变量A的值在执行时才确定,它等于test,是延时变量
A := $@
test:
@echo $A
变量A的值在立即确定,它等于空,是立即变量
变量的含义似乎用处不大,或者说我还没找到实际的用处,欢迎大家进行指正说明。
顶层Makefile
CROSS_COMPILE = #这里需要填入你用的gcc交叉编译工具,例如gcc-linux-arm-
AS = $(CROSS_COMPILE)as #指定汇编工具
LD = $(CROSS_COMPILE)ld #指定链接工具
CC = $(CROSS_COMPILE)gcc #指定gcc编译工具
CPP = $(CC) -E
AR = $(CROSS_COMPILE)ar
NM = $(CROSS_COMPILE)nm
STRIP = $(CROSS_COMPILE)strip
OBJCOPY = $(CROSS_COMPILE)objcopy
OBJDUMP = $(CROSS_COMPILE)objdump
export AS LD CC CPP AR NM #导出一些变量
export STRIP OBJCOPY OBJDUMP
CFLAGS := -Wall -O2 -g # -Wall 打开警告信息 -O2 2级优化 -g 产生调试信息,可以不要
CFLAGS += -I $(shell pwd)/include #指定头文件目录
LDFLAGS := #链接标记,可以填入你需要的链接标记
export CFLAGS LDFLAGS #导出
#获取顶部目录,并且导出
TOPDIR := $(shell pwd)
export TOPDIR
TARGET := test #目标名字,以后编译生成的可执行文件的名字就是test
obj-y += main.o #这里指定当前文件需要编译的C文件
obj-y += sub.o
obj-y += a/ #这里指定当前文件夹下面需要递归编译的子文件夹
all : start_recursive_build $(TARGET) #第一个目标,直接执行make的默认目标,它有两个依赖
@echo $(TARGET) has been built!
start_recursive_build: #第一个依赖,生成的命令是指定makefile进行编译
make -C ./ -f $(TOPDIR)/Makefile.build
$(TARGET) : built-in.o #第二个依赖是把当前文件夹下面的built-in.o,链接成为一个可执行文件
$(CC) -o $(TARGET) built-in.o $(LDFLAGS)
clean: #清空,假想目标,无条件执行
rm -f $(shell find -name "*.o")
rm -f $(TARGET)
distclean: #删除目录下面所有.o文件,假想目标
rm -f $(shell find -name "*.o")
rm -f $(shell find -name "*.d")
rm -f $(TARGET)
.PHONY: clean
.PHONY: distclean
顶层Makefile的一个作用是导出各种变量,第二个作用是设定了一个目标all
,它有两个依赖,第一个依赖start_recursive_build
就是去执行make文件Makefile.build
,第二个依赖把当前目录的build-in.o
链接生成test
这个可执行文件。
Makefile.build文件解析
PHONY := __build #定义一个PHONY变量,立即变量
__build: #设定一个目标,使其成为Makefile.build的第一个目标,注意,最后一行把这个目标设定为伪目标
obj-y := #立即变量,清空
subdir-y := #立即变量,清空
EXTRA_CFLAGS :=
include Makefile #包含了当前目录的Makefile,在Makefile中我们定义了obj-y += xxx(main.o compress/ ...... ),以及EXTRA_CFLAGS等变量,这里会全部导入进来
# obj-y := a.o b.o c/ d/
# $(filter %/, $(obj-y)) : c/ d/
# __subdir-y : c d
# subdir-y : c d
__subdir-y := $(patsubst %/,%,$(filter %/, $(obj-y))) #筛选出当前目录的目标变量中的子目录,并且去掉/,在这里我们获得了子目录的名字
subdir-y += $(__subdir-y) #获取所有文件子目录名称,赋值给subdir-y
# 获取到所有当前子目录的build-in.o c/built-in.o d/built-in.o #built-in.o
subdir_objs := $(foreach f,$(subdir-y),$(f)/built-in.o) #对于subdir-y里面的每一个值(目录),增加一个相应的目录/built-in.o的变量值
# a.o b.o
cur_objs := $(filter-out %/, $(obj-y)) #获取当前目录下面的所有的a.o b.o之类的文件,去掉文件夹
dep_files := $(foreach f,$(cur_objs),.$(f).d) #获取当前文件的头文件依赖,
dep_files := $(wildcard $(dep_files)) #挑选真正存在的依赖文件,在第一次编译的时候才会将这些文件生成,
# 第一次编译的时候,dep_files变量肯定是空的,因为根本没有.main.o.d之类的文件存在,但是第一次编译之后,就会通过-Wp,-MD,$(dep_file)生成这些.main.o.d类型的文件了
# 第一次编译肯定会编译到所有的头文件
# 第二次以及以后的编译,就有.main.o.d之类的文件存在了,他们会作为依赖,修改了头文件,涉及到的C文件会被重新编译了
ifneq ($(dep_files),)
include $(dep_files) #导入真正存在的依赖文件,第一次编译该变量是空的,啥也导入不进来
endif
PHONY += $(subdir-y) #子目录文件夹名称也作为一个依赖
__build : $(subdir-y) built-in.o # 第一个规则,有两个依赖,一个$(subdir-y)子目录名称,另一个是当前目录的build-in.o
$(subdir-y): #子目录文件名依赖,就再去子目录里面编译一下,生成build-in.o文件。如果没有子目录的话,这个命令就不执行了。
make -C $@ -f $(TOPDIR)/Makefile.build # 依次进入该子目录变量里面存储的值,使用的Makefile.build进行编译,这里是应用了递归的思想了
built-in.o : $(cur_objs) $(subdir_objs) # 当前目录的build-in.o的生成规则。第一个依赖是当前目录下面的所有.o文件,第二个规则是所有子目录下面的build-in.o文件
$(LD) -r -o $@ $^ # 该规则的命令:将该目录下的.o和$(subdir_obj)打包(链接)成built-in.o文件
dep_file = .$@.d #第一次编译会生成这个隐藏文件,这个是.c文件的头文件依赖列表
%.o : %.c #当前目录下面的.o文件的生成规则
$(CC) $(CFLAGS) $(EXTRA_CFLAGS) $(CFLAGS_$@) -Wp,-MD,$(dep_file) -c -o $@ $<
# EXTRA_CFLAGS是对当前目录下面所有的.c文件指定的额外的编译选项,在当前层的Makefile中指定
# CFLAGS_$@是对当前目录下面的指定的.c文件指定的额外的的编译选项,在当前层的Makefile中指定,形式如CFLAGS_main.o := -D DEBUG_SUB3
# -c 把预处理、编译、汇编都做了,但是不链接
.PHONY : $(PHONY) # 将PHONY声明为伪目标,无条件执行
Makefile.build就是一个在整个工程下面的通用Makefile文件,每一级目录的编译都会调用这个文件来进行编译,每一级的目录下面的Makefile功能只是提供obj-y以及额外的编译选项,他会被Makefile.build导入进去。Makefile.build使用的编译方法是递归调用
,他会把子目录生成的build-in.o以及当前目录的所有.o文件全部连接成文当前目录的build-in.o,最后一路链接到顶层的build-in.o之后,就可以链接成为一个可执行文件。
我上传了demo工程,可以使用demo工程进行进一步加深印象。
参考链接:韦东山Makefile系列知识教程