0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

Makefile是如何编译代码文件的?

冬至子 来源:MBD开发 作者:dingxu 2023-05-18 15:27 次阅读

Makefile 简介

makefile文件最常用的作用是,告诉make程序,如何来编译以及连接程序,最终生成可执行的二进制文件。

Makefile 最基础的规则格式如下:

Target … : Prerequisites

Recipe

Target 是目标文件,通常就是程序编译最终所要产生的文件,比如二进制可执行文件,或者是obj文件

Prerequisite是先决条件,也就是生成目标文件所需的输入文件,一个目标文件通常依赖于多个文件

Recipe是执行命令。可以有多个执行命令,可以在同一行,也可以分成多行。特别要注意的是,每个Recipe之前都要加Tab

通常Recipe中会包含Prerequisite,并且任意一个Prerequisite文件有更改时,都会重新生成Target文件

生成目标hex文件的流程

makefile最广泛的应用就是把一堆代码文件编译成二进制的可执行文件,比如hex文件。

图片

首先把所有的.c文件以及其包含的.h文件,分别都编译为.o文件。

然后把所有生成的.o文件与link文件一起,生成.elf文件和.map文件

最后由.elf文件生成.hex文件

循序渐进的例子

1. 首先准备好编译器和demo代码

可以在hightec的官网上下载hightec的评估版软件

然后在HighTec中新建一个HelloSerial的Demo例子

图片

图片

创建完就会自动生成如下的.c和.h文件,这个就可以直接在Hightec中编译了。

图片

2. 最最直接的makefile

为了说明makefile是如何工作的,先从最简单的情况开始说明。

首先为了消除路径的影响,先把所有的.c和.h文件还有link文件(.ld文件)都放到同一个文件夹内,然后新建一个名叫makefile的文件

图片

下面我们看一下最最最直接的makefile长什么样,相信在任何一个项目中,都不会有人这么写makefile的,暂且叫他版本0

图片

红色的就是Target,所谓目标文件。比如我们在文件夹路径下的命令窗口输入MakeAll

图片

就是告诉make命令,想要生成all这个目标,然后all这个目标又依赖于HelloSerial_Demo.elf 这个Prerequisite 也就是先决条件

于是就要生成HelloSerial_Demo.elf,这时,HelloSerial_Demo.elf就变成了目标文件。

然后为了要生成HelloSerial_Demo.elf这个目标文件,就需要hello.o,system_tc27x.o,uart_init_poll.o,uart_poll.o,usr_sprintf.o这些Prerequisite

于是这些.o文件又变成了目标文件,需要对应的.c文件和其中包含的.h文件来生成

比如要生成hello.o 这个文件,就需要hello.c,led.huart_poll.h,system_tc2x.h,usr_sprintf.h 这些文件,由于这些文件已经在目录下面了,所以就可以直接用了。

然后还需要Recipe,也就是执行命令来生成目标文件,这边的执行命令就是"C:/HIGHTEC/toolchains/tricore/v4.6.6.1/bin/tricore-gcc"-c hello.c

其中"C:/HIGHTEC/toolchains/tricore/v4.6.6.1/bin/tricore-gcc"是调用HighTec安装的编译器,tricore-gcc.exe

图片

这个不是固定的,我们需要用哪个编译器来编译.c文件,那么这边就调用哪个编译器的.exe文件。

-c 表示编译源文件,但不进行link。这条指令就是告诉tricore-gcc.exe把hello.c这个文件编译成hello.o

类似的,把所有的.c文件都编译为.o文件后,就会进行link的操作,生成HelloSerial_Demo.elf这个文件。具体的执行命令如下:

"C:/HIGHTEC/toolchains/tricore/v4.6.6.1/bin/tricore-gcc" \\

-o "HelloSerial_Demo.elf" \\

-T"iROM.ld" \\

hello.o system_tc27x.o uart_init_poll.o uart_poll.ousr_sprintf.o \\

-Wl,--gc-sections -mcpu=tc27xx \\

-Wl,--no-warn-flags \\

-Wl,-Map="HelloSerial_Demo.map"

其中

