4. Makefile构建工具

在Linux C/C++开发中,当项目规模较小时,可通过GCC/G++命令直接编译源代码; 但当项目包含多个源文件、多个模块,且存在复杂的依赖关系时,手动输入编译命令会变得繁琐且易出错。 此时,就需要借助构建工具来自动化完成编译、链接、清理等操作,提升开发效率。

Makefile是Linux下最经典、最常用的构建工具配置文件,配合make命令使用,可定义编译规则、管理文件依赖、实现自动化构建。 无论是小型项目还是大型工程,Makefile都是必备的工具。

本章将详细讲解Makefile的核心知识,包括其简介、基础语法、多文件编译实操、自动化构建技巧,帮助学习者熟练掌握Makefile的编写与使用,解决多文件项目的构建难题。

本章的示例代码目录为:base_linux/makefile/

4.1. Makefile简介

Makefile是一种用于定义项目构建规则的文本文件,其核心作用是“指定文件依赖关系”和“定义编译命令”,让make工具能够自动完成项目的构建与更新。 Makefile的设计理念是“按需编译”,即只重新编译被修改过的源文件及其依赖的文件,避免重复编译所有文件,节省编译时间,尤其适合大型项目。

4.1.1. Makefile的核心优势

相比手动输入GCC/G++命令,使用Makefile具有以下核心优势,也是其成为Linux开发必备工具的原因:

  • 自动化构建:只需编写一次Makefile,后续执行make命令即可自动完成所有编译、链接操作,无需手动输入繁琐的GCC命令,避免遗漏文件或输入错误。

  • 按需编译:make工具会自动检测源文件和目标文件的修改时间,仅重新编译被修改过的文件及其依赖的文件,大幅节省编译时间,尤其大型项目,效果明显。

  • 管理依赖关系:可清晰定义源文件、头文件、目标文件之间的依赖关系,当依赖文件被修改时,依赖该头文件的源文件会被自动重新编译。

  • 统一构建标准:一个项目的Makefile可被所有开发者共享,确保所有开发者使用相同的编译规则、编译选项,避免因编译环境、编译参数不同导致的程序运行异常。

  • 支持多目标构建:可在Makefile中定义多个目标,如生成可执行程序、清理编译产物、生成静态库/动态库,通过make 目标名即可执行对应的操作,灵活适配不同需求。

4.1.2. Makefile的适用场景

Makefile适用于所有包含多文件、存在依赖关系的C/C++项目,尤其适合以下场景:

  • 项目包含多个源文件(.c、.cpp)和头文件(.h),手动编译繁琐;

  • 源文件之间存在复杂的依赖关系,如A文件依赖B文件,B文件依赖C头文件;

  • 需要统一编译参数、优化选项,确保所有开发者的编译环境一致;

  • 项目需要频繁修改、编译,需要节省重复编译的时间;

  • 需要生成静态库、动态库,或实现多版本构建。

4.1.3. 安装Makefile

LubanCat-RK系列板卡出厂并没有自带Makefile工具,需要我们自行下载安装:

1
sudo apt update && sudo apt install make

4.2. make命令

make和Makefile它们的关系如下图所示:

未找到图片

4.2.1. make命令的基础使用

make工具的使用非常简单,核心命令只有几个,配合Makefile即可完成构建操作,常用命令如下:

  • make:默认执行Makefile中的第一个目标,自动完成编译、链接操作;

  • make 目标名:执行Makefile中指定的目标,如make clean清理编译产物,make lib生成静态库;

  • make -f 文件名:若Makefile的文件名不是默认的Makefile,使用该命令指定配置文件,如make -f myMakefile;

  • make clean:清理编译生成的中间文件(.o文件)和可执行程序,恢复项目初始状态;

  • make -j N:多线程编译,N为线程数(如make -j4表示4线程编译),可大幅提升编译速度;

  • make -n:模拟执行编译操作,不实际生成文件,用于检查Makefile的语法是否正确、编译命令是否符合预期。

4.3. Makefile使用示例

4.3.1. Makefile小实验

为了直观地演示Makefile的作用,我们使用一个示例进行讲解,首先使用编辑器创建一个名为“Makefile”的文件, 输入如下代码并保存,其中使用“#”开头的行是注释,自己做实验时可以不输入, 另外要注意在“ls -lh”、”touch test.txt”等命令前要使用Tab键,不能使用空格代替。

base_linux/makefile/test1/Makefile
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#Makefile格式
#目标:依赖的文件或其它目标
#Tab 命令1
#Tab 命令2
#第一个目标,是最终目标及make的默认目标
#目标a,依赖于目标targetc和targetb
#目标要执行的shell命令 ls -lh,列出目录下的内容
targeta: targetc targetb
     ls -lh
#目标b,无依赖
#目标要执行的shell命令,使用touch创建test.txt文件
targetb:
     touch test.txt
#目标c,无依赖
#目标要执行的shell命令,pwd显示当前路径
targetc:
     pwd
#目标d,无依赖
#由于abc目标都不依赖于目标d,所以直接make时目标d不会被执行
#可以使用make targetd命令执行
targetd:
     rm -f test.txt

这个Makefile文件主要是定义了四个目标操作,先大致了解它们的关系:

  • targeta:这是Makefile中的第一个目标代号,在符号“:”后面的内容表示它依赖于targetc和targetb目标,它自身的命令为“ls -lh”,列出当前目录下的内容。

  • targetb:这个目标没有依赖其它内容,它要执行的命令为“touch test.txt”,即创建一个test.txt文件。

  • targetc:这个目标同样也没有依赖其它内容,它要执行的命令为“pwd”,就是简单地显示当前的路径。

  • targetd:这个目标无依赖其它内容,它要执行的命令为“rm -f test.txt”,删除 目录下的test.txt文件。与targetb、c不同的是,没有任何其它目标依赖于targetd,而且 它不是默认目标。

下面使用这个Makefile执行各种make命令,对比不同make命令的输出,可以清楚地了解Makefile的机制。 在主机Makefile所在的目录执行如下命令:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#在主机上Makefile所在的目录执行如下命令
#查看当前目录的内容
ls

#执行make命令,make会在当前目录下搜索“Makefile”或“makefile”,并执行
make

#可看到make命令后的输出,它执行了Makefile中编写的命令

#查看执行make命令后的目录内容,多了test.txt文件
ls

#执行Makefile的targetd目标,并查看,少了test.txt文件
make targetd
ls

#执行Makefile的targetb目标,并查看,又生成了test.txt文件
make targetb
ls

#执行Makefile的targetc目标
make targetc
未找到图片02|

上图中包含的原理说明如下:

