STM32的backtrace深度讲解(cortex-m的栈布局与栈回溯的原理和方案)

发布者:诚信与爱最新更新时间:2024-01-04 来源: elecfans关键字:STM32 手机看文章 扫描二维码
随时随地手机看文章
  • 1.说明

  • 2.cortex-m上的栈布局

    • 2.1 cortex-m上的寄存器

    • 2.2 cortex-m上的自动压栈

    • 2.3 cortex-m上的函数执行流程

  • 3.cmbacktrace原理分析

    • 3.1 问题分析

  • 4.实际应用

  • 5.总结

1.说明

对于一个嵌入式产品的开发流程来说,一般都需要经过如下几个阶段:

1.方案预研

2.产品功能设计

3.开发调试

4.工厂测试

5.产品上线售后

一般来说,1,2,3板子都是在开发者手上,一旦遇到bug,只要可以复现,基本上都可以排查出来,然后修复或者规避。但一旦进入到4,5阶段,产品已经成型之后,再想排查BUG就比较麻烦了。例如工厂测试阶段,有可能连续运行好几天或者好几个星期才能复现的问题,排查起来就十分的复杂。对于这种情况,backtrace是十分必要的。可以在离线的状态下分析系统的关键信息,通过函数的栈回溯,从而找到出错的对应的执行函数,然后结合程序设计,基本上大部分的bug基本上也可以找到。我之前写过一篇文章arm上backtrace的分析与实现原理。分析了在cortex-a上的分析情况。但是对于cortex-m来说,问题就会复杂许多,因为cortex-m对于固件的体积的限制以及特殊的架构,让backtrack的方案占用了过大的flash。这是设计者所不能接受的,而且更加难受的是cortex-m并没有栈回溯指针。这就让栈的深度的计算变的十分复杂。本文主要分析cortex-m的栈布局以及一些栈回溯的底层原理和方案。

2.cortex-m上的栈布局

在cortex-m上弄清楚栈的布局,就必须理解cortex-m上的压栈入栈的机制和原理。下面从该体系架构上说说cortex-m上比较重要的细节。

2.1 cortex-m上的寄存器

一旦涉及到C语言函数,必须要考虑到的问题就是函数的入栈出栈的问题,也就是SP指针的增加或者减少。下面还是来复习一下arm cortex-m上的寄存器。

按照arm cortex-m的设计,一共有32个寄存器。

  • 13个通用寄存器,r0-r12
  • 2个不同模式下使用的SP, PSP(SP_process) 和MSP(SP_main)
  • 1个链接寄存器LR(r14)
  • 1个程序计数器(PC)
  • 1个程序状态寄存器(xPSR)

在不同的模式下,R0-R12、SP、LR是各有一份的,所以这样算下来,总共是32个寄存器,但是在不同的模式下,并不能完全看到这32个寄存器的状态,只能看到其中的一部分。

通用寄存器R0-R12

上图将通用寄存器分为low register和high registers就是根据指令集来说的,对于thumb指令,是16位的,只能访问到low register,也就是R0-R7,而对于32位的arm指令,是所有的指令都可以访问到。所以有这样的划分。

栈指针SP

一旦涉及到参数的压栈与入栈,或者函数的执行返回的时候,必须会涉及到栈指针的变化。在cortex-m由于涉及到两种不同的sp的切换,所以在使用SP的时候要格外的小心。

程序链接寄存器LR

程序的链接寄存器在函数返回的时候会被使用到,比如一个函数A中执行的另外一个函数B,如下

void fun_A()
{
 fun_B()
}

那么当执行到fun_B的时候,首先编译器编译的汇编代码会将func_A的地址自动存放LR压栈,然后压入其他的参数。待func_B执行完成之后,会弹出LR到PC,此时就会返回到fun_A函数去执行了。

程序计数寄存器

该寄存器会自动指向当前指向的程序地址。

2.2 cortex-m上的自动压栈

不同于其他的处理器架构,cortex-m的定位一开始就是为实时性、小体积容量的设计考虑的,所以在中断处理这一块,也做了一个十分有意思的设计--自动压栈处理。