"C:/HIGHTEC/toolchains/tricore/v4.6.6.1/bin/tricore-gcc" 是调用编译器

-o "HelloSerial_Demo.elf" 是指定输出文件的名字叫"HelloSerial_Demo.elf"

-T"iROM.ld" 是指定link文件

hello.o system_tc27x.o uart_init_poll.o uart_poll.o usr_sprintf.o 这些是要被link的.o文件

-Wl 由于这边是通过tricore-gcc 编译器驱动链接的指令,所以对链接的指令需要加前缀'-Wl',

--gc-sections -mcpu=tc27xx 表示section的分配是按照 tc27xx 这个系列的cpu来执行的

--no-warn-flags 表示不会产生警告信息

-Map="HelloSerial_Demo.map" 表示会生成一个map文件

""是换行符

3. makefile中的变量

在版本0中的makefile中,只用了makefile最基本的规则,这样的makefile具体做什么,看着是挺清楚的,但写起来很麻烦。

makefile中可以使用变量,这样,会看起来简单一些

比如对于版本0中的makefile,需要用到的.o文件定义了多次,编译器的路径也定义了多次,那我们就可以把这些做成一个变量,使用的时候就可以直接使用了。

定义变量的方法是:<变量名> = <内容>

使用变量的方法时:$(变量名)

我们把所有的.o文件对象和编译器路径定义为变量,得到如下的makefile版本1

图片

4. <-I "dir">指令

对于版本1的makefile还有个问题,就是每次要编译.c文件为.o文件时,需要把这个.c文件所用的.h文件,以及.h文件里用到的.h文件都要写出来。

这样写当然也有好处,就是任何.h文件有变化时,包含这个.h文件的.c文件也会重新编译一个新的.o文件。

但是如果有成百上千个.c文件时,把所有.c文件用到的.h文件都找出来,这个工作量也是巨大的。

所以可以用一种妥协的方法,就是无论编译哪个.c文件时,把所有的.h文件都包含进来,这样就不用管这个.c文件到底用到了哪些.h文件。

想要实现这点,那就可以在编译.c文件时,增加<-I "dir">这个指令

于是我们可以得到版本2的makefile,这样看起来又简单了一些。不过这种方法会有一个问题,就是.h文件变更时,不会导致对应的.c文件重新编译

这个问题其实也是可以解决的,后面引入依赖文件时,就可以 解决这个问题

图片

相比于之前的makefile,这里生成.o文件时的先决条件中就没有.h文件了,而是在执行命令中添加了包含.h文件夹的参数

  1. vpath指令

可能大家也发现,现在每个.c文件的编译命令都一样了,只有文件名不一样,这个是不是可以优化呢,当然是可以的

vpath指令可以在指定的目录下面找特定的文件,比如我要找在某个文件夹下面所有的.c文件,指令如下:

vpath %.c E:/c10_Workspace/complier_demo

这就表示需要的时候,make就会在E:/c10_Workspace/complier_demo找,看有没有make需要的.c文件,%.c 表示所有后缀名为.c的文件

利用vpath指令,我们就可以继续简化makefile,得到版本2的makefile。

这里把.c编译为.o的指令合并为一个指令了。所有需要的.o文件都可以在vpath指定的路径中找到对应的.c文件(这里是make的一个默认规则,.o和.c文件的名称是一致的),然后进行编译

图片

  1. 生成依赖文件 (.d)

我们可以让编译器在编译.c文件的时候,同时生成一个.d的依赖文件,在依赖文件中,会列出这个.c文件所包含的所有.h文件。

生成的.d文件如下,会把这个.d文件作为一个Traget,Prerequisite文件就是其对应的.c文件,以及.c文件所引用的所有头文件。

这样就把这个.c文件所生成的.d文件和这个.c文件引用的所有.h文件就关联起来了。

图片

当某一个.h文件有修改时,引用这个头文件的.c文件所生成的.d文件就会被认为要重新生成,再配合上对应生成规则,就可以保证当某一个.h文件修改时,包含这个.h的所有.c文件都会重新编译。

于是就得到了版本3的makefile

图片

首先增加了依赖文件的变量定义(DEPS),里面定义了所有的依赖文件。