make命令:

  • 在终端上执行make命令时,make会在当前目录下搜索名为“Makefile”或“makefile”的文件,然后根据该文件的规则解析执行。 如果要指定其它文件作为输入规则,可以通过“-f”参数指定输入文件,如“make -f 文件名”。

  • 此处make命令读取我们的Makefile文件后,发现targeta是Makefile的第一个目标,它会被当成默认目标执行。

  • 又由于targeta依赖于targetc和targetb目标,所以在执行targeta自身的命令之前,会先去完成targetc和targetb。

  • targetc的命令为pwd,显示了当前的路径。

  • targetb的命令为touch test.txt ,创建了test.txt文件。

  • 最后执行targeta自身的命令ls -lh ,列出当前目录的内容,可看到多了一个test.txt文件。

make targetd 、make targetb、make targetc命令:

  • 由于targetd不是默认目标,且不被其它任何目标依赖,所以直接make的时候targetd并没有被执行,想要单独执行Makefile中的某个目标,可以使用 make 目标名 的语法, 例如上图中分别执行了 make targetdmake targetbmake targetc 指令,在执行 make targetd 目标时,可看到它的命令 rm -f test.txt 被执行,test.txt文件被删除。

从这个过程,可了解到make程序会根据Makefile中描述的目标与依赖关系,执行达成目标需要的shell命令。简单来说,Makefile就是用来指导make程序如何干某些事情的清单。

4.3.2. 编译程序对比

4.3.2.1. 使用GCC编译多个文件

接着我们使用Makefile来控制程序的编译,为方便说明,先把前面章节 的hello.c程序分开成三个文件来写,分别为hello_main.c主文件,hello_func.c函数文 件,hello_func.h头文件,其内容如下代码所示,

base_linux/makefile/test2/hello_main.c
1
2
3
4
5
6
7
#include "hello_func.h"

int main()
{
    hello_func();
    return 0;
}
base_linux/makefile/test2/hello_func.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include <stdio.h>
#include "hello_func.h"

void hello_func(void)
{
    printf("hello, world! This is a C program.\n");
    for (int i=0; i<10; i++ ) {
        printf("output i=%d\n",i);
    }
}
base_linux/makefile/test2/hello_func.h
1
void hello_func(void);

也就是说hello_main.c的main主函数调用了hello_func.c文件的打 印函数,而打印函数在hello_func.h文件中声明,在复杂的工程中这是常见的程序结构。

如果我们直接使用GCC进行编译,需要使用如下命令:

1
2
3
4
5
6
#在主机上示例代码目录执行如下命令
#注意最后的"-I ."包含名点"."
gcc -o hello_main hello_main.c hello_func.c -I .

#运行生成的hello_main程序
./hello_main
未找到图片03|

相对于基础的hello.c编译命令,此处主要是增加了输入的文件 数量,如“hello_main.c”、“hello_func.c”,另外新增的“-I .”是告诉编译器头文件路径 让它在编译时可以在“.”(当前目录)寻找头文件,其实不加”-I .”选项也是能正常编译通过的, 此处只是为了后面演示Makefile的相关变量。

4.3.2.2. 使用Makefile编译

可以想像到,只要把gcc的编译命令按格式写入到Makefile,就能直接 使用make编译,而不需要每次手动直接敲gcc编译命令。

操作如下使用编辑器在hello_main.c所在的目录新建一个名为“Makefile”的文件,并 输入如下内容并保存。

base_linux/makefile/test2/Makefile
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#Makefile格式
#目标:依赖
#Tab 命令1
#Tab 命令2
#默认目标
#hello_main依赖于hello_main.c和hello_func.c文件
hello_main: hello_main.c hello_func.c
	gcc -o hello_main hello_main.c hello_func.c -I .


#clean目标,用来删除编译生成的文件
clean:
	rm -f *.o hello_main

该文件定义了默认目标hello_main用于编译程序,clean目标用于删除 编译生成的文件。特别地,其中hello_main目标名与gcc编译生成的文件名”gcc -o hello_main”设置成一致了,也就是说,此处的目标hello_main在Makefile看来,已经是 一个目标文件hello_main。

这样的好处是make每次执行的时候,会检查hello_main文件和依赖 文件hello_main.c、hello_func.c的修改日期,如果依赖文件的修改日期比hello_main文件的 日期新,那么make会执行目标其下的Shell命令更新hello_main文件,否则不会执行。

请运行以下命令进行实验:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#在主机上Makefile所在的目录执行如下命令
#若之前有编译生成hello_main程序,先删除

rm hello_main
ls

#使用make根据Makefile编译程序
make
ls

#执行生成的hello_main程序
./hello_main

#再次make,会提示hello_main文件已是最新
make

#使用touch命令更新一下hello_func.c的时间
touch hello_func.c

#再次make,由于hello_func.c比hello_main新,所以会再编译
make
ls
未找到图片04|

如上图所示,有了Makefile后,我们实际上只需要执行一下make命令就可以完成整个编译流程。

图中还演示了make会对目标文件和依赖进行更新检查,当依赖文件有改动时,才会再次执行命令更新目标文件。

4.4. Makefile基础语法

Makefile的语法规则简洁但严谨,核心由“目标-依赖-命令”三部分组成,同时支持变量、注释、模式匹配等功能。

4.4.1. 目标与依赖

Makefile的最基本单元是“规则”,每一条规则用于定义一个目标的构建方式,核心结构如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
[目标1][依赖]

   [命令1]

   [命令2]

[目标2][依赖]

   [命令1]

   [命令2]
  • 目标:指make要做的事情,可以是一个简单的代号,也可以是目标文件,需要顶格书写, 前面不能有空格或Tab。一个Makefile可以有多个目标,写在最前面的第一个目标, 会被Make程序确立为“默认目标”,例如前面的targeta、hello_main。

  • 依赖:要达成目标需要依赖的某些文件或其它目标。例如前面的targeta依赖于targetb和targetc, 又如在编译的例子中,hello_main依赖于hello_main.c、hello_func.c源文件, 若这些文件更新了会重新进行编译。

  • 命令1,命令2…命令n:make达成目标所需要的命令。只有当目标不存在或依赖文件的修改时间比目标文件还要新时,才会执行命令。 要特别注意命令的开头要用“Tab”键,不能使用空格代替,有的编辑器会把Tab键自动转换成空格导致出错,若出现这种情况请检查自己的编辑器配置。

4.4.2. 伪目标

伪目标不是具体的文件,而是一个操作指令,用于执行特定操作,如清理、全量编译、生成文档等。 若不声明伪目标,当项目目录中存在与伪目标同名的文件时,make工具会误认为该文件是目标,导致伪目标无法执行。

