makefile从入门到实战 第一章 认识makefile(二)

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

相关推荐
Zevalin爱灰灰1 天前
makefile从入门到实战 第一章 认识makefile(一)
linux·makefile
A_humble_scholar1 天前
Linux(三)深入理解 Makefile:自动变量、增量编译原理与文件时间属性
linux·服务器·c++·makefile
sulikey6 天前
个人Linux操作系统学习笔记4 - makefile
linux·makefile·make·构建
Irissgwe15 天前
二、Linux基础开发工具(2)
linux·makefile·gcc·g++·
量子炒饭大师19 天前
【Linux系统编程】——【自动化构建-make/Makefile】拒绝手动编译!构建你的赛博代码加工厂,重塑逻辑矩阵效率极限
linux·运维·自动化·makefile·make·自动化构建
yuanyuan2o21 个月前
从最小项目开始的 CMake 教程
c语言·开发语言·arm开发·c++·makefile·make·cmake
李日灐1 个月前
< 6 > Linux 自动化构建工具:makefile 详解 + 进度条实战小项目
linux·运维·服务器·后端·自动化·进度条·makefile
送外卖的CV工程师2 个月前
STM32+Makefile编译+OpenOCD 烧录调试
stm32·单片机·嵌入式硬件·makefile·调试·烧录·openocd
zhang-ge2 个月前
Makefile调试技巧:打印信息与变量调试
makefile