目录:
目录:
一、什么是Makefile
1.1 makefile的作用:
1.2 makefile的基本组成:
二、Linux编译过程:
2.1 linux编译过程:
2.1.1 预处理(Preprocessing)
2.1.2 编译(Compilation)
2.1.3 汇编(Assembly)
2.1.4 链接(Linking)
2.1.5 过程总结
2.2 脚本语言: Makefile
2.2.1 第一层显式规则:
2.2.2 第二层变量:
2.2.3 第三层隐含规则:
2.2.4 第四层通配符:
2.2.5 第五层函数:
1. $(foreach var,list,text)
2. $(wildcard pattern)
3. $(wildcard pattern)
4. $(patsubst pattern,replacement,$(files2))
三、Makefile实操:
3.1 支持头文件依赖
3.2 添加CFLAGS
四、通用Makefile的使用:
4.1 本程序的Makefile分为3类:
4.2 各级子目录的Makefile:
4.3 顶层目录的Makefile:
4.4 顶层目录的Makefile.build:
4.5 怎么使用这套Makefile:
4.6 顶层目录
4.7 Makefile.build
4.8 子目录
五、通用 Makefile 的解析:
5.1 通用 Makefile 的设计思想
5.2 顶层 Makefile 中把顶层目录的 built-in.o 链接成 APP:
一、什么是Makefile
Makefile
是一个用来管理自动化构建过程的文件,主要用于编译和构建项目。在软件开发中,尤其是在编译大型项目时,手动编译每个源文件并将它们链接成一个可执行文件或库可能会非常繁琐。Makefile
提供了一种自动化和高效的方式来管理这些任务。
1.1 makefile的作用:
1. 自动化构建:Makefile
允许开发者定义一组规则来说明如何编译和链接程序。这些规则可以通过简单的命令自动执行,从而避免了手动执行每个步骤。
2. 依赖管理:Makefile
可以跟踪文件之间的依赖关系。例如,如果一个源文件被修改,Makefile
会只重新编译受影响的文件,而不是重新编译整个项目。这大大提高了编译效率。
3. 可重用性:Makefile
通常用于跨多个项目的通用构建规则,开发者可以在不同的项目中重用这些规则。
1.2 makefile的基本组成:
一个简单的 Makefile
通常包含以下部分:
1. 目标(Target):要生成的文件(如可执行文件)。
2. 依赖(Dependencies):生成目标所依赖的文件或其他目标。
3. 命令(Commands):生成目标所需要执行的命令。这些命令通常是 Unix/Linux 的 shell 命令。
二、Linux编译过程:
2.1 linux编译过程:
微观的C/C++编译执行过程。
在 C 语言中,源代码文件(.c
文件)到最终的可执行文件(.exe
文件)的生成过程,通常包括以下几个步骤:预处理、汇编、编译、和链接。
2.1.1 预处理(Preprocessing)
预处理是 C 语言编译的第一个阶段,它的任务是处理以 #
开头的预处理指令,比如 #include
、#define
等。预处理的结果仍然是 C 语言代码。
- 宏替换:将代码中的宏(如
#define
)替换为定义的内容。例如,#define STR "Jiuxia is a teacher\n"
会把代码中的STR
替换成"Jiuxia is a teacher\n"
。 - 头文件包含:将头文件中的内容插入到
#include
指令的地方。例如,#include <stdio.h>
会被替换为stdio.h
中的所有代码。 - 条件编译:处理诸如
#ifdef
、#ifndef
这些条件编译指令,确定哪些代码需要保留,哪些需要移除。
预处理后的代码通常是一个扩展的 .i
文件。
2.1.2 编译(Compilation)
编译阶段将经过预处理的 C 代码转换为汇编代码。在这个过程中,编译器会检查代码的语法是否正确,并将高层次的 C 代码翻译成低级的汇编语言代码。
- 编译器将把每个 C 语句转换成对应的汇编语言指令。
- 输出的文件通常是
.s
文件(汇编代码文件)。
2.1.3 汇编(Assembly)
汇编阶段将汇编语言代码转换为机器代码。这个阶段的输出是目标文件(Object File),目标文件包含机器代码和一些二进制数据,但它仍然不是一个完整的可执行文件。
- 汇编器将
.s
文件(汇编代码)转换成二进制的机器代码。 - 生成的文件通常是
.o
(在 Linux 下)或.obj
(在 Windows 下)文件。
2.1.4 链接(Linking)
链接是整个编译过程的最后一个阶段,它的作用是将所有目标文件(以及可能依赖的库)链接成一个可执行文件。
- 符号解析:确定每个函数和变量的内存地址,确保所有的引用都能正确找到定义。
- 库链接:如果你的程序使用了外部库(比如标准 C 库
libc
),链接器会将这些库链接进来。 - 生成可执行文件:链接器将所有目标文件和库组合起来,生成最终的可执行文件(
.exe
或.out
)。
链接完成后,输出的就是一个可以运行的可执行文件。
2.1.5 过程总结
预处理:处理 #define
、#include
等预处理指令,输出扩展后的源代码文件(.i
文件)。
编译:将 C 语言代码转换为汇编代码(.s
文件)。
汇编:将汇编代码转换为目标文件(.o
或 .obj
文件)。
链接:将目标文件与外部库链接在一起,生成最终的可执行文件(.exe
或 .out
文件)。
2.2 脚本语言: Makefile
Linux C/C++ 必须要使用的一个编译脚本。
2.2.1 第一层显式规则:
目标文件:依赖文件
[TAB] 指令
第一个目标文件是我的最终目标!!!(递归)
最简单的C语言编程:
hello: hello.ogcc hello.o -o hellohello.o: hello.Sgcc -c hello.S -o hello.ohello.S: hello.igcc -S hello.i -o hello.Shello.i: hello.cgcc -E hello.c -o hello.i
使用rm -rf hello.o hello.S hello.i hello对这些编译文件进行删除处理。这种没有目标文件的操作我们可以叫做伪目标。
伪目标:.PHONY:
hello: hello.ogcc hello.o -o hellohello.o: hello.Sgcc -c hello.S -o hello.ohello.S: hello.igcc -S hello.i -o hello.Shello.i: hello.cgcc -E hello.c -o hello.i.PHONY:
clear:rm -rf hello.o hello.S hello.i
circle.c circle.h cube.c cube.h main.c main.h
得到可执行文件test省去预编译汇编
test: circle.o cube.o main.ogcc circle.o cube.o main.o -o testcircle.o: circle.cgcc -c circle.c -o circle.ocube.o: cube.cgcc -c cube.c -o cube.omain.o: main.cgcc -c main.c -o main.o.PHONY: clearall clear
cleanall:rm -rf circle.o cube.o main.o testclean:rm -rf circle.o cube.o main.o
2.2.2 第二层变量:
=(替换) +=(追加) :=(恒等于,常量)
$(变量名)(使用变量进行替换)
TAR = test
OBJ = circle.o cube.o main.o
CC := gcc$(TAR): $(OBJ)$(CC) $(OBJ) -o $(TAR)circle.o: circle.c$(CC) -c circle.c -o circle.ocube.o: cube.c$(CC) -c cube.c -o cube.omain.o: main.c$(CC) -c main.c -o main.o.PHONY: clearall clearcleanall:rm -rf circle.o cube.o main.o testclean:rm -rf circle.o cube.o main.o
即时变量、延时变量,export:
A := xxx # A的值即刻确定,在定义时即确定
B = xxx # B的值使用到时才确定
:= #即时变量
= #延时变量
?= #延时变量,如果是第1次定义才起效,如果在前面该变量已定义则忽略这句
+= #附加,它是即时变量还是延时变量取决于前面的定义
A := $(C)
B = $(C)
C = abc#D = 100ask
D ?= weidongshanall:@echo A = $(A)@echo B = $(B)@echo D = $(D)C += 123
2.2.3 第三层隐含规则:
%.c(任意的.c文件) %.o(任意的.o文件)
*.c (所有的.c文件) *.o(所有的.o文件)
TAR = test
OBJ = circle.o cube.o main.o
CC := gcc$(TAR): $(OBJ)$(CC) $(OBJ) -o $(TAR)%.o: %.c$(CC) -c %.c -o %.o .PHONY: clearall clearcleanall:rm -rf $(OBJ) $(TAR)clean:rm -rf $(OBJ)
2.2.4 第四层通配符:
$^ (所有的依赖文件)
$@(所有的目标文件)
$< (所有依赖文件的第一个文件)
$* (不包含扩展名的目标文件名称)
$? (所有时间戳比目标文件晚的依赖文件,并以空格分开)
TAR = test
OBJ = circle.o cube.o main.o
CC := gcc
RMRF := rm -rf$(TAR): $(OBJ)$(CC) $^ -o $@%.o: %.c$(CC) -c $^ -o $@ .PHONY: clearall clearcleanall:$(RMRF) $(OBJ) $(TAR)clean:$(RMRF) $(OBJ)
2.2.5 第五层函数:
1. $(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)
2. $(wildcard pattern)
pattern 所列出的文件是否存在,把存在的文件都列出来。
$(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)
3. $(wildcard pattern)
pattern定义了文件名的格式,wildcard取出其中存在的文件。
files = $(wildcard *.c) #列出符合后缀是.c的文件都有哪些all:@echo files = $(files)
files2 = a.c b.c c.c d.c e.c
files3 = $(wildcard $(files2))all:@echo files = $(files)@echo files3 = $(files3)
4. $(patsubst pattern,replacement,$(files2))
从列表中取出每—个值,如果符合pattern,则替换为replacement
files2 = a.c b.c c.c d.c e.c
files3 = $(wildcard $(files2))dep_files = $(patsubst %.c,%.d,$(files2)) #将files2中.c后缀文件替换为.d后缀文件all:@echo dep_files = $(dep_files)
三、Makefile实操:
3.1 支持头文件依赖
a.c
#include <stdio.h>void func_b();
void func_c();int main()
{func_b();func_c();return 0;
}
b.c
#include <stdio.h>void func_b()
{printf("This is B\n");
}
c.c
#include <stdio.h>
#include "c.h"void func_c()
{printf("This is C = %d\n", C);
}
c.h
#define C 4
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
Makefile
objs = a.o b.o c.odep_files := $(patsubst %,.%.d, $(objs))
dep_files := $(wildcard $(dep_files))test: $(objs)gcc -o test $^ifneq ($(dep_files),) #将依赖文件添加进去
include $(dep_files)
endif%.o : %.cgcc -c -o $@ $< -MD -MF .$@.d #自动生成依赖文件clean:rm *.o testdistclean:rm $(dep_files).PHONY: clean
3.2 添加CFLAGS
c.c
#include <stdio.h>
#include <c.h>void func_c()
{printf("This is C = %d\n", C);
}
CFLAGS = -Werror -I.include #-Werror将所有的警告变为错误
#-I.include执行当前目录下的include文件
%.o : %.c
gcc $(CFLAGS) -c -o $@ $< -MD -MF .$@.d #自动生成依赖文件
Makefile
objs = a.o b.o c.odep_files := $(patsubst %,.%.d, $(objs))
dep_files := $(wildcard $(dep_files))CFLAGS = -Werror -I.include #-Werror将所有的警告变为错误#-I.include执行当前目录下的include文件test: $(objs)gcc -o test $^ifneq ($(dep_files),) #将依赖文件添加进去
include $(dep_files)
endif%.o : %.cgcc $(CFLAGS) -c -o $@ $< -MD -MF .$@.d #自动生成依赖文件clean:rm *.o testdistclean:rm $(dep_files).PHONY: clean
四、通用Makefile的使用:
参考 Linux 内核的 Makefile 编写了一个通用的 Makefile,它可以用来 编译应用程序:
1.支持多个目录、多层目录、多个文件;
2.支持给所有文件设置编译选项;
3.支持给某个目录设置编译选项;
4.支持给某个文件单独设置编译选项;
5.简单、好用。
4.1 本程序的Makefile分为3类:
1. 顶层目录的Makefile
2. 顶层目录的Makefile.build
3. 各级子目录的Makefile
4.2 各级子目录的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的编译选项
4.3 顶层目录的Makefile:
它除了定义obj-y来指定根目录下要编进程序去的文件、子目录外,
主要是定义工具链前缀CROSS_COMPILE,
定义编译参数CFLAGS,
定义链接参数LDFLAGS,
这些参数就是文件中用export导出的各变量。
4.4 顶层目录的Makefile.build:
这是最复杂的部分,它的功能就是把某个目录及它的所有子目录中、需要编进程序去的文件都编译出来,打包为built-in.o
4.5 怎么使用这套Makefile:
1.把顶层Makefile, Makefile.build放入程序的顶层目录
在各自子目录创建一个空白的Makefile
2.确定编译哪些源文件
修改顶层目录和各自子目录Makefile的obj-y :
obj-y += xxx.o
obj-y += yyy/
上一句表示要编译当前目录下的xxx.c文件, 下一句表示要编译当前目录下的yyy子目录
3. 确定编译选项、链接选项
CFLAGS := -Wall -O2 -g
CFLAGS += -I $(shell pwd)/includeLDFLAGS := export CFLAGS LDFLAGS
修改顶层目录Makefile的CFLAGS,这是编译所有.c文件时都要用的编译选项;
修改顶层目录Makefile的LDFLAGS,这是链接最后的应用程序时的链接选项;
4. 修改各自子目录下的Makefile:
EXTRA_CFLAGS := -D DEBUG
CFLAGS_sub3.o := -D DEBUG_SUB3
"EXTRA_CFLAGS", 它给当前目录下的所有文件(不含其下的子目录)设置额外的编译选项, 可以不设置
"CFLAGS_xxx.o", 它给当前目录下的xxx.c设置它自己的编译选项, 可以不设置
5. 使用哪个编译器?
CROSS_COMPILE =
修改顶层目录Makefile的CROSS_COMPILE, 用来指定工具链的前缀(比如arm-linux-)
6. 确定应用程序的名字:
TARGET := test
修改顶层目录Makefile的TARGET, 这是用来指定编译出来的程序的名字
7. 执行"make"来编译,执行"make clean"来清除,执行"make distclean"来彻底清除。
clean:rm -f $(shell find -name "*.o")rm -f $(TARGET)
4.6 顶层目录
Makefile
CROSS_COMPILE =
AS = $(CROSS_COMPILE)as
LD = $(CROSS_COMPILE)ld
CC = $(CROSS_COMPILE)gcc
CPP = $(CC) -E
AR = $(CROSS_COMPILE)ar
NM = $(CROSS_COMPILE)nmSTRIP = $(CROSS_COMPILE)strip
OBJCOPY = $(CROSS_COMPILE)objcopy
OBJDUMP = $(CROSS_COMPILE)objdumpexport AS LD CC CPP AR NM
export STRIP OBJCOPY OBJDUMPCFLAGS := -Wall -O2 -g
CFLAGS += -I $(shell pwd)/includeLDFLAGS := export CFLAGS LDFLAGSTOPDIR := $(shell pwd)
export TOPDIRTARGET := testobj-y += main.o
obj-y += sub.o
obj-y += a/all : start_recursive_build $(TARGET)@echo $(TARGET) has been built!start_recursive_build:make -C ./ -f $(TOPDIR)/Makefile.build$(TARGET) : start_recursive_build$(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)
4.7 Makefile.build
PHONY := __build
__build:obj-y :=
subdir-y :=
EXTRA_CFLAGS :=include Makefile# 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)# c/built-in.o d/built-in.o
subdir_objs := $(foreach f,$(subdir-y),$(f)/built-in.o)# a.o b.o
cur_objs := $(filter-out %/, $(obj-y))
dep_files := $(foreach f,$(cur_objs),.$(f).d)
dep_files := $(wildcard $(dep_files))ifneq ($(dep_files),)include $(dep_files)
endifPHONY += $(subdir-y)__build : $(subdir-y) built-in.o$(subdir-y):make -C $@ -f $(TOPDIR)/Makefile.buildbuilt-in.o : $(subdir-y) $(cur_objs)$(LD) -r -o $@ $(cur_objs) $(subdir_objs)dep_file = .$@.d%.o : %.c$(CC) $(CFLAGS) $(EXTRA_CFLAGS) $(CFLAGS_$@) -Wp,-MD,$(dep_file) -c -o $@ $<.PHONY : $(PHONY)
4.8 子目录
EXTRA_CFLAGS := -D DEBUG
CFLAGS_sub3.o := -D DEBUG_SUB3obj-y += sub2.o
obj-y += sub3.o
五、通用 Makefile 的解析:
5.1 通用 Makefile 的设计思想
在 Makefile 文件中确定要编译的文件、目录,比如:
obj-y += main.o
obj-y += a/
“Makefile”文件总是被“Makefile.build”包含的。
在 Makefile.build 中设置编译规则,有 3 条编译规则:
怎么编译子目录? 进入子目录编译:
$(subdir-y):
make -C $@ -f $(TOPDIR)/Makefile.build
$(subdir-y)
被展开为所有子目录的列表。- 对于列表中的每个子目录,
make
命令都会被调用。-C $@
将Make的工作目录切换到当前子目录。-f $(TOPDIR)/Makefile.build
指定使用位于顶层目录下的Makefile.build
文件来构建当前子目录。
顶层Makefile会递归地构建所有子目录,每个子目录都会使用自己的Makefile来构建其目标文件。
怎么编译当前目录中的文件?
规则部分:
%.o : %.c
%.o
:这是一个模式规则的模式部分,表示所有以.o
结尾的目标文件。%
是一个占位符,代表目标文件名中除扩展名以外的部分。: %.c
:这是模式规则的结果部分,表示所有以.c
结尾的源文件都会生成对应的目标文件。同样,%.c
中的%
是一个占位符,代表源文件名中除扩展名以外的部分。这条规则的意思是,对于任何以
.c
结尾的源文件,都会生成一个与之对应且以.o
结尾的目标文件。命令部分:
$(CC)
:这是一个变量,代表编译器的名称。通常,这个变量被设置为gcc
或clang
等。$(CFLAGS)
:这是一个变量,包含了编译器的基本编译选项。$(EXTRA_CFLAGS)
:这是一个变量,包含了额外的编译器选项,可能由项目配置或特定文件指定。$(CFLAGS_$@)
:这是一个变量,其中$@
是当前目标文件的名称。它允许为不同的目标文件指定不同的编译器选项。-Wp,-MD,$(dep_file)
:这是一个用于生成依赖文件的选项。-MD
告诉编译器生成依赖文件,而$(dep_file)
是依赖文件的名称。-c
:这是一个编译选项,指示编译器只进行编译,不进行链接。-o $@
:这个选项告诉编译器将输出文件(目标文件)命名为当前目标文件的名称,即$@
。$<
:这是一个自动变量,代表当前规则中第一个依赖文件的名称,即源文件。
使用指定的编译器($(CC)
)和一系列编译选项($(CFLAGS)
、$(EXTRA_CFLAGS)
、$(CFLAGS_$@)
),编译名为$<
的源文件(.c
),生成一个名为$@
的目标文件(.o
),并且同时生成一个依赖文件($(dep_file)
),其中包含了源文件和目标文件之间的依赖关系。
当前目录下的.o 和子目录下的 built-in.o 要打包起来:
built-in.o : $(cur_objs) $(subdir_objs)
$(LD) -r -o $@ $^
规则部分:
built-in.o : $(cur_objs) $(subdir_objs)
built-in.o
:这是目标文件,表示最终生成的文件名。: $(cur_objs) $(subdir_objs)
:冒号后面的部分是依赖列表,表示生成built-in.o
所需的依赖项。这里,$(cur_objs)
和$(subdir_objs)
是变量,它们分别包含当前目录和子目录中所有对象文件的列表。这条规则的意思是,
built-in.o
这个目标文件依赖于$(cur_objs)
和$(subdir_objs)
中列出的所有对象文件。命令部分:
$(LD) -r -o $@ $^
$(LD)
:这是一个变量,代表链接器的名称。通常,这个变量被设置为gcc
或ld
等。-r
:这是一个链接器选项,指示链接器生成可重定位的输出文件。这意味着生成的文件可以在不同的程序中使用,而不是一个独立的可执行文件。-o $@
:这个选项告诉链接器将输出文件命名为当前目标文件的名称,即$@
。$^
:这是一个自动变量,代表当前规则中所有依赖项的列表,即$(cur_objs)
和$(subdir_objs)
中列出的所有对象文件。
使用指定的链接器($(LD)
)和选项(-r
),将所有依赖的对象文件($^
,即$(cur_objs)
和$(subdir_objs)
中的所有.o
文件)链接成一个可重定位的输出文件($@
,即built-in.o
)。
5.2 顶层 Makefile 中把顶层目录的 built-in.o 链接成 APP:
$(TARGET) : built-in.o$(CC) $(LDFLAGS) -o $(TARGET) built-in.o