一般的CPU进入中断后都会去进行压栈操作,因为栈就是函数的现场,保护了栈内容,中断退出的时候只需要恢复栈数据就可以恢复到程序执行的状态了。以往这个阶段都是通过人工操作写程序完成的,在cortex-m上,将部分栈由硬件自动压入。其压入栈的顺序一般如下:

xPSR->PC(返回地址)->LR->R12->R3->R2->R1->R0

这些寄存器硬件自动压入,效率上应该有较大的提升。另外的一些寄存器可以手动处理。

2.3 cortex-m上的函数执行流程

在分析函数的执行的时候,主要是想弄清楚底层的硬件寄存器做了哪些操作,这就需要进行汇编翻译进行。此处我们用arm gcc编译出cortex-m的elf固件,通过objdump随便看一个函数体的执行。

对于一个arm函数的汇编代码,基本上都是上面的执行逻辑。根据指令机器码,得到对应的指令。

我们知道,在函数执行的时候,保存在内存上的都是机器码,只有在通过objdump工具的时候,才会将这些机器码变成程序。也就是说,在程序执行时,如果此时查看0x8004794这个地址,看到的数据是80b5 84b0这样的内容。那么这些又该如何进行翻译呢?该函数的sp指针到底该如何计算。

PUSH指令分析

PUSH指令所对应的机器码如下:

1011 010R rrrr rrrr -- PUSH reg_list

按照解析,R表示的是LR寄存器,后面的是R0-R7寄存器的列表。所以解释起来机器码b580翻译成二进制b1011 0101 1000 0000。对应的实际含义就是压入LR与R7寄存器,当执行PUSH后,SP指针会自动减去两个寄存器的大小,也就是8个字节。

SUB指令分析

SUB指令对应的机器码如下:

1011 0000 1vvv vvvv -- SUB Sp,#immed_7*4

根据含义,v表示分别乘以4。也就是最低位为4,第二位是8,第三位是16,第四位为32,以此类推,得到其偏移的立即数。目前的机器码为b084 翻译成二进制为b1011 0000 1000 0100,所以表示的立即数为16.

两者结合,得到当前函数会使得sp指针的值减少16+8=24。

3.cmbacktrace原理分析

在做cortex-m上的backtrace的时候,查阅了一些资料,其中发现一个CmBacktrace。

https://github.com/armink/CmBacktrace

设计的目的:针对 ARM Cortex-M 系列 MCU 的错误代码自动追踪、定位,错误原因自动分析的开源库。

其实现的机理是利用cortex-m的压栈特性所决定的。当指定好栈地址后,sp指针就会在自己的栈空间内进行偏移。函数入栈的时候,会压入参数,也会压入lr寄存器,利用lr寄存器的值就可以找到是谁调用该函数的。

对于裸机情况,栈地址指向一个

当程序出现异常的时候,只需知道当前的栈顶以及当前的sp的偏移量,这些在程序中是很好得到的。然后开始便利栈中的数据,每四个字节遍历一次得到地址,该地址不一定是函数地址,有可能是参数的地址,人工去审阅这些地址的时候,只要细心一点是可以找到线索的。在CmBacktrace上通过判断地址的前面2个字节的thumb指令的机器码是否为BL或者bLx来进行判断该地址是否为函数。这样也是可以的。

如果在cortex-m上使用了操作系统,原理上基本上是类似的,由于每个线程都会有自己的线程栈,所以会有多个线程栈的情况。要想得到当前运行的线程栈的backtrack,原理上是和裸机一样。但是如果想要分析其他的线程的栈的backtrace,则需要注意操作系统的压栈问题。

例如在rt-thread中,进行线程切换的时候,会调用pendsv进行自动压栈一次,然后在手动压栈其他的寄存器。如果要做解析,首先去掉前面操作系统压栈的部分。rt-thread操作系统前面压栈的数据

#  xPSR->PC->LR->R12->R3->R2->R1->R0#  R11 R10 R9 R8 R7 R6 R5 R4 FLAG

一共压了16个寄存器,如果不做处理,解析到的PC为rt_hw_interrupt_enable,解析到的LR为rt_schedule。

3.1 问题分析

