gpt4 book ai didi

gcc - 使用 GCC 编译器的 ARM 核心堆栈回溯(当有 MSP 到 PSP 切换时)

转载 作者:行者123 更新时间:2023-12-05 03:07:26 31 4
gpt4 key购买 nike

核心 - ARM Cortex-M4

编译器——GCC 5.3.0 ARM EABI

操作系统 - 免费实时操作系统

我正在使用 gcc 库函数 _Unwind_Reason_Code _Unwind_Backtrace(_Unwind_Trace_Fn,void*); 进行堆栈回溯;

在我们的项目中,MSP栈用于异常处理。在其他情况下,使用 PSP 堆栈。当我在异常处理程序中调用 _Unwind_Backtrace() 时,我能够正确地回溯到在异常中调用的第一个函数。在此之前,堆栈是 MSP。

但在异常之前,我们无法回溯。此时使用的堆栈为PSP。

例如:假设

Task1
{
func1()
}



func1
{
func2()
}

func2
{
an exception occurs here
}

**Inside Exception**
{
func1ex()
}

func1ex
{
func2ex()
}



func2ex
{
unwind backtrace()
}

Unwind backtrace 能够回溯到 func1ex() 但不能回溯路径 task1-->func1-->func2

因为在异常期间在 PSP 和 MSP 堆栈之间进行切换,所以它无法回溯使用 PSP 的函数。

在控制进入异常处理程序之前,内核将寄存器 R0、R1、R2、R3、LR、PC 和 XPSR 堆栈在 PSP 中。我可以看到。但是我不知道如何使用这个堆栈框架来为 PSP 做回溯。

谁能告诉我们在这种情况下该怎么做,以便我们可以回溯到任务级别?

谢谢,

阿什温。

最佳答案

这是可行的,但需要访问 libgcc 如何实现 _Unwind_Backtrace 函数的内部细节。幸运的是,代码是开源的,但依赖于此类内部细节的代码是脆弱的,因为它可能会在未来版本的 armgcc 中中断,恕不另行通知。

通常,通过读取执行回溯的 libgcc 源代码,它会创建 CPU 核心寄存器的内存虚拟表示,然后使用该表示在堆栈中向上移动,模拟异常抛出。 _Unwind_Backtrace 做的第一件事是从当前 CPU 寄存器中填充这个上下文,然后调用一个内部实现函数。

