开始先来一个Makefie例子:
CROSS = riscv64-unknown-elf-
cc = $(CROSS)gcc
Current_DIR=$(shell pwd)
INCLUDE_DIR+= -I $(Current_DIR)/../Include
C_FLAGS=$(Current_DIR)/Driver_USART.c
all:
@cc $(INCLUDE_DIR) $(C_FLAGS)
.PHONY: target
target:
@echo "$(Current_DIR)"
@echo "$(INCLUDE_DIR)"
@echo "$(C_FLAGS)"
~
一、Makefile 结构说明
src:源码
incl:头文件
bin:执行码
lib:静态/动态库
Makefile里主要包含了五个东西:变量定义、显式规则、隐晦规则、文件指示和注释。
1、变量的定义。在Makefile中我们要定义一系列的变量,变量一般都是字符串,这个有点像C语言中的宏,当Makefile被执行时,其中的变量都会被扩展到相应的引用位置上。
2、显式规则。显式规则说明了,如何生成一个或多的的目标文件。这是由Makefile的书写者明显指出,要生成的文件,文件的依赖文件,生成的命令。
3、隐晦规则。由于我们的make有自动推导的功能,所以隐晦的规则可以让我们比较粗糙地简略地书写Makefile,这是由make所支持的。
4、文件指示。其包括了三个部分,一个是在一个Makefile中引用另一个Makefile,就像C语言中的include一样。
5、注释。Makefile中只有行注释,和UNIX的Shell脚本一样,其注释是用“#”字符,这个就像C/C++中的“//”一样。如果你要在你的Makefile中使用“#”字符。
我们在写代码的时候不一定都是有自己来完成,一个工程中会大量使用一些比较优秀的动态库、静态库等,我们在使用这些库完成所有的代码后,需要在编译的时候将这些库使用的头文件添加到我们的工程上,将他的库文件也添加到我们的工程中,接下来我们就来看一下怎么来添加。我们在项目中很少将第三方库与我们自己的代码放到同一个目录中,而是有一些约定俗成的存放方法:将第三方的静态库放到lib文件夹,将动态库放到bin文件夹,
二、代码与函数分析
Makefile常用函数
- wildcard 自动搜索当前目录下的所有源文件~·
SRCS = $(wildcard ./*.cpp)
OBJS = $(patsubst %.cpp, %.o, $(SRCS)) 通过cpp文件获得与之同名的*.o类型的文件
$(OBJS):%.o : %.cpp
$(CXX) $(CFLAGS) $< -o $@
当需要指定文件目录时:指定方法 - SRCS = $(wildcard ./src/*.cpp)
将*.o文件和*.cpp文件想对应起来(这里会默认看成一个循环,即一个*.o和一个*.cpp文件均对应,直到所有的cpp文件和*.o文件都编译完成)
- 增加第三方库和头文件
HEADER_PATH = -I./include/
LIB_PATH = -L./lib/
LIBS = -ldiv
# LIBS = libdiv.a
$(TARGET) : $(OBJS)
$(CXX) $^ -o $@ $(LIB_PATH) $(LIBS)
$(OBJS):%.o : %.cpp
$(CXX) $(CFLAGS) $< -o $@ $(HEADER_PATH)
PS: $(CXX) $^ -o $@ $(LIB_PATH) $(LIBS),这是在最后链接是需要告诉编译器我的库放在了那个目录,以及该库的名字
LIBS = -ldiv的方式一般用于动态库,LIBS = libdiv.a适用于静态库加载,而且只有当libdiv.a与Makefile在同一目录下才可以,一般静态库使用将目录写死的方式,但是这样的话又不方便代码的传播
- 遍历当前目录和其子目录
SRC_PATH = ./src
DIRS = $(shell find $(SRC_PATH) -maxdepth 3 -type d)
# 为了更大幅度的支持项目的搭建,将三种文件格式的后缀都单独便利到变量中
SRCS_CPP += $(foreach dir, $(DIRS), $(wildcard $(dir)/*.cpp))
SRCS_CC += $(foreach dir, $(DIRS), $(wildcard $(dir)/*.cc))
SRCS_C += $(foreach dir, $(DIRS), $(wildcard $(dir)/*.c))
OBJS_CPP = $(patsubst %.cpp, %.o, $(SRCS_CPP))
OBJS_CC = $(patsubst %.cc, %.o, $(SRCS_CC))
OBJS_C = $(patsubst %.c, %.o, $(SRCS_C))
- 去空格函数——strip。
功能:去掉字串中开头和结尾的空字符。
返回:返回被去掉空格的字符串值。
示例:
$(strip a b c )
把字串“a b c ”去掉开头和结尾的空格,结果是“a b c”。
字符转换 tr
$(shell echo $(CORE) | tr a-z A-Z)
tr: 主要用于删除文件中控制字符或进行字符转换。使用tr时要转换两个字符串:字符串1用于查询,字符串2用于处理各种转换。ifneq
ifeq ($(变量名), 变量值 )
比如需要判断两个变量 VALUE1 和 VALUE2 的值都存在才执行某个动作,这需要逻辑与的判断ifneq ($(VALUE1)$(VALUE2),)
.果变量 VALUE1 和 VALUE2 都有具体的值,比如需要进行这样的判断: VALUE1 == V1 && VALUE2 == V2, 可以按如下的写法;ifeq ($(VALUE1)_$(VALUE2), V1_V2) ### 当然中间的下划线 "_" 可以用其他字符代替
逗号字符串和空格字符串
空格和逗号是makefile中常见的分隔符,但是如果要进行字符串处理,空格和逗号就不能直接出现了subset 字符串替换函数
$(subst
;, ;, ;) words:单词统计函数
word 取单词函数
$(word 1,hello jello yello)
上面的语句执行后的结果为hello,意为取字符串的第一个单词减号
有些命令会在开头加一个'-',作用是发生错误时Makefile继续。foreach 函数
foreach函数和别的函数非常的不一样。因为这个函数是用来做循环用的。$(foreach ,,)。这个函数的意思是,把参数中的单词逐一取出放到参数所指定的变量中,然后再执行所包含的表达式。每一次会返回一个字符串,循环过程中所返回的每个字符串会以空格分隔,最后当整个循环结束时,所返回的每个字符串所组成的整个字符串(以空格分隔)将会是foreach函数的返回值。
names := a b c d
files := $(foreach n,$(names),$(n).o)
通过foreach遍历names,每个值存到n中,并通过表达式$(n).o输出,多个值则以空格隔开
(files)的值是“a.o b.o c.o d.o”。
foreach中的参数是一个临时的局部变量,foreach函数执行完后,参数的变量将不在作用,其作用域只在foreach函数当中
对于同样功能的for而言,
all:
names = a b c d
files := $(foreach n,$(names),$(n).o)
demo:
cd subdir && \
for x in $(files); do \
echo $$a; \
done
先进入subdir目录,然后遍历files中的每个值,输出【注意】要加空格\,这样会视为一条命令
三、 问题记录
- recipe for target xxx”错误,不妨先看看编码是否正确
- -T 选项用以指定自己的链接脚本
- -wl: makefile中一般默认的 lib 的加载路径是/lib /usr/lib 如果想要改变程序运行时的libs的加载路径 就需要用到 -wl , rpath 参数来添加lib 加载路径。
Question: 编译程序时得到undefined reference to 'xxxx'这样的错误提示
Answer: 那你一定是缺少某个库,用 -l参数将库加入。Linux的库命名是一致的,一般为libxxx.so,或libxxx.a,libxxx.la,那么你要链接某个库就用-lxxx,去掉头lib及"."后面的so,la,a等即可。
数学库 -lm ; posix线程 -lpthread
lc 是link libc
lm 是link libm
lz 是link libz
- -I:加载头文件路径.
- --gc-sections: 把每个函数作为一个section,每个数据(应该是指全局变量之类的吧)也作为一个section,这样链接的时候,--gc-sections会把没用到的section丢弃掉,最终的可执行文件就只包含用到了的函数和数据。
- -L:指定linker的路径
- 当没有办法打开链接脚本时,可能时因为linker的路径不对,这个时候就要找路径是否正确,是否能在正确的路径下寻找文件。
第一种是指定连接器LD的flag
arm-linux-ld -Ttext 0x0 -o led.elf $^(这句表示使用依赖编译链接生成led.elf目标文件,
编译链接工具为arm-linux-ld),链接地址依靠链接器的flag(Ttext)来指定,为0
第二种:依靠链接脚本来指定
如:arm-linux-ld -Tlink.lds -o led.elf $^,这句功能同上,只是链接地址依赖连接脚本link.lds来指定。
在连接脚本的开头以 .=xxx的形式指定连接地址。
- 特殊字符
$@ 表示目标文件的完整名称。
$^ 表示所有的依赖文件,以空格分开,不包含重复的依赖文件。
$< 表示第一个依赖文件
$? 所有的依赖文件,以空格分开,这些依赖文件的修改日期比目标的创建日期晚。
$% 仅当目标是函数库文件中,表示规则中的目标成员名。例如,如果一个目标是“foo.a(bar.o)”,那么,“$%”就是“bar.o”,“$@”就是“foo.a”。如果目标不是函数库文件(Unix下是[.a],Windows下是[.lib]),那么,其值为空
$+ 这个变量很像“$^”,也是所有依赖目标的集合。只是它不去除重复的依赖目标。
$* 不包含扩展名的目标文件名称。
如果目标是“foo.c”,因为“.c”是make所能识别的后缀名,所以,“$*”的值就是“foo”.
$$: 要表示它的字面意思则需要写两个$.两个$$在shell中表示当前进程的id,一般用来临时给文件起名字。
$$表示$,用来shell下引用变量,而$A或者$(A)则是Makefile的变量
$(1)$(2):有点类似于执行shell脚本中的第一个参数和第二个参数...
在写Makefile时需要查看源代码,找出它们依赖于哪些头文件,这很容易出错,
一是因为有的头文件包含在另一个头文件中,在写规则时很容易遗漏,
二是如果以后修改源代码改变了依赖关系,很可能忘记修改Makefile的规则。
为了解决这个问题,可以用gcc的-M选项自动生成目标文件和源文件的依赖关系:(gcc -M main.c)
-M选项把所有包含的系统头文件也找出来了,如果我们不需要输出系统头文件的依赖关系,可以用-MM选项
举例:
all: main
main: main.o stack.o maze.o
gcc $^ -o $@
clean:
-rm main *.o
.PHONY: clean
sources = main.c stack.c maze.c
include $(sources:.c=.d)
%.d: %.c
set -e; rm -f $@; \
$(CC) -MM $(CPPFLAGS) $< > $@.$$$$; \
sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \
rm -f $@.$$$$
解析:
1、sources变量包含我们要编译的所有.c文件,$(sources:.c=.d)是一个变量替换语法,
把sources变量中每一项的.c替换成.d,所以include这一句相当于
2、set -e; rm -f maze.d; \
cc -MM maze.c > maze.d.$$; \
sed 's,\(maze\)\.o[ :]*,\1.o maze.d : ,g' < maze.d.$$ > maze.d; \
rm -f maze.d.$$
注意,虽然在Makefile中这个命令写了四行,但其实是一条命令,make只创建一个Shell进程执行这条命令,
这条命令分为5个子命令,用;号隔开,并且为了美观,用续行符\拆成四行来写。执行步骤为:
1. set -e命令设置当前Shell进程为这样的状态:如果它执行的任何一条命令的退出状态非零则立刻终止,不再执行后续命令。
2. 把原来的maze.d删掉5
3. 重新生成maze.c的依赖关系,保存成文件maze.d。1234
%.d: %.c 其实这里主要是为每个C文件建立一个同名的后缀为.d。该文件的作用是使用gcc的-M属性来自动生成.o文件的头文件依赖关系。
4. sed的s命令是替换命令。
sed是一个非交互式的流编辑器。流编辑器是指sed每次只从文件(或输入)读入一行,然后对该行进行指定的处理,并将结果输出到屏幕,接着读入下一行。
- GCC GCC参数详解
1、预处理,生成 .i 的文件[预处理器cpp]
2、将预处理后的文件转换成汇编语言, 生成文件 .s [编译器egcs]
3、有汇编变为目标代码(机器代码)生成 .o 的文件[汇编器as]
4、连接目标代码, 生成可执行程序 [链接器ld]
-x language filename: 设定文件所使用的语言,使后缀名无效。
-x none filename: 关掉上一个选项,让gcc根据后缀名自动识别文件类型
-c: 只激活预处理编译和汇编,做成obj文件(.o 的obj文件)
-C: 在预处理是,不删除注释信息,一般和-E使用,分析程序。
-S:只激活预处理和编译,做成汇编代码。
-E:只激活预处理,不生产文件,但是需要把它重定向到一个输出文件(gcc -E hello.c > hello.txt)
-o :制定目标名称,因为默认的名字是a.out
-include file: 包含某个代码,需要另外一个文件的时候使用
-ldir:在用#include "file" 时候,gcc会在当前目录查找头文件,如果没有找到,就在-I(大写i)目录下找。
-M: 生成文件关联的信息。包含目标文件所依赖的所有源代码你可以用 gcc -M hello.c 来测试一下
-MM: 和上面一样,但是忽略由#include 造成的依赖关系,就是不显示出来。
-MD:和-M相同,但是输出将导入到.d的文件里面
-MMD:和 -MM 相同,但是输出将导入到 .d 的文件里面。
-Wa,option:此选项传递 option 给汇编程序; 如果 option 中间有逗号, 就将 option 分成多个选项, 然 后传递给会汇编程序。
-Wl.option:此选项传递 option 给连接程序; 如果 option 中间有逗号, 就将 option 分成多个选项, 然 后传递给会连接程序
-llibrary:指定编译的时候使用的库:如gcc -lcurses hello.c,使用ncurses库编译程序
-Ldir:指定编译的时候,搜索库的路径。比如你自己的库,可以用它制定目录,不然编译器将只在标准库的目录找。这个dir就是目录的名称。
-O0 、-O1 、-O2 、-O3:编译器的优化选项的 4 个级别,-O0 表示没有优化, -O1 为默认值,-O3 优化级别最高
常用的指令查看:
-ansi 只支持 ANSI 标准的 C 语法。这一选项将禁止 GNU C 的某些特色, 例如 asm 或 typeof 关键词。
-c 只编译并生成目标文件。
-DMACRO 以字符串"1"定义 MACRO 宏。
-DMACRO=DEFN 以字符串"DEFN"定义 MACRO 宏。
-E 只运行 C 预编译器。
-g: 生成调试信息。GNU 调试器可利用该信息。
-IDIRECTORY 指定额外的头文件搜索路径DIRECTORY。
-LDIRECTORY 指定额外的函数库搜索路径DIRECTORY。
-lLIBRARY 连接时搜索指定的函数库LIBRARY。
-m486 针对 486 进行代码优化。
-o FILE 生成指定的输出文件。用在生成可执行文件时。
-O0 不进行优化处理。
-O 或 -O1 优化生成代码。
-O2 进一步优化。
-O3 比 -O2 更进一步优化,包括 inline 函数。
-shared 生成共享目标文件。通常用在建立共享库时。
-static 禁止使用共享连接。
-UMACRO 取消对 MACRO 宏的定义。
-w 不生成任何警告信息。
-Wall:生成所有警告信息。
四、cmake
cmke与make
CMake与Make最简单直接的区别
CMake 入门实战(精)
在 linux 平台下使用 CMake 生成 Makefile 并编译的流程如下:
1、编写 CMake 配置文件 CMakeLists.txt 。
2、执行命令 cmake PATH 或者 ccmake PATH 生成 Makefile ,其中, PATH 是 CMakeLists.txt 所在的目录。 ccmake 和 cmake 的区别在于前者提供了一个交互式的界面。
3、使用 make 命令进行编译。