前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Makefile学习1

Makefile学习1

作者头像
用户9645905
发布2023-10-17 18:02:05
3060
发布2023-10-17 18:02:05
举报
文章被收录于专栏:Linux学习~Linux学习~

Makefile学习1

Makefile简介

Makefile是在Linux环境下 C/C++ 程序开发必须要掌握的一个工程管理文件。当你使用make命令去编译一个工程项目时,make工具会首先到这个项目的根目录下去寻找Makefile文件,然后才能根据这个文件去编译程序。

linux下编写程序,因为早期没有成熟的IDE,一般都是使用不同的命令进行编译:将源文件分别使用编译器、汇编器、链接器编译成可执行文件,然后手动运行。

要将 .c源文件编译成可执行文件,一般需要预处理、编译、汇编、链接四个步骤,每个步骤会分别调用预处理器、编译器、汇编器、链接器来完成。

在Linux环境下,安装了GCC编译器,在程序的安装目录下面会有各种二进制可执行文件:

  • cpp:预处理器
  • ccl:编译器
  • as:汇编器
  • ld:链接器
  • ar:静态库制作工具

程序在编译过程中会分别使用这些工具,完成程序编译的每个流程。

为了简化程序编译流程,GCC编译器一般会提供一个gcc命令:

代码语言:javascript
复制
gcc -o a.out helloworld.c

gcc会分别调用预处理器、编译器、汇编器和链接器来自动完成程序编译的整个过程,不需要用户一个命令一个命令分别输入了。

gcc还提供了一些列参数,用来控制编译流程:

代码语言:javascript
复制
-E #进行预处理,不作编译
-S #只做汇编处理
-c #进行编译,不链接
-o #指定生成可执行程序名

对于大型项目使用gcc编译的话,每编译一次,都要敲进去几万个源文件,太折腾了,此时自动化编译工具make就派上用场了:使用make编译程序,不需要每次都输入源文件,直接在命令行下敲击make命令,就可一键自动化完成编译。

使用make命令编译程序之前,需要编写一个Makefile文件:

代码语言:javascript
复制
a.out: helloworld.o
    gcc -o a.out helloworld.o
helloworld.o: helloworld.c
    gcc -c -o helloworld.o helloworld.c
clean:
    rm -f a.out helloworld.o

makefile的文件名通常有三种格式:Makefile、makefile、GNUmakefile,make会在当前目录下自动寻找找三个文件名

如果同时存在执行makefile;如果没有找到的话,make就无法继续编译程序,产生一个错误并退出;如果名称自定的话,可以使用 -f 选项指定执行的文件

Makefile重要性

会不会写Makefile,是侧面可以看出一个人是否具有完成大型项目工程的能力。如果你是在Linux下进行C/C++开发,掌握Makefile可能让你更深地去理解项目,去掌控整个项目的构建和维护。

Makefile也是一个研究开源项目的利器。很多开源项目可能文档不完整,而Makefile就是开源项目的地图,从Makefile入手,可以让你快速窥探整个开源项目的框架和概貌,让你深入代码而不至于迷路。

掌握Makefile是一门必备技能,它和git、vim一样,掌握了这个“Linux三剑客”会让你的工作事半功倍、更加高效。

Makefile内容

简单的概括一下Makefile 中的内容,它主要包含有五个部分,分别是:

1) 显式规则

显式规则说明了,如何生成一个或多的的目标文件。这是由 Makefile 的书写者明显指出,要生成的文件,文件的依赖文件,生成的命令。

2) 隐晦规则

由于我们的 make 命名有自动推导的功能,所以隐晦的规则可以让我们比较粗糙地简略地书写 Makefile,这是由 make 命令所支持的。

3) 变量的定义

在 Makefile 中我们要定义一系列的变量,变量一般都是字符串,这个有点像C语言中的宏,当 Makefile 被执行时,其中的变量都会被扩展到相应的引用位置上。

4) 文件指示

