二、makefile进阶
1、makefile编译动态链接库
(1)动态链接库的概念:
①动态:动态链接库的函数不会把代码编译到二进制文件中,编译打包阶段只记录函数的地址,程序运行的时候才去磁盘加载库。
②链接:库文件和二进制程序分离,用某种特殊手段维护二者之间的关系。
③库:一堆编译好的函数、代码打包成的文件,不用重复编译的源码。Windows中的库文件后缀为.dll,Linux中的库文件后缀为.so。
(2)5个关键编译参数:
|---------|---------------------------------|----------------------------------------------------------|
| 参数 | 全称&作用 | 使用场景 |
| -fPIC | Position-Independent Code位置无关代码 | 生成不绑定固定内存地址的目标文件,动态库必须加;系统加载.so文件到任意内存地址都能正常运行,没有地址越界报错 |
| -shared | 生成共享动态库 | gcc指令加这个参数,编译产物从普通可执行文件变成.so动态库 |
| -l(小写L) | -lxxx:指定链接名为libxxx.so的动态库 | 例如-lpthread→链接系统libpthread.so,自动省略前缀lib和后缀.so |
| -I(大写i) | -I./inc:指定头文件搜索目录 | 默认只在当前目录找.h文件,如果头文件放在别的文件夹,就要用-I指明路径 |
| -L | -L./lib:指定库文件搜索目录 | 默认只在系统/usr/lib等系统路径找.so文件;自定义库放在项目lib文件夹,则必须用-L./lib指定目录 |
(3)Linux实操Makefile编译动态链接库示例:
①编译生成动态库:
bash
# 1. 先编译源码生成位置无关.o文件
func.o:func.c
gcc -c -fPIC func.c # func.o是位置无关代码,才能打包进动态库
# 2. 用-shared打包.o成动态库libfunc.so
libfunc.so:func.o
gcc -shared func.o -o libfunc.so # 告诉gcc,输出产物是共享库,不是exe
②编译主程序并链接自定义动态库:
bash
# -L./ :在当前目录搜索库文件libfunc.so
# -lfunc:链接libfunc.so(自动补全"lib"和".so")
# -I./inc:main.c的头文件在inc文件夹
main:main.c
gcc main.c -o main -L./ -lfunc -I./inc
③编译完成后,直接运行./main大概率会报错,因为Linux默认只去系统库目录找.so文件,对此有两种解决办法:
1临时方案:使用命令"export LD_LIBRARY_PATH=./:$LD_LIBRARY_PATH",把当前目录加入库文件搜索列表,不过命令只在当前终端会话生效。(如果当前目录有和系统库同名的文件,由于"./"写在前面,会优先加载当前目录的版本,可能带来风险)
2永久方案:把库文件放在系统库目录中,然后执行"sudo ldconfig",更新系统的动态连链接器缓存。
(4)动态链接库的优点:
①省空间:多个程序可以共用一个.so/dll文件,不用每个程序都内置一份代码。
②易升级:替换新版本库文件即可更新功能,不用重新编译所有调用它的程序。
③按需加载:程序运行时才载入内存,启动更快。
2、makefile编译静态链接库
(1)静态链接库的概念:
①静态:静态链接库的函数会把代码编译到二进制文件中,编译完成后库文件可以删除,程序运行的时候无需去磁盘加载库(不过这也使得程序体积更大)。
②链接:在编译链接阶段,即把库代码直接嵌入可执行文件。
③库:一堆编译好的函数、代码打包成的文件,不用重复编译的源码。Windows中的库文件后缀为.lib,Linux中的库文件后缀为.a。
(2)Linux实操Makefile编译静态链接库示例:
①编译生成静态库:
bash
# 1. 先把源码编译成目标文件(-c只编译不链接)
func.o: func.c
gcc -c func.c -o func.o
# 2. 用ar工具打包成静态库libfunc.a
libfunc.a: func.o
ar rcs libfunc.a func.o
# ar是Linux提供的静态库打包工具
# 参数r表示将目标文件插入到库中(替换已存在的同名文件)
# 参数c表示如果库不存在则创建
# 参数s表示生成库的索引,加速链接过程
②编译主程序并链接静态库:
bash
# 链接静态库libfunc.a,生成可执行文件main
# -L./ :在当前目录搜索库文件libfunc.a
# -lfunc:链接libfunc.a(自动补全"lib"和".a")
# -I./inc:main.c的头文件在inc文件夹
main: main.c libfunc.a
gcc main.c -o main -L./ -lfunc -I./inc
需要注意的是,Makefile里用-lfunc链接时,如果当前目录同时存在libfunc.so和libfunc.a,gcc默认优先链接动态库.so,不会用静态库.a,对此可以在"-lfunc"后面加" -static",强制链接静态库
3、makefile中通用部分复用
(1)Make提供内置关键字include,它的作用是读取并加载指定文件的内容,相当于把被包含文件里的所有变量、规则、宏直接"粘贴"到当前的Makefile里。
include <文件路径名(相对于当前Makefile所在目录)>
(2)通用部分复用举例:
①假设项目结构如下所示。

