stm32 栈回溯工具——功能和原理介绍
以上的工程我放在我的gitee上,代码仓库如下韦东山老师的工具在群里面可以拿到,我这边也提供一下通过网盘分享的文件:百问网单片机RTOS调试专题_CoreDump使用方法.zip链接: https://pan.baidu.com/s/14kjwgfRKtEhwOjBnJRHjRg 提取码: 9g9q如果你只想快速使用,你只需要设置 COREDUMP_SP_SIZE大小为你最大栈的最大值。
之前基于韦东山老师的调试课提供的资料做了些优化,实现了自己的一套栈回溯工具,特分享给大家。
在看了解我这个工具前最好先看一下韦东山老师的课程。
本篇文章需要一些栈回溯的基础知识,如硬件压栈顺序,SP,PC,LR寄存器作用,本工程提供的代码中包含了韦东山老师提供的栈回溯框架。
1.功能
1.可以选择全部输出和有限输出,全部输出的情况下会输出全局变量和静态变量,和栈的内容,
有限输出只输出栈的内容,两者都支持通过bt实现查看栈回溯,全部输出的情况可以查看任意变量的状态,有限输出只能查看调用的堆栈中的局部变量状态。
2.支持FPU和非FPU的栈回溯功能
3.支持开启硬件栈对齐回溯
4.支持裸机和RTOS的栈回溯
如果你看了我上面标注的课程并且实际使用,你可能会觉得只不过是添加了 有限输出并且可以栈回溯的功能,其他的功能不过是对于原有功能的一些不同和修复,事实上确实是这样的,只不过有限输出并且可以栈回溯会带来很多问题和思考,后面我会展现我的修改过程。
2.原理
栈回溯工具的本质就是将 栈的内容打印出来,并且根据axf文件来解析,实现出栈,观察一层一层调用状态。也可以进行手动栈回溯,只需要你将其中的axf文件换成dis汇编文件后,手动来实现入栈出栈的操作,也可以实现。
基于以上原理,我们需要做的事情只有三个
1.将发生hardfault时的栈内容打印出来
2.提供axf文件
3.找一个能够将以上内容实现栈回溯的工具
接下来我将一一回答上面的内容
第一步是最难的,需要将发生hardfault上一时刻的内容输出,那就需要知道发生hardfault是什么流程,根据 Corterx M3 权威指南可以知道,进入hardfault或者其他中断时,ST芯片会自动硬件压栈r0-r3,r12,pc,lr,xpsr一共八个寄存器的值到堆栈中,因此如果我们要知道发生hardfault状态下现场的全部内容,我们还需要保存r4-r11,并且我们需要判断在发生hardfault时是在使用msp还是psp(硬件压栈会压入发生hardfault时使用的堆栈),代码如下
PRESERVE8
THUMB
AREA |.text|, CODE, READONLY
IMPORT DumpCore
EXPORT HardFault_Handler
HardFault_Handler PROC
; 栈帧保存在MSP还是PSP?
TST lr, #0x04 ; if(!EXC_RETURN[2]) // 看看LR寄存器的BIT2是否为0
MRSEQ r0, msp ; [2]=0 ==> 为0则使用MSP
MRSNE r0, psp ; [2]=1 ==> 为1则使用PSP
STMFD r0!, {r4 - r11} ; 软件保存r4 - r11
STMFD r0!, {lr} ; 把LR即exec_return也保存起来
TST lr, #0x04 ; if(!EXC_RETURN[2]) // 看看LR寄存器的BIT2是否为0
MSREQ msp, r0 ; [2]=0 ==> 更新MSP, 下面要恢复MSP
PUSH {lr}
BL DumpCore
POP {lr}
TST lr, #0x04 ; if(!EXC_RETURN[2]) // 看看LR寄存器的BIT2是否为0
POPEQ {lr} ; 恢复MSP
POPEQ {r4 - r11} ; 恢复MSP
BX lr
ENDP
END
以上逻辑很简单,通过汇编重写了hardfault,判断了发生错误时使用的psp还是msp,将内容保存到对应的堆栈把保存内容后的sp作为参数调用DumpCore函数,接下来我们看DumpCore函数。
#include <stdio.h>
#include "mystring.h"
#include "coredump.h"
#include "coredump_interface.h"
extern int * Image$$ER_IROM1$$Base;
extern int * Image$$ER_IROM1$$Length;
extern int * Load$$ER_IROM1$$Base;
extern int * Image$$RW_IRAM1$$Base;
extern int * Image$$RW_IRAM1$$Length;
extern int * Load$$RW_IRAM1$$Base;
extern int * Image$$RW_IRAM1$$ZI$$Base;
extern int * Image$$RW_IRAM1$$ZI$$Length;
extern uint32_t __initial_sp;
extern uint32_t Stack_Size; // 声明符号地址
#if 0
/* text relocate */
memcpy(&Image$$ER_IROM1$$Base, &Load$$ER_IROM1$$Base, &Image$$ER_IROM1$$Length);
/* data relocate */
memcpy(&Image$$RW_IRAM1$$Base, &Load$$RW_IRAM1$$Base, &Image$$RW_IRAM1$$Length);
/* bss clear */
memset(&Image$$RW_IRAM1$$ZI$$Base, 0, &Image$$RW_IRAM1$$ZI$$Length);
#endif
/**********************************************************************
* 函数名称: DumpRegisters
* 功能描述: 打印寄存器的值
* 输入参数: sp - hardfault.s里设置的这个参数,它是栈的地址
* thread - 线程名
* 输出参数: 无
* 返 回 值: 无
* 修改日期 版本号 修改人 修改内容
* -----------------------------------------------
* 2023/09/20 V1.0 韦东山 创建
***********************************************************************/
static void DumpRegisters(pregs sp, char *thread)
{
myputs("Registers@");
myputs(thread);
myputs("\n\r");
myput_s_hex("R0: ", sp->r0); myputs("\n\r");
myput_s_hex("R1: ", sp->r1); myputs("\n\r");
myput_s_hex("R2: ", sp->r2); myputs("\n\r");
myput_s_hex("R3: ", sp->r3); myputs("\n\r");
myput_s_hex("R4: ", sp->r4); myputs("\n\r");
myput_s_hex("R5: ", sp->r5); myputs("\n\r");
myput_s_hex("R6: ", sp->r6); myputs("\n\r");
myput_s_hex("R7: ", sp->r7); myputs("\n\r");
myput_s_hex("R8: ", sp->r8); myputs("\n\r");
myput_s_hex("R9: ", sp->r9); myputs("\n\r");
myput_s_hex("R10: ", sp->r10); myputs("\n\r");
myput_s_hex("R11: ", sp->r11); myputs("\n\r");
myput_s_hex("R12: ", sp->r12); myputs("\n\r");
myput_s_hex("R13(SP): ", (uint32_t)sp + sizeof(*sp)); myputs("\n\r");
myput_s_hex("R14(LR): ", sp->lr); myputs("\n\r");
myput_s_hex("R15(PC): ", sp->pc); myputs("\n\r");
myput_s_hex("xPSR: ", sp->xpsr); myputs("\n\r");
}
#if !COREDUMP_IMPERFECTION_OUT
static void DumpMem(uint32_t addr, uint32_t len)
{
uint32_t *paddr;
uint32_t i;
paddr = (uint32_t *)addr;
myput_s_hex("mem@", addr);
myput_s_hex(",", len);
myputs("\n\r");
for (i = 0; i < len;)
{
myputhex(*paddr);
paddr++;
i+= 4;
if (i % 16 == 0)
myputs("\n\r");
else
myputs(" ");
}
myputs("\n\r");
}
#endif
static void StackMem(uint32_t addr, uint32_t len, uint32_t Offset)
{
uint32_t *paddr;
uint32_t i;
paddr = (uint32_t *)addr;
myput_s_hex("mem@", addr);
myput_s_hex(",", len);
myputs("\n\r");
for (i = 0; i < sizeof(regs);)
{
myputhex(*paddr);
paddr++;
i+= 4;
if (i % 16 == 0)
myputs("\n\r");
else
myputs(" ");
}
paddr = (uint32_t *)((uint8_t *)paddr + Offset);
for (i = sizeof(regs); i < len;)
{
myputhex(*paddr);
paddr++;
i+= 4;
if (i % 16 == 0)
myputs("\n\r");
else
myputs(" ");
}
myputs("\n\r");
}
uint32_t ulOffset;
void DumpCore(pregs sp)
{
#if COREDUMP_USE_COUNTTIME
uint32_t ulTickStart;
ulTickStart = __HAL_TIM_GET_COUNTER(&htim2);
#endif
#if COREDATA_SAVE
FatFs_DeleteFile((TCHAR *)"CoreDump.txt");
#endif
ulOffset = 0;
// Save FPU Registers
if((sp->exc_return & 0x10) == 0)
{
ulOffset = FPU_OFFSET;
myputs("SP Save Fpu Registers, But No OutPut\n\r");
}
if((sp->xpsr & 0x200) == 0x200)
{
ulOffset += ALIGN_OFFSET;
myputs("Hard Align Save Fpu Registers, But No OutPut\n\r");
}
/* 打印寄存器 */
DumpRegisters(sp, "main_thread");
myputs("Stack segment:\n\r");
StackMem((uint32_t)sp, COREDUMP_SP_SIZE + sizeof(*sp), ulOffset);
#if !COREDUMP_IMPERFECTION_OUT
/* 打印数据段 */
myputs("Data segment:\n\r");
DumpMem((uint32_t)&Image$$RW_IRAM1$$Base, (uint32_t)&Image$$RW_IRAM1$$Length);
/* 打印ZI段 */
myputs("ZI segment:\n\r");
DumpMem((uint32_t)&Image$$RW_IRAM1$$ZI$$Base, (uint32_t)&Image$$RW_IRAM1$$ZI$$Length);
#endif
#if COREDATA_SAVE
FatfsBufferWrite((uint8_t *)"CoreDump.txt", (uint8_t *)0, 0, 1);
#endif
#if COREDUMP_USE_COUNTTIME
printf("CoreDump OutPut Time = %0.2f ms \n\r", (__HAL_TIM_GET_COUNTER(&htim2) - ulTickStart) / 10.0);
#endif
CoreDataOut("Enter HardFault, Input Any Reset\n\r");
while (1)
{
uint8_t ch;
if(HAL_UART_Receive(&huart1, &ch,1,0xFFFFFFFF) == HAL_OK)
{
NVIC_SystemReset();
}
};
}
直接从 DumpRegisters(sp, "main_thread"); 开始看,后面我会在对上面对FPU和Stack Align标志位判断进行解析,CoreDump 文件本质上就时输出两组内容,第一组内容时触发hardfault时的现场,第二组内容则是由内存位置和内存长度组成的一串内存数据,事实上,gdb并不在意你输出的内存到底属于RW DATA,ZI DATA还是Stack,他只会根据axf找对应位置的数据,以下是输出的数据格式
Registers@main_thread
R0: 0x0000000E
R1: 0x00000040
R2: 0x0000000D
R3: 0x20000004
R4: 0x1C71C6E0
R5: 0x00000000
R6: 0x200054C0
R7: 0x00000000
R8: 0x200054AC
R9: 0x00000000
R10: 0x00000000
R11: 0x00000000
R12: 0x00000009
R13(SP): 0x20007728
R14(LR): 0x08003E05
R15(PC): 0x080008F8
xPSR: 0x61000000
Stack segment:
mem@0x20007728,0x00000400
0x1C71C6E0 0x00000000 0x200054C0 0x08000951
0x1C71C6E0 0x00000000 0x200054C0 0x08000875
0x1C71C6E0 0x00000000 0x200054C0 0x08000841
0x00000064 0x00000000 0x200054C0 0x08000803
0x0800B468 0x00000001 0x200054C0 0x0800093B
0x0800B468 0x0800857D 0x08008272 0x08008270
0x00000002 0x00000000 0x00000000 0x00000000
Data segment:
mem@0x20000000,0x000000A4
0x0A037A00 0x00000000 0x12345678 0x00000000
0x08003E89 0x08003EF9 0x08003ED3 0x08003F0D
0x08003E9D 0x2F3A3000 0x00000000 0x000016A5
0x0000000F 0x00000001 0x200013BC 0x00000001
0x0800B0D8 0x0800B074 0x0800B01C 0x0800B030
0x0800B040 0x0800B050 0x0800B060 0x0800B0C4
0x0800B0A8 0x0800B098 0x0800B248 0x0800B278
0x0800B28C 0x0800B234 0x0800B2AC 0x0800B258
0x0800B260 0x0800B268 0x0800B2B8 0x0800B270
0x0800B2A4 0x00002222 0x08007C29 0x00000401
0x200054AC
ZI segment:
mem@0x200000A4,0x0000778C
0x40013000 0x00000104 0x00000000 0x00000000
0x00000002 0x00000001 0x00000200 0x00000000
0x00000000 0x00000000 0x00000000 0x0000000A
0x200023F0 0x00001000 0x200023F0 0x00001000
0x00000000 0x00000000 0x00000000 0x00000000
0x00000100 0x00000000 0x40000000 0x000020D0
0x00000000 0xFFFFFFFF 0x00000000 0x00000000
0x00000000 0x00000000 0x00000000 0x00000000
0x00000000 0x00000000 0x00000000 0x00000000
0x00000000 0x01010200 0x01010101 0x00010101
0x40002000 0x00000053 0x00000000 0x0000FFFF
接下来我们看一下,CoreDump是如何打印现场的
typedef struct _regs {
/* 软件保存进来的寄存器 */
uint32_t exc_return;
uint32_t r4;
uint32_t r5;
uint32_t r6;
uint32_t r7;
uint32_t r8;
uint32_t r9;
uint32_t r10;
uint32_t r11;
/* 硬件保存进来的栈帧 */
uint32_t r0;
uint32_t r1;
uint32_t r2;
uint32_t r3;
uint32_t r12;
uint32_t lr;
uint32_t pc;
uint32_t xpsr;
}regs, *pregs;
static void DumpRegisters(pregs sp, char *thread)
{
myputs("Registers@");
myputs(thread);
myputs("\n\r");
myput_s_hex("R0: ", sp->r0); myputs("\n\r");
myput_s_hex("R1: ", sp->r1); myputs("\n\r");
myput_s_hex("R2: ", sp->r2); myputs("\n\r");
myput_s_hex("R3: ", sp->r3); myputs("\n\r");
myput_s_hex("R4: ", sp->r4); myputs("\n\r");
myput_s_hex("R5: ", sp->r5); myputs("\n\r");
myput_s_hex("R6: ", sp->r6); myputs("\n\r");
myput_s_hex("R7: ", sp->r7); myputs("\n\r");
myput_s_hex("R8: ", sp->r8); myputs("\n\r");
myput_s_hex("R9: ", sp->r9); myputs("\n\r");
myput_s_hex("R10: ", sp->r10); myputs("\n\r");
myput_s_hex("R11: ", sp->r11); myputs("\n\r");
myput_s_hex("R12: ", sp->r12); myputs("\n\r");
myput_s_hex("R13(SP): ", (uint32_t)sp + sizeof(*sp)); myputs("\n\r");
myput_s_hex("R14(LR): ", sp->lr); myputs("\n\r");
myput_s_hex("R15(PC): ", sp->pc); myputs("\n\r");
myput_s_hex("xPSR: ", sp->xpsr); myputs("\n\r");
}
你理解了硬件压栈就能理解typedef struct _regs结构体,你理解了typedef struct _regs结构体就能理解如何打印现场,我们之前说过当触发hardfault时,硬件压栈会保存r0-r3,r12,pc,lr,xpsr,而我们会手动保存r4-r11,exc_return,又因为栈是向下增长,所以结构顺序应该是反的,先是exc_return
然后是r4-r11,后面就不需要我说了吧在看回DumpRegisters,这个函数实际上就是通过栈顶指针和结构体的结合,把现场输出。
接下来看打印栈内容的函数
static void StackMem(uint32_t addr, uint32_t len, uint32_t Offset)
{
uint32_t *paddr;
uint32_t i;
paddr = (uint32_t *)addr;
myput_s_hex("mem@", addr);
myput_s_hex(",", len);
myputs("\n\r");
for (i = 0; i < sizeof(regs);)
{
myputhex(*paddr);
paddr++;
i+= 4;
if (i % 16 == 0)
myputs("\n\r");
else
myputs(" ");
}
paddr = (uint32_t *)((uint8_t *)paddr + Offset);
for (i = sizeof(regs); i < len;)
{
myputhex(*paddr);
paddr++;
i+= 4;
if (i % 16 == 0)
myputs("\n\r");
else
myputs(" ");
}
myputs("\n\r");
}
在offset 为0时,实际上就是输出了栈地址,大小以及对应的内容。不为0的情况后面说。
接下来看RW DATA 和 ZI DATA部分内容怎么输出
/* 打印数据段 */
myputs("Data segment:\n\r");
DumpMem((uint32_t)&Image$$RW_IRAM1$$Base, (uint32_t)&Image$$RW_IRAM1$$Length);
/* 打印ZI段 */
myputs("ZI segment:\n\r");
DumpMem((uint32_t)&Image$$RW_IRAM1$$ZI$$Base, (uint32_t)&Image$$RW_IRAM1$$ZI$$Length);
这部分代码十分简单,keil定义了运行域的位置和大小,分别是以下内容
extern int * Image$$ER_IROM1$$Base;
extern int * Image$$ER_IROM1$$Length;
extern int * Load$$ER_IROM1$$Base;
extern int * Image$$RW_IRAM1$$Base;
extern int * Image$$RW_IRAM1$$Length;
extern int * Load$$RW_IRAM1$$Base;
extern int * Image$$RW_IRAM1$$ZI$$Base;
extern int * Image$$RW_IRAM1$$ZI$$Length;
输出的内容即是存储在RAM中的全局变量和静态变量
以上基本原理就讲完了,接下来是我做的一些优化
ulOffset = 0;
// Save FPU Registers
if((sp->exc_return & 0x10) == 0)
{
ulOffset = FPU_OFFSET;
myputs("SP Save Fpu Registers, But No OutPut\n\r");
}
if((sp->xpsr & 0x200) == 0x200)
{
ulOffset += ALIGN_OFFSET;
myputs("Hard Align Save Fpu Registers, But No OutPut\n\r");
}
第一个if用于判断在堆栈中是否保存了FPU的内容,arm gdb是一个通用的arm框架,不支持某些特性,比如FPU,因此如果保存了FPU中的内容,需要跳过这部分内容,否则影响栈回溯
第二个if用于判断在进行硬件压栈时,是否执行了硬件对齐,由于AAPCS 规则,Cortex-M3要求中断入口时栈必须8字节对齐,因此如果不满足该条件,会硬件填充4字节保证8字节对齐,并且会在执行中断压栈前将 xpsr 的bit9 置为1,并会将此时的xpsr保存到sp中,之后在进入中断后恢复xpsr,有熟悉xpsr寄存器的兄弟要问了,我记得xpsr寄存器的bit 9不是保留嘛,是的,我看的时候也只看到了关于保留的描述,但我在ARM V7 架构手册,ARM V6 架构手册,以及 Cortex M3 技术手册中确实找到了这个描述
以下是ARMv 7 架构手册648页