其次在.c编译为.o文件时,增加了指令 -MMD -MP -MF,以及-MT。这些指令的作用都是生成.d的依赖文件,"(@:%.o=%.d)"是依赖文件的文件名,由于依赖文件和目标的.o文件是同名,只是后缀由.o变为.d,所以这边"(@:%.o=%.d)"的作用就是把目标.o文件的后缀改为.d,作为生成的依赖文件的文件名。

同时增加了目标文件.d对.c文件的规则,表示.d文件如果需要重新生成的话,就会按照这个规则,把.c文件重新编译一份.o文件。由于这边目标文件就是.d文件,所以这里就直接使用了"$@"来指定生成.d文件的名称。

最后增加了include的指令,把所有的生成的依赖文件都包含进来。include的作用就是把include的文件里的内容直接加到当前的makefile中。也就是增加.d文件和对应.c文件和.h文件的规则。保证了当.h文件被修改时,对应的.d也会被要求重新生成,从而去重新编译对应的.c文件,生成新的.o文件。

  1. 通过定义代码文件夹,自动推导出编译所需的文件

通过之前的步骤,一个功能比较完善的makefile就写完了。但还有一个问题,目前所有的.c和.h文件都在一个文件夹内,而且生成的.o和.d文件也都在同一个文件夹,这样很不利于管理。

我们把文件夹路径调整的跟实际更为贴切一些,如下图所示,.c分件分别在5个不同source_code的文件夹中,.h文件分别在6个不同的include_file中,makefile和link文件在complie_env中,编译完产生的map文件和elf文件,也在complie_env中,同时在编译时,complie_env中还会生成一个临时文件夹tmp,用来存放.o和.d文件。

图片

同时,手动定义.o文件和.d文件也很麻烦,我们也可以直接从给定的文件夹中寻找所有的.c文件,然后根据文件夹的相对路径推导出.o和.d文件

于是就有了版本4的makefile

图片

首先定义存放临时文件(.d和.o文件)的文件夹,makefile中都推荐使用相对路径,这样不管工程放在什么路径下面,只要文件夹结构不变就都能正常运行。"./"表示makefile当前文件夹的路径。

然后定义包含所有需要参与编译的.c文件的文件夹,这些文件里所有的.c文件都会参与编译,"../"表示makefile当前文件夹的上一层文件夹路径。

接着定义所有被引用的.h文件的文件夹。

再接着根据定义的.c文件的文件夹,找出这些文件夹里所有的.c文件。这里有两个指令可以解释一下,一个是 foreach,"foreach DIR, **(SRC_DIRS), **(wildcard (DIR)/*.c)"表示对SRC_DIRS中定义的所有文件夹做一个for循环,当前循环的文件夹名叫DIR。另一个是wildcard,"(wildcard $(DIR)/*.c"表示找到DIR这个文件夹下面所有的.c文件。这样就把SRC_DIRS中定义的所有的文件夹里所有的.c文件都找出来了,给到SRCS这个变量中。

有了所有.c文件后,就可以推导出所有的.o文件,由于.o文件的位置和.c文件不一样,所以具体推导.o文件的步骤就是将.c文件的文件夹路径去掉,只保留.c文件的文件名,然后加上将要存放.o文件的路径,以及把.c的后缀换成.o。这一些列的动作,都可以用"OBJS = **(patsubst %.c, {TMP_DIR}/%.o,(notdir **{SRCS}))"这个命令来实现。"nodir"就是去掉文件夹路径,"patsubst"是替换指令,这里是把所有.c文件替换为.o文件,并加上路径。

最后就是根据.o文件定义.d依赖文件,由于.d文件和.o文件都会放在临时文件夹中,所以只要简单的将.o文件的后缀.o换成.d就可以了。

由于.o文件和.d文件的位置发生了变化,执行语句的命令也要做修改,.o和.d的目标文件都加了路径,命令中也加了"-o",明确指定了编译后的.o文件存放的位置。

  1. 将makefile按功能分类

现在可以看到,由于加了代码文件夹和头文件文件夹的定义,makefile文件变长了不少,如果是大型项目,那么代码文件夹和头文件夹的数目会更多。为了更好的维护makefile,我们可以按照功能多定义几个makefile,然后在主makefile中include这些makefile,这样使得层次更加清晰,也更好维护。

