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

很多人学习嵌入式开发首先遇到的问题肯定是我的代码写在哪里?如何让我写的代码编译进系统 ?如果你是在培训班学习,老师肯定会告诉你先不要管他怎么编译进系统,你只需要在代码所在目录下的Makefile中添加上你的代码文件的名字(后缀.c改成.o)就行了。如下:

Makefile:
  obj-y += xxx.o //xxx你的代码文件名称

如果你是自学汪,这个时候你肯定是。。。

看了好多网文,博客之后,你是否仍然一头雾水,这TM到底是怎么联系起来的,为什么会有这么多Makefile文件?

如果你想进一步深入学习Linux系统的Makefile规则,那么Let go ...

编译一个文件

gcc  -o app main.c

编译多个文件

gcc -o app main.c cmd.c hehe.c haha.c aaa.c

使用Makefile编译文件

all:
    gcc -o app main.c cmd.c

使用"高级"Makefile编译

TARGET := app

$(TARGET): main.o cmd.o
	gcc -o $@  $^

main.o: main.c
	gcc -c $<

cmd.o: cmd.c cmd.h
	gcc -c $<

使用“更高级”Makefile编译

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个源文件

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)取出变量内容来就好了。

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 选项可以导出源文件的依赖关系,如下:

gcc -MM main.c

我们可以把导出的依赖关系保存成一个文件,然后在下次编译的时候使用Makefile的include功能包含该文件。这样就可以自实现只编译被修改文件的梦想了。。。。?

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:

#include "cmd.h"
#ifdef DEBUG
#include "debug.h"
#endif

如上,我在cmd.c里面判断宏DEBUG_EN的值,来决定是否包含debug.h头文件,假设该宏一开始没有定义,其生成的cmd.d依赖文件如下。

cmd.o: cmd.c cmd.h

当我在cmd.h中定义了该宏时。

cmd.h:

#define DEBUG_EN 1

编译过程如下,并没有重新生成cmd.d依赖文件:

这时候我修改debug.h的内容,按照逻辑来说,cmd.c应该重新被编译,但是结果并没有。

所以为了避免这种情况的发生,我们应该确保在头文件被修改时,其对应的依赖文件需要重新生成。如下代码,使用 “sed 's,\(.*\)\.o[:]*,\1.o \1.d:,g' < $@.$$ > $@” 在依赖关系文件中添加xxx.d,使得对应的依赖文件也依赖于对应头文件。

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规则等等,源码如下:

############################################################################
#
# 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来控制递归编译的进程。代码如下:

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当前目录下有哪些源文件需要编译,有哪些目录需要递归进入编译。如下:

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

本文分享自微信公众号 - 隔壁科技宅(gh_92322dc3947c)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-06-28

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏复盘总结文章集合

eclipse本地svn插件与库版本不一致更新等操作出错解决方法

org.apache.subversion.javahl.ClientException: The working copy needs to be upgra...

54040
来自专栏Eureka伽罗的技术时光轴

bochs compile 编译

The standard compile uses the configure script, but the Windows platform cannot ...

10730
来自专栏程序员

Ubuntu下安装软件的三种方式

版权声明:本文为博主原创文章,转载请注明博客地址: ...

4.4K30
来自专栏sofu456

linux下源码安装

 源码安装:配置(configure)、编译(make)、安装(make install),所有操作中间错误可以忽略,最后段末尾统一报错。 ####1.配置 ...

34430
来自专栏信息安全小学生

C++ 和 Makefile 笔记

比如工程目录下,将CPP文件放置在 src 目录下,H文件放在 header下,则makefile可以这样写

18330
来自专栏微卡智享

Android NDK编程(二)---CMakeList.txt详解

前一篇我们介绍了《Android NDK编程(一)---NDK介绍及环境搭建》,简单介绍了一下什么是NDK和JNI,以前NDK环境的配置及怎么创建第一个NDK的...

32130
来自专栏Eureka伽罗的技术时光轴

vscode基于Linux和Windows下c/c++的多文件编译与连接

cygwin64/home/xxx/.bash_profile ,末尾加上如下代码(后面vscodeMake.bat要用到环境变量"_T"):

24560
来自专栏有三AI

【AI白身境】只会用Python?g++,CMake和Makefile了解一下

在学习CMake和和Makefile之前我们先学下g++这个工具,大家或许会问为什么要学g++,不应该直接学CMake和Makefile吗。实际上如果你不掌握g...

9620
来自专栏Eureka伽罗的技术时光轴

makefile 的 ifdef, ifeq 使用及辨析

#可以用命令行传递变量 RELEASE = abc #ifdef 变量名称不能加$() ifdef RELEASE $(warning RELEAS...

44330

扫码关注云+社区

领取腾讯云代金券

年度创作总结 领取年终奖励