其包括了三个部分,一个是在一个 Makefile 中引用另一个 Makefile,就像C语言中的 include 一样;另一个是指根据某些情况指定 Makefile 中的有效部分,就像C语言中的预编译 #if 一样;还有就是定义一个多行的命令。有关这一部分的内容,我会在后续的部分中讲述。

5) 注释

Makefile 中只有行注释,和 UNIX 的 Shell 脚本一样,其注释是用“#”字符,这个就像 C/C++ 中的“//”一样。如果你要在你的 Makefile 中使用“#”字符,可以用反斜框进行转义,如:“#”。

Makefile规则

规则

Makefile通过规则进行构建可执行文件编译所依赖的关系树

规则是Makefile的基本组成单元。一个规则通常由目标、目标依赖和命令三部分构成:

代码语言:javascript
复制
目标:目标依赖
    命令
a.out: hello.c
    gcc -o a.out hello.c

说明:

a.out就是我们要生成的目标,目标一般是一个可执行文件。

目标依赖是指生成这个可执行文件所依赖的源文件,如 hello.c。

而命令则是如何将这些目标依赖生成对应的目标,一般是gcc命令、链接命令、objcopy命令,一些shell命令等。

命令必须使用tab键进行缩进,否则Makefile就会报错。

有的规则可能无目标依赖,仅仅是为了实现某种操作,如下面的clean命令:

代码语言:javascript
复制
clean:
    rm -f a.out hello.o

使用make clean命令清理编译的文件时,会调用这个规则中的命令,不需要什么依赖,仅仅是执行删除操作,所以这个规则中并没有目标依赖。

规则中也可以没有命令,仅仅包含目标和目标依赖,仅仅用来描述一种依赖关系,但一个规则中一定要有一个目标。

默认目标

一个Makefile文件里通常会有多个目标,一般会选择第一个作为默认目标。

正常情况下,当你想编译生成a.out时,要使用make a.out命令。

Makefile文件中a.out是文件中的第一个目标,当我们在make编译时没有给make指定要生成的目标,make就会选择Makefile文件中的第一个目标作为默认目标。

多目标

一个规则中也可以有多个目标,多个目标具有相同的生成命令和依赖文件。

如一个目标文件%.o都是由其对应的源文件%.c编译生成的,生成命令也是相同的:

代码语言:javascript
复制
%.o: %.c
    gcc -o %.o %.c
多规则目标

多个规则可能是同一个目标,make在解析Makefile文件时,会将具有相同目标的规则的依赖文件合并。

伪目标

伪目标并不是一个真正的文件,可以看做是一个标签。

伪目标一般没有依赖关系,也不会生成对应的目标文件,可以无条件执行,纯粹是为了执行某一个命令。

伪目标可以在执行默认目标之前先执行。

代码语言:javascript
复制
.PHONY: clean
a.out: hello.o
    gcc -o a.out hello.o
hello.o: hello.c
    gcc -c -o hello.o hello.c
clean:
    rm -f a.out hello.o

Makefile目标依赖

make第一次编译某个项目时,会依次编译所有的源文件。

但是当我们修改程序后,再次使用make编译,make只编译你新添加或修改了的源文件。

make是根据时间戳来判断一个规则中的目标依赖文件是否有更新。

make在编译程序时,会依次检查依赖关系树中的所有源文件的时间戳,如果发现某个文件的时间戳有更新,会认为这个文件有改动过,会重新编译这个源文件。

但是还有一种情况:在Makefile的规则中,一般不会把头文件添加到目标依赖中。当一个.c文件中包含多个头文件时,如果对应的头文件发生了变化,因为头文件没有包含在依赖关系树中,所以这个.c文件就不会重新编译:

