1. 前言
本文主要记录一次调试代码crash的过程,在这个过程中得到了同事MikeZheng的指导,在此表示感谢!
ARM : cotext-m4
RTOS: Zephyr
2. 问题描述
调试过程中发现执行reset操作导致了代码崩溃,打印的信息如下:
***** USAGE FAULT *****
Illegal use of the EPSR
**** Unknown Fatal Error 0! ****
Current thread ID = 0xc003ad40
Faulting instruction address = 0x0
Fatal fault in thread 0xc003ad40! Aborting.
遇到这个问题我们首先会想到的是去查看当前调用栈的现场信息,我们通过keil查看:
很不幸,调用栈信息里面什么都没有,程序已经跑飞。
3. 背景知识
通过前面的问题描述,可知此时发生了异常导致系统crash,根据armv7m_arm手册对异常的进入和返回都做了描述。
异常进入
当发生异常的时候,硬件保存当前CPU上下文到SP寄存器指示的栈中,此时的SP既可以指向PSP(线程栈),也可以指向MSP(异常栈),这由发生异常时cpu所处的工作模式决定的,如果异常发生在线程上下文,则当前的CPU上下文将保存在PSP,如果异常发生在异常上下文,则当前的CPU上下文将保存在MSP寄存器,且以降序保存,异常发生时保存的CPU上下文主要包括:xPSR, ReturnAddress, LR(R14), R12, R3, R2, R1, and R0.
On preemption of the instruction stream, the hardware saves context state onto a stack pointed to by one of the SP
registers, see The SP registers on page B1-516. The stack used depends on the mode of the processor at the time of
the exception.
The stacked context supports the ARM Architecture Procedure Calling Standard (AAPCS). This means the
exception handler can be an AAPCS-compliant procedure.
The ARMv7-M architecture uses a full-descending stack, where:
• When pushing context, the hardware decrements the stack pointer to the end of the new stack frame before
it stores data onto the stack.
• When popping context, the hardware reads the data from the stack frame and then increments the stack
pointer.
When pushing context to the stack, the hardware saves eight 32-bit words, comprising xPSR, ReturnAddress, LR
(R14), R12, R3, R2, R1, and R0.
异常返回
中关于异常返回部分的描述,异常返回将会把0xFXXXXXXX写入到PC,而在cotex-m4中实际会写入LR寄存器。
An exception return occurs when the processor is in Handler mode and one of the following instructions loads a
value of 0xFXXXXXXX into the PC:
• POP/LDM that includes loading the PC.
• LDR with PC as a destination.
• BX with any register
4. 分析过程
通过如上打印可以看出出现异常的指令地址为 0地址,什么时候才会到0地址取指呢?
通过搜索“Fatal fault in thread” 关键字,我们最终找到如下的调用流程:
__hard_fault
_Fault
_NanoFatalErrorHandler
_SysFatalErrorHandler(Sys_fatal_error_handler.c)
__hard_fault位于fault_s.S文件中,此时我们想到的是在异常处理函数入口halt cpu来查看异常现场
重现问题后,停在了断点位置,此时的寄存器信息如下:
根据前述异常返回时EXC_RETURN的值,从LR的信息我们看出值为0XFFFFFFED,意味着执行进程上下文而发生的异常(非异常上下文发生的异常),因而我们进一步通过PSP查看当前的压栈信息:
我们看一下crash现场的pc值为0X0, 这个与我们crash现场打印一致,我们只能继续看LR,值为266c7(与实际地址差1,实际为266c6)
通过反汇编指令:
fromelf -text -a -c -output=xxx.dis out\xxx.axf
可以得到反汇编文件
我们通过LR地址266c6可以找到如下:
也就是发生异常时的地址为266c4 BLX r7
我们进入gpio_pin_write:
static inline int gpio_pin_write(struct device *port, u32_t pin,
u32_t value)
{
return gpio_write(port, GPIO_ACCESS_BY_PIN, pin, value);
}
__syscall int gpio_write(struct device *port, int access_op, u32_t pin,
u32_t value);
static inline int _impl_gpio_write(struct device *port, int access_op,
u32_t pin, u32_t value)
{
const struct gpio_driver_api *api =
(const struct gpio_driver_api *)port->driver_api;
return api->write(port, access_op, pin, value);
}
由于发生异常的地址是BLX r7,即调用了某一个函数,且函数地址为0,因此我们大胆猜测api->write地址为零,通过加打印判断验证了我们的想法,api->write的确为空。
看到这里,我们其实已经大概知道了发生异常的代码位置,实际是在代码里做了一个赋值为空的操作,后面调用gpio_pin_write的时候就会引发崩溃
正是这个操作引发了crash,这里我们继续追一下api->write为何为空?
(1)r0实际就是参数port,而此处已经被赋值为0,因此R4也为0
(2)根据下面的device结构体的定义,r4+4就是driver_api,也就是r9,也就是api_funcs的地址
(3)r9+4为write的地址,也就是r7,而r7地址为空
struct device
{
struct device_config *config;
const void *driver_api;
void *driver_data;
#ifdef CONFIG_DEVICE_POWER_MANAGEMENT
volatile int pm_device_can_be_suspended_flag;
volatile int pm_device_suspend_state;
#endif
};
static const struct gpio_driver_api api_funcs =
{
.config = gpio_gm_config,
.write = gpio_gm_write,
.read = gpio_gm_read,
.manage_callback = gpio_gm_manage_callback,
.enable_callback = gpio_gm_enable_callback,
.disable_callback = gpio_gm_disable_callback,
};
参考文档
- ARM Cortex-M 异常-HardFault(UsageFault) INVPC置1解决过程
- ARM®v7-M Architecture Reference Manual