前面我们在Makefile中编写的目标,在make看来其实都是目标文件,例如make在执行的时候由于在目录找不到targeta文件, 所以每次make targeta的时候,它都会去执行targeta的命令,期待执行后能得到名为targeta的同名文件。 如果目录下真的有targeta、targetb、targetc的文件,即目标文件和依赖文件都存在且是最新的,那么make targeta就不会被正常执行了,这会引起误会。

为了避免这种情况,Makefile使用“.PHONY”前缀来区分目标代号和目标文件,并且这种目标代号被称为“伪目标”, phony单词翻译过来本身就是假的意思。

也就是说,只要我们不期待生成目标文件,就应该把它定义成伪目标,前面的演示代码修改如下。

  • 在test2的Makefile上添加.PHONY

base_linux/makefile/test3/Makefile
1
2
3
4
5
6
7
8
#默认目标
#hello_main依赖于hello_main.c和hello_func.c文件
hello_main: hello_main.c hello_func.c
	gcc -o hello_main hello_main.c hello_func.c -I .
#clean伪目标,用来删除编译生成的文件
.PHONY:clean
clean:
	rm -f *.o hello_main

GNU组织发布的软件工程代码的Makefile,常常会有类似以上代码中定义的clean伪目标,用于清除编译的输出文件。 常见的还有“all”、“install”、“print”、“tar”等分别用于编译所有内容、安装已编译好的程序、列出被修改的文件及打包成tar文件。 虽然并没有固定的要求伪目标必须用这些名字,但可以参考这些习惯来编写自己的Makefile。

如果以上代码中不写“.PHONY:clean”语句,并且在目录下创建一个名为clean的文件,那么当执行“make clean”时,clean的命令并不会被执行。

4.4.3. 默认规则

在前面“GCC编译过程”章节中提到整个编译过程包含如下图中的步骤,make在执行时也是使用同样的流程,不过在Makefile的实际应用中,通常会把编译和最终的链接过程分开。

未找到图片05|

也就是说,我们的hello_main目标文件本质上并不是依赖hello_main.c和hello_func.c文件,而是依赖于hello_main.o和hello_func.o, 把这两个文件链接起来就能得到我们最终想要的hello_main目标文件。另外,由于make有一条默认规则, 当找不到xxx.o文件时,会查找目录下的同名xxx.c文件进行编译。 根据这样的规则,我们可把Makefile改修改如下。

base_linux/makefile/test4/Makefile
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#Makefile格式
#目标文件:依赖的文件
#Tab 命令1
#Tab 命令2

hello_main: hello_main.o hello_func.o
	gcc -o hello_main hello_main.o hello_func.o
	
#以下是make的默认规则,下面两行可以不写
#hello_main.o: hello_main.c
# gcc -c hello_main.c

#以下是make的默认规则,下面两行可以不写
#hello_func.o: hello_func.c
# gcc -c hello_func.c

以上代码的第6~7行把依赖文件由C文件改成了.o文件,gcc编译命令也做了相应的修改。 第10~15行分别是hello_main.o文件和hello_func.o文件的依赖和编译命令, 不过由于C编译成同名的.o文件是make的默认规则,所以这部分内容通常不会写上去。

使用修改后的Makefile编译结果如下图所示。

未找到图片06|

从make的输出可看到,它先执行了两条额外的“cc”编译命令,这是由make默认规则执 行的,它们把C代码编译生成了同名的.o文件,然后make根据Makefile的命令链接这两 个文件得到最终目标文件hello_main。

4.4.4. 变量

使用C自动编译成*.o的默认规则有个缺陷,由于没有显式地表示*.o依赖于.h头文件, 假如我们修改了头文件的内容,那么*.o并不会更新,这是不可接受的。并且默认规则使用固定的“cc”进行编译, 假如我们想使用ARM-GCC进行交叉编译,那么系统默认的“cc”会导致编译错误。

要解决这些问题并且让Makefile变得更加通用,需要引入变量和分支进行处理。

4.4.4.1. 基本语法

在Makefile中的变量,有点像 C语言的宏定义,在引用变量的地方使用变量值进行替换。 变量的命名可以包含字符、数字、下划线,区分大小写,定义变量的方式有以下四种:

  • “=” :延时赋值,该变量只有在调用的时候,才会被赋值

  • “:=” :直接赋值,与延时赋值相反,使用直接赋值的话,变量的值定义时就已经确定了。

  • “?=” :若变量的值为空,则进行赋值,通常用于设置默认值。

  • “+=” :追加赋值,可以往变量后面增加新的内容。

当我们想使用变量时,其语法如下:

1
$(变量名)

下面通过一个实验来讲解这四种定义方式,对于后两种赋值方式 比较简单,主要思考延时赋值和直接赋值的差异,实验代码如下所示。

base_linux/makefile/test5/Makefile
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
VAR_A = FILEA

VAR_B = $(VAR_A)
VAR_C := $(VAR_A)

VAR_A += FILEB

VAR_D ?= FILED

.PHONY:check
check:
	@echo "VAR_A:"$(VAR_A)
	@echo "VAR_B:"$(VAR_B)
	@echo "VAR_C:"$(VAR_C)
	@echo "VAR_D:"$(VAR_D)

这里主要关心VAR_B和VAR_C的赋值方式,实验结果如下图所示。执行完make命令 后,只有VAR_C是FILEA。这是因为VAR_B采用的延时赋值,只有当调用时,才会进行 赋值。当调用VAR_B时,VAR_A的值已经被修改为FILEA FILEB,因此VAR_B的变量值也就等于FILEA FILEB。

未找到图片07|

4.4.4.2. 改造默认规则

接下来使用变量对前面hello_main的Makefile进行大改造,如下所示。

base_linux/makefile/test6/Makefile
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#定义变量
CC=gcc
CFLAGS=-I.
DEPS = hello_func.h

#目标文件
hello_main: hello_main.o hello_func.o
	$(CC) -o hello_main hello_main.o hello_func.o

#*.o文件的生成规则
%.o: %.c $(DEPS)
	$(CC) -c -o $@ $< $(CFLAGS)

