二、makefile进阶1、makefile编译动态链接库1动态链接库的概念①动态动态链接库的函数不会把代码编译到二进制文件中编译打包阶段只记录函数的地址程序运行的时候才去磁盘加载库。②链接库文件和二进制程序分离用某种特殊手段维护二者之间的关系。③库一堆编译好的函数、代码打包成的文件不用重复编译的源码。Windows中的库文件后缀为.dllLinux中的库文件后缀为.so。25个关键编译参数参数全称作用使用场景-fPICPosition-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指定目录3Linux实操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./incmain.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中的库文件后缀为.libLinux中的库文件后缀为.a。2Linux实操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./incmain.c的头文件在inc文件夹 main: main.c libfunc.a gcc main.c -o main -L./ -lfunc -I./inc需要注意的是Makefile里用-lfunc链接时如果当前目录同时存在libfunc.so和libfunc.agcc默认优先链接动态库.so不会用静态库.a对此可以在“-lfunc”后面加“ -static”强制链接静态库3、makefile中通用部分复用1Make提供内置关键字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的最终值22立即赋值“:”定义时就立即计算当前变量的值后续变量再修改也不会影响它。A 1 B : $(A) # 定义时就立即计算此时A1所以B直接变成1 A 2 # 后面修改A不影响B的值 all: echo $(B) # B的最终值为13条件赋值“?”只有变量当前未被定义或被定义为空时才会赋值如果已经有值就不做任何修改。# 情况1变量未定义 TARGET ? test # 此时TARGET不存在所以赋值为test # 情况2变量已定义 TARGET main # 先定义了TARGETmain TARGET ? test # 因为TARGET已经有值所以这行不会生效 all: echo $(TARGET) # 输出main5、makefile中调用shell命令1核心语法$(shell 命令)“$(shell)”是Make的内置函数在Make解析阶段能够调用后面的Shell命令并把命令的标准输出结果赋值给前面的变量它的执行时机是变量定义时而不是执行目标命令时2举例FILEabc # 定义了一个普通变量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中的嵌套调用1Makefile中的嵌套调用也叫递归调用/递归make指的是在一个Makefile里调用make命令去执行另一个目录下的Makefile是多目录工程里管理子模块的标准方式。2核心语法# 进入指定目录执行该目录下的Makefilemake -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 main4在嵌套调用子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 CCg CFLAGS-Wall -g -O0 TARGETapp这样目标all的执行结果为CC g CFLAGS -Wall -g -O0 TARGET app4命令行传参的变量优先级高于Makefile里的赋值①如果Makefile里用“”或“:”赋值命令行的传参会直接覆盖它。②如果Makefile里用“?”赋值仅未定义时生效命令行传参会让“?”失效保持命令行的值。5其它注意事项①变量名不要加 $比如命令行里直接写“CCg”不是“$(CC)g”。②伪目标和变量不要重名否则会导致Make解析错误。③不要传Make的内置变量比如MAKEFLAGS、SHELL等容易导致行为异常。8、makefile中的条件判断1关键字ifeq可判断两个输入参数是否相等相等则返回true不相等则返回falseifneq可判断两个输入参数是否不相等不相等则返回true相等则返回falseifdef可判断变量是否存在存在则返回true不存在则返回falseifndef可判断变量是否不存在不存在返回true存在则返回false2核心语法①ifeqifeq (参数1, 参数2)# 如果两个参数相等执行此处的代码else# 如果两个参数不相等执行此处的代码endif②ifneqifneq (参数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中的循环1Makefile里的循环结构通常有两种实现方式①Shell循环在规则的命令行里写for/while这种实现方式最常用、最直观。②Make函数式循环用foreach实现适合在变量处理阶段批量生成内容。2Shell循环①基础语法“\”为换行符表示这几行是一个命令而不是独立的命令目标: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会把“$$”变成“$”传给ShellShell再解析为循环变量dir执行make时会按顺序进入lib、src、test 目录执行Makefile3foreach循环①基础语法处理体就是对每个元素执行的逻辑$(foreach 变量, 列表列表, 处理体)②举例批量生成目标文件列表SRCS : foo.c bar.c baz.c OBJS : $(foreach src,$(SRCS),$(patsubst %.c,%.o,$(src))) # 等价于OBJS foo.o bar.o baz.o10、makefile中的自定义函数1Makefile里的自定义函数是用“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 # 步骤1make all: $(TARGET) $(TARGET): $(OBJS) $(CC) -o $ $^ -lpthread # 步骤2-4make 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 管理服务 # 步骤5make clean clean: rm -rf $(OBJS) $(TARGET)6在本章的最后说明一下本章主要介绍的是makefile中的一些规则和编写方法由makefile延伸出的相关知识点可能没有过多展开这需要在其它教程中进行了解和学习。