图解嵌入式系统开发之Makefile篇

2019-07-05 10:45:26 浏览数 (1)

很多人学习嵌入式开发首先遇到的问题肯定是我的代码写在哪里?如何让我写的代码编译进系统 ?如果你是在培训班学习,老师肯定会告诉你先不要管他怎么编译进系统,你只需要在代码所在目录下的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

0 人点赞