在对栈的解析过程中,我们往往会涉及到一些脏数据来破坏我们的分析。比如,参数中传递东西是函数的地址,这是读到的可能会误以为这是LR,这样分析起来会有一定的风险,虽然说在大多数情况下CmBacktrace的解析可以做的很好,但是遇到参数是函数地址的时候,就很难去做分析了,此时可能会借助人工来做分析。需要一定的工作量。那么有没有比较想的办法,不需要便利,直接跳转到下一个LR去执行呢?

根据在《2.3 cortex-m上的函数执行流程》的分析,我们基本上可以算出来一个函数的栈数据偏移,这样就可以顺利的解决这个问题了。每次都会跳转到固定的函数中,结合当前的数据栈的内容,从而得到想要的结果。

4.实际应用

上述的分析是有实际应用的价值的,在每次出错的情况下,我们可以保存栈的数据到掉电非易失性存储介质的某个特定的地址处,因为栈的大小并不会很大,一般512字节或者1k或者2k等等数据量,问题出现后,取出栈里面的内容,然后通过外部工具例如python脚本进行分析,与对应的elf文件结合起来,就能很准确的定位函数的backtrace了。然后对于问题的查询也会变得有迹可循,大大减少后期调试工作的复杂性。

5.总结

未雨绸缪是设计中必须考虑的问题,做出的产品都不能保证一点问题都不会出现,当出现问题的时候,也不用怕,因为有了分析的手段和数据。这样也能够减少产品设计的风险,做出更好用的嵌入式产品。


关键字:STM32 引用地址:STM32的backtrace深度讲解(cortex-m的栈布局与栈回溯的原理和方案)

上一篇:嵌入式开发:如何使用机智云+STM32F407+ESP8266+freeRTOS设计
下一篇:一文读懂STM32之独立看门狗/窗口看门狗的原理

推荐阅读最新更新时间:2024-11-12 21:07