比如可以新建一个source.mk的文件,里面定义想要生成的二进制文件的名称,c代码文件夹,头文件文件夹,临时文件夹,推导出所有的.c,.o,.d文件

图片

还可以再建一个congif.mk的文件,里面定义跟编译器相关的一些设置,比如编译器的路径,删除命令的定义,编译时的参数等

图片

这样我们的主makefile又变得简洁了,而且这个文件基本就可以不用修改了。如果编译设置要修改的话,就修改config.mk,如果需要编译代码的内容有修改的话,就去修改source.mk。维护起来就很清晰了,美滋滋~

图片

  1. 删除编译产生的文件

我们有时不但需要编译文件,也需要删除之前编译的文件,这个也可以在makefile中完成。

我们可以添加clean功能,在clean为目标的情况下,执行删除命令,删除.o文件,.d文件,elf文件和map文件

图片

这边介绍一下PHONY功能,因为all和clean作为Target时,这两个命令并不是真正的文件名,也就是并不是要生成名为all或者clean的文件。但是万一有文件名叫all或者clean的文件话,就会产生歧义,这时就可以用.PHONY来定义all和clean,告诉make,这两个是命令,而不是文件,防止发生其想不到的问题。

另外,由于执行clean命令时,仅仅是想删除文件,并不需要去推导依赖文件,所以这边使用ifneq来判断,如果命令是clean的话,就不包含依赖文件了。

同时,也增加了rebuild功能,如果执行make rebuild的话,就会先clean,删除之前的文件,然后再生成目标文件。

  1. 调用makefile

如果makefile的名字就叫makefile或Makefile,那么可以在cmd的命令框中,将路径切换到makefile的路径,直接输入make all,进行编译,或者make clean,删除之前编译的文件。

但makefile的名字也不一定非要这么叫,我们可以任意起名字,调用的时候只要增加参数 "-f" 然后加上文件名就可以了,比如:

make -f makefile_ver5 all,或者make -f makefile_ver5 clean

另外,还有一个参数比较使用的就是"-j"加数字,比如

make all -j8

数字可以根据自己电脑cpu的线程数进行调整,这个表示可以多个线程并行处理命令,也就是可以同时编译多个.c文件,这样可以提高编译速度。

另外如果嫌每次要打开cmd窗口比较麻烦的话,也可以建一个build.bat的批处理文件,这样想要编译时,就直接双击这个文件就可以了。

Cmd /k的作用是运行完仍然保留cmd窗口,这样如果有错误的话就能看到了。

build.bat文件的内容如下:

图片

后记

这篇文将主要是介绍makefile的规则和大概的运行原理,知道这些之后,大家可以根据实际需要来自己写makefile,或者把之前项目中的makefile按自己的理解优化。

关于make的参数和详细说明可以参考"make.pdf",关于编译器的参数详细说明可以参考“tricore-gcc.pdf”,关于链接的参数详细说明,可以参考“tricore-ld.pdf”。这些都在百度网盘里,代码和makefile也在里面。感兴趣的可以下下来看看。或者根据项目实际的情况查阅对应的make,编译和link的对应文档。

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 单片机
    +关注

    关注

    6001

    文章

    43978

    浏览量

    620862
  • HEX文件
    +关注

    关注

    0

    文章

    21

    浏览量

    12668
  • 编译器
    +关注

    关注

    1

    文章

    1577

    浏览量

    48621
  • Makefile
    +关注

    关注

    1

    文章

    122

    浏览量

    19094
