文章目录
Make
概述
Makefile文件中所描述的模块与模块、模块与源代码文件之间的依赖关系,将源代码文件编译成obj文件,再将obj文件链接成库文件或可执行程序文件的过程。
但软件项目中,模块之间的依赖关系一般比较复杂,想要通过一条命令gcc命令完成项目的编译非常困难,并且不利于扩展和维护。
以下图所示的工程为例,利用makefile快速编写编译脚本
# test依赖于libtest.a main.o
test: libtest.a main.o
gcc main.o -ltest -L. -o test
# main.o依赖于main.c
main.o:main.c
gcc main.c -c -o main.o
# libtest.a依赖于test1.o test2.o
libtest.a: test1.o test2.o
ar -r libtest.a test1.o test2.o
# test1.o依赖于test1.c
test1.o:test1.c
gcc test1.c -c -o test1.o
# test2.o依赖于test2.c
test2.o:test2.c
gcc test2.c -c -o test2.o
标准文件构成与工作流程
makefile的规则,也就是makefile中最核心的内容如下所示。
target ... : prerequisites ...
command
target可以是一个object file(目标文件),也可以是一个执行文件,还可以是一个标签(label)。prerequisites:生成该target所依赖的文件和/或target。command:该target要执行的命令(任意的shell命令)。
makefile可以使用变量。例如使用变量添加文件,如下所示。
objects = main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
make能够自动推导。比如make看到一个 .o 文件,它就会自动的把 .c 文件加在依赖关系中,如果make找到一个whatever.o ,那么 whatever.c 就会是 whatever.o 的依赖文件。并且cc -c whatever.c 也会被推导出来,
在Makefile使用 include 关键字可以把别的Makefile包含进来。在 include 前面可以有一些空字符,但是绝不能是 Tab 键开始。 include 和< filename > 可以用一个或多个空格隔开。举个例子,你有这样几个Makefile:a.mk、b.mk 、 c.mk ,还有一个文件叫 foo.make ,以及一个变量 $(bar) ,其包含了 e.mk 和 f.mk ,那么,下面的语句相互等价:
include foo.make *.mk $(bar)
include foo.make a.mk b.mk c.mk e.mk f.mk
如果make执行时,有 -I 或-include-dir 参数,那么make就会在这个参数所指定的目录下去寻找。
make支持三个通配符:* , ? 和 ~ 。波浪号( ~ )字符在文件名中也有比较特殊的用途。如果是 ~/test ,这就表示当前用户的 $HOME 目录下的test目录。而 ~hchen/test 则表示用户hchen的宿主目录下的test目录。
要让通配符在变量中展开,也就是让objects的值是所有 .o 的文件名的集合。
objects := $(wildcard *.c)
特殊变量VPATH,如果没有指明这个变量,make只会在当前的目录中去找寻依赖文件和目标文件。如果定义了这个变量,那么,make就会在当前目录找不到的情况下,到所指定的目录中去找寻文件了。
VPATH = src:../headers
上面的定义指定两个目录,“src”和“…/headers”,make会按照这个顺序进行搜索。目录由“冒号”分隔。(当然,当前目录永远是最高优先搜索的地方)
“vpath”关键字(注意,它是全小写的),这不是变量,这是一个make的关键字,这和上面提到的那个VPATH变量很类似,但是它更为灵活。它可以指定不同的文件在不同的搜索目录中。这是一个很灵活的功能。它的使用方法有三种:
vpath <pattern> <directories> # 为符合模式的文件指定搜索目录。
vpath <pattern> # 清除符合模式的文件的搜索目录。
vpath # 清除所有已被设置好了的文件搜索目录。
vpath %.h ../headers # 该语句表示,要求make在“../headers”目录下搜索所有以 .h 结尾的文件。
“.PHONY”来显式地指明一个目标是“伪目标”,向make说明,不管是否有这个文件,这个目标就是“伪目标”。只要有这个声明,不管是否有“clean”文件,要运行“clean”这个目标,只有“make clean”这样。
.PHONY : clean
clean :
rm *.o temp
伪目标
伪目标一般没有依赖的文件。但是,我们也可以为伪目标指定所依赖的文件。伪目标同样可以作为“默认目标”,只要将其放在第一个。一个示例就是,如果你的Makefile需要一口气生成若干个可执行文件,但你只想简单地敲一个make完事,并且,所有的目标文件都写在一个Makefile中,那么你可以使用“伪目标”这个特性:
all : prog1 prog2 prog3
.PHONY : all
prog1 : prog1.o utils.o
cc -o prog1 prog1.o utils.o
prog2 : prog2.o
cc -o prog2 prog2.o
prog3 : prog3.o sort.o utils.o
cc -o prog3 prog3.o sort.o utils.o
目标也可以成为依赖。所以,伪目标同样也可成为依赖。
.PHONY : cleanall cleanobj cleandiff
cleanall : cleanobj cleandiff
rm program
cleanobj :
rm *.o
cleandiff :
rm *.diff
标准工程
##########################################################################################################################
# File automatically-generated by tool: [projectgenerator] version: [3.18.0-B7] date: [Mon Apr 03 14:51:26 CST 2023]
##########################################################################################################################
# ------------------------------------------------
# Generic Makefile (based on gcc)
#
# ChangeLog :
# 2017-02-10 - Several enhancements + project update mode
# 2015-07-22 - first version
# ------------------------------------------------
######################################
# target
######################################
TARGET = XXX
######################################
# building variables
######################################
# debug build?
DEBUG = 1
# optimization
OPT = -Og
#######################################
# paths
#######################################
# Build path
BUILD_DIR = build_make
######################################
# source
######################################
# C sources
C_SOURCES = \
main.c
# ASM sources
# ASM_SOURCES =
#######################################
# binaries
#######################################
#GCC_PATH = D:/Download/gcc-arm-none-eabi-10.3-2021.10/bin
#PREFIX = arm-none-eabi-
# The gcc compiler bin path can be either defined in make command via GCC_PATH variable (> make GCC_PATH=xxx)
# either it can be added to the PATH environment variable.
ifdef GCC_PATH
CC = $(GCC_PATH)/$(PREFIX)gcc
AS = $(GCC_PATH)/$(PREFIX)gcc -x assembler-with-cpp
CP = $(GCC_PATH)/$(PREFIX)objcopy
SZ = $(GCC_PATH)/$(PREFIX)size
else
CC = $(PREFIX)gcc
AS = $(PREFIX)gcc -x assembler-with-cpp
CP = $(PREFIX)objcopy
SZ = $(PREFIX)size
endif
HEX = $(CP) -O ihex
BIN = $(CP) -O binary -S
#######################################
# CFLAGS
#######################################
# macros for gcc
# AS defines
AS_DEFS =
# C defines
# C_DEFS =
# ifeq ($(DEBUG), 1)
# C_DEFS += \
# -DDEBUG \
# -DGCC_ARM
# endif
# C includes
C_INCLUDES = \
-ICore/Inc
# compile gcc flags
ASFLAGS = $(MCU) $(AS_DEFS) $(AS_INCLUDES) $(OPT) -Wall -fdata-sections -ffunction-sections
CFLAGS += $(MCU) $(C_DEFS) $(C_INCLUDES) $(OPT) -Wall -fdata-sections -ffunction-sections
ifeq ($(DEBUG), 1)
CFLAGS += -g -gdwarf-2
endif
# Generate dependency information
CFLAGS += -MMD -MP -MF"$(@:%.o=%.d)"
#######################################
# LDFLAGS
#######################################
# link script
LDSCRIPT = STM32F103ZETx_FLASH.ld
# libraries
LIBS = -lc -lm -lnosys
LIBDIR =
LDFLAGS = $(MCU) -specs=nano.specs -T$(LDSCRIPT) $(LIBDIR) $(LIBS) -Wl,-Map=$(BUILD_DIR)/$(TARGET).map,--cref -Wl,--gc-sections
# default action: build all
all: $(BUILD_DIR)/$(TARGET).elf $(BUILD_DIR)/$(TARGET).hex $(BUILD_DIR)/$(TARGET).bin
#######################################
# build the application
#######################################
# list of objects
OBJECTS = $(addprefix $(BUILD_DIR)/,$(notdir $(C_SOURCES:.c=.o)))
vpath %.c $(sort $(dir $(C_SOURCES)))
# list of ASM program objects
OBJECTS += $(addprefix $(BUILD_DIR)/,$(notdir $(ASM_SOURCES:.s=.o)))
vpath %.s $(sort $(dir $(ASM_SOURCES)))
$(BUILD_DIR)/%.o: %.c Makefile | $(BUILD_DIR)
$(CC) -c $(CFLAGS) -Wa,-a,-ad,-alms=$(BUILD_DIR)/$(notdir $(<:.c=.lst)) $< -o $@
$(BUILD_DIR)/%.o: %.s Makefile | $(BUILD_DIR)
$(AS) -c $(CFLAGS) $< -o $@
$(BUILD_DIR)/$(TARGET).elf: $(OBJECTS) Makefile
$(CC) $(OBJECTS) $(LDFLAGS) -o $@
$(SZ) $@
$(BUILD_DIR)/%.hex: $(BUILD_DIR)/%.elf | $(BUILD_DIR)
$(HEX) $< $@
$(BUILD_DIR)/%.bin: $(BUILD_DIR)/%.elf | $(BUILD_DIR)
$(BIN) $< $@
$(BUILD_DIR):
mkdir $@
#######################################
# clean up
#######################################
clean:
-rm -fR $(BUILD_DIR)
#######################################
# dependencies
#######################################
-include $(wildcard $(BUILD_DIR)/*.d)
# *** EOF ***
Cmake
安装目录
安装目录,可执行文件,动态库等可通过以下变量设置。
安装目录可通过 CMAKE_INSTALL_PREFIX 变量指定。
变量 | 注释 |
---|---|
CMAKE_ARCHIVE_OUTPUT_DIRECTORY | 默认存放静态库的文件夹位置 .a |
CMAKE_LIBRARY_OUTPUT_DIRECTORY | 默认存放动态库的文件夹位置 .so |
LIBRARY_OUTPUT_PATH | 默认存放库文件的位置,如果产生的是静态库并且没有指定CMAKE_ARCHIVE_OUTPUT_DIRECTORY则存放在该目录下,动态库也类似 |
CMAKE_RUNTIME_OUTPUT_DIRECTORY | 存放可执行软件的目录 |
mkdir build && cd build/ && cmake .. -DCMAKE_INSTALL_PREFIX=/home/ubuntu/Disk/ProgrameFiles -DLIBRARY_OUTPUT_PATH=/home/ubuntu/Disk/share/lib -DCMAKE_RUNTIME_OUTPUT_DIRECTORY=/home/ubuntu/Disk/share/bin # 通用配置
添加搜索路径
可以通过 PKG_CONFIG_PATH 指定路径pc文件路径。通过 CMAKE_PREFIX_PATH 指定安装目录文件,CMAKE_PREFIX_PATH 变量与 CMAKE_INSTALL_PREFIX 变量值一致。
例如/home/ubuntu/Disk/programFiles/share/pkgconfig/eigen3.pc,可以指定为/home/ubuntu/Disk/programFiles/share/pkgconfig/
由于PKG_CONFIG_PATH需要其他配置,所以可以自己编写 XXXXXConfig.cmake 文件。文件构成如下:
set(Module_Name "XXXXX")
find_path(${Module_Name}_INCLUDE_DIR /usr/include/ /usr/local/include ${CMAKE_SOURCE_DIR}/ModuleMode)
find_library(${Module_Name}_LIBRARY NAMES add PATHS /usr/lib/add /usr/local/lib/add ${CMAKE_SOURCE_DIR}/ModuleMode)
if (${Module_Name}_INCLUDE_DIR AND ${Module_Name}_LIBRARY)
set(${Module_Name}_FOUND TRUE)
endif (${Module_Name}_INCLUDE_DIR AND ${Module_Name}_LIBRARY)
FindXXX.cmake文件
在Cmake中使用find_package时,会尝试查找FindXXX.cmake 文件。该文件一般存在CMAKE_MODULE_PATH变量中。
gcc
概述
GCC(GNU Compiler Collection)是由 GNU 开发的编程语言编译器。 GCC最初代表“GNU C Compiler”,当时只支持C语言。 后来又扩展能够支持更多编程语言,包括 C++、Fortran 和 Java 等。 因此,GCC也被重新定义为“GNU Compiler Collection”,成为历史上最优秀的编译器, 其执行效率与一般的编译器相比平均效率要高 20%~30%。GCC编译工具链(toolchain),是指以GCC编译器为核心的一整套工具。它主要包含以下三部分内容:
- gcc-core:即GCC编译器,用于完成预处理和编译过程,把C代码转换成汇编代码。
- Binutils :除GCC编译器外的一系列小工具包括了链接器ld,汇编器as、目标文件格式查看器readelf等。
- glibc:包含了主要的 C语言标准函数库,C语言中常常使用的打印函数printf、malloc函数就在glibc 库中。
在很多场合下会直接用GCC编译器来指代整套GCC编译工具链。
GCC 编译工具链在编译一个C源文件时需要经过以下 4 步:
最后将每个源文件对应的目标.o文件链接起来,就生成一个可执行程序文件,这是链接器ld完成的工作。例如一个工程里包含了A和B两个代码文件,在链接阶段, 链接过程需要把A和B之间的函数调用关系理顺,也就是说要告诉A在哪里能够调用到fun函数, 建立映射关系,所以称之为链接。若链接过程中找不到fun函数的具体定义,则会链接报错。
链接分为两种:
- 动态链接:GCC编译时的默认选项。动态是指在应用程序运行时才去加载外部的代码库,不同的程序可以共用代码库。 所以动态链接生成的程序比较小,占用较少的内存。
- 静态链接:链接时使用选项 “–static”,它在编译阶段就会把所有用到的库打包到自己的可执行程序中。 所以静态链接的优点是具有较好的兼容性,不依赖外部环境,但是生成的程序比较大。
在Ubuntu下,可以使用 ldd 工具查看动态文件的库依赖,尝试执行如下命令:
ldd hello # 动态文件会显示依赖库
ldd hello_static # 静态文件会显示 不是动态可执行文件
gcc/g++一些常用指令如下表所示:
指令 | 说明 |
---|---|
-c | 生成目标文件,不进行链接 |
-o | 指定生成的文件名 |
-g | 在目标文件中添加调试信息,便于gdb调试或objdump反汇编 |
-Wall | 显示所有的警告信息 |
-Werror | 视警告为错误,出现警告即放弃编译 |
-w | 不显示任何警告信息 |
-v | 显示编译步骤 |
-On | (n=0,1,2,3) 设置编译器优化等级,O0为不优化,O3为最高等级优化,O1为默认优化等级 |
-L | 指定库文件的搜索目录 |
-l | (小写的L)链接某一库 |
-I | (大写的i)指定头文件路径 |
-D | 定义宏,例如-DAAA=1,-DBBBB |
-U | 取消宏定义,例如-UAAA |
Binutils工具集
在进行程序开发的时候通常不会直接调用这些工具,而是在使用GCC编译指令的时候由GCC编译器间接调用。下面是其中一些常用的工具:
- as:汇编器,把汇编语言代码转换为机器码(目标文件)。
- ld:链接器,把编译生成的多个目标文件组织成最终的可执行程序文件。
- readelf:可用于查看目标文件或可执行程序文件的信息。
- nm : 可用于查看目标文件中出现的符号。
- objcopy: 可用于目标文件格式转换,如.bin 转换成 .elf 、.elf 转换成 .bin等。
- objdump:可用于查看目标文件的信息,最主要的作用是反汇编。
- size:可用于查看目标文件不同部分的尺寸和总尺寸,例如代码段大小、数据段大小、使用的静态内存、总大小等。
glibc库
glibc库是GNU组织为GNU系统以及Linux系统编写的C语言标准库,因为绝大部分C程序都依赖该函数库,该文件甚至会直接影响到系统的正常运行,例如常用的文件操作函数read、write、open,打印函数printf、动态内存申请函数malloc等。
gdb调试
可执行文件要能够被gdb调试,必须在编译时加上调试信息,也即是加上-g选项。生成之后可以通过gdb命令进行调试。
gcc -g hello.c -o hello # 生成带调试信息的可执行文件
gdb hello # 调用GDB调试
启动之后可以看到命令行提示符为(gdb),接着我们就可以在这个gdb的命令行提示符上面输入各种gdb的调试命令了(补充:这里也可以在shell中输入gdb,然后回车,这样直接进入到gdb的调试命令行,之后可以通过file hellp)。
载入待调试的可执行文件之后,在gdb的命令行中输入list或者其简写l可以查看到程序的源码以及行号,输入l之后,默认会显示10行源代码,按回车之后会显示接下来的10行,直到文件的末尾。
在gdb下添加断点使用命令break或简写 b,有下面几个常见用法:
b 函数名
b 行号
b 文件名:行号
b 行号 if条件
加上断点之后,我们可以通过 info break 命令查看断点的信息。通过 disable < number > 来禁用指定Num的断点,通过 enable < number > 可以来解禁断点,用 delete < number > 命令来删除掉一个断点。
可以使用run命令或者简写r来启动程序的执行,如果函数带有参数可以在 run 的收带上参数,也可以 set [ args ] XXX。p < name >/print < name > 可以查看某一个变量的当前值。 next 命令或者n可以单步执行。
我在 func() 函数调用行加上了断点,然后r开始执行程序,之后程序在断点处停住,此时我执行 step 命令或其简写 s 来跳入func()函数内部调试,在内部依然像执行外部调试一样,如果要从函数跳出则执行 finished ,这时会导致函数执行完毕,并且打印出一些函数的返回信息,并且程序停在函数后的第一条语句处。 quit(简写 q ) 退出gdb调试 。
使用 watch < name > 命令可以实现监控变量,使用 info watch 命令可以查看监控的变量。同时 break 所拥有的 enable,disable,delete 等动词对于 watch 依然使用,且用法大同小异。
远程调试
targeet remote < ip > < port > ,根据官方文档可以通过串行总线或者网络连接。连接方法分别如下所示:
gdb hello # 调试可执行文件
gdb hello -tui # 带显示的调试可执行文件
target remote /dev/ttya
targeet remote IP:PORT
交叉编译
交叉编译器与本地编译器使用起来并没有多大区别。同样的C代码文件,使用交叉编译器编译后,生成的hello已经变成了ARM平台的可执行文件,可以通过readelf工具来查看具体的程序信息。
arm-linux-gnueabihf-gcc hello.c –o hello_arm # 编译
readelf -a hello_arm # 查看程序具体信息
编译器还有很多版本,如arm-linux-gnueabi-gcc,本地编译器gcc全名为x86_64-linux-gnu-gcc,这些编译器是有一定的命名规则的:
arch [-os] [-(gnu)eabi(hf)] -gcc
其中的各字段如下表所示:
字段 | 含义 |
---|---|
arch | 目标芯片 |
os | 操作系统 |
gnu | C标准库类型 |
eabi | 应用二进制接口 |
hf | 浮点模式 |
OpenOCD
概述
OpenOCD(Open On-Chip Debugger)开源片上调试器,是一款开源软件,最初是由Dominic Rath同学还在大学期间发起的(2005年)项目。OpenOCD旨在提供针对嵌入式设备的调试、系统编程和边界扫描功能。
OpenOCD的功能是在仿真器的辅助下完成的,仿真器是能够提供调试目标的电信号的小型硬件单元。仿真器是必须的,因为调试主机(运行OpenOCD的主机)通常不具备这种电信号的直接解析功能。
仿真器支持一个或多个传输协议,每个协议涉及不同的电信号,且使用不同的协议栈进行消息传递。市面上有很多种仿真器,并且这些仿真器的命名没有统一的规律。
仿真器有时候会被封装成独立的加密狗,这种称为硬件接口加密狗。一些开发板上面直接集成了硬件接口加密狗,这样可以使开发板通过USB直接连到主机上进行调试。
以STMMCU为例,使用仿真器调试框架如下图所示:
指令
指令 | 缩减指令 | 说明 | 示例 |
---|---|---|---|
--help | -h | 显示帮助 | |
--version | -v | 显示openocd版本 | -f config1.cfg |
--file | -f | 使用配置文件,可以有多个 -f | |
--serach | -s | 在该路径下搜寻配置文件或者脚本 | |
--debug | -d | 设置仿真等级 | |
--log_output | -l | 重定向输出到文件 | |
--command | -c | 运行指令 |
GDB配合
终端1,用于打开OpenOCD
openocd -f board/stm32f429discovery.cfg -c "program Yourtarget.bin verify reset exit 0x08000000"
终端2,用于打开GDB
gdb Yourtarget.elf # 开启GDB
target remote localhost:3333 # 连接openocd服务
monitor reset # 复位MCU,从而让MCU处于确定的状态
load # 往MCU中加载调试文件,也就是常见的烧录过程