以下是Cortex M3 技术手册,链接为

我实际验证也确实是这个样子,刚开始遇到这个问题问了很多ai都左右脑互搏
以上内容是兼容FPU和硬件压栈对齐的优化
接下来的内容是关于 仅输出栈地址依旧可以栈回溯的优化
myputs("Stack segment:\n\r");
StackMem((uint32_t)sp, COREDUMP_SP_SIZE + sizeof(*sp), ulOffset);
在以上代码中,栈地址输出的实际地址为保存了现场后的位置,并且输出的栈大小为设置长度+保存现场的字节,后续在通过 uloffset 跳过了FPU和硬件栈对齐的内容,原先的代码如下
myputs("Stack segment:\n");
DumpMem((uint32_t)sp + sizeof(*sp), 1024);
原来的代码没有输出保存了现场的寄存器,导致如果只输出栈信息无法进行栈回溯
除此以外,我还调整了ZI Data 和 Stack Data 打印的顺序,由 RW Data -> ZI Data -> Stack Data 变成Stack Data -> RW Data -> ZI Data,为什么要这样,首先我们要知道在ZI data中的内容实际上是包含了堆栈的大小的,因此我们输出堆栈信息是会和ZI Data 重合的,平时这个不影响,因为即使重合数据也是一样的,不影响实际使用,但是当出现FPU压栈或者硬件对齐时,ZI Data和栈中的内容就不一样了,此时如果先输出ZI Data中的内容,则会按照ZI Data中栈的信息进行回溯,因此需要优先输出栈的内容。
3.总结
以上的工程我放在我的gitee上,代码仓库如下
zxy/W25QXX_Templent
韦东山老师的工具在群里面可以拿到,我这边也提供一下
通过网盘分享的文件:百问网单片机RTOS调试专题_CoreDump使用方法.zip
链接: https://pan.baidu.com/s/14kjwgfRKtEhwOjBnJRHjRg 提取码: 9g9q
如果你只想快速使用,你只需要设置 COREDUMP_SP_SIZE大小为你最大栈的最大值
COREDATA_OUT和COREDATA_SAVE适用控制是打印还是保存数据,并且需要在coredump_interface.c中实现
COREDUMP_IMPERFECTION_OUT为1时仅输出栈的内容,否则输出全部,如果要输出全部内容,往往 全局变量极大最后的数据也非常多,因此这边也建议打开该宏
COREDUMP_USE_COUNTTIME 是我计算堆栈信息输出和保存时间的一个宏,直接设置为0 即可
#define ALIGN_OFFSET (4)
#define FPU_OFFSET (72)
#define COREDUMP_SP_SIZE (1024)
#define COREDATA_OUT (0)
#define COREDATA_SAVE (1)
#define COREDUMP_IMPERFECTION_OUT (0)
#define COREDUMP_USE_COUNTTIME (1)
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include "tim.h"
#include "usart.h"
#include "file_operate.h"
#include "driver_w25qxx_basic.h"
void CoreDataOut(char *ucpData);
void CoreDataSave(char *ucpData);
以下是实现的CoreDataOut 和 CoreDataSave的接口根据个人设备实现即可
#include "coredump_interface.h"
void CoreDataOut(char *ucpData)
{
#if COREDATA_OUT
uint32_t ulLen;
ulLen = strlen(ucpData);
HAL_UART_Transmit(&huart1, (const uint8_t *)ucpData, ulLen, 0xFFFF);
#endif
}
void CoreDataSave(char *ucpData)
{
#if COREDATA_SAVE
uint32_t ulLen;
ulLen = strlen(ucpData);
FatfsBufferWrite((uint8_t *)"CoreDump.txt", (uint8_t *)ucpData, ulLen, 0);
#endif
}
最后记得把原来的hardfault注释,否则会报错
少的图后面会补,后期可能还会来一篇简单的移植和使用教程
以上
更多推荐



所有评论(0)