代码语言:javascript
复制
//hello.c
#include <stdio.h>
#include "module.h"
int main(void)
{
    printf("PI = %f\n", PI);
    func();
    printf("hello zhaixue.cc!\n");
    return 0;
}
//module.c
#include <stdio.h>
void func(void)
{
    printf("hello func!\n");
}
//module.h
#ifndef __MODULE_H__
#define __MODULE_H__
#define PI 3.14
#endif
代码语言:javascript
复制
//Makefile
.PHONY: clean
a.out: hello.o module.o
    gcc -o a.out hello.o module.o
hello.o: hello.c
    gcc -c -o hello.o hello.c
module.o: module.c
    gcc -c -o module.o module.c
clean:
    rm -f a.out hello.o

修改module.h中的宏定义PI值为3.1415,再次使用make编译程序,make并没有重新编译,因为module.h并没有添加到Makefile的规则依赖目标中,所以无论你怎么修改module.h,都不会重新编译helloworld.c源文件。

头文件依赖

其中一个解决方法是将头文件module.h添加到规则的目标依赖列表中:

代码语言:javascript
复制
//Makefile
.PHONY: clean
a.out: hello.o module.o module.h
    gcc -o a.out hello.o module.o

缺点:包含几十个头文件时,把包含的这些头文件都手工添加进去,工作量还是蛮大的。

自动生成头文件依赖关系

更高效的解决方法是:使用gcc -M 命令自动生成头文件依赖关系

通过gcc -M命令,我们就可以自动生成一个hello.o目标文件的依赖关系,就不需要我们手动将头文件添加到规则中了。

Makefile命令

命令一般由shell命令(echo、ls)和编译器的一些工具(gcc、ld、ar、objcopy等)组成,使用tab键缩进。

命令是make在编译程序时真正要执行的部分。对于规则中的每一个命令,make会开一个进程执行,每条命令执行完,make会监测每个命令的返回码。

若命令返回成功,make继续执行下一个命令;若命令执行出错,make会终止执行当前的规则,退出编译流程。

make每执行一条命令,会把当前的命令打印出来。

如果你不想在make编译的时候打印正在执行的执行,可以在每条命令的前面加一个@:

代码语言:javascript
复制
.PHONY: clean
a.out: hello.c
    @echo "start compiling..."
    @gcc -o a.out hello.c
    @echo "compile done"
clean:
    rm -f a.out hello.o

Makefile变量

变量定义和使用

可以在Makefile中定义一个变量val,使用使用 (val) 或 {val} 的形式去引用它。

可以定义一些变量,分别表示编译器名称、目标、目标依赖文件:

代码语言:javascript
复制
PHONY: clean
CC  = gcc
BIN = a.out
OBJS = hello.o module.o
$(BIN): $(OBJS)
    @echo "start compiling..."
    @echo $(CC)
    $(CC) -o $(BIN) $(OBJS)
    @echo "compile done"
hello.o: hello.c
    $(CC) -c -o hello.o hello.c
module.o: module.c
    $(CC) -c -o module.o module.c
clean:
    rm -f $(BIN) $(OBJS)

好处:便于维护makefile文件,例如当项目中需要添加更多的源文件时,你只需要更改OBJS的值就可以了。如果不使用变量的话,你得修改Makefile多处地方。

赋值

Makefile中的变量赋值有多种形式,比如:

  • 条件赋值:?=
  • 追加赋值:+=

条件赋值是指一个变量如果没有被定义过,就直接给它赋值;如果之前被定义过,那么这条赋值语句就什么都不做。

代码语言:javascript
复制
CC = gcc
CC ?= arm-linux-gnueabi-gcc #不执行
$(BIN): $(OBJS)
    @echo $(CC)
    $(CC) -o $(BIN) $(OBJS)

追加赋值是指一个变量,以前已经被赋值,现在想给它增加新的值,此时可以使用+=追加赋值。

代码语言:javascript
复制
OBJS = hello.o
OBJS += module.o
立即变量/延迟变量

立即变量和延迟变量是按展开时间来划分的。

立即变量使用 := 操作符进行赋值,在解析阶段就直接展开了,顾名思义,立即展开变量。

