数码相框(九、编写通用的Makefile)

注:本人已购买韦东山老师第三期项目视频,内容来源《数码相框项目视频》,只用于学习记录,如有侵权,请联系删除。

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的功能是一样的。
        在这里插入图片描述
    • 缺点:② 修改头文件,包含该头文件的.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,$@.d -c -o $@ $<,其中$@.d是生成的依赖文件。修改的Makefile代码如下:
        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,再次重新编译,编译结果如下图所示: 可见,与前面的效果一样。
        在这里插入图片描述

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目标文件。
在这里插入图片描述

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

[email protected].

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值