ARM GNU 编译与链接01: 工程创建, 程序烧写和调试
基于 STM32 平台, 对编译与链接原理进行探究,以及学习 ARM 汇编指令集, GNU 的汇编语法。
从零开始的工程创建
这里以一个简单的汇编程序为例
-
源代码 start.s /* 代码段 */
.section .text
.type reset, %function
.globl reset
/* 程序入口 */
reset:
mov r0, #0x66
mov r1, #255
push {r0}
mov r0, r1
pop {r0}
b .
/* 中断向量表段 */
.section .isr_vector, "a"
.word _estack
.word reset
-
链接文件:link.ld /* 程序入口 */
ENTRY(reset)
/* 栈指针初始位置 */
_estack = 0x20020000;
/* 存储器分布 */
MEMORY
{
DTCMRAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K
FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 128K
}
/* 段组织 */
SECTIONS
{
/* FLASH 起始地址放置中断向量表 */
.my_vertor :
{
/* 中断向量表的数据一般是由CPU中断自动调用的, 在程序中有可能没被引用,
容易被GCC的优化程序当成垃圾回收, 因此使用 KEEP 保持原样不变 */
. = ALIGN(4);
KEEP(*(.isr_vector))
. = ALIGN(4);
} >FLASH
/* 代码段紧随其后 */
.text :
{
. = ALIGN(4);
*(.text)
. = ALIGN(4);
} >FLASH
}
上述定义了两个段, 分别是代码段和中断向量表段。在 STM32 中 FLASH的起始地址为0x08000000, 可以通过设置BOOT0引脚来设置程序从FLASH启动, 这时复位后 MCU 在执行完内固化 程序后将会从0x08000000 地址加载 sp 指针, 并从 0x08000004 加载 pc 指针并转跳。 因此上面定义中断向量表段, 包含了栈指针初值和入口地址, 这个栈指针初值其实就是RAM内存的末 尾, 因为 ARM 的入栈时sp指针自减, 放在末尾保证最开始初始化时有足够大的栈空间。
-
编译文件: Makefile ######################################
# 项目设置
######################################
TARGET = demo
OPT = -g -gdwarf-2 -Og
BUILD_DIR = build
######################################
# 源代码区
######################################
C_SOURCES =
ASM_SOURCES = start.s
#######################################
# GNU 工具链
#######################################
PREFIX = arm-none-eabi-
CC = $(PREFIX)gcc
AS = $(PREFIX)gcc -x assembler-with-cpp
CP = $(PREFIX)objcopy
SZ = $(PREFIX)size
HEX = $(CP) -O ihex
BIN = $(CP) -O binary -S
#######################################
# 编译参数
#######################################
# MCU
CPU = -mcpu=cortex-m7
FPU = -mfpu=fpv5-d16
FLOAT-ABI = -mfloat-abi=hard
MCU = $(CPU) -mthumb $(FPU) $(FLOAT-ABI)
# 宏定义
AS_DEFS =
C_DEFS =
# 头文件路径
AS_INCLUDES =
C_INCLUDES =
# 编译标志
ASFLAGS = $(MCU) $(AS_DEFS) $(AS_INCLUDES) $(OPT) -Wall -fdata-sections -ffunction-sections
CFLAGS = $(MCU) $(C_DEFS) $(C_INCLUDES) $(OPT) -Wall -fdata-sections -ffunction-sections
# 生成C语言的头文件依赖信息
CFLAGS += -MMD -MP -MF $(BUILD_DIR)/$*.d
-include $(wildcard $(BUILD_DIR)/*.d)
#######################################
# 链接信息
#######################################
LDSCRIPT = link.ld
LIBS = -lc -lm -lnosys
LIBDIR =
LDFLAGS = $(MCU) -specs=nano.specs -T$(LDSCRIPT) $(LIBDIR) $(LIBS) -Wl,-Map=$(BUILD_DIR)/$(TARGET).map,--cref -Wl,--gc-sections
#######################################
# 构建程序
#######################################
# 列出所有目标文件
OBJECTS = $(addprefix $(BUILD_DIR)/,$(notdir $(C_SOURCES:.c=.o)))
vpath %.c $(sort $(dir $(C_SOURCES)))
OBJECTS += $(addprefix $(BUILD_DIR)/,$(notdir $(ASM_SOURCES:.s=.o)))
vpath %.s $(sort $(dir $(ASM_SOURCES)))
# 默认动作: 编译所有
all: $(BUILD_DIR)/$(TARGET).elf $(BUILD_DIR)/$(TARGET).hex $(BUILD_DIR)/$(TARGET).bin
$(BUILD_DIR)/%.o: %.c Makefile | $(BUILD_DIR)
@echo "$< -> $@"
@$(CC) -c $(CFLAGS) -Wa,-a,-ad,-alms=$(BUILD_DIR)/$(notdir $(<:.c=.lst)) $< -o $@
$(BUILD_DIR)/%.o: %.s Makefile | $(BUILD_DIR)
@echo "$< -> $@"
@$(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:
-rm -fR $(BUILD_DIR)
Makefile 这么长是为了向后面程序兼容, 后面写多文件的话只需设置C_SOURCES 和 ASM_SOURCES 文件就可以了. Makefile 详情参考
-
下载和调试文件 openocd.cfg # 设置调试器和MCU, openocd 已经给我们写好了
source [find interface/stlink.cfg]
source [find target/stm32h7x.cfg]
# 复位设置, 对于 ST-link SWD 模式, 必须设置才能软复位
reset_config none separate
# 下载程序
program build/demo.hex verify
openocd 的使用详情参考
编译
$ ls
link.ld Makefile openocd.cfg start.s
$ make
start.s -> build/start.o
arm-none-eabi-size build/demo.elf
text data bss dec hex filename
28 0 0 28 1c build/demo.elf
arm-none-eabi-objcopy -O ihex build/demo.elf build/demo.hex
arm-none-eabi-objcopy -O binary -S build/demo.elf build/demo.bin
下载和开启调试服务
$ openocd
...
** Programming Started **
...
** Programming Finished **
** Verify Started **
** Verified OK **
...
Info : Listening on port 6666 for tcl connections
Info : Listening on port 4444 for telnet connections
使用GDB进行调试:
$ arm-none-eabi-gdb build/demo.elf
...
Reading symbols from build/demo.elf...
(gdb) target remote localhost:3333
reset () at start.s:10
10 mov r0, #0x66
(gdb) n
halted: PC: 0x0800000a
11 mov r1, #255
使用 VSCode
使用 VSCode 进行调试需要设置 launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "(gdb) 启动",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/build/demo.elf",
"args": [],
"stopAtEntry": true,
"cwd": "${fileDirname}",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"miDebuggerPath": "arm-none-eabi-gdb",
"setupCommands": [
{"text": "set remotetimeout 5"},
{"text": "target extended-remote localhost:3333"},
{"text": "monitor reset halt"},
]
}
]
}
确保使用 openocd 打开调试服务, 然后开始调试如图所示
发现 VSCode 没能在汇编上打断点, 不过可以在左边栏手动添加断点标签, 比如上面在reset标签打了断点, 所以程序就在入口出停了下来, 这时观察左边栏的寄存器信息, 发现 sp指针正是 0x20020000, pc 指针是 0x8000008, 因为中断向量表只放了2 个word数据, 也就是8字节, 在 link.ld 中 中断向量表后面立刻是程序了, 所以程序复位后pc值就是0x8000008。 同时我们可以在下面的输出栏的调试控制台对GDB发送命令来查看更多信息, 比如打印FLASH初始地址的内容 -exec x /4x 0x8000000 , 查看某个程序的汇编内容 -exec disassemble reset 成功进入调试后, 单步运行查看寄存器和pc、sp指针的变化, 理解这个过程
|