很多人学习嵌入式开发首先遇到的问题肯定是我的代码写在哪里?如何让我写的代码编译进系统 ?如果你是在培训班学习,老师肯定会告诉你先不要管他怎么编译进系统,你只需要在代码所在目录下的Makefile中添加上你的代码文件的名字(后缀.c改成.o)就行了。如下:
代码语言:javascript复制Makefile:
obj-y = xxx.o //xxx你的代码文件名称
如果你是自学汪,这个时候你肯定是。。。
看了好多网文,博客之后,你是否仍然一头雾水,这TM到底是怎么联系起来的,为什么会有这么多Makefile文件?
如果你想进一步深入学习Linux系统的Makefile规则,那么Let go ...
编译一个文件
代码语言:javascript复制gcc -o app main.c
编译多个文件
代码语言:javascript复制gcc -o app main.c cmd.c hehe.c haha.c aaa.c
使用Makefile编译文件
代码语言:javascript复制all:
gcc -o app main.c cmd.c
使用"高级"Makefile编译
代码语言:javascript复制TARGET := app
$(TARGET): main.o cmd.o
gcc -o $@ $^
main.o: main.c
gcc -c $<
cmd.o: cmd.c cmd.h
gcc -c $<
使用“更高级”Makefile编译
代码语言:javascript复制TARGET := app
$(TARGET): main.o cmd.o
gcc -o $@ $^
main.o: main.c
cmd.o: cmd.c cmd.h
#使用静态规则,定义通用编译方法
%.o: %.c
gcc -c $<
clean:
rm -rf *.o $(TARGET)
编译50个源文件
代码语言:javascript复制TARGET := app
SRC:= cmd1.o cmd2.o cmd3.o cmd4.o cmd5.o cmd6.o cmd7.o cmd8.o cmd9.o cmd10.o
cmd11.o cmd12.o cmd13.o cmd14.o cmd15.o cmd16.o cmd17.o cmd18.o cmd19.o cmd20.o
cmd21.o cmd22.o cmd23.o cmd24.o cmd25.o cmd26.o cmd27.o cmd28.o cmd29.o cmd30.o
cmd31.o cmd32.o cmd33.o cmd34.o cmd35.o cmd36.o cmd37.o cmd38.o cmd39.o cmd40.o
cmd41.o cmd42.o cmd43.o cmd44.o cmd45.o cmd46.o cmd47.o cmd48.o cmd49.o cmd50.o
$(TARGET): main.o cmd.o
gcc -o $@ $^
cmd1.o : cmd1.c cmd1.h
cmd2.o : cmd2.c cmd2.h
cmd3.o : cmd3.c cmd3.h
cmd4.o : cmd4.c cmd4.h
cmd5.o : cmd5.c cmd5.h
cmd6.o : cmd6.c cmd6.h
cmd7.o : cmd7.c cmd7.h
cmd8.o : cmd8.c cmd8.h
cmd9.o : cmd9.c cmd9.h
cmd10.o : cmd10.c cmd10.h
cmd11.o : cmd11.c cmd11.h
cmd12.o : cmd12.c cmd12.h
cmd13.o : cmd13.c cmd13.h
cmd14.o : cmd14.c cmd14.h
cmd15.o : cmd15.c cmd15.h
cmd16.o : cmd16.c cmd16.h
cmd17.o : cmd17.c cmd17.h
cmd18.o : cmd18.c cmd18.h
cmd19.o : cmd19.c cmd19.h
cmd20.o : cmd20.c cmd20.h
cmd21.o : cmd21.c cmd21.h
cmd22.o : cmd22.c cmd22.h
cmd23.o : cmd23.c cmd23.h
cmd24.o : cmd24.c cmd24.h
cmd25.o : cmd25.c cmd25.h
cmd26.o : cmd26.c cmd26.h
cmd27.o : cmd27.c cmd27.h
cmd28.o : cmd28.c cmd28.h
cmd29.o : cmd29.c cmd29.h
cmd30.o : cmd30.c cmd30.h
cmd31.o : cmd31.c cmd31.h
cmd32.o : cmd32.c cmd32.h
cmd33.o : cmd33.c cmd33.h
cmd34.o : cmd34.c cmd34.h
cmd35.o : cmd35.c cmd35.h
cmd36.o : cmd36.c cmd36.h
cmd37.o : cmd37.c cmd37.h
cmd38.o : cmd38.c cmd38.h
cmd39.o : cmd39.c cmd39.h
cmd40.o : cmd40.c cmd40.h
cmd41.o : cmd41.c cmd41.h
cmd42.o : cmd42.c cmd42.h
cmd43.o : cmd43.c cmd43.h
cmd44.o : cmd44.c cmd44.h
cmd45.o : cmd45.c cmd45.h
cmd46.o : cmd46.c cmd46.h
cmd47.o : cmd47.c cmd47.h
cmd48.o : cmd48.c cmd48.h
cmd49.o : cmd49.c cmd49.h
cmd50.o : cmd50.c cmd50.h
#使用静态规则,定义通用编译方法
%.o: %.c
gcc -c $<
clean:
rm -rf *.o $(TARGET)
编译100个源文件:
对于一般小的项目来说,代码量不大,源文件数量不多,大家Makefile随便爱怎么写就怎么写。但是对于像linux系统这种量级的工程来说,文件数量实在太庞大,如果像上述这种方法去描述整个工程的依赖关系,估计程序员都被累死了。有的同学可能会说,为什么要把所有的C文件都具体列出来那?使用wildcard命令不就好了?如下,使用wildcard列出当前路径下所有的源文件名称,保存到变量SRCS中,然后编译的时候使用$(SRCS)取出变量内容来就好了。
代码语言:javascript复制TARGET := app
#使用wildcard命令可以列出当前目录下所有.c文件
SRCS := $(wildcard *.c)
$(TARGET): $(SRCS)
gcc -o $@ $^
%.o: %.c
gcc -c $<
clean:
rm -rf *.o $(TARGET)
是的,这样是可以编译的,但要求每次都是完整编译。因为该依赖关系中只是 列出来了.c的依赖,没有描述对头文件的依赖,任何一个头文件的更改都需要重新编译所有文件。归根到底这还是代码量级的问题,如果代码数量太大,编译一次需要数个小时,那么我们不可能每次都完整编译,最理想的情况是只编译修改过的C文件和受修改过的头文件影响的源文件。
使用GCC自带功能导出文件依赖
使用gcc自带的-MM 选项可以导出源文件的依赖关系,如下:
代码语言:javascript复制gcc -MM main.c
我们可以把导出的依赖关系保存成一个文件,然后在下次编译的时候使用Makefile的include功能包含该文件。这样就可以自实现只编译被修改文件的梦想了。。。。?
代码语言:javascript复制TARGET := app
all: $(TARGET)
#使用wildcard命令可以列出当前目录下所有.c文件
SRCS := $(wildcard *.c)
#尝试导入对于xxx.c的依赖文件xxx.d
-include $(patsubst %.c, %.d, $(SRCS))
#最终目标生成规则
$(TARGET): $(patsubst %.c, %.o, $(SRCS))
gcc -o $@ $^
#源文件编译规则
%.o: %.c
gcc -c $<
#依赖文件的生成规则
%.d: %.c
gcc -MM -c $< > $@
clean:
rm -rf *.o *.d $(TARGET)
如上, cmd.c main.c被重新编译,其对应的依赖文件也被生成。简直式大功告成啊,哈哈哈哈。但是,等等,仿佛还有哪里不对劲,如果我修改了头文件的内容,同时该头文件的内容会影响到源文件的依赖关系那?
cmd.c:
代码语言:javascript复制#include "cmd.h"
#ifdef DEBUG
#include "debug.h"
#endif
如上,我在cmd.c里面判断宏DEBUG_EN的值,来决定是否包含debug.h头文件,假设该宏一开始没有定义,其生成的cmd.d依赖文件如下。
代码语言:javascript复制cmd.o: cmd.c cmd.h
当我在cmd.h中定义了该宏时。
cmd.h:
代码语言:javascript复制#define DEBUG_EN 1
编译过程如下,并没有重新生成cmd.d依赖文件:
这时候我修改debug.h的内容,按照逻辑来说,cmd.c应该重新被编译,但是结果并没有。
所以为了避免这种情况的发生,我们应该确保在头文件被修改时,其对应的依赖文件需要重新生成。如下代码,使用 “sed 's,(.*).o[:]*,1.o 1.d:,g' < $@.$$ > $@” 在依赖关系文件中添加xxx.d,使得对应的依赖文件也依赖于对应头文件。
代码语言:javascript复制TARGET := app
all: $(TARGET)
#使用wildcard命令可以列出当前目录下所有.c文件
SRCS := $(wildcard *.c)
#尝试导入对于xxx.c的依赖文件xxx.d
-include $(patsubst %.c, %.d, $(SRCS))
#最终目标生成规则
$(TARGET): $(patsubst %.c, %.o, $(SRCS))
gcc -o $@ $^
#源文件编译规则
%.o: %.c
gcc -c $<
#依赖文件的生成规则
%.d: %.c
gcc -E -MM $< > $@.$$
@sed 's,(.*).o[:]*,1.o 1.d:,g' < $@.$$ > $@
@rm -rf $@.$$
clean:
rm -rf *.o *.d $(TARGET)
效果如下:
以上是我们一般中小工程的Makefile写法,但是对于像Linux这种超大型的系统来说,以上Makefile还远远不够,很多时候为了控制编译产物的体积,我们Linux系统需要按需裁剪调不需要的功能,控制某些源文件或者某些目录下的所有文件都不参与编译,所有我们需要更加灵活的Makefile。
Linux系统中的Makefile
(图一 递归编译系统架构)
如上图是Linux系统编译系统的主要框架。主要包括三类Makefile文件。
主Makefile: 主Makefile一般在源码的根目录下, 是执行Make命令读取的第一个Makefile文件,该文件中定义了最终产物的名字,源文件的子目录,启动递归编译,合成最终产物规则,clean规则等等,源码如下:
代码语言:javascript复制############################################################################
#
# The Main Makefile for start compile
#
############################################################################
#Target application name
TARGET :=app
#Source file Directory
SRCS := entry/ cmd/
#Root path for all Makefile
export ROOTPATH=$(shell pwd)
.PHONY: all pre clean
#Virtual Final target
all: pre $(TARGET)
@echo "$(TARGET) is Ready"
#Recursive compile start
pre:
@for d in `echo $(SRCS)`; do
make -C $$d -f Makefile -f $(ROOTPATH)/Makefile.build objs=$$d all;
done
#Generate final app
$(TARGET): $(patsubst %/, %/build-in.o, $(SRCS))
gcc -o $@ $^
#Clean all the compiling generations
clean:
@for d in `echo $(SRCS)`; do
make -C $$d -f Makefile -f $(ROOTPATH)/Makefile.build clean;
done
rm -rf $(TARGET)
Makefile.build:
Makefile.build是主要的编译主体,提供了具体的代码编译规则,递归编译控制,依赖文件生成规则,递归Clean规则等等。该文件第一次是被主Makefile调用,后续该Makefile通过不断递归调用自己,同时搭配次Makefile来控制递归编译的进程。代码如下:
代码语言:javascript复制SRCS:=$(filter %.o, $(obj-y))
DIRS:=$(filter %/, $(obj-y))
-include $(patsubst %.o, %.d, $(SRCS))
.PHONY: pre clean
all: pre build-in.o
pre: $(patsubst %.o, %.d, $(SRCS))
@for i in `echo $(DIRS)`;do
make -C $$i -f Makefile -f $(ROOTPATH)/Makefile.build objs=$$i all;
done;
build-in.o: $(SRCS) $(patsubst %/, %/build-in.o, $(DIRS))
ld -r -o $@ $^
%.o: %.c
gcc -c $<
%.d: %.c
@gcc -E -MM $< > $@.$$
@sed 's,(.*).o[:]*,1.o 1.d:,g' < $@.$$ > $@
@rm -rf $@.$$
clean:
@for i in `echo $(DIRS)`; do
make -C $$i -f Makefile -f $(ROOTPATH)/Makefile.build clean;
done;
rm -rf *.o *.d
次Makefile:
次Makefile数量非常多,一般在每个代码目录下都会有一个Makefile文件,该文件通过给obj-y赋值,告诉Makefile.build当前目录下有哪些源文件需要编译,有哪些目录需要递归进入编译。如下:
代码语言:javascript复制obj-y := main.o
obj-y = cmd/
Makefile.build递归到该目录的时候首先包含了该目录下的Makefile文件,然后读取obj-y的值,读到obj-y中 main.o时,Makefile.build在当前目录下找到main.c,然后编译成main.o, 读到 cmd/时,Makefile.build意识到需要进入到cmd目录下进行编译,并将cmd目录下的文件编译成build-in.o文件,从cmd目录返回后,Makefile.build将当前目录下编译产生的.o文件和cmd子目录下编译产生的build-in.o文件共同打包成当前目录下的build-in.o文件。所以这是一个不断递归的过程,进入到一个目录下,通过当前目录下Makefile判断是否有子目录,如果有子目录,就按照同样的方式先进入到子目录下去处理。直到最深一级目录下的源文件编译完,再一级一级返回,编译上一级目录的代码,并同时将子目录下生成的build-in.o文件也打包在一起。生成当前目录下的build-in.o文件。
其编译执行流程如下图:
图x 编译执行过程
实际编译输出:
实际Clean过程:
以上完整样例代码可以在我们的github上下载:
Makefile 示例代码
git@github.com:tech-eric/magicbox.git