"../"表示上一级目录,也就是从当前Makefile所在目录往上跳一层,读取那里的makefile文件
②根目录的公共Makefile可以写所有子目录通用的配置,比如:
bash
# 根目录makefile(公共部分)
SOURCE=$(wildcard ./*.cpp ./*.c)
OBJ=$(patsubst %.cpp,%.o,$(SOURCE))
OBJ:=$(patsubst %.c,%.o,$(OBJ))
.PHONY:clean show
$(TARGET):$(OBJ)
$(CXX) $^ -o $@
clean:
$(RM) $(TARGET) $(OBJ)
show:
echo $(SOURCE)
echo $(OBJ)
③子目录的Makefile可以加载根目录公共Makefile的内容,比如:
bash
# src/Makefile
TARGET = main
include ../makefile # 自动继承上面的所有配置
(3)配置被覆盖的问题及解决方案:
①如果被包含的公共Makefile里定义了变量,当前Makefile里也定义了同名变量,那么同名变量的最终变量值,则取决于变量的定义顺序(比如公共Makefile在后面include,那么公共配置就会覆盖当前Makefile的同名配置)。
②为了避免这种情况带来麻烦,Makefile提供了条件编译指令ifndef & endif,它和C语言中的条件编译类似,实现的是"只有变量未定义时才执行某段代码"的逻辑。
ifndef VAR_NAME
如果变量VAR_NAME未被定义,就执行这里的代码
... 定义变量、写规则等
endif
③上例中的根目录公共Makefile优化:
bash
# 根目录makefile(公共部分)
SOURCE=$(wildcard ./*.cpp ./*.c)
OBJ=$(patsubst %.cpp,%.o,$(SOURCE))
OBJ:=$(patsubst %.c,%.o,$(OBJ))
.PHONY:clean show
ifndef TARGET
TARGET:=test
endif
ifndef LDLIBS
LDLIBS:=
endif
$(TARGET):$(OBJ)
#$(CXX) $^ -o $@
g++ $(LDLIBS) $^ -o $@
clean:
$(RM) $(TARGET) $(OBJ)
show:
echo $(SOURCE)
echo $(OBJ)
④上例中的子目录的Makefile优化:
bash
# src/Makefile
TARGET = main
LDLIBS:=-lstdc++
include ../makefile # 自动继承上面的所有配置
4、makefile中的三种赋值方式
(1)延迟赋值"=":定义时不立即计算,引用时才取变量的最终值,不管变量定义写在哪里。
bash
A = 1
B = $(A) # 这里只是记住了"引用A",不会立刻算成1
A = 2 # 后面又给A赋值为2
all:
@echo $(B) # 运行时才展开,取A的最终值2
(2)立即赋值":=":定义时就立即计算当前变量的值,后续变量再修改,也不会影响它。
cpp
A = 1
B := $(A) # 定义时就立即计算,此时A=1,所以B直接变成1
A = 2 # 后面修改A,不影响B的值
all:
@echo $(B) # B的最终值为1
(3)条件赋值"?=":只有变量当前未被定义(或被定义为空)时,才会赋值;如果已经有值,就不做任何修改。
bash
# 情况1:变量未定义
TARGET ?= test # 此时TARGET不存在,所以赋值为test
# 情况2:变量已定义
TARGET = main # 先定义了TARGET=main
TARGET ?= test # 因为TARGET已经有值,所以这行不会生效
all:
@echo $(TARGET) # 输出main
5、makefile中调用shell命令
(1)核心语法:
$(shell <命令>)
"$(shell)"是Make的内置函数,在Make解析阶段,能够调用后面的Shell命令,并把命令的标准输出结果赋值给前面的变量,它的执行时机是变量定义时,而不是执行目标命令时
(2)举例:
bash
FILE=abc # 定义了一个普通变量FILE,值为abc
A:=$(shell ls ../) # 执行Shell命令"ls ../",把上级目录的文件列表结果赋值给变量A
B:=$(shell pwd) # 执行pwd命令,把当前工作目录的路径赋值给变量B
C :=$(shell if [ ! -f $(FILE) ];then touch $(FILE);fi;) # 执行一段Shell脚本,内容是判断FILE(即abc)是否存在,如果不存在,就创建这个文件
a:
echo $(A)
echo $(B)
echo $(C)
clean:
$(RM) $(FILE)
6、makefile中的嵌套调用
(1)Makefile中的嵌套调用(也叫递归调用/递归make),指的是在一个Makefile里调用make命令去执行另一个目录下的Makefile,是多目录工程里管理子模块的标准方式。
(2)核心语法:
进入指定目录,执行该目录下的Makefile
make -C <目录路径>
$(MAKE) -C <目录路径>
(3)多目录工程举例:
①假设项目结构如下所示。

②顶层Makefile:
bash
# 顶层目标:编译所有子模块
all: lib src
# 调用lib/目录下的Makefile
lib:
$(MAKE) -C lib/
# 调用src/目录下的Makefile
src:
$(MAKE) -C src/
# 清理所有子模块
clean:
$(MAKE) -C lib/ clean
$(MAKE) -C src/ clean
.PHONY: all lib src clean # all等目标,一定要声明为伪目标,避免和同名文件冲突
③子目录lib/Makefile:
bash
# 编译静态库libfunc.a
libfunc.a: func.c
$(CC) -c func.c -o func.o
ar rcs $@ func.o
clean:
rm -rf *.o *.a
④子目录src/Makefile:
bash
# 编译主程序main,链接libfunc.a
main: main.c ../lib/libfunc.a
$(CC) main.c -o $@ -L../lib -lfunc
clean:
rm -rf *.o main
(4)在嵌套调用子Makefile时,父Makefile中定义的变量默认不会自动传给子进程,比如CC这些系统内置常量,对此可以用export关键字对它们进行导出,那么子Makefile就能够继承父Makefile中的变量(如果单写"export",后面不写任何变量,即导出所有所有变量,这种方式不推荐)。
bash
CC = gcc
CFLAGS = -Wall -O2
export CC CFLAGS # 把这两个变量导出
# 顶层目标:编译所有子模块
all: lib src
# 调用lib/目录下的Makefile
lib:
$(MAKE) -C lib/
# 调用src/目录下的Makefile
src:
$(MAKE) -C src/
# 清理所有子模块
clean:
$(MAKE) -C lib/ clean
$(MAKE) -C src/ clean
.PHONY: all lib src clean # all等目标,一定要声明为伪目标,避免和同名文件冲突
7、命令行传参
(1)在Makefile里,命令行传参就是在执行make命令时,直接给Makefile里的变量赋值,用来覆盖Makefile里的默认值,实现"一次写好,多种场景运行"的效果。
(2)核心语法:
传递多个参数时,用空格分隔即可,带空格的参数要用引号括起来
make <变量名1>=<值1> <变量名2>=<值2> ...... 目标名
(3)举例:
一个makefile中的内容如下所示
bash
CC ?= gcc
CFLAGS ?= -Wall -O2
TARGET ?= test
all:
@echo "CC = $(CC)"
@echo "CFLAGS = $(CFLAGS)"
@echo "TARGET = $(TARGET)"
可以在命令行中这样传参
bash
# 用g++代替gcc,开启调试模式,指定目标名为app
make CC=g++ CFLAGS="-Wall -g -O0" TARGET=app
这样,目标all的执行结果为
bash
CC = g++
CFLAGS = -Wall -g -O0
TARGET = app
(4)命令行传参的变量,优先级高于Makefile里的赋值:
①如果Makefile里用"="或":="赋值,命令行的传参会直接覆盖它。
②如果Makefile里用"?="赋值(仅未定义时生效),命令行传参会让"?="失效,保持命令行的值。
(5)其它注意事项:
①变量名不要加 ,比如命令行里直接写"CC=g++",不是"(CC)=g++"。
②伪目标和变量不要重名,否则会导致Make解析错误。
③不要传Make的内置变量,比如MAKEFLAGS、SHELL等,容易导致行为异常。
8、makefile中的条件判断
(1)关键字:
ifeq可判断两个输入参数是否相等,相等则返回true,不相等则返回false
ifneq可判断两个输入参数是否不相等,不相等则返回true,相等则返回false
ifdef可判断变量是否存在,存在则返回true,不存在则返回false
ifndef可判断变量是否不存在,不存在返回true,存在则返回false
(2)核心语法:
①ifeq:
ifeq (<参数1>, <参数2>)
如果两个参数相等,执行此处的代码
else
如果两个参数不相等,执行此处的代码
endif
②ifneq:
ifneq (<参数1>, <参数2>)
如果两个参数不相等,执行此处的代码
else
如果两个参数相等,执行此处的代码
endif
③ifdef和ifndef类似,而ifndef在前面也有介绍,此处不再赘述。
(3)条件判断常用于给变量设默认值、区分编译模式、处理交叉编译等场景。
(4)条件判断没有elseif的用法,如果想要实现多条件/多情况判断,需要写嵌套条件判断结构,如下为示例。
bash
A:=321123
RS1:=
RS2:=
ifeq ($(A),123)
RS1:=123
else
ifeq ($(A),321)
RS1:=321
else
RS1:=no-123-321
endif
endif
ifndef A
RS2:=yes
else
RS2:=no
endif
all:
echo $(RS1) # 输出 no-123-321
echo $(RS2) # 输出 no
9、makefile中的循环
(1)Makefile里的循环结构,通常有两种实现方式:
①Shell循环:在规则的命令行里写for/while,这种实现方式最常用、最直观。
②Make函数式循环:用foreach实现,适合在变量处理阶段批量生成内容。
(2)Shell循环:
①基础语法("\"为换行符,表示这几行是一个命令而不是独立的命令):
<目标>:
for <变量> in <取值列表>; do \
<循环体命令>; \
done
②举例(批量编译多个子目录):
bash
SUBDIRS := lib src test
all:
@for dir in $(SUBDIRS); do \
echo "Entering $$dir..."; \
$(MAKE) -C $$dir; \
done
clean:
@for dir in $(SUBDIRS); do \
echo "Cleaning $$dir..."; \
$(MAKE) -C $$dir clean; \
done
.PHONY: all clean
"dir":第一个"$"是转义符,Make会把""变成"$"传给Shell,Shell再解析为循环变量dir
执行make时,会按顺序进入lib、src、test 目录执行Makefile
(3)foreach循环:
①基础语法(处理体就是对每个元素执行的逻辑):
$(foreach <变量>, <列表列表>, <处理体>)
②举例(批量生成目标文件列表):
bash
SRCS := foo.c bar.c baz.c
OBJS := $(foreach src,$(SRCS),$(patsubst %.c,%.o,$(src)))
# 等价于:OBJS = foo.o bar.o baz.o
10、makefile中的自定义函数
(1)Makefile里的自定义函数是用"define + call"实现的,它的本质是把一段重复的逻辑封装起来,并不是真正意义上的函数,也没有返回值。
(2)核心语法:
define <函数名>
<函数体>(可以有多行命令或Makefile逻辑)
endef
函数的参数在函数体中用(1)、(2)、$(3)引用,对应调用时传入的第1、2、3个参数
在函数体中用$(0)表示它自己的函数名
调用方式:$(call <函数名>, <参数1>, <参数2>, <参数3>...)
(3)举例:
bash
# 定义:进入指定目录执行 make
define build_subdir
@echo "Building $(1)..."
$(MAKE) -C $(1)
endef
# 定义:清理指定目录
define clean_subdir
@echo "Cleaning $(1)..."
$(MAKE) -C $(1) clean
endef
SUBDIRS := lib src test
all:
$(call build_subdir,lib)
$(call build_subdir,src)
$(call build_subdir,test)
clean:
$(foreach dir,$(SUBDIRS),$(call clean_subdir,$(dir)))
# 调用$(call build_subdir,lib)时,$(1)会被替换成lib
.PHONY: all clean
11、项目的构建与安装流程
(1)项目的构建与安装流程可分为3个make指令(5个步骤):
|------------------|------------------------------------------------|
| 命令 | 对应步骤 |
| make(编译) | 将源文件编译成二进制可执行文件(包括各种库文件) |
| make install(部署) | 创建目录,将可执行文件拷贝到指定目录中 |
| make install(部署) | 添加全局可执行的路径(让程序不只是在当前目录中键入"./<程序名>"才能运行) |
| make install(部署) | 添加全局的启停脚本(让系统可以用systemctl这类命令管理程序,实现开机自启、后台守护) |
| make clean(清理) | 重置编译环境,清理编译过程产生无用的临时文件 |
(2)编译源码的动作一般写成默认目标,比如下例中的all,执行make命令时,就会完成第一步,生成可执行文件myserver。
bash
# 定义编译器和目标
CC := gcc
TARGET := myserver
SRCS := main.c server.c
OBJS := $(SRCS:.c=.o)
# 默认目标,执行 make 时会执行
all: $(TARGET)
$(TARGET): $(OBJS)
$(CC) -o $@ $^ -lpthread # 链接依赖库
(3)第二步、第三步和第四步一般写成名为"install"的目标。
①第二步:把编译好的可执行文件,复制到系统的标准路径(如/usr/local/bin)。
bash
# 安装路径约定
PREFIX ?= /usr/local
BINDIR := $(PREFIX)/bin
DESTDIR ?= # 用于打包/交叉编译的临时根目录
install: all
# 创建安装目录
install -d $(DESTDIR)$(BINDIR)
# 拷贝可执行文件并设置权限为 755(可执行)
install -m 755 $(TARGET) $(DESTDIR)$(BINDIR)/
②第三步:添加全局可执行的路径。这里有两种常见的实现方式:
1直接安装到(PREFIX)/bin(如/usr/local/bin),而这个路径本身就在系统的PATH中,用户直接输入命令名就能运行。
2对于非标准路径,需要额外修改~/.bashrc或/etc/profile,但make install通常不会自动修改,而是通过安装脚本或提示用户手动添加。
③第四步:添加全局的启停脚本。比如安装.service文件到/etc/systemd/system/,让用户可以用systemctl start myserver管理服务。
bash
SYSTEMD_DIR := /etc/systemd/system
install: all
# ... 前面的步骤
# 安装 systemd 服务文件
install -m 644 myserver.service $(DESTDIR)$(SYSTEMD_DIR)/
@echo "服务文件已安装,请执行 systemctl daemon-reload 生效"
(4)重置编译环境的动作一般写成名为"clean"的目标,它主要删除编译过程中生成的.o文件、可执行文件等,让项目回到"干净"状态,方便重新编译。
bash
clean:
rm -rf $(OBJS) $(TARGET)
rm -rf *.log *.core # 也可以清理日志、core dump 等
(5)把上面的五个步骤整合起来,就是一个完整的makefile。
bash
CC := gcc
TARGET := myserver
SRCS := main.c server.c
OBJS := $(SRCS:.c=.o)
PREFIX ?= /usr/local
BINDIR := $(PREFIX)/bin
SYSTEMD_DIR := /etc/systemd/system
DESTDIR ?=
.PHONY: all install clean
# 步骤1:make
all: $(TARGET)
$(TARGET): $(OBJS)
$(CC) -o $@ $^ -lpthread
# 步骤2-4:make install
install: all
install -d $(DESTDIR)$(BINDIR)
install -m 755 $(TARGET) $(DESTDIR)$(BINDIR)/
install -m 644 myserver.service $(DESTDIR)$(SYSTEMD_DIR)/
@echo "安装完成!请执行 systemctl daemon-reload 后使用 systemctl 管理服务"
# 步骤5:make clean
clean:
rm -rf $(OBJS) $(TARGET)
(6)在本章的最后说明一下,本章主要介绍的是makefile中的一些规则和编写方法,由makefile延伸出的相关知识点可能没有过多展开,这需要在其它教程中进行了解和学习。