注:本人已购买韦东山老师第三期项目视频,内容来源《数码相框项目视频》,只用于学习记录,如有侵权,请联系删除。
1. 程序的编译过程
(1) 一个C/C++程序要经过预处理、编译、汇编、链接 4个步骤才可以变成可执行文件:
- 预处理: ① 把包含的头文件插入源文件中;② 将宏定义展开;④ 根据条件编译选择要使用的代码;⑤ 最后把代码输出到一个“.i” 格式的文件中等待下一步的处理;
- 编译:把C/C++代码(比如上述的 “.i” 文件)“翻译” 成 “.s” 汇编代码;
- 汇编:就是把上一步编译出来的 “.s” 汇编代码翻译成 “.o” 格式的机器代码;
- 链接:就是把上一步的 “.o” 格式的文件、系统的库文件等链接起来,最终生成可以在特定平台运行的可执行文件。
注:上面的 预处理 、编译、汇编这三个步骤就是我们常说的编译。
(2) 对于以下代码:
a.h 代码:
#define A 1
a.c 代码:
#include <stdio.h>
#include "a.h"
int main(void)
{
printf("Hello world!\n");
printf("A = %d\n", A);
test_fun(); /* 调用 b.c 的 test_fun 函数 */
return 0;
}
b.c 代码:
#include <stdio.h>
int test_fun(void)
{
printf("it is B\n");
return 0;
}
编译上面a.h、a.c、b.c代码的方法:
① gcc -o test a.c b.c
执行./test
命令调用结果如下:
那么gcc -o test a.c b.c
做了哪些事情?
- 对于a.c:预处理、编译、汇编;
- 对于b.c:预处理、编译、汇编;
- 最后链接成test可执行文件。
使用这边编译方法的优缺点:
- 优点:命令简单;
- 缺点:如果文件很多,即使你只修改了一个文件,但是所有的文件都有重新 “预处理、编译、汇编”,最后链接,效率低。
② 写Makefile
- Makefile的核心:规则
- 规则:
目标:依赖1 依赖2 ... <TAB> 命令
- 上述规则的意义:使用命令,根据依赖生成目标。
- 命令执行的条件:① “依赖” 文件 比 “目标” 文件 的时间新;② 没有 “目标” 文件;
- 以下是 a.h、a.c、b.c 的一个简单的 Makefile:
test: a.c b.c a.h gcc -o test a.c b.c
- 当我们在命令终端输入make命令,就会读取Makefile里的规则,执行里面的命令生成目标文件。
- 执行上面Makefile里的命令的条件:① a.c、b.c、a.h 文件的时间比 test 文件的时间新;② 没有 test 目标文件;当都不符合这两个条件时,不执行命令。例如,连续执行两次 make 命令,结果如下图所示,第二次执行时,提示 “‘test’ is up to date”,表示当前目标文件已经是最新的。
- 加入我们重新保存 a.c,重新执行 make 命令,结果如下图所示,可见执行了
gcc -o test a.c b.c
重新编译;修改b.c、a.h 同理。可见,只要其中一个依赖修改了,就会执行对应的命令重新编译出目标文件。
- 上面 Makefile 的命令还是之前的
gcc -o test a.c b.c
,只有一个依赖文件修改了,其他所有的依赖文件都会重新编译,效率低。
2. Makefile 改进
① 改进方案一:Makefile 代码如下:
test:a.o b.o
gcc -o test a.o b.o
# -c:表示只预处理、编译、汇编,不链接
a.o:a.c
gcc -c -o a.o a.c
b.o:b.c
gcc -c -o b.o b.c
- 在执行make之前,分析上面的Makefile所做的事情:为了生成第一个目标test,test依赖于 a.o、b.o,但是当前目录并没有 a.o、b.o 这两个文件,会先用第一个依赖 a.o 往下查找,找到 a.o 依赖于 a.c,a.c 在当前目录是存在的,那么是否使用 a.c 生成 a.o 呢?需要符合Makefile命令执行的两个条件(① “依赖” 文件 比 “目标” 文件 的时间新;② 没有 “目标” 文件),此时当前目录并没有 a.o 这个目标文件,因此执行
gcc -c -o a.o a.c
命令生成 a.o 目标文件;对于第二个依赖 b.o 与 a.o 同理;当 a.o、b.o 都生成后,就使用gcc -o test a.o b.o
。总的来说就是先执行gcc -c -o a.o a.c
命令,再执行gcc -c -o b.o b.c
命令,最后执行gcc -o test a.o b.o
命令。 - 使用上面的Makefile编译 a.c、b.c,编译结果如下图所示,执行结果与上面的分析一致。
- 现在修改 a.c,然后重新 make 编译,编译结果如下图所示,只重新编译了 a.c,并没有编译 b.c,从而节省了编译时间。由于修改了 a.c 文件,a.c 的时间比 a.o 的时间新,因此执行了
gcc -c -o a.o a.c
,而新生成的 a.o 比 test新,所以执行gcc -o test a.o b.o
重新生成 test 目标文件。
- 这个 Makefile 还有两个明显的缺点:
- 缺点:① 假如像 a.o 、b.o 这样的 .o 文件有很多,需要编写很多像
gcc -c -o a.o a.c
这样的命令,效率低。改进方法:使用通配符%。 改进的Makefile代码如下:test:a.o b.o gcc -o test a.o b.o # $@:表示目标 # $<:表示第一个依赖 # $^:表示所有的依赖 %.o:%.c gcc -c -o $@ $<
- 执行该Makefile的编译结果如下图所示:可见,最终的编译效果是一样的。
- 接着修改 b.c ,然后重新编译,编译结果如下图所示:可见,只重新编译了 b.c,跟之前Makefile的功能是一样的。
- 执行该Makefile的编译结果如下图所示:可见,最终的编译效果是一样的。
- 缺点:② 修改头文件,包含该头文件的.c源文件不能重新编译:
- 修改头文件 a.h,重新编译,编译结果如下图所示:可见,没有任何编译反应;
- 把 a.h 里的
#define A 1
修改为#define A 2
,重新编译,运行,运行结果如下图所示:可见,A 依然还是修改前的 1。
- 修改Makefile解决次缺点,修改 Makefile 把 a.h 头文件作为 a.o 的依赖,代码如下:
test:a.o b.o gcc -o test a.o b.o a.o:a.c a.h %.o:%.c gcc -c -o $@ $<
- 使用上面的Makefile,两次(第一次:
#define A 1
,第二次:#define A 2
)修改a.h 后重新编译,编译运行结果如下图所示:可见,修改头文件也能重新编译对应的.c 源文件了。
- 对于Makefile的
a.o : a.c a.h
,当头文件很多的时候,显然也有编写效率低的问题,那么有什么自动的规则呢?利用gcc生成依赖文件:gcc -Wp,-MD,[email protected] -c -o $@ $<
,其中[email protected]
是生成的依赖文件。修改的Makefile代码如下:
重新编译,然后修改a.h,再次重新编译,编译结果如下图所示: 可见,与前面的效果一样。objs := a.o b.o test:$(objs) gcc -o test $^ # dep_files := .a.o.d .b.o.d dep_files := $(foreach f,$(objs),.$(f).d) # 对于objs里的每一个成员都使用.$(f).d来替换 dep_files := $(wildcard $(dep_files)) # wildcard 取出所有符合$(dep_files)格式的存在的文件 # 如果dep_files变量不为空,则包含dep_files变量的文件 ifneq ($(dep_files),) include $(dep_files) endif %.o:%.c gcc -Wp,-MD,.$@.d -c -o $@ $< clean: rm -rf *.o test
- 修改头文件 a.h,重新编译,编译结果如下图所示:可见,没有任何编译反应;
- 缺点:① 假如像 a.o 、b.o 这样的 .o 文件有很多,需要编写很多像
3. Makefile 支持工程
回顾以前数码相框(六、在LCD上显示任意编码的文本文件)的Makefile,它有一个缺陷就是当我们修改某个 .h 头文件之后,对应的 .c 源文件不能够重新编译。这一小节将以电子书的工程文件为基础,仿照linux内核的Makefile编写一个支持工程文件的通用Makefile。
步骤:
① 在每个子目录下建立一个Makefile,子目录的Makefile的内容如下:
obj-y += file1.o
obj-y += file2.o
...
其中,file1.o、file2.o 是涉及的 .c 源文件对应的 .o 文件。
② 假如有子目录下又有子目录,子目录的 Makefile 如下:
obj-y += file1.o
obj-y += file2.o
obj-y += test/ # test是子目录下的子目录
...
那么,test目录下的 Makefile 如下:(假设test目录下有 test.c 源文件)
obj-y += test.o
③ 编写顶层目录下的 Makefile:
# 工具链
CROSS_COMPILE = arm-linux-
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
# 导出变量
export AS LD CC CPP AR NM
export STRIP OBJCOPY OBJDUMP
# 指定编译参数: -Wall:开启全部警告; -O2: 优化选项; -g: 加上调试信息
CFLAGS := -Wall -O2 -g
# 指定编译头文件目录 (为什么要指定编译目录?假如没有指定头文件目录,编译器自动到系统 /usr/include 目录寻找头文件,例如stdio.h,当使用一个编译器时,编译器会默认有一个系统目录。那么我们是否能够指定头文件目录呢?使用 -I 选项指定头文件目录,格式:-I 头文件目录;同样,链接的时候加上 -L 选项指定库文件目录,格式:-L 库文件目录)
CFLAGS += -I $(shell pwd)/include
# 指定连接参数: -lm: 表示数学库; -lfreetype: 表示freetype库
LDFALGS := -lm -lfreetype
# 导出 CFLAGS LDFALGS
export CFLAGS LDFALGS
TOPDIR := $(shell pwd)
export TOPDIR
# = 表示延时变量,它的值只有使用的时候,才可以确定。它最大的缺点是不能在变量后面追加内容。
# := 表示立即变量,它的值立马确定
# 最终编译出来的目标文件 show_file
TARGET := show_file
obj-y += main.o
obj-y += display/
obj-y += draw/
obj-y += encoding/
obj-y += fonts/
# 第一个规则
all :
# 进入当前目录使用 Makefile.build 来编译
make -C ./ -f $(TOPDIR)/Makefile.build
$(CC) $(LDFALGS) -o $(TARGET) built-in.o
# 清除
clean:
rm -rf $(shell find -name "*.o")
rm -rf $(TARGET)
distclean:
rm -rf $(shell find -name "*.o")
rm -rf $(shell find -name "*.d")
rm -rf $(TARGET)
到此,顶层目录的Makefile已经写完,可见,这个Makefile 严重依赖于 Makefile.build 这个文件,接下来需要写出 Makefile.build。
show_file 工程目录如下:
show_file 工程编译思路:
- ① 把 display 目录下的 test 目录的 test.c 编译成 test.o;
- ② 把 test 目录的所有的 .o 文件打包成 built-in.o;
- ③ 然后返回到上一层 display 目录,把 disp_manager.c 编译成 disp_manager.o;
- ④ 把 fb.c 编译成 fb.o;
- ⑤ 把 disp_manager.o、fb.o 和 test 目录下的 built-in.o 打包成 display 目录下的 built-in.o;
- ⑥ 同理,把 draw 目录下的 draw.c 编译成 draw.o;
- ⑦ 然后把 draw 目录下的 draw.o 打包成 draw 目录下的 built-in.o;
- ⑧ 经过若干个目录里的 .c 文件编译打包成 built-in.o 后,main.c 也被编译成了 main.o,然后把 main.o 与 main.o 所在目录对应的子目录的 built-in.o (例如 display 目录下的 built-in.o),打包成顶层目录下的 built-in.o;
- ⑨ 最后链接成目标文件。
④ Makefile.build 编写:
# 假目标, 直接make生成第一个目标
PHONY := __build
__build:
# obj-y 赋空值
obj-y :=
# 子目录
subdir-y :=
# 包含当前目录的 Makefile
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)
# cur_objs := a.o b.o
cur_objs := $(filter-out %/, $(obj-y))
# 依赖文件
dep_files := $(foreach f,$(cur_objs),.$(f).d)
dep_files := $(wildcard $(dep_files))
# 如果dep_files变量不为空,则包含dep_files变量的文件
ifneq ($(dep_files),)
include $(dep_files)
endif
PHONY += $(subdir-y)
__build:$(subdir-y) built-in.o
# 进入子目录编译
$(subdir-y):
# 进入子目录, 使用 Makefile.build 来编译
make -C $@ -f $(TOPDIR)/Makefile.build
built-in.o: $(cur_objs) $(subdir_objs)
$(LD) -r -o $@ $^
dep_file = .$@.d
%.o : %.c
$(CC) $(CFLAGS) -Wp,-MD, $(dep_file) -c -o $@ $<
.PHONY : $(PHONY)
⑤ 编写好Makefile 后,重新编译 show_file 工程,编译结果如下图所示:
⑥ 我们在test目录下添加 test.h,重新编译,编译结果如下图所示:可见,只重新编译了test.c,然后重新打包、链接成 show_file目标文件。