收藏 人收藏

    评论

    相关推荐

    Makefile】C文件包含的头文件修改,但不重新编译

    【Linux + MakefileMakefile的高阶用法:解决C文件包含的头文件修改了,但C文件不重新
    的头像 发表于 09-08 08:53 4215次阅读
    【<b class='flag-5'>Makefile</b>】C<b class='flag-5'>文件</b>包含的头<b class='flag-5'>文件</b>修改,但不重新<b class='flag-5'>编译</b>?

    如何编译多个源文件代码

    嵌入式Linux开发工具-工程管理器 make1.什么是工程/项目?(项目:多个源文件、资源文件构成的项目代码)2.如何编译多个源文件
    发表于 12-21 08:11

    如何写Makefile编译汇编和C文件

    经常在一个项目中包含多个.c源文件,而且一个.c源文件包含了一堆的头文件,这种情况下如何编写makefile,使得能成功编译整个项目?本博文
    发表于 11-24 09:03 1.3w次阅读
    如何写<b class='flag-5'>Makefile</b><b class='flag-5'>编译</b>汇编和C<b class='flag-5'>文件</b>

    windows平台下makefile操作教程

    Makefile一个工程中的源文件不计其数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定,哪些文件需要先编译
    发表于 11-24 17:14 1.1w次阅读
    windows平台下<b class='flag-5'>makefile</b>操作教程

    在Linux下实现进度条程序,通过makefile进行编译

    1. 在Linux下实现进度条程序。 通过makefile进行编译。 建议自主完成一个彩色的进度条。 写Makefile文件的原因:Makefil
    发表于 03-12 16:31 1827次阅读

    Linux0.11-Makefile 文件

    这个 Makefile 文件的主要作用是指示 make 程序最终使用独立编译连接成的 tools/目录中的 build 执行程序将所有内核编译代码
    发表于 05-15 14:30 550次阅读
    Linux0.11-<b class='flag-5'>Makefile</b> <b class='flag-5'>文件</b>

    虚拟机:Linux的Makefile使用for编译多个目标文件

    假如,有很多文件,每个文件都要变成一个单独的目标文件,如果使用makefile的话,最好能用一个 for 循环来做。
    的头像 发表于 06-22 17:40 4142次阅读
    虚拟机:Linux的<b class='flag-5'>Makefile</b>使用for<b class='flag-5'>编译</b>多个目标<b class='flag-5'>文件</b>

    Linux内核的Makefile、Kconfig和.config文件

    Linux内核源码文件繁多,搞不清Makefile、Kconfig、.config间的关系,不了解内核编译体系,编译修改内核有问题无从下手,自己写的驱动不知道怎么编进内核,不知道怎么配
    的头像 发表于 11-12 17:31 2744次阅读

    ARM代码编译链接的工作流程

    编译过程编译过程就是把源代码编译生成目标代码的过程。而采用ARM编译命令,可以将源
    的头像 发表于 12-22 16:57 1903次阅读

    芯片设计中的Makefile简单教程

    Makefile可以根据指定的依赖规则和文件是否有修改来执行命令。常用来编译软件源代码,只需要重新编译修改过的
    的头像 发表于 12-24 17:41 607次阅读

    一个STM32编译Makefile模板

    一个STM32编译Makefile模板
    发表于 11-13 20:06 10次下载
    一个STM32<b class='flag-5'>编译</b><b class='flag-5'>Makefile</b>模板

    交叉编译链下的Makefile(STM32F4xx)

    文章围绕makefile文件的编写方式,向读者讲述如何在ubuntu平台上用交叉编译链 arm-none-eabi- 编译出 STM32F4xx 系列 MCU 的执行
    发表于 12-04 12:36 7次下载
    交叉<b class='flag-5'>编译</b>链下的<b class='flag-5'>Makefile</b>(STM32F4xx)

    什么是Makefile

    如果您有多个 c、c++ 和其他语言的文件,并且想通过终端命令编译它们,我们该如何编译他们呢?为了解决这类问题,Makefile就出现了。
    的头像 发表于 02-17 10:41 3460次阅读
    什么是<b class='flag-5'>Makefile</b>?

    Makefile文件的编写规则及实例

    Makefile带来直接好处就是——“自动化编译”。一旦写好,只需要一个make命令,整个工程完全自动编译,所以十分方便。而Makefile文件
    的头像 发表于 05-19 14:52 2834次阅读

    Makefile可以做什么?Makefile的基本格式

    Makefile可以根据指定的依赖规则和文件是否有修改来执行命令。常用来编译软件源代码,只需要重新编译修改过的
    的头像 发表于 01-25 11:18 235次阅读