在大多数情况下,从堆栈异常结构手动创建上下文足以伪造从处理程序模式向上通过调用堆栈的回溯。这是一些示例代码(来自 https://github.com/bakerstu/openmrn/blob/62683863e8621cef35e94c9dcfe5abcaf996d7a2/src/freertos_drivers/common/cpu_profile.hxx#L162 ):

/// This struct definition mimics the internal structures of libgcc in
/// arm-none-eabi binary. It's not portable and might break in the future.
struct core_regs
{
unsigned r[16];
};

/// This struct definition mimics the internal structures of libgcc in
/// arm-none-eabi binary. It's not portable and might break in the future.
typedef struct
{
unsigned demand_save_flags;
struct core_regs core;
} phase2_vrs;

/// We store what we know about the external context at interrupt entry in this
/// structure.
phase2_vrs main_context;
/// Saved value of the lr register at the exception entry.
unsigned saved_lr;

/// Takes registers from the core state and the saved exception context and
/// fills in the structure necessary for the LIBGCC unwinder.
void fill_phase2_vrs(volatile unsigned *fault_args)
{
main_context.demand_save_flags = 0;
main_context.core.r[0] = fault_args[0];
main_context.core.r[1] = fault_args[1];
main_context.core.r[2] = fault_args[2];
main_context.core.r[3] = fault_args[3];
main_context.core.r[12] = fault_args[4];
// We add +2 here because first thing libgcc does with the lr value is
// subtract two, presuming that lr points to after a branch
// instruction. However, exception entry's saved PC can point to the first
// instruction of a function and we don't want to have the backtrace end up
// showing the previous function.
main_context.core.r[14] = fault_args[6] + 2;
main_context.core.r[15] = fault_args[6];
saved_lr = fault_args[5];
main_context.core.r[13] = (unsigned)(fault_args + 8); // stack pointer
}
extern "C"
{
_Unwind_Reason_Code __gnu_Unwind_Backtrace(
_Unwind_Trace_Fn trace, void *trace_argument, phase2_vrs *entry_vrs);
}

/// Static variable for trace_func.
void *last_ip;

/// Callback from the unwind backtrace function.
_Unwind_Reason_Code trace_func(struct _Unwind_Context *context, void *arg)
{
void *ip;
ip = (void *)_Unwind_GetIP(context);
if (strace_len == 0)
{
// stacktrace[strace_len++] = ip;
// By taking the beginning of the function for the immediate interrupt
// we will attempt to coalesce more traces.
// ip = (void *)_Unwind_GetRegionStart(context);
}
else if (last_ip == ip)
{
if (strace_len == 1 && saved_lr != _Unwind_GetGR(context, 14))
{
_Unwind_SetGR(context, 14, saved_lr);
allocator.singleLenHack++;
return _URC_NO_REASON;
}
return _URC_END_OF_STACK;
}
if (strace_len >= MAX_STRACE - 1)
{
++allocator.limitReached;
return _URC_END_OF_STACK;
}
// stacktrace[strace_len++] = ip;
last_ip = ip;
ip = (void *)_Unwind_GetRegionStart(context);
stacktrace[strace_len++] = ip;
return _URC_NO_REASON;
}

/// Called from the interrupt handler to take a CPU trace for the current
/// exception.
void take_cpu_trace()
{
memset(stacktrace, 0, sizeof(stacktrace));
strace_len = 0;
last_ip = nullptr;
phase2_vrs first_context = main_context;
__gnu_Unwind_Backtrace(&trace_func, 0, &first_context);
// This is a workaround for the case when the function in which we had the
// exception trigger does not have a stack saved LR. In this case the
// backtrace will fail after the first step. We manually append the second
// step to have at least some idea of what's going on.
if (strace_len == 1)
{
main_context.core.r[14] = saved_lr;
main_context.core.r[15] = saved_lr;
__gnu_Unwind_Backtrace(&trace_func, 0, &main_context);
}
unsigned h = hash_trace(strace_len, (unsigned *)stacktrace);
struct trace *t = find_current_trace(h);
if (!t)
{
t = add_new_trace(h);
}
if (t)
{
t->total_size += 1;
}
}

/// Change this value to runtime disable and enable the CPU profile gathering
/// code.
bool enable_profiling = 0;

/// Helper function to declare the CPU usage tick interrupt.
/// @param irq_handler_name is the name of the interrupt to declare, for example
/// timer4a_interrupt_handler.
/// @param CLEAR_IRQ_FLAG is a c++ statement or statements in { ... } that will
/// be executed before returning from the interrupt to clear the timer IRQ flag.
#define DEFINE_CPU_PROFILE_INTERRUPT_HANDLER(irq_handler_name, CLEAR_IRQ_FLAG) \
extern "C" \
{ \
void __attribute__((__noinline__)) load_monitor_interrupt_handler( \
volatile unsigned *exception_args, unsigned exception_return_code) \
{ \
if (enable_profiling) \
{ \
fill_phase2_vrs(exception_args); \
take_cpu_trace(); \
} \
cpuload_tick(exception_return_code & 4 ? 0 : 255); \
CLEAR_IRQ_FLAG; \
} \
void __attribute__((__naked__)) irq_handler_name(void) \
{ \
__asm volatile("mov r0, %0 \n" \
"str r4, [r0, 4*4] \n" \
"str r5, [r0, 5*4] \n" \
"str r6, [r0, 6*4] \n" \
"str r7, [r0, 7*4] \n" \
"str r8, [r0, 8*4] \n" \
"str r9, [r0, 9*4] \n" \
"str r10, [r0, 10*4] \n" \
"str r11, [r0, 11*4] \n" \
"str r12, [r0, 12*4] \n" \
"str r13, [r0, 13*4] \n" \
"str r14, [r0, 14*4] \n" \
: \
: "r"(main_context.core.r) \
: "r0"); \
__asm volatile(" tst lr, #4 \n" \
" ite eq \n" \
" mrseq r0, msp \n" \
" mrsne r0, psp \n" \
" mov r1, lr \n" \
" ldr r2, =load_monitor_interrupt_handler \n" \
" bx r2 \n" \
: \
: \
: "r0", "r1", "r2"); \
} \
}

此代码旨在使用定时器中断获取 CPU 配置文件,但可以从任何处理程序(包括故障处理程序)中重用回溯展开。从下往上阅读代码:

  • 用属性__naked__ 定义 IRQ 函数很重要,否则 GCC 的函数入口头将以不可预知的方式操纵 CPU 的状态,例如修改堆栈指针。
  • 首先,我们保存不在异常入口结构中的所有其他核心寄存器。我们需要从一开始就从汇编开始执行此操作,因为当它们用作临时寄存器时,它们通常会被以后的 C 代码修改。
  • 然后我们重建中断前的堆栈指针;无论处理器之前处于处理程序模式还是线程模式,代码都可以工作。这个指针就是异常入口结构。这段代码不处理不是 4 字节对齐的堆栈,但我从未见过 armgcc 这样做过。
  • 其余代码是C/C++,我们填充从libgcc中获取的内部结构,然后调用展开过程的内部实现。我们需要进行一些调整,以解决 libgcc 的某些假设,这些假设不适用于异常条目。
  • 有一种特定情况下展开不起作用,即异常发生在叶函数中,该函数在进入时未将 LR 保存到堆栈。当您尝试从进程模式执行回溯时,这永远不会发生,因为被调用的回溯函数将确保调用函数不是叶。我试图通过在回溯过程本身期间调整 LR 寄存器来应用一些变通方法,但我不相信它每次都有效。我对如何更好地做到这一点的建议很感兴趣。

关于gcc - 使用 GCC 编译器的 ARM 核心堆栈回溯(当有 MSP 到 PSP 切换时),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/47331426/

31 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com