STM32学习笔记之堆空间
1.堆和栈大小 定义大小在startup_stm32f2xx.s Stack_Size EQU 0x00000400 AREA STACK, NOINIT, READWRITE, ALIGN=3 Stack_Mem SPACE Stack_Size __initial_sp ; h Heap Configuration ; o Heap Size (in Bytes) 0x0-0xFFFFFFFF:8 ; /h Heap_Size EQU 0x00000200 AREA HEAP, NOINIT, READWRITE, ALIGN=3 __heap_base
[单片机]
<font color='red'>STM32</font>学习笔记之堆<font color='red'>栈</font>空间
STM32调试过程中常见的问题及解决方法?
STM32调试过程中常见的问题及解决方法 一、 在 Debug选项卡 下设置好仿真器的类型后,下载程序时却提示 No ULINK Device found. 解决办法: Keil MDK默认使用ULINK仿真器下载程序,在 Project --- Option for Target 'xxx' --- Utilities选项卡 下把编程所使用的仿真器改为相应的类型即可。 二、 编译工程时提示如下信息: main.axf: Error: L6218E: Undefined symbol __BASEPRICONFIG (referred from stm32f10x_nvic.o). main.axf: E
[单片机]
STM32外部中断详解
一、基本概念 STM32可支持68个中断通道,已经固定分配给相应的外部设备,每个中断通道都具备自己的中断优先级控制字节PRI_n(8位,但是STM32中只使用4位,高4位有效),每4个通道的8位中断优先级控制字构成一个32位的优先级寄存器。68个通道的优先级控制字至少构成17个32位的优先级寄存器。 4bit的中断优先级可以分成2组,从高位看,前面定义的是抢占式优先级,后面是响应优先级。按照这种分组,4bit一共可以分成5组 第0组:所有4bit用于指定响应优先级; 第1组:最高1位用于指定抢占式优先级,后面3位用于指定响应优先级; 第2组:最高2位用于指定抢占式优先级,后面2位用于指定响应优先级; 第3组
[单片机]
STM32 SPI DMA 的使用
一是想总结一下SPI总线的特点与注意点,二是总结一下SPI DMA的使用 一、SPI信号线说明   通常SPI通过4个引脚与外部器件相连: MISO:主设备输入/从设备输出引脚。该引脚在从模式下发送数据,在主模式下接收数据。 MOSI:主设备输出/从设备输入引脚。该引脚在主模式下发送数据,在从模式下接收数据。 SCK:串口时钟,作为主设备的输出,从设备的输入 NSS:从设备选择。这是一个可选的引脚,用来选择主/从设备。它的功能是用来作为 片选引脚 ,让主设备可以单独地与特定从设备通讯,避免数据线上的冲突。 二、原理   MOSI脚相互连接,MISO脚相互连接。这样,数据在主和从之间串行地传输(MSB位在前)。  
[单片机]
<font color='red'>STM32</font> SPI DMA 的使用
STM32之GPIO学习笔记
复位后,调试引脚处于复用功能上拉/下拉状态: PA15:JTDI处于上拉状态 PA14:JTCK/SWCLK处于下拉状态 PA13:JTMS/SWDAT处于下拉状态 PB4:NJTRST处于上拉状态 PB3:JTDO处于浮空状态
[单片机]
<font color='red'>STM32</font>之GPIO学习笔记
基于STM32的平衡小车设计过程分享(1)
一、简介 接触STM32开发一段时间了,想用STM32做一个有意思的项目,经历了无数的调参调参再调参,终于让它站稳了,接一下就一步步的跟大家介绍一下,项目的整体实现过程— 二、项目介绍 STM32平衡小车是一种基于STM32芯片的智能小车,它可以通过自动控制来保持平衡,使其可以在不同的地形上稳定行驶。其使用范围非常广泛。需要用到一些基本的硬件组件,例如电机、轮子、陀螺仪、加速度计、电池等。通过设计的电路板进行连接,组成一个完整的系统。 三、硬件设计 根据上述需求,我进行了电路图设计 四、软件设计 4.1电机驱动编写 4.1.1电机引脚说明 编码电机 引脚说明: M1电机电源线(12V) GND编码器地线 C
[单片机]
基于<font color='red'>STM32</font>的平衡小车设计过程分享(1)
STM32调试DEBUG时,需要知道的知识!
学习STM32开发,肯定少不了DEBUG调试这一步骤。那么,本文带你了解一下这个调试相关的知识。 本文以STM32F1、Cortex-M3为例,其它系列芯片或内核,原理相同或类似。 1概况 在STM32中,有很多调试组件。使用它们可以执行各种调试功能,包括断点、数据观察点、 闪存地址重载以及各种跟踪。 STM32F1使用Cortex-M3内核,该内核内含硬件调试模块,支持复杂的调试操作。 硬件调试模块允许内核在取指(指令断点)或访问数据(数据断点)时停止。内核停止时,内核的内部状态和系统的外部状态都是可以查询的。完成查询后,内核和外设可以被复原,程序将继续执行。 当STM32F10x微控制器连接到调试器并开始
[单片机]
一步步写STM32 OS【四】OS基本框架
一、上篇回顾 上一篇文章中,我们完成了两个任务使用PendSV实现了互相切换的功能,下面我们接着其思路往下做。这次我们完成OS基本框架,即实现一个非抢占式(已经调度的进程执行完成,然后根据优先级调度等待的进程)的任务调度系统,至于抢占式的,就留给大家思考了。上次代码中Task_Switch实现了两个任务的切换,代码如下: void Task_Switch() { if(g_OS_Tcb_CurP == &TCB_1) g_OS_Tcb_HighRdyP=&TCB_2; else g_OS_Tcb_HighRdyP=&TCB_1; OSCtxSw(); } 我们把要切换任务指针付给跟_OS_Tcb_High
[单片机]
一步步写<font color='red'>STM32</font> OS【四】OS基本框架
小广播
设计资源 培训 开发板 精华推荐

最新单片机文章
何立民专栏 单片机及嵌入式宝典

北京航空航天大学教授,20余年来致力于单片机与嵌入式系统推广工作。

换一换 更多 相关热搜器件

 
EEWorld订阅号

 
EEWorld服务号

 
汽车开发圈

电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号 Copyright © 2005-2024 EEWORLD.com.cn, Inc. All rights reserved