#伪目标
.PHONY: clean
clean:
	rm -f *.o hello_main
  • 代码的1~4行:分别定义了CC、CFLAGS、DEPS变量,变量的值就是等号右侧的内容, 定义好的变量可通过”$(变量名)”的形式引用, 如后面的”$(CC)”、”$( CFLAGS)”、”$(DEPS)”等价于定义时赋予的变量值”gcc”、”-I.”和”hello_func.h”。

  • 代码的第8行:使用$(CC)替代了gcc,这样编写的Makefile非常容易更换不同的编译器, 如要进行交叉编译,只要把开头的编译器名字修改掉即可。

  • 代码的第11行:”%”是一个通配符,功能类似”*”,如”%.o”表示所有以”.o”结尾的文件。 所以”%.o:%.c”在本例子中等价于”hello_main.o: hello_main.c”、”hello_func.o:hello_func.c”, 即等价于o文件依赖于c文件的默认规则。不过这行代码后面的”$(DEPS)”表示它除了依赖c文件, 还依赖于变量”$(DEPS)”表示的头文件,所以当头文件修改的话,o文件也会被重新编译。

  • 代码的第12行:这行代码出现了特殊的变量”$@”,”$<”,可理解为Makefile文件保留的关键字, 是系统保留的自动化变量,”$@”代表了目标文件,”$<”代表了第一个依赖文件。 即”$@”表示”%.o”,”$<”表示”%.c”,所以,当第11行的”%”匹配的字符为”hello_func”的话,第12行代码等价于:

1
2
3
4
#当"%"匹配的字符为"hello_func"时
$(CC) -c -o $@ $< $(CFLAGS)
#等价于:
gcc -c -o hello_func.o func_func.c -I .

也就是说makefile可以利用变量及自动化变量,来重写.o文件的默认生成规则,以及增加头文件的依赖。

4.4.4.3. 改造链接规则

与*.o文件的默认规则类似,我们也可以使用变量来修改生成最终目标 文件的链接规则,具体参考如下代码。

base_linux/makefile/test7/Makefile
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#定义变量
TARGET = hello_main
CC = gcc
CFLAGS = -I.
DEPS = hello_func.h
OBJS = hello_main.o hello_func.o

#目标文件
$(TARGET): $(OBJS)
	$(CC) -o $@ $^ $(CFLAGS)

#*.o文件的生成规则
%.o: %.c $(DEPS)
	$(CC) -c -o $@ $< $(CFLAGS)

#伪目标
.PHONY: clean
clean:
	rm -f *.o hello_main

这部分说明如下:

  • 代码的第2行:定义了TARGET变量,它的值为目标文件名hello_main。

  • 代码的第6行:定义了OBJS变量,它的值为依赖的各个o文件,如hello_main.o、hello_func.o文件。

  • 代码的第9行:使用TARGET和OBJS变量替换原来固定的内容。

  • 代码的第10行:使用自动化变量“$@”表示目标文件“$(TARGET)”,使用自动化变量“$^”表示所有的依赖文件即“$(OBJS)”。

也就是说以上代码中的Makefile把编译及链接的过程都通过变量表示出来了,非常通用。 使用这样的Makefile可以针对不同的工程直接修改变量的内容就可以使用。

4.4.4.4. 其它自动化变量

Makefile中还有其它自动化变量,此处仅列出方便以后使用到的时候进行查阅,见下表。

自动化变量:

符号

意义

$@

匹配目标文件

$%

与$@类似,但$%仅匹配“库”类型的目标文件

$<

依赖中的第一个目标文件

$^

所有的依赖目标,如果依赖中有重复的,只保留一份

$+

所有的依赖目标,即使依赖中有重复的也原样保留

$?

所有比目标要新的依赖目标

4.4.5. 函数

在更复杂的工程中,头文件、源文件可能会放在二级目录,编译生成的*.o或可执行文件也放到专门的编译输出目录方便整理, 如下图所示。示例中*.h头文件放在includes目录下,*.c文件放在sources目录下,编译输出存放在build中。

实现这些复杂的操作通常需要使用Makefile的函数。

未找到图片09|

4.4.5.1. 函数格式及示例

在Makefile中调用函数的方法跟变量的使用 类似,以“$()”或“${}”符号包含函数名和参数,具体语法如下:

1
2
3
4
5
$(函数名 参数)

#或者

${函数名 参数}

下面以常用的notdir、patsubst、wildcard函数为例进行讲解,并且示例中都是我们后面Makefile中使用到的内容。

4.4.5.2. notdir函数

notdir函数用于去除文件路径中的目录部分,它的格式如下:

1
$(notdir 文件名)

例如输入参数“./sources/hello_func.c”,函数执行后 的输出为“hell_func.c”,也就是说它会把输入中的“./sources/”路径部分去掉,保留文件名。使用范例如下:

1
2
#以下是范例
$(notdir ./sources/hello_func.c)

#上面的函数执行后会把路径中的“./sources/”部分去掉,输出为:hello_func.c

4.4.5.3. wildcard函数

wildcard函数用于获取文件列表,并使用空格分隔开,格式如下:

1
$(wildcard 匹配规则)

例如函数调用 $(wildcard *.c) ,函数执行后会把当前目录的所有c文件列出。使用范例如下:

1
2
3
4
5
6
7
#sources目录下有hello_func.c、hello_main.c、test.c文件