延迟变量则是使用 = 操作符进行赋值,在make解析Makefile阶段不会立即展开,而是等到实际使用这个变量时才展开,获得其真正的值。

代码语言:javascript
复制
a = 1
b = 2
val_a := $(a)
val_b  = $(b)
a = 10
b = 20
test:
    echo $(val_a)
    echo $(val_b)

解释:

val_a是立即变量,当make解析到:=赋值符号时,会把$(a)变量的值立即赋值给val_a,虽然后面a的值发生了变化,但val_a因为已经展开,所以值就不再发生变化。

而val_b则不同,因为是延迟展开变量,所以,当make解析到 = 符号时,并没有立即把(b)的值赋值给val_b,而是在运行echo命令时才对其展开,因为此时b的值已经是20,所以(val_b)的值是20。

应用:

立即展开变量一般用在规则中的目标、目标依赖中。make在解析Makefile阶段,需要这些变量有确切的值来构建依赖关系树。一个项目中的文件依赖关系在程序编译期间是固定不变的,因此需要立即变量在解析阶段就要有明确的值,立即展开。

延迟展开变量一般用在规则的命令行中,这些变量在make编译过程中被引用到才会展开,获得其实际的值。

自动变量

Makefile中,大家经常会见到类似

@、

^、$<这种类型的变量。

这种变量一般称为自动变量,自动变量是局部变量,作用域范围在当前的规则内,它们分别代表不同的含义:

  • $@:目标
  • $^:所有目标依赖
  • $<:目标依赖列表中的第一个依赖
  • $?:所有目标依赖中被修改过的文件
代码语言:javascript
复制
.PHONY: clean
CC  = gcc
BIN = a.out
OBJS = hello.o module.o
$(BIN): $(OBJS)
    @echo "start compiling..."
    @echo $(OBJS)
    $(CC) -o $@ $^
    @echo "compile done"
hello.o: hello.c
    $(CC) -c -o $@ $^
module.o: module.c
    $(CC) -c -o $@ $^
clean:
    rm -f $(BIN) $(OBJS)

还有一些自动变量不太常用,但是大家在以后阅读Makefile时可能会遇到,比如:

代码语言:javascript
复制
$%:当规则的目标是一个静态库文件时,$%代表静态库的一个成员名
$+:类似$^,但是保留了依赖文件中重复出现的文件
$*:在模式匹配和静态模式规则中,代表目标模式中%的部分。比如hello.c,当匹配模式为%.c时,$*表示hello
$(@D):表示目标文件的目录部分
$(@F):表示目标文件的文件名部分
$(*D):在模式匹配中,表示目标模式中%的目录部分
$(*F):在模式匹配中,表示目标模式中%的文件名部分
-: :告诉make在编译时忽略所有的错误
@: :告诉make在执行命令前不要显示命令
变量替换
字符串替换
代码语言:javascript
复制
.PHONY: all
SRC := main.c sub.c
OBJ := $(SRC:.c=.o)
all:
    @echo "SRC = $(SRC)"
    @echo "OBJ = $(OBJ)"
代码语言:javascript
复制
# make
SRC = main.c sub.c
OBJ = main.o sub.o
模式匹配替换

使用匹配符%匹配变量,使用 % 保留变量值中的指定字符串,然后其他部分使用指定字符串代替。

代码语言:javascript
复制
.PHONY: all
SRC := main.c sub.c
OBJ := $(SRC:%.c=%.o)
all:
    @echo "SRC = $(SRC)"
    @echo "OBJ = $(OBJ)"
环境变量

除了用户自定义的一些变量,make在解析Makefile中还会引入一些系统环境变量,如编译参数CFLAGS、SHELL、MAKE等。这

些变量在make开始运行时被载入到Makefile文件中,因为是全局性的系统环境变量,所以这些变量对所有的Makefile都有效。

Makefile中有用户自定义的同名变量,系统环境变量将会被用户自定义的变量覆盖。若用户在命令行中传递跟系统环境变量同名的变量,系统环境变量也会被传递的同名变量覆盖。

