电子书完善一:编写通用Makefile

一、写在前面
对于前面电子书的Makefile文件,只是简单地列出所有编译所需的目标文件,但是当修改某个头文件时并不会
重编译包含该头文件的源文件。另一方面,当存在成千上万个源文件,这样一 一列出xxx.o是不现实的。
因此很有必要写出一个更通用的编译体系。
二、电子书的源码树
./
|-- display
|   |-- disp_manager.c
|   |-- fb.c
|   |-- Makefile
|   `-- test
|       |-- Makefile
|       `-- test.c
|-- draw
|   |-- draw.c
|   `-- Makefile
|-- encoding
|   |-- ascii.c
|   |-- encoding_manager.c
|   |-- Makefile
|   |-- utf-16be.c
|   |-- utf-16le.c
|   `-- utf-8.c
|-- fonts
|   |-- ascii.c
|   |-- fonts_manager.c
|   |-- freetype.c
|   |-- gbk.c
|   `-- Makefile
|-- include
|   |-- config.h
|   |-- disp_manager.h
|   |-- draw.h
|   |-- encoding_manager.h
|   `-- fonts_manager.h
|-- log.txt
|-- main.c
`-- Makefile
说明:源码树来源自韦老师课堂笔记(幸苦了!
三、通用Makefile 的说明
本程序的Makefile分为3类:
1. 顶层目录的Makefile
2. 顶层目录的Makefile.build
3. 各级子目录的Makefile

一、各级子目录的Makefile:
它最简单,形式如下:
obj-y += file.o
obj-y += subdir/
	"obj-y += file.o"表示把当前目录下的file.c编进程序里,
        "obj-y += subdir/"表示要进入subdir这个子目录下去寻找文件来编进程序里,
	 			  是哪些文件由subdir目录下的Makefile决定。
	注意: "subdir/"中的斜杠"/"不可省略
二、顶层目录的Makefile:
 	它除了定义obj-y来指定根目录下要编进程序去的文件、子目录外,主要是定义工具链、编译参数、链接参数──
 	就是文件中用export导出的各变量。
三、顶层目录的Makefile.build:
 这是最复杂的部分,它的功能就是把某个目录及它的所有子目录中、需要编进程序去的文件都编译出来,
 打包为built-in.o

四、怎么使用这套Makefile:
1.把顶层Makefile, Makefile.build放入程序的顶层目录
2.修改顶层Makefile
	2.1 修改工具链
	2.2 修改编译选项、链接选项
	2.3 修改obj-y决定顶层目录下哪些文件、哪些子目录被编进程序
	2.4 修改TARGET,这是用来指定编译出来的程序的名字

3. 在各一个子目录下都建一个Makefile,形式为:
obj-y += file1.o
obj-y += file2.o
obj-y += subdir1/
obj-y += subdir2/

4. 执行"make"来编译,执行"make clean"来清除,执行"make distclean"来彻底清除
说明:来源自韦东山老师课堂笔记
四、实现通用Makefile
实际,表达为通用Makefile并不准确,更准确来说,应该是一套Makefile编译体系。
下面讲解实现这编译体系的思路

1)在各子目录下编写Makefile,形式如
	obj-y +=  xxx.o
	obj-y += subdir/
2) 编写顶层Makefile文件
	2.1> 参考内核顶层Makefile,定义工具链、编译参数、链接参数
	        然后再把这些 export 导出去供子目录的Makefile使用
	2.2> 定义目标TARGET以及生成该目标的依赖文件(obj-y += subdir/ 或 main.o )
		并编写生成TARGET的命令,最关键是执行make -C ./ -f $(TOPDIR)/Makefile.build
		以生成最终的依赖built-in.o
	2.3> 定位伪目标clean 及 distclean ,方便清理重新make。(distclean相比clean只是多了清楚生成的依赖文件.xxx.d )
3) 编写 Makefile.build 文件
	该文件的这套编译体系的核心,对于啥是Makefile.build呢?通俗来说,就是定义了通用规则的一个Makefile文件。
	
	何为通用?即它适用所有子目录包括顶层目录,对于子目录,它首先包含子目录的Makefile(获得obj-y ),若该子目录还有其他目录
	则进入其他目录递归调用make以生成build-in.o,最后该built-in.o与该子目录的所有 *.o 目标文件链接生成 build-in.o 
	作为顶层Makefile的依赖文件。对于顶层目录,也会首先包含顶层Makefile,然后依次进入子目录递归make,将所有子目录生成的
	build-in.o 和 顶层目录下的所有*.o目标文件“链接”生成最终的目标TARGET

     	如何编写呢? 首先明确思路,然后逐步实现。
     	始终不要忘记这一目的:生成"最终"的 build-in.o (= *.o 或 *.o + build-in.o )
     	编写思路:
     	3.1> 参考内核顶层目录下的scripts目录的Makefile.build文件
     		首先定义PHONY变量(其值为__build )(后面会声明为伪变量), 并把它的值作为Makefile.build文件的最终目标
     		并写出生成这一目标的依赖:当前目录下所有*.o 目标文件  + 当前目录下所有子目录的build-in.o(subdir/build-in.o)
     	3.2> 生成当前目录所有子目录的build-in.o
     		套路都一样, 命令都为: make -C $@ -f $(TOPDIR)/Makefile.build
     		即进入到子目录,然后适用顶层目录下的Makefile.build编译生成build-in.o
     	3.3> 生成当前目录所有*.o 目标文件
     		%.o : %.c
     			$(CC) $(CFLAGS) -Wp,-MD,$(dep_file) -c -o $@ $<
     	3.4> 链接 3.2、3.3生成的依赖文件以生成最终的build-in.o
     		build-in.o : $(cur_objs) $(subdir_objs)
		 	$(LD)  -r  -o  $@  $^
	备注:Makefile.build还涉及根据当前目录的obj-y 变量提取出 *.o 和 /subdir1 /subdir2(最终被替换为 subdir1  subdir2 )

	总结来说,Makefile.build实现的是通用的规则,差异性由子目录或顶层目录的Makefile实现。
	下面,按上面的步骤编码这套编译体系。(省略子目录的Makefile文件,太简单了)
# 顶层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

CFLAGS := -Wall -O2 -g
CFLAGS += -I $(shell pwd)/include

LDFLAGS := -lm -lfreetype

export CFLAGS LDFLAGS

TOPDIR := $(shell pwd)
export TOPDIR

TARGET := show_file

obj-y += main.o
obj-y += display/
obj-y += draw/
obj-y += encoding/
obj-y += fonts/

all : 
	make -C ./ -f $(TOPDIR)/Makefile.build
 	$(CC) $(LDFLAGS) -o $(TARGET) built-in.o
 
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)
# 顶层的Makefile.build
PHONY := __build
__build:

obj-y := 
subdir-y := 

include Makefile

__subdir-y := $(patsubst %/, %, $(filter %/, $(obj-y)))
subdir-y += $(__subdir-y)

subdir_objs := $(foreach f, $(subdir-y), $(f)/build-in.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)
endif

PHONY += $(subdir-y)

$(subdir-y):
	make -C $@ -f $(TOPDIR)/Makefile.build

__build: $(subdir-y) build-in.o 

build-in.o : $(cur_objs) $(subdir_objs)
	$(LD) -r -o $@ $^
 
dep_file = .$@.d 

%.o : %.c 
	$(CC) $(CFLAGS) -Wp,-MD,$(dep_file) -c -o $@ $<
 
.PHONY: $(PHONY)
最后执行make即可生成show_file,这里就不贴出编译过程了,太占篇幅。
五、Makefile用到的参数说明
下面解释上面两个文件用到的一些命令和参数。
make -C  <dir_name> -f  <file_name>
-C <dir_name>: 即进入dir_name目录
-f <file_name> : 指定make的文件<file_name>
.PHONY : $(PHONY)  声明$(PHONY)变量的值为伪变量

编译、链接时用到的参数详解:
CFLAGS := -Wall -O2 -g 指定编译的参数
-Wall : 打印所有的警告信息
-O2   : 
	-O0~3编译器的优化选项的4个级别,-O0表示没有优化, -O1为缺省值,-O3优化级别最高
	级别越大优化效果越好,但编译时间越长。知道用于优化即可,深究可参考其它文章。
-I<dir>
	指定从<dir>查找头文件,否则在标准库路径查找
-L<dir>
	指定搜索库的路径为<dir>,否则在标准库路径查找
	放在/lib和/usr/lib和/usr/local/lib里的库直接用-l参数就能链接了,但如果库文件没放在这三个目录里,
	而是放在其他目录里,这时我们只用-l参数的话,链接还是会出错,出错信息大概是:“/usr/bin/ld: cannot find -lxxx”,
	也就是链接程序ld在那3个目录里找不到libxxx.so,这时另外一个参数-L就派上用场了,比如常用的X11的库,
	它放在/usr/X11R6/lib目录下,我们编译时就要用-L/usr/X11R6/lib -lX11参数,-L参数跟着的是库文件所在的目录名。
	再比如我们把libtest.so放在/aaa/bbb/ccc目录下,那链接参数就是-L/aaa/bbb/ccc -ltest

-l<lib_name>
	指定程序要链接的库<lib_name>
	比如我们自已要用到一个第三方提供的库名字叫libtest.so,那么我们只要把libtest.so拷贝到/usr/lib里,
	编译时加上-ltest参数,我们就能用上libtest.so库了
	(当然要用libtest.so库里的函数,我们还需要与libtest.so配套的头文件)。
-g 
	在编译的时候,产生调试信息。
-M  
	生成文件关联的信息。包含目标文件所依赖的所有源代码你可以用 gcc -M hello.c 来测试一下,很简单。
-MD  
	和-M相同,但是输出将导入到.d的文件里面
-r 
	--relocateable 产生可重定向的输出
arm-elf-ar
	将多个可重定位的源目标模块归档为一个函数库文件。采用函百数库文件,应用程序能够从该文件中自动装载要参考的函数模块,
	同时将应用程序中频知繁调用的函数放入函数库文件中,易于应用程序的开发管理道。arm-elf-ar支持ELF格式的函数库文件
	1)     创建test.a静态库
	arm-linux-gcc  -c  a.o  a.c
	arm-linux-gcc  -c  b.o  b.c
	arm-linux-ar  -rc  test.a  a.o  b.o 
                   
	2)     查看test.a库中目标文件
	arm-linux-ar -t test.a
	arm-linux-ar –tv  test.a
	v显示出具体创建库的时间

	3)     提取test.a库中的目标文件
	arm-linux-ar -x test.a

	4)     修改test.a库中加入c.o目标文件
	arm-linux-ar –r  test.a  c.o

	5)     删除test.a库中c.o目标文件
	arm-linux-ar –d  test.a  c.o
arm-linux-nm 
	列出目标文件中的符号。

几个函数:
$(patsubst %/, %, $(filter %/, $(obj-y)))
	假设: obj-y :=  a.o b.o c.o  display/  draw/
	则$(filter %/, $(obj-y)) --->得到 display/  draw/
	    patsubst  将得到的 display/ draw/ 替换为 display draw
	    
cur_objs := $(filter-out %/, $(obj-y))
	去掉$(obj-y)变量中所有符合 %/ 的 值,返回不匹配的值
dep_files := $(foreach f, $(cur_objs), .$(f).d)    
	从$(cu_objs)变量遍历取出一个值f ,并替换为 $(f).d  
	后缀为.d 表明是dependent 依赖文件
dep_files := $(wildcard $(dep_files))
	从dep_files变量中取出所有后缀 .d 的文件

OK,本文讲到这里,上面的文件已经放到我的代码集合(https://blog.csdn.net/qq_42800075/article/details/105670841);			
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值