前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >图解嵌入式系统开发之Makefile篇

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

作者头像
用户5759494
发布2019-07-05 10:45:26
1.2K0
发布2019-07-05 10:45:26
举报
文章被收录于专栏:隔壁科技宅隔壁科技宅

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

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-06-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 隔壁科技宅 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 编译一个文件
  • 编译多个文件
  • 使用Makefile编译文件
  • 使用"高级"Makefile编译
  • 使用“更高级”Makefile编译
  • 编译50个源文件
  • 编译100个源文件:
  • 使用GCC自带功能导出文件依赖
  • Linux系统中的Makefile
  • Makefile.build:
  • 次Makefile:
相关产品与服务
命令行工具
腾讯云命令行工具 TCCLI 是管理腾讯云资源的统一工具。使用腾讯云命令行工具,您可以快速调用腾讯云 API 来管理您的腾讯云资源。此外,您还可以基于腾讯云的命令行工具来做自动化和脚本处理,以更多样的方式进行组合和重用。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档