Makefile学习记录
最近又把之前的makefile重新看了一遍,在日常还是用的很广泛的,因此记录一下,方便以后查阅
1、编译过程
我觉得要学习Makefile就必须仔细了解编译过程,不清楚这个编译过程就不知道makefile在说什么东西,特别是对于习惯了从win平台用ide来写代码的人来说,另外就是需要了解一点点的shell知识。
关于编译过程,我总结如下图所示
主要是下面的四个阶段构成,可以把这四个过程使用gcc展示出来,可以更深刻的认识一下
使用下面的代码进行测试
#include <stdio.h>
int add(int a, int b);
int minus(int a, int b);
int main()
{
int a = 30;
int b = 5;
int c;
c = add(a, b);
printf("a+b = %d\n", c);
c = minus(a, b);
printf("a-b = %d\n", c);
return 0;
}
int add(int a, int b)
{
return a+b;
}
int minus(int a, int b)
{
return a-b;
}
可以线使用预处理看一下
gcc -E main.c
加上 -o指定输出文件 gcc -E main.c -o main.i
可以看到就是吧stdio.h加进来的样子(实际情况一般不需要看预处理的文件!)
查看汇编程序:gcc -S main.c -o main.s
生成汇编程序,不过实际情况也一般不需要,这里可以看到汇编还是文本,是可以直接打开的
下面看第三阶段,生成机器指令gcc -c main.c -o main.o
这个时候已经是二进制指令了,无法打开
当然我们也可以直接输出可执行文件gcc main.c -o main
,这个输出的main就直接是可执行文件了,可以尝试执行一下,结果如下所示!
回顾一下上面的过程
这里需要注意这四条语句不是相互独立的,比如直接说最后一条语句,生成可执行文件,是经过了生成.i
.s
.o
的这几个过程的,只是他的文件没有输出出来罢了!
下面将上面的程序做一点小改动,将add 和 minus函数单独放到一个源文件中去,那我们就需要同时编译多个文件
gcc main.c add.c minus.c -o main
运行结果如下,可以看到是正常的
2、编译静态库并使用
编译静态库的规则如下所示
- 编译成 .o 的文件
gcc -c [.c] -o [自定义文件名] gcc -c [.c] [.c] ...
- 编静态库
ar -r [lib自定义库名.a] [.o] [.o] ...
- 链接成可执行文件
gcc [.c] [.a] -o [自定义输出文件名] gcc [.c] -o [自定义输出文件名] -l[库名] -L[库所在路径]
下面将上面程序依赖的两个文件加入编成静态库,如下所示
3、编译动态库并使用
编译静态库的规则如下所示
- 编译二进制.o文件
gcc -c -fpic [.c/.cpp][.c/.cpp]...
- 编库
gcc -shared [.o][.o]... -o [lib自定义库名.so]
- 链接库到可执行文件
gcc [.c/.cpp] -o [自定义可执行文件名] -l[库名] -L[库路径] -Wl,-rpath=[库路径]
但是这里要注意,如果按照跟我一样的下面方式进行操作
这样会直接提示找不到动态库,因为系统寻找动态库是需要指定一下路径的,不然只会在默认的路径下去找,因此需要首先
sudo vim .bashrc
在末尾加上这个路径
之后source
一下
source .bashrc
这样之后再次运行就不会报错了
4、makefile实现
makefile的基本格式
targets : prerequisties
[tab键]command
用中文翻译一下就是:
目标: 生成目标需要的文件或者是目标(目标套娃)
[tab键]执行的命令
伪目标,伪目标的格式如下
.PHONY : clean
为什么要有伪目标这个概念,意思就是不管有没有clean
这个文件,当运行make clean
的时候就是运行clean
的命令,不然的话,还是用上面的程序进行实验:
#include <stdio.h>
int add(int a, int b);
int minus(int a, int b);
int main()
{
int a = 30;
int b = 5;
int c;
c = add(a, b);
printf("a+b = %d\n", c);
c = minus(a, b);
printf("a-b = %d\n", c);
return 0;
}
int add(int a, int b)
{
return a+b;
}
int minus(int a, int b)
{
return a-b;
}
这里要注意:当只有一个文件的时候可以不编写makefile,直接make这个文件就是用gcc编译了
这样如果我们不使用伪目标,当有一个clean.c
的文件或者clean
的可执行文件,就回去执行clean
这个文件而不是我们准备好的makefile命令
1、makefile变量
1、定义变量
直接用=
的形式就可以,当然这个=
有几种变化的形式,比如下面使用变量表示源文件和.o文件的位置
src := main.c
obj := objs/main.o
bin := main
几种 = 的介绍
:=
- 立即赋值运算符
- 用于在定义变量时立即求值
- 该值在定义后不再更改
- 即使在后面的语句中重新定义了该变量
?=
- 默认赋值运算符
- 如果该变量已经定义,则不进行任何操作
- 如果该变量尚未定义,则求值并分配
+=
- 累加
2、使用变量
使用
$(bin) : $(obj)
gcc $(obj) -o $(bin)
$(obj) : $(src)
gcc -c $(src) -o $(obj)
.PHONY : clean
clean:
rm -rf *.o main
执行如下所示:
3、一些预定义变量,可以用预定义变量来提高效率
$@
: 目标(target)的完整名称$<
: 第一个依赖文件的名称$^
: 所有的依赖文件,以空格分开,不包含重复的依赖文件
修改makefile如下所示:
src := main.c
obj := objs/main.o
bin := main
# $(bin) : $(obj)
# gcc $(obj) -o $(bin)
# $(obj) : $(src)
# gcc -c $(src) -o $(obj)
#使用自动化变量实现
$(bin) : $(obj)
gcc $^ -o $(bin)
$(obj) : $(src)
gcc -c $^ -o $@
.PHONY : clean
clean:
rm -rf *.o main
这里在介绍一下通配符*
和%
这两个都是表示匹配到任意字符串,*
常用于目录名或者文件名,比如在make clean
中经常用到 %
可以将匹配到的字符串作为变量使用
修改makefile如下所示:
src := main.c
obj := main.o add.o minus.o
bin := main
# $(bin) : $(obj)
# gcc $(obj) -o $(bin)
# $(obj) : $(src)
# gcc -c $(src) -o $(obj)
#使用自动化变量实现
# $(bin) : $(obj)
# gcc $^ -o $(bin)
# $(obj) : $(src)
# gcc -c $^ -o $@
#使用通配符
$(bin) : $(obj)
gcc $^ -o $(bin)
%.o : %.c
gcc -c $< -o $@
.PHONY : clean
clean:
rm -rf *.o main
使用通配符之后效果如下,就不用我们一个个去写对应的.c
到.o
的转换了
运行结果如下
2、shell语法在makefile中使用
shell在makefile中的使用如下所示
$(shell <command> <arguments>)
可以在makefile里面加上一句这个:
之后输出如下:
这里第一行输出了原本的命令,makefile会把命令输出,因此可以修改为不输出
3、makefile中的一些函数
makefile中的函数用法如下所示
$(subst <from>,<to>,<text>)
1、使用subst函数
对上面的makefile再次进行修改
src := $(shell find . -name "*.c")
obj := $(subst .c,.o, $(src))
bin := main
#使用通配符
$(bin) : $(obj)
gcc $^ -o $(bin)
%.o : %.c
gcc -c $< -o $@
#shell输出
HOST_ARCH := $(shell uname -m)
echo:
@echo $(HOST_ARCH)
@echo $(obj)
.PHONY : clean
clean:
rm -rf *.o main
修改说明如下
2、使用patsubst函数
再次修改mafefile如下所示
# src := $(shell find . -name "*.c")
# obj := $(subst .c,.o, $(src))
#使用patsubst
src := $(shell find . -name "*.c")
obj := $(patsubst %.c,%.o, $(src))
bin := main
#使用通配符
$(bin) : $(obj)
gcc $^ -o $(bin)
%.o : %.c
gcc -c $< -o $@
#shell输出
HOST_ARCH := $(shell uname -m)
echo:
@echo $(HOST_ARCH)
@echo $(obj)
.PHONY : clean
clean:
rm -rf *.o main
结果和上面的是差不多的
3、使用foreach函数
函数用法如下,和shell脚本的for循环很类似
$(foreach <var>,<list>,<text>)
为了测试这个韩式,新建一个文件夹
foreach测试程序如下所示
src := $(shell find . -name "*.c")
obj := $(patsubst %.c,%.o, $(src))
bin := main
#使用通配符
$(bin) : $(obj)
gcc $^ -o $(bin)
%.o : %.c
gcc -c $< -o $@
#shell输出
HOST_ARCH := $(shell uname -m)
echo:
@echo $(HOST_ARCH)
@echo $(obj)
foreach_test:
@echo $(foreach item,$(shell ls ./test_dir),$(item))
.PHONY : clean
clean:
rm -rf *.o main
输出如下
4、makefile中的条件语句
下面是一个简单的条件语句规则
if_test:
ifeq ($(CC), gcc)
@echo 1111
else
@echo 1111
endif
作用是判断两个是否相等,效果如下
相类似的条件语句还有(用来判断宏定义的)
ifneq / else / endif
ifdef / else / endif
5、编译选项
下面是一些常见的编译选项和说明
-m64
: 指定编译为 64 位应用程序-std=
: 指定编译标准,例如:-std=c++11、-std=c++14-g
: 包含调试信息-w
: 不显示警告-O
: 优化等级,通常使用:-O3-I
: 加在头文件路径前fPIC
: (Position-Independent Code), 产生的没有绝对地址,全部使用相对地址,代码可以被加载到内存的任意位置,且可以正确的执行。这正是共享库所要求的,共享库被加载时,在内存的位置不是固定的
链接选项
-l
: 加在库名前面-L
: 加在库路径前面-Wl,<选项>
: 将逗号分隔的 <选项> 传递给链接器-rpath=
: "运行" 的时候,去找的目录。运行的时候,要找 .so 文件,会从这个选项里指定的地方去找
6、常见编译错误
下面是两个常见的编译报错说明
*** missing separator. Stop.
- 原因: Makefile 语法出错
- 解决方法: 根据报错的行数,检查 tab 缩进,空格问题
*** commands commence before first target. Stop
- 原因: if等语句里面用了 tab 缩进
- 解决方法: 缩进的地方全部改为空格
5、内核态makefile
makefile如下所示:
obj-m += my_module.o
my_module-y := main.o add.o minus.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
可以直接make之后insmod查看结果
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?