#执行如下函数
$(wildcard sources/*.c)

#函数的输出为:
sources/hello_func.c sources/hello_main.c sources/test.c

4.4.5.4. patsubst函数

patsubst函数功能为模式字符串替换,格式如下:

1
$(patsubst 匹配规则, 替换规则, 输入的字符串)

当输入的字符串符合匹配规则,那么使用替换规则来替换字符串,当匹配规则中有“%”号时, 替换规则也可以例程“%”号来提取“%”匹配的内容加入到最后替换的字符串中。使用范例如下:

1
2
3
4
5
6
7
8
#hello_main.c匹配%.c规则,得到%为hello_main,替换build_dir/%.o
$(patsubst %.c, build_dir/%.o, hello_main.c )

#函数的输出为:
build_dir/hello_main.o

#hello_main.xxx无法匹配%.c规则,因此不进行替换,函数无输出
$(patsubst %.c, build_dir/%.o, hello_main.xxx )

第一个函数调用中,由于“hello_main.c”符合“%.c”的匹配规则(%在Makefile中的类似于*通配符),而且“%”从“hello_main.c”中提取出了“hello_main”字符,把这部分内容放到替换规则“build_dir/%.o”的“%”号中,所以最终的输出为”build_di r/hello_main.o”。

第二个函数调用中,由于由于“hello_main.xxx”不符合“%.c”的匹配规则,“.xxx”与“.c”对不上,所以不会进行替换,函数直接返回空的内容。

4.4.5.5. 多级结构工程的Makefile

接下来我们使用上面三个函数修改我们的Makefile,以适应包含多级目录的工程,修改后的内容如下所示。

创建多级结构工程如下:

1
2
3
4
5
6
7
.
├── includes
│   └── hello_func.h
├── Makefile
└── sources
   ├── hello_func.c
   └── hello_main.c

其中:

includes/hello_func.h内容如下:

base_linux/makefile/test8/includes/hello_func.h
1
void hello_func(void);

sources/hello_func.c内容如下:

base_linux/makefile/test8/sources/hello_func.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include <stdio.h>
#include "hello_func.h"

void hello_func(void)
{
    printf("hello, world! This is a C program.\n");
    for (int i=0; i<10; i++ ) {
        printf("output i=%d\n",i);
    }
}

sources/hello_main.c内容如下:

base_linux/makefile/test8/sources/hello_main.c
1
2
3
4
5
6
7
#include "hello_func.h"

int main()
{
    hello_func();
    return 0;
}

Makefile内容如下:

base_linux/makefile/test8/Makefile
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#定义变量
TARGET = hello_main
#存放中间文件的路径
BUILD_DIR = build
#存放源文件的文件夹
SRC_DIR = sources
#存放头文件的文件夹
INC_DIR = includes .
#源文件
SRCS = $(wildcard $(SRC_DIR)/*.c)
#目标文件(*.o)
OBJS = $(patsubst %.c, $(BUILD_DIR)/%.o, $(notdir $(SRCS)))
#头文件
DEPS = $(wildcard $(INC_DIR)/*.h)
#指定头文件的路径
CFLAGS = $(patsubst %, -I%, $(INC_DIR))

#目标文件
$(BUILD_DIR)/$(TARGET): $(OBJS)
	$(CC) -o $@ $^ $(CFLAGS)

#*.o文件的生成规则
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c $(DEPS)

#创建一个编译目录,用于存放过程文件
#命令前带“@”,表示不在终端上输出
	@mkdir -p $(BUILD_DIR)
	$(CC) -c -o $@ $< $(CFLAGS)

#伪目标
.PHONY: clean cleanall

#删除输出文件夹
clean:
	rm -rf $(BUILD_DIR)
      
#全部删除
cleanall:
	rm -rf $(BUILD_DIR)

具体可以直接参考配套示例代码“test8”中的内容。修改后的Makefile文件分析如下:

  • 代码的4~8行:定义了变量BULID_DIR、SRC_DIR、INC_DIR分别赋值为工程的编译输出路径build、 源文件路径sources以及头文件路径includes和当前目录“.”。

  • 代码的第10行:定义了变量SRCS用于存储所有需要编译的源文件,它的值为wildcard函 数的输出,本例子中该函数的输出为“sources/hello_func.c sources/hello_main.c sources/test.c”。

  • 代码的第12行:定义了OBJS变量用于存储所有要生成的的.o文件,它的值为patsubst函数 的输出,本例子中该函数是把所有c文件名替换为同名的.o文件,并添加build目录,即函数的输 出为”build/hello_func.o build /hello_main.o build /test.o”。

  • 代码的第14行:与SRCS变量类似,定义一个DEPS变量存储所有依赖的头文件,它的值为wildcard函 数的输出,本例子中该函数的输出为“includes/hello_func.h ”。

  • 代码的第16行:定义了CFLAGS变量,用于存储包含的头文件路径,它的值为patsubst函数的 输出,本例子中该函数是把includes目录添加到“-I”后面,函数的输出为“-Iincludes”。

  • 代码的第19行:相对于之前的Makefile,我们在$(TARGET)前增加了$(BUILD_DIR)路径,使得最终的可执行程序放在build目录下。

  • 代码的第23行:与上面类似,给.o目标文件添加$(BUILD_DIR)路径。

  • 代码的第27行:在执行编译前先创建build目录,以存放后面的.o文件,命令前的“@”表示执行该命令时不在终端上输出。

  • 代码的第34行:rm删除命令也被修改成直接删除编译目录$(BUILD_DIR)。

  • 代码的38-39行:增加了删除所有架构编译目录的伪目标cleanall。

使用该Makefile时,直接在Makefile的目录执行make即可:

1
2
3
4
5
6
#使用tree命令查看目录结构
#若提示找不到命令,使用 sudo apt install tree安装
tree

#编译
make

如下图:

未找到图片10|

4.4.5.6. 分支

为方便直接切换GCC编译器,我们还可以使用条件分支增加切换编译器的功能。在Makefile中的条件分支语法如下:

1
2
3
4
5
ifeq(arg1, arg2)
分支1
else
分支2
endif

分支会比较括号内的参数“arg1”和“arg2”的值是否相同,如果相同,则为真,执行分支1的内容,否则的话,执行分支2的内容, 参数arg1和arg2可以是变量或者是常量。

使用分支切换GCC编译器的Makefile如下所示:

base_linux/makefile/test9/Makefile
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#定义变量
#ARCH默认为x86,使用gcc编译器,
#否则使用aarch64-gcc编译器
ARCH ?= x86
TARGET = hello_main

#存放中间文件的路径
BUILD_DIR = build_$(ARCH)
#存放源文件的文件夹
SRC_DIR = sources
#存放头文件的文件夹
INC_DIR = includes .

#源文件
SRCS = $(wildcard $(SRC_DIR)/*.c)
#目标文件(*.o)
OBJS = $(patsubst %.c, $(BUILD_DIR)/%.o, $(notdir $(SRCS)))
#头文件
DEPS = $(wildcard $(INC_DIR)/*.h)

#指定头文件的路径
CFLAGS = $(patsubst %, -I%, $(INC_DIR))

#根据输入的ARCH变量来选择编译器
#ARCH=x86,使用gcc
#ARCH=arm64,使用aarch64-gcc
ifeq ($(ARCH),x86)
CC = gcc
else
CC = aarch64-linux-gnu-gcc
endif

#目标文件
$(BUILD_DIR)/$(TARGET): $(OBJS)
	$(CC) -o $@ $^ $(CFLAGS)

#*.o文件的生成规则
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c $(DEPS)
#创建一个编译目录,用于存放过程文件
#命令前带“@”,表示不在终端上输出
	@mkdir -p $(BUILD_DIR)
	$(CC) -c -o $@ $< $(CFLAGS)

#伪目标
.PHONY: clean cleanall

#按架构删除
clean:
	rm -rf $(BUILD_DIR)

#全部删除
cleanall:
	rm -rf build_*

注意这个Makefile文件需要配合前面test8的工程结构,否则即使Makefile写对了也会因为目录对不上而编译错误。 具体可以直接参考配套示例代码“test9”中的内容。修改后的Makefile文件分析如下:

  • 代码的4行:增加了ARCH的赋值,若ARCH的值为空,则进行赋值为X86

  • 代码的第8行:增加了不同编译方式的输出结果的文件夹

  • 代码的27~31行:增加了gcc编译器的选择

  • 代码的第49行:选择删除不同编译器的输出

  • 代码的第53行:把所有输出结果都删除

使用该Makefile时,直接在Makefile的目录执行make即可:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#使用tree命令查看目录结构
#若提示找不到命令,使用 sudo apt install tree安装
tree

#清除编译输出,确保不受之前的编译输出影响
make cleanall

#使用ARM64平台
make ARCH=ARM64

#信息输出如下
aarch64-linux-gnu-gcc -c -o build_ARM64/hello_func.o sources/hello_func.c  -Iincludes  -I.
aarch64-linux-gnu-gcc -c -o build_ARM64/hello_main.o sources/hello_main.c  -Iincludes  -I.
aarch64-linux-gnu-gcc -o build_ARM64/hello_main build_ARM64/hello_func.o build_ARM64/hello_main.o  -Iincludes  -I.

#清除编译输出
make cleanall

#默认是x86平台
make

#信息输出如下
gcc -c -o build_x86/hello_func.o sources/hello_func.c  -Iincludes  -I.
gcc -c -o build_x86/hello_main.o sources/hello_main.c  -Iincludes  -I.
gcc -o build_x86/hello_main build_x86/hello_func.o build_x86/hello_main.o  -Iincludes  -I.

4.5. 自动化构建

自动化构建是Makefile的核心优势之一,通过一些进阶技巧,可进一步简化Makefile编写、提升构建效率, 实现“一键构建、一键清理、一键安装”,适配大型项目的开发需求。

4.5.1. 自动获取所有源文件与头文件

在多文件、多目录项目中,手动列出所有源文件和头文件非常繁琐,且容易遗漏。 通过前面介绍的wildcard函数和路径匹配,可自动获取指定目录下的所有源文件和头文件,实现文件列表的自动化更新。

常用示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 自动获取src目录下所有.c和.cpp源文件
SRCS_C = $(wildcard $(SRC_DIR)/*.c)
SRCS_CPP = $(wildcard $(SRC_DIR)/*.cpp)
SRCS = $(SRCS_C) $(SRCS_CPP)

# 自动获取include目录下所有.h头文件
HDRS = $(wildcard $(INC_DIR)/*.h)

# 自动生成中间文件列表
OBJS = $(patsubst $(SRC_DIR)/%.c, $(OBJ_DIR)/%.o, $(SRCS_C))
OBJS += $(patsubst $(SRC_DIR)/%.cpp, $(OBJ_DIR)/%.o, $(SRCS_CPP))

4.5.2. 多版本构建

实际开发中,通常需要调试版和发布版两种版本的程序,通过自定义变量和目标,可实现多版本的自动化构建。

多版本构建示例:

base_linux/makefile/test10/Makefile
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
SRC_DIR = sources
INC_DIR = includes
# 调试版中间文件目录
BUILD_DIR_DEBUG = build_debug
# 发布版中间文件目录
BUILD_DIR_RELEASE = build_release

# 编译器
CC = gcc
# 调试版编译选项:-g生成调试信息,-Wall显示所有警告,指定头文件目录
CFLAGS_DEBUG = -Wall -g -I$(INC_DIR) -MMD
# 发布版编译选项:-O2代码优化无调试信息,-Wall显示所有警告
CFLAGS_RELEASE = -Wall -O2 -I$(INC_DIR) -MMD

# 自动获取所有源文件
SRCS = $(wildcard $(SRC_DIR)/*.c)
# 调试版中间文件列表
OBJS_DEBUG = $(patsubst $(SRC_DIR)/%.c, $(BUILD_DIR_DEBUG)/%.o, $(SRCS))
# 发布版中间文件列表
OBJS_RELEASE = $(patsubst $(SRC_DIR)/%.c, $(BUILD_DIR_RELEASE)/%.o, $(SRCS))
# 调试版依赖文件列表
DEPS_DEBUG = $(patsubst $(BUILD_DIR_DEBUG)/%.o, $(BUILD_DIR_DEBUG)/%.d, $(OBJS_DEBUG))
# 发布版依赖文件列表
DEPS_RELEASE = $(patsubst $(BUILD_DIR_RELEASE)/%.o, $(BUILD_DIR_RELEASE)/%.d, $(OBJS_RELEASE))

# 声明伪目标
.PHONY: all debug release clean clean_debug clean_release

# 默认目标:同时构建调试版和发布版
all: debug release

# 调试版目标:可执行程序名加后缀_debug,便于区分
debug: TARGET = hello_main_debug
debug: $(OBJS_DEBUG)
	@mkdir -p $(BUILD_DIR_DEBUG)
	$(CC) $(OBJS_DEBUG) -o $(TARGET)

# 发布版目标:可执行程序名加后缀_release
release: TARGET = hello_main_release
release: $(OBJS_RELEASE)
	@mkdir -p $(BUILD_DIR_RELEASE)
	$(CC) $(OBJS_RELEASE) -o $(TARGET)

# 调试版中间文件编译规则
$(BUILD_DIR_DEBUG)/%.o: $(SRC_DIR)/%.c
	@mkdir -p $(BUILD_DIR_DEBUG)
	$(CC) $(CFLAGS_DEBUG) -c $< -o $@

# 发布版中间文件编译规则
$(BUILD_DIR_RELEASE)/%.o: $(SRC_DIR)/%.c
	@mkdir -p $(BUILD_DIR_RELEASE)
	$(CC) $(CFLAGS_RELEASE) -c $< -o $@

# 加载两个版本的依赖文件
-include $(DEPS_DEBUG)
-include $(DEPS_RELEASE)

# 清理所有版本产物
clean: clean_debug clean_release

# 单独清理调试版
clean_debug:
	rm -rf $(BUILD_DIR_DEBUG) hello_main_debug

# 单独清理发布版
clean_release:
	rm -rf $(BUILD_DIR_RELEASE) hello_main_release

执行说明:

  • 版本区分:调试版可执行程序名为hello_main_debug,中间文件存放在build_debug;发布版为hello_main_release,中间文件存放在build_release,互不干扰;

  • 编译选项差异:调试版添加-g,生成调试信息,发布版添加-O2,二级代码优化,提升运行效率,无调试信息;

  • 执行命令:make debug构建调试版、make release构建发布版、make all同时构建两个版本、make clean_debug单独清理调试版;

编译并对比:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#进行编译
make
#信息输出如下
gcc -Wall -g -Iincludes -MMD -c sources/hello_func.c -o build_debug/hello_func.o
gcc -Wall -g -Iincludes -MMD -c sources/hello_main.c -o build_debug/hello_main.o
gcc  build_debug/hello_func.o  build_debug/hello_main.o -o hello_main_debug
gcc -Wall -O2 -Iincludes -MMD -c sources/hello_func.c -o build_release/hello_func.o
gcc -Wall -O2 -Iincludes -MMD -c sources/hello_main.c -o build_release/hello_main.o
gcc  build_release/hello_func.o  build_release/hello_main.o -o hello_main_release

#查看大小差异
ls -lh hello_main_debug hello_main_release
#信息输出如下
-rwxrwxr-x 1 guest guest  12K Mar  4 02:06 hello_main_debug
-rwxrwxr-x 1 guest guest 8.3K Mar  4 02:06 hello_main_release

#用readelf检查debug版本的调试段
readelf -S hello_main_debug | grep -i debug
#信息输出如下
f -S hello_main_release | grep -i debug  [26] .debug_aranges    PROGBITS         0000000000000000  00001039
[27] .debug_info       PROGBITS         0000000000000000  00001099
[28] .debug_abbrev     PROGBITS         0000000000000000  00001423
[29] .debug_line       PROGBITS         0000000000000000  00001552
[30] .debug_str        PROGBITS         0000000000000000  0000168c

#用readelf检查release版本的调试段
readelf -S hello_main_release | grep -i debug
#无信息输出

4.5.3. 静态库与动态库的自动化构建

在大型项目中,通常会将一些通用功能,如工具函数、常用模块封装为静态库(.a文件)或动态库(.so文件),便于复用和维护。

4.5.3.1. 静态库构建

静态库是将多个.o文件打包生成的归档文件,编译时会被直接链接到可执行程序中,优点是可执行程序无需依赖外部库,缺点是体积较大。

base_linux/makefile/test11/Makefile
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# 基础目录定义
SRC_DIR := sources
INC_DIR := includes
OBJ_DIR := build_obj
LIB_DIR := build_lib

# 编译器和编译选项
CC := gcc
CFLAGS := -Wall -g -I$(INC_DIR)

# 明确指定所有文件
LIB_SRC := $(wildcard $(SRC_DIR)/hello_func.c)
MAIN_SRC := $(wildcard $(SRC_DIR)/hello_main.c)
LIB_OBJ := $(patsubst $(SRC_DIR)/%.c, $(OBJ_DIR)/%.o, $(LIB_SRC))
MAIN_OBJ := $(patsubst $(SRC_DIR)/%.c, $(OBJ_DIR)/%.o, $(MAIN_SRC))

STATIC_LIB := $(LIB_DIR)/libhello.a
TARGET := hello_main

# 伪目标声明
.PHONY: all clean

# 默认目标
all: $(TARGET)

# 创建目录
$(OBJ_DIR) $(LIB_DIR):
	mkdir -p $@

# 编译静态库的.o 文件
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c | $(OBJ_DIR)
	$(CC) $(CFLAGS) -c $< -o $@

# 打包生成静态库
$(STATIC_LIB): $(LIB_OBJ) | $(LIB_DIR)
	ar rcs $@ $^

# 编译链接生成可执行程序
$(TARGET): $(MAIN_OBJ) $(STATIC_LIB)
	$(CC) $(MAIN_OBJ) -o $@ -L$(LIB_DIR) -lhello $(CFLAGS)

# 清理命令
clean:
	rm -rf $(OBJ_DIR) $(LIB_DIR) $(TARGET)

ar rcs是生成静态库的核心命令,ar是归档工具,rcs是选项,用于创建、替换和生成索引。

编译得到build_lib/libhello.a,具体用途如下:

  1. 用于编译和链接

在编译链接阶段,其代码已经被复制并合并到了hello_main可执行文件内部, 一旦hello_main生成完毕,libhello.a对程序的运行不再需要。

1
2
3
$(CC) $< -o $@ -L$(LIB_DIR) -lhello $(CFLAGS)

# -L$(LIB_DIR) -lhello告诉编译器去build_lib找libhello.a

如果没有这个库,编译器就找不到hello_func函数的实现,链接会报错undefined reference。

  1. 代码模块化与封装

管理方便:如果项目有100个.c 文件,生成100个.o 文件会很乱,打包成一个.a文件,管理更清晰。 隐藏实现:发布软件时,可以只给头文件(.h)和静态库(.a),不给源码(.c),别人可以用你的功能,但看不到代码逻辑。

  1. 链接优化

如果libhello.a里有10个函数,但hello_main只调用了 1 个,链接器通常只会把那1个函数的代码拷进可执行文件,从而减小最终程序的体积,相比之下,动态库通常是整个加载。

4.5.3.2. 动态库构建

动态库是在程序运行时才被加载的库文件,编译时不会被链接到可执行程序中,优点是可执行程序体积小, 多个程序可共享一个动态库,缺点是程序运行时需要依赖动态库。

base_linux/makefile/test12/Makefile
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# 基础目录定义
SRC_DIR = sources
INC_DIR = includes
OBJ_DIR = build_obj
LIB_DIR = build_lib

# 编译器和编译选项
CC = gcc
# 编译选项:-fPIC动态库必需,位置无关代码
CFLAGS = -Wall -g -I$(INC_DIR) -fPIC

# 明确指定所有文件
SRCS_LIB = $(wildcard $(SRC_DIR)/hello_func.c)
SRCS_MAIN = $(SRC_DIR)/hello_main.c
OBJS_LIB = $(patsubst $(SRC_DIR)/%.c, $(OBJ_DIR)/%.o, $(SRCS_LIB))

LIB_DYNAMIC = libhello.so
LIB_DYNAMIC_PATH = $(LIB_DIR)/$(LIB_DYNAMIC)

TARGET = hello_main

# 伪目标声明
.PHONY: all lib_dynamic main clean

# 默认目标
all: lib_dynamic main

# 动态库目标:使用gcc -shared生成动态库
lib_dynamic: $(OBJS_LIB)
	@mkdir -p $(OBJ_DIR)
	@mkdir -p $(LIB_DIR)
# -shared:生成动态库
	$(CC) -shared -o $(LIB_DYNAMIC_PATH) $(OBJS_LIB)

# 生成可执行程序,链接动态库
main: $(SRCS_MAIN) $(LIB_DYNAMIC_PATH)
	$(CC) $(SRCS_MAIN) -o $(TARGET) -L$(LIB_DIR) -lhello $(CFLAGS)

# 编译动态库所需的中间文件
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c
	@mkdir -p $(OBJ_DIR)
	$(CC) $(CFLAGS) -c $< -o $@

# 清理目标
clean:
	rm -rf $(OBJ_DIR) $(LIB_DIR) $(TARGET)

-fPIC编译.o时必需,生成位置无关代码,使库可以被加载到内存的任意位置,供多个程序共享。

-shared是链接.so时必需,告诉GCC不要生成可执行文件,而是生成一个共享库文件(.so)。

编译并测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#编译
make

#查看编译生成的hello_main所需的动态库
ldd hello_main
#信息输出如下,可以看到默认找不到libhello.so动态库
linux-vdso.so.1 (0x00007fff573ed000)
libhello.so => not found
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fb024869000)
/lib64/ld-linux-x86-64.so.2 (0x00007fb024e5c000)

#直接运行测试
./hello_main
#信息输出如下,找不到libhello.so动态库,无法运行
./hello_main: error while loading shared libraries: libhello.so: cannot open shared object file: No such file or directory

#临时指定库路径运行
export LD_LIBRARY_PATH=$(pwd)/build_lib:$LD_LIBRARY_PATH
./hello_main
#信息输出如下,可以正常运行
hello, world! This is a C program.
output i=0
output i=1
output i=2
output i=3
output i=4
output i=5
output i=6
output i=7
output i=8
output i=9

#再次查看hello_main所需的动态库,能找到libhello.so位置
linux-vdso.so.1 (0x00007fff135d7000)
libhello.so => /home/guest/lubancat_rk_code_storage/base_linux/makefile/test12/build_lib/libhello.so (0x00007efd6f8aa000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007efd6f4b9000)
/lib64/ld-linux-x86-64.so.2 (0x00007efd6fcae000)

与前面test11项目生成的程序大小对比如下:

1
2
3
4
5
6
7
8
9
#查看大小
ls -lh test11/hello_main
#信息输出如下
-rwxrwxr-x 1 guest guest 12K Mar  4 03:02 test11/hello_main

#查看大小
ls -lh test12/hello_main
#信息输出如下
-rwxrwxr-x 1 guest guest 9.1K Mar  4 02:52 test12/hello_main

可以看到test12使用动态库的方式要比test11正常编译的要小,如果是大项目,此对比将更明显。

4.5.4. 自动化安装与卸载

当项目编译完成后,可通过Makefile的install目标,将可执行程序、库文件、头文件自动安装到系统指定目录,便于系统全局调用; 通过uninstall目标,可自动卸载已安装的文件。

在test12项目基础上进行修改,添加安装和卸载功能,修改如下:

base_linux/makefile/test13/Makefile
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# 基础目录定义
SRC_DIR = sources
INC_DIR = includes
OBJ_DIR = build_obj
LIB_DIR = build_lib

# 编译器和编译选项
CC = gcc
# 编译选项:-fPIC动态库必需,位置无关代码
CFLAGS = -Wall -g -I$(INC_DIR) -fPIC

# 安装配置,PREFIX是Linux标准变量,默认为/usr/local
PREFIX ?= /usr/local
INSTALL_BIN = $(PREFIX)/bin
INSTALL_LIB = $(PREFIX)/lib
INSTALL_INC = $(PREFIX)/include

# 明确指定所有文件
SRCS_LIB = $(wildcard $(SRC_DIR)/hello_func.c)
SRCS_MAIN = $(SRC_DIR)/hello_main.c
OBJS_LIB = $(patsubst $(SRC_DIR)/%.c, $(OBJ_DIR)/%.o, $(SRCS_LIB))

LIB_DYNAMIC = libhello.so
LIB_DYNAMIC_PATH = $(LIB_DIR)/$(LIB_DYNAMIC)

TARGET = hello_main

# 伪目标声明
.PHONY: all lib_dynamic main clean install uninstall

# 默认目标
all: lib_dynamic main

# 动态库目标:使用gcc -shared生成动态库
lib_dynamic: $(OBJS_LIB)
	@mkdir -p $(OBJ_DIR)
	@mkdir -p $(LIB_DIR)
# -shared:生成动态库
	$(CC) -shared -o $(LIB_DYNAMIC_PATH) $(OBJS_LIB)

# 生成可执行程序,链接动态库
main: $(SRCS_MAIN) $(LIB_DYNAMIC_PATH)
	$(CC) $(SRCS_MAIN) -o $(TARGET) -L$(LIB_DIR) -lhello $(CFLAGS)

# 编译动态库所需的中间文件
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c
	@mkdir -p $(OBJ_DIR)
	$(CC) $(CFLAGS) -c $< -o $@


# 安装目标,需要sudo权限
install: all
	@echo "Installing to $(PREFIX)..."
	# 1. 安装可执行程序,权限755
	sudo install -m 755 $(TARGET) $(INSTALL_BIN)/
	# 2. 安装动态库,权限755
	sudo install -m 755 $(LIB_DYNAMIC_PATH) $(INSTALL_LIB)/
	# 3. 安装头文件,权限644
	sudo install -m 644 $(INC_DIR)/hello_func.h $(INSTALL_INC)/
	# 4. 刷新动态库缓存
	sudo ldconfig
	@echo "Installation complete."


# 卸载目标,需要sudo权限
uninstall:
	@echo "Uninstalling from $(PREFIX)..."
	# -f 表示文件不存在也不报错
	sudo rm -f $(INSTALL_BIN)/$(TARGET)
	sudo rm -f $(INSTALL_LIB)/$(LIB_DYNAMIC)
	sudo rm -f $(INSTALL_INC)/hello_func.h
	# 刷新缓存
	sudo ldconfig
	@echo "Uninstallation complete."

clean:
	rm -rf $(OBJ_DIR) $(LIB_DIR) $(TARGET)

执行说明:

  • 执行sudo make install:需要管理员权限,将可执行程序、头文件、库文件安装到系统目录;

  • 执行sudo make uninstall:删除系统目录下安装的文件,彻底卸载项目;

  • sudo ldconfig:用于刷新动态库缓存,安装动态库后,必须运行ldconfig,它会更新/etc/ld.so.cache,让系统知道有了新库。

编译并测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#编译
make

#安装
sudo make install

#程序被复制到 /usr/local/bin/hello_main
#库被复制到 /usr/local/lib/libhello.so
#头文件被复制到 /usr/local/include/hello_func.h

#直接运行,如果成功输出,说明动态库路径配置正确
hello_main

#检查库依赖
ldd $(which hello_main)

#信息输出如下,可以找到libhello.so路径
linux-vdso.so.1 (0x0000007f8702e000)
libhello.so => /usr/local/lib/libhello.so (0x0000007f86fb8000)
libc.so.6 => /lib/aarch64-linux-gnu/libc.so.6 (0x0000007f86e47000)
/lib/ld-linux-aarch64.so.1 (0x0000007f87000000)

#卸载
sudo make uninstall

#系统目录下的文件将被清除,再次运行hello_main会提示command not found