代码语言:javascript
复制
PHONY:all
CFLAGS = -g
all:
    @echo "CFLAGS = $(CFLAGS)"
    @echo "SHELL = $(SHELL)"
    @echo "MAKE = $(MAKE)"
    @echo "HOSTNAME = $(HOSTNAME)"
代码语言:javascript
复制
wit@pc:/home/makefile/demo# make HOSTNAME=zz.cc #命令行传入
CFLAGS = -g
SHELL = /bin/sh
MAKE = make
HOSTNAME = zz.cc

除此之外,我们还可以通过export命令给Makefile传递变量,在shell环境下使用export命令,就相当于将对应变量声明为系统环境变量

Override指示符

override的作用及使用:

在一个Makefile中使用define、:=、= 定义的变量,我们可以在执行make命令时重新指定这个变量的值。

如果不希望在命令行指定的变量值替代在Makefile中的原来定义,那么我们可以在Makefile中使用指示符 override 对这个变量进行声明:

代码语言:javascript
复制
.PHONY: all
override web = www.baidu.com
all:
    @echo "web = $(web)"

Makefile中的变量分为多种:追加变量、立即变量、展开变量、使用define定义的变量,它们都可以使用override修饰。

当一个追加变量在定义时使用了override,后续对它的值进行追加时,也需要使用带有override指示符的追加方式。否则对此变量值的追加不会有效。

代码语言:javascript
复制
.PHONY: all
override fruits = apple
override fruits += banana
all:
    @echo "fruits = $(fruits)"

override的存在目的:

为了使用户可以改变或者追加哪些使用make命令行指定的变量的定义。从另一个角度上看,就是实现了在Makefile中增加或者修改命令行参数的一种机制。

比如在编译程序时,无论在命令行指定什么参数,编译器在编译时必需打开 -Wall选项,那么在Makefile中的CFLAGS应该这样定义:

代码语言:javascript
复制
.PHONY: all
override CFLAGS += -Wall
all:
    @echo "CFLAGS = $(CFLAGS)"
代码语言:javascript
复制
# make
CFLAGS = -Wall
# make CFLAGS=-g
CFLAGS = -g -Wall

不使用override修饰:

代码语言:javascript
复制
# make
CFLAGS = -Wall
# make CFLAGS=-g
CFLAGS = -g

Makefile递归执行

在实际工程项目中,各个源文件通常存放在各个不同的目录中,make在编译工程项目时,会依次遍历各个不同的子目录,编译每个子目录下的源文件。

代码语言:javascript
复制
make -C subdir1 subdir2 subdir3 ...

上面的make 命令就等价于:

代码语言:javascript
复制
cd subdir1 && $(MAKE)
cd subdir2 && $(MAKE)
cd subdir3 && $(MAKE)

顶层Makefile:

代码语言:javascript
复制
.PHONY:all
all:
    @echo "make start"
    make -C subdir1
    make -C subdir2
    make -C subdir3
    @echo "make done"

make通过 -C subdir参数,会分别到各个子目录下去执行,解析各个子目录下的Makefile并运行,遍历完所有的子目录

make依次遍历到各个子目录下解析新的Makefile时,项目顶层目录的主Makefile定义的一些变量,如何传递到子目录的Makefile文件中:将对应变量使用export声明为环境变量

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2023-10-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Makefile学习1
    • Makefile简介
      • Makefile重要性
        • Makefile内容
          • Makefile规则
            • 规则
            • 默认目标
            • 多目标
            • 多规则目标
            • 伪目标
          • Makefile目标依赖
            • 头文件依赖
            • 自动生成头文件依赖关系
          • Makefile命令
            • Makefile变量
              • 变量定义和使用
              • 赋值
              • 立即变量/延迟变量
              • 自动变量
              • 变量替换
              • 环境变量
              • Override指示符
            • Makefile递归执行
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档