二、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编译动态链接库示例:
①编译生成动态库:
# 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②编译主程序并链接自定义动态库:
# -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编译静态链接库示例:
①编译生成静态库:
# 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表示生成库的索引,加速链接过程②编译主程序并链接静态库:
# 链接静态库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可以写所有子目录通用的配置,比如:
# 根目录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的内容,比如:
# src/Makefile TARGET = main include ../makefile # 自动继承上面的所有配置(3)配置被覆盖的问题及解决方案:
①如果被包含的公共Makefile里定义了变量,当前Makefile里也定义了同名变量,那么同名变量的最终变量值,则取决于变量的定义顺序(比如公共Makefile在后面include,那么公共配置就会覆盖当前Makefile的同名配置)。
②为了避免这种情况带来麻烦,Makefile提供了条件编译指令ifndef & endif,它和C语言中的条件编译类似,实现的是“只有变量未定义时才执行某段代码”的逻辑。
ifndef VAR_NAME
# 如果变量VAR_NAME未被定义,就执行这里的代码
# ... 定义变量、写规则等
endif
③上例中的根目录公共Makefile优化:
# 根目录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优化:
# src/Makefile TARGET = main LDLIBS:=-lstdc++ include ../makefile # 自动继承上面的所有配置4、makefile中的三种赋值方式
(1)延迟赋值“=”:定义时不立即计算,引用时才取变量的最终值,不管变量定义写在哪里。
A = 1 B = $(A) # 这里只是记住了“引用A”,不会立刻算成1 A = 2 # 后面又给A赋值为2 all: @echo $(B) # 运行时才展开,取A的最终值2(2)立即赋值“:=”:定义时就立即计算当前变量的值,后续变量再修改,也不会影响它。
A = 1 B := $(A) # 定义时就立即计算,此时A=1,所以B直接变成1 A = 2 # 后面修改A,不影响B的值 all: @echo $(B) # B的最终值为1(3)条件赋值“?=”:只有变量当前未被定义(或被定义为空)时,才会赋值;如果已经有值,就不做任何修改。
# 情况1:变量未定义 TARGET ?= test # 此时TARGET不存在,所以赋值为test # 情况2:变量已定义 TARGET = main # 先定义了TARGET=main TARGET ?= test # 因为TARGET已经有值,所以这行不会生效 all: @echo $(TARGET) # 输出main5、makefile中调用shell命令
(1)核心语法:
$(shell <命令>)
“$(shell)”是Make的内置函数,在Make解析阶段,能够调用后面的Shell命令,并把命令的标准输出结果赋值给前面的变量,它的执行时机是变量定义时,而不是执行目标命令时
(2)举例:
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:
# 顶层目标:编译所有子模块 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:
# 编译静态库libfunc.a libfunc.a: func.c $(CC) -c func.c -o func.o ar rcs $@ func.o clean: rm -rf *.o *.a④子目录src/Makefile:
# 编译主程序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”,后面不写任何变量,即导出所有所有变量,这种方式不推荐)。
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中的内容如下所示
CC ?= gcc CFLAGS ?= -Wall -O2 TARGET ?= test all: @echo "CC = $(CC)" @echo "CFLAGS = $(CFLAGS)" @echo "TARGET = $(TARGET)"可以在命令行中这样传参
# 用g++代替gcc,开启调试模式,指定目标名为app make CC=g++ CFLAGS="-Wall -g -O0" TARGET=app这样,目标all的执行结果为
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的用法,如果想要实现多条件/多情况判断,需要写嵌套条件判断结构,如下为示例。
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) # 输出 no9、makefile中的循环
(1)Makefile里的循环结构,通常有两种实现方式:
①Shell循环:在规则的命令行里写for/while,这种实现方式最常用、最直观。
②Make函数式循环:用foreach实现,适合在变量处理阶段批量生成内容。
(2)Shell循环:
①基础语法(“\”为换行符,表示这几行是一个命令而不是独立的命令):
<目标>:
for <变量> in <取值列表>; do \
<循环体命令>; \
done
②举例(批量编译多个子目录):
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 <变量>, <列表列表>, <处理体>)
②举例(批量生成目标文件列表):
SRCS := foo.c bar.c baz.c OBJS := $(foreach src,$(SRCS),$(patsubst %.c,%.o,$(src))) # 等价于:OBJS = foo.o bar.o baz.o10、makefile中的自定义函数
(1)Makefile里的自定义函数是用“define + call”实现的,它的本质是把一段重复的逻辑封装起来,并不是真正意义上的函数,也没有返回值。
(2)核心语法:
define <函数名>
<函数体>(可以有多行命令或Makefile逻辑)
endef
# 函数的参数在函数体中用$(1)、$(2)、$(3)引用,对应调用时传入的第1、2、3个参数
# 在函数体中用$(0)表示它自己的函数名
# 调用方式:$(call <函数名>, <参数1>, <参数2>, <参数3>...)
(3)举例:
# 定义:进入指定目录执行 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 clean11、项目的构建与安装流程
(1)项目的构建与安装流程可分为3个make指令(5个步骤):
命令 | 对应步骤 |
make(编译) | 将源文件编译成二进制可执行文件(包括各种库文件) |
make install(部署) | 创建目录,将可执行文件拷贝到指定目录中 |
添加全局可执行的路径(让程序不只是在当前目录中键入“./<程序名>”才能运行) | |
添加全局的启停脚本(让系统可以用systemctl这类命令管理程序,实现开机自启、后台守护) | |
make clean(清理) | 重置编译环境,清理编译过程产生无用的临时文件 |
(2)编译源码的动作一般写成默认目标,比如下例中的all,执行make命令时,就会完成第一步,生成可执行文件myserver。
# 定义编译器和目标 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)。
# 安装路径约定 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管理服务。
SYSTEMD_DIR := /etc/systemd/system install: all # ... 前面的步骤 # 安装 systemd 服务文件 install -m 644 myserver.service $(DESTDIR)$(SYSTEMD_DIR)/ @echo "服务文件已安装,请执行 systemctl daemon-reload 生效"(4)重置编译环境的动作一般写成名为“clean”的目标,它主要删除编译过程中生成的.o文件、可执行文件等,让项目回到“干净”状态,方便重新编译。
clean: rm -rf $(OBJS) $(TARGET) rm -rf *.log *.core # 也可以清理日志、core dump 等(5)把上面的五个步骤整合起来,就是一个完整的makefile。
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延伸出的相关知识点可能没有过多展开,这需要在其它教程中进行了解和学习。