Makefile
会不会写makefile,从一个侧面决定一个人是否具备完成大型工程的能力。
Makefile和make命令一起配合使用,为什么要使用makefile,原因以及优点在下文解释。
简单辨析一下建立工程的三种方式
-
Makefile
使用非常广泛,通用性强,可跨平台
但是语法比较严格,写一个通用,便于管理,兼容性强的makefile比较困难
-
cmake
简单易用,使用广泛,便于管理,可跨平台
自动生成的makefile过于臃肿
-
sh脚本
自由,高度定制,简单易用,可操作性强,方便维护。
sh建立的工程太少
makefile带来的好处就是“自动化编译”
-
把源文件编译成中间代码文件 ,在windows下就是.obj文件,在UNIX下是.o文件。这个动作叫做编译(compile)
-
再把目标文件合成为可执行文件的过程或者称为动作叫做链接
源文件首先会生成中间目标文件,再由中间目标文件生成执行文件。在编译时,编译器只检测程序语法,和函数、变量是否被声明。
如果函数未被声明,编译器会给出一个警告,但可以生成 ObjectFile.而在链接程序时,链接器会在所有的ObjectFile中找寻函数的实现,如果找不到,那到就会报链接错误码(Linker Error)。
Makefile告诉make命令如何编译和链接工程中需要的源文件和头文件。
Makefile文件包含了一系列的“规则”,规则的基本结构如下: 目标、依赖、命令
编译规则如下:
1)如果这个工程没有编译过,则需要所有的C文件都要编译并链接
2)如果工程中的某几个C文件被修改,那么我们只编译被修改后的C文件,并链接目标程序。
3)如果这个工程的头文件被改变了,需要编译引用这几个头文件的C文件,并链接目标程序。
make命令会自动智能地根据当前的文件修改的情况来确定哪些文件需要重新编译,从而自己编译所需要的文件和链接目标程序。
目标文件(Target)包含:执行文件 和 中间目标文件
依赖文件就是冒号之后的.c文件和.h文件。每一个.o文件都有一组依赖文件,而.o文件又是执行文件的依赖文件。
依赖文件的实质就是说明 目标文件由哪些文件生成。换言之,就是目标文件是哪些文件更新的。
定义好依赖文件之后的命令定义了如何生成目标文件的操作系统命令。
make会比较依赖文件和目标文件的修改日期,如果依赖文件的修改日期比目标文件的修改日期新或者目标文件不存在时,执行make后续定义的命令。
在Makefile中使用变量
使用变量来简化规则的编写。变量可以在Makefile的任何地方,引用变量时需要使用$(变量名)的形式。
赋值:=和:=
·使用=进行赋值时,Makefile会延迟展开,这意味着变量的值不会立即确定,而是在每次变量被引用时根据变量的当前值重新计算。可以使用后面定义的变量。最后再展开
·使用:=进行赋值时,Makefile会进行直接展开,这意味着变量的值在赋值时立即确定,并且之后不会改变(除非有其他机制,如override指令),只能使用前面定义好的变量。
+=追加变量的值
$(变量名) 自定义变量名
$@ $< $^ 自动变量
$@
表示规则中的目标
$<
表示第一个依赖文件
$^
表示所有的依赖文件
模式规则允许使用%通配符来匹配文件名,从而可以为一组文件定义相同的编译规则。
CC=gcc
%.o: %.c
$(CC) -c $< -o $@
这条规则表示对于所有的.c文件,都使用$(CC) -c命令编译成对应的.o文件
让make自动推导:自动推导文件以及文件依赖关系后面的命令
清空目标文件的规则:伪目标
是那些不真正生成文件的目标,如clean,是为了和同名文件冲突,可以使用.PHONY声明伪目标
.PHONY :clean
clean :
-rm edit $(objects)
不成文的规矩是 --“clean从来都是放在文件的最后”
Makefile五大项:
显式规则、隐晦规则、变量定义、文件指示和注释
-
显示规则
显式规则。显式规则说明了,如何生成一个或多的的目标文件。这是由 Makefile的书写者明显指出,要生成的文件,文件的依赖文件,生成的命令。
-
隐晦规则
由于makefile有自动推导的功能,所以隐晦的规则可以让我们粗糙地简略地书写Makefile
-
变量的定义
在Make file中我们要定义一系列的变量,变量一般都是字符串,这个有点你像C语言中的宏,当Makefile被执行时,其中的变量都会被扩展到相应的引用位置上。
变量的高级使用方法
一是变量值的替换。
二是一种变量替换的技术以“静态模式”定义。
三是把变量的值再变成变量。
-
文件指示
其包含了三个部分
一是在一个makefile中引用另一个makefile,类似C语言#include
另一个是指根据某些情况指定Makefile中的有效部分,类似C语言中的预编译#if,
-
注释
makefile只有行注释 #
还值得一提的是:makefile的命令 必须要以[tab]键开始
追加变量值
override指示符
多行变量
有点类似于C语言的define宏定义
define two-lines
echo foo
echo $(bar)
endef
环境变量
make运行时的系统环境变量可以在make开始运行时被载入到makefile文件中,
如果makefile中已经定义了这个变量,或者这个变量由命令行带入,那么系统的环境变量的值将被覆盖。
如果我们在环境变量中设置“CFLAGS”环境变量。那么可以在所有的Makefile中使用这个变量。
Make的工作方式
GNU的make工作方式的执行步骤
1、读入所有的 Makefile。
2、读入被 include 的其它 Makefile。
3、初始化文件中的变量。
4、推导隐晦规则,并分析所有规则。
5、为所有的目标文件创建依赖关系链。
6、根据依赖关系,决定哪些目标要重新生成。
7、执行生成命令
第一个阶段:1-5步;如果变量被使用,make会把其展开在使用的位置,但不是立马完全展开,如果变量出现在依赖关系的规则中,仅当这条依赖被决定使用了才在内部展开。
第二个阶段:6-7;
自动生成依赖性
在makefile中,我们的依赖关系可能会需要包含一系列的头文件,可以使用编译器的“-M”自动寻找源文件中包含的头文件,并生成一个依赖关系
实例参考
objects = main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
edit : $(objects)
cc -o edit $(objects)
main.o : main.c defs.h
cc -c main.c
kbd.o : kbd.c defs.h command.h
cc -c kbd.c
command.o : command.c defs.h command.h
cc -c command.c
display.o : display.c defs.h buffer.h
cc -c display.c
insert.o : insert.c defs.h buffer.h
cc -c insert.c
search.o : search.c defs.h buffer.h
cc -c search.c
files.o : files.c defs.h buffer.h command.h
cc -c files.c
utils.o : utils.c defs.h
cc -c utils.c
clean :
rm edit $(objects)
objects = main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
cc = gcc
edit : $(objects)
cc -o edit $(objects)
main.o : defs.h
kbd.o : defs.h command.h
command.o : defs.h command.h
display.o : defs.h buffer.h
insert.o : defs.h buffer.h
search.o : defs.h buffer.h
files.o : defs.h buffer.h command.h
utils.o : defs.h
.PHONY : clean
clean :
rm edit $(objects)
实例Makefile解析
偶然看到一个实例:
在linux下使用纯命令行开发调试单片机(刚好单片机用的是RISC-V单片机 CH32系列)
大致工具就是使用OPENOCD进行的一个编程,先下载RISC-V内核芯片的交叉编译工具链和调试下载器,然后在linux下下载OPENOCD进行编译和安装,使用官网提供的外设库文件,openocd属于第三方软件,移植安装的过程大致分为(a)配置config文件,在这里选择使用哪个调试工具,例如jtag,jlink什么之类的。然后运行之后会生成makefile文件(b)然后编译(c)然后下载安装。
openocd把hex文件写到flash里面去的、openocd是一个服务进程
下面摘抄了某个开源的makefile(ch32)
CROSS_COMPILER_PREFIX = C:/MounRiver/MounRiver_Studio/toolchain/RISC-V Embedded GCC/bin/riscv-none-embed-
#CROSS_COMPILER_PREFIX = $(HOME)/MRS_Toolchain_Linux_x64_V1.80/RISC-V Embedded GCC/bin/riscv-none-embed-
OPENOCD = C:/MounRiver/MounRiver_Studio/toolchain/OpenOCD/bin/openocd.exe
OPENOCD_ARGS = -f C:/MounRiver/MounRiver_Studio/toolchain/OpenOCD/bin/wch-riscv.cfg
#OPENOCD = $(HOME)/MRS_Toolchain_Linux_x64_V1.80/OpenOCD/bin/openocd
#OPENOCD_ARGS = -f $(HOME)/MRS_Toolchain_Linux_x64_V1.80/OpenOCD/bin/wch-riscv.cfg
#OPENOCD = /usr/local/bin/openocd
#OPENOCD_ARGS = -f interface/wlink.cfg -f target/wch-riscv.cfg
#ARCH = -march=rv32imafc -mabi=ilp32f
#ARCH = -march=rv32imac -mabi=ilp32
ARCH = -march=rv32ecxw -mabi=ilp32e
CH32_STD_LIB_DIR = ../ch32-standard-library/ch32v00x
#CH32_STD_LIB_DIR = $(HOME)/playground/ch32-standard-library/ch32v00x
CROSS_C_SOURCE_FILES += $(wildcard $(CH32_STD_LIB_DIR)/peripheral/src/*.c)
CROSS_C_SOURCE_FILES += $(wildcard $(CH32_STD_LIB_DIR)/core/*.c)
CROSS_C_SOURCE_FILES += $(wildcard ./src/*.c)
CROSS_ASM_SOURCE_FILES += $(CH32_STD_LIB_DIR)/sample/startup.S
CROSS_C_FLAGS += -fno-common -fno-builtin -Os
CROSS_C_FLAGS += -DCHIP_CH32V00X
CROSS_LD_FLAGS += -Wl,--no-relax -specs=nosys.specs -specs=nano.specs -nostartfiles \
-T$(CH32_STD_LIB_DIR)/sample/default.ld
#CROSS_LD_FLAGS += -lm
CROSS_C_INCLUDES = $(CH32_STD_LIB_DIR)/peripheral/inc $(CH32_STD_LIB_DIR)/core ./src
OPENOCD_FLASH_COMMANDS = -c "program $< verify" -c wlink_reset_resume -c exit
include ./miscellaneous-makefiles/cross-gcc-mcu.mk
target_detail: $(BUILD_DIR)/$(TARGET).elf
$(CROSS_OBJDUMP) -S -D -M xw $< > $<.lss
$(CROSS_SIZE) --radix=16 --format=SysV $<
OPENOCD
1.调试
Openocd允许开发人员在嵌入式操作系统中执行单步调试、断电设置、查看寄存器状态、访问内存操作,这对识别和解决软件或硬件问题非常有用。
2.编程
可以用于烧写程序(固件)到嵌入式设备的内存中,确保正确的程序在硬件上允许。
3.支持多种调试接口
支持多种调试接口标准,包括JTAG和SWD,适合不同类型的嵌入式芯片。
4.支持多种目标设备
支持许多不同芯片和处理器体系结构的调试支持。
5.可拓展性
开源,可根据需要进行定制和扩展,以满足特定的需求。
·openocd --file | -f 加载配置文件,后跟配置文件名
·interface:指定调试接口,如 interface/stlink.cfg
·adapter_khz:配置适配器的时钟频率。
OpenOCD的cfg文件配置方法
- 启动openocd服务,需要指定interface和target(target和board可以选择,最小系统板选target, 板载DRAM, 外部Flash的可以带上board的cfg文件)