makefile从入门到实战 第一章 认识makefile(二)
2026/6/8 13:29:00 网站建设 项目流程

二、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) # 输出main

5、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) # 输出 no

9、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.o

10、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 clean

11、项目的构建与安装流程

(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延伸出的相关知识点可能没有过多展开,这需要在其它教程中进行了解和学习。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询