堆栈跟踪如何在ARM上工作

下午好 几天前,我在我们的项目中遇到了一个小问题-在gdb中断处理程序中,Cortex-M的堆栈跟踪显示不正确。 因此,再次进行查找很有用,我可以通过哪些方式获得ARM的堆栈跟踪信息? 哪些编译标志会影响ARM上的堆栈可追溯性? 如何在Linux内核中实现? 基于研究,我决定写这篇文章。

让我们看一下Linux内核中的两个主要堆栈跟踪方法。

堆叠展开框架


让我们以一种可以在Linux内核中找到但目前在GCC中已弃用的简单方法开始。

想象一下某个程序正在RAM中的堆栈上运行,并且在某个时候我们中断了它并想调出调用堆栈。 假设我们有一个指向由处理器(PC)执行的当前指令的指针,以及一个指向栈顶(SP)的当前指针。 现在,为了将堆栈“跳”到上一个函数,您需要了解它是什么类型的函数以及我们应该在哪里跳转到该函数。 ARM为此使用链接寄存器(LR)。
链接寄存器(LR)是寄存器R14。 它存储子例程,函数调用和异常的返回信息。 复位时,处理器将LR值设置为0xFFFFFFFF
接下来,我们需要向上堆栈并从堆栈中加载新的LR寄存器值。 编译器的堆栈框架结构如下:

/* The stack backtrace structure is as follows: fp points to here: | save code pointer | [fp] | return link value | [fp, #-4] | return sp value | [fp, #-8] | return fp value | [fp, #-12] [| saved r10 value |] [| saved r9 value |] [| saved r8 value |] ... [| saved r0 value |] r0-r3 are not normally saved in a C function. */ 

该描述取自GCC头文件gcc / gcc / config / arm / arm.h。

即 可以以某种方式通知编译器(在我们的情况下为GCC)我们要进行堆栈跟踪。 然后,在每个函数的序言中,编译器将准备某种辅助结构。 您会注意到,在此结构中,我们需要的是LR寄存器的“下一个”值,最重要的是,它包含下一帧的地址。 | return fp value | [fp, #-12]

此编译器模式由-mapcs-frame选项指定。 在选项的描述中提到“使用此选项指定-fomit-frame-pointer会导致不为叶函数生成堆栈帧”。 在这里,叶函数被理解为是指不对其他函数进行任何调用的那些函数,因此可以使它们更容易一些。

您可能还想知道在这种情况下如何处理汇编器功能。 实际上,没有什么棘手的-您需要插入特殊的宏。 从Linux内核中的tools / objtool / Documentation / stack-validation.txt文件中:
每个可调用函数都必须使用ELF进行注释
功能类型。 在asm代码中,通常使用
ENTRY / ENDPROC宏。
但是同一文档讨论了这也是该方法的明显缺点。 objtool实用程序检查内核中的所有功能是否都以正确的格式写入了堆栈跟踪。

以下是从Linux内核展开堆栈的功能:

 #if defined(CONFIG_FRAME_POINTER) && !defined(CONFIG_ARM_UNWIND) int notrace unwind_frame(struct stackframe *frame) { unsigned long high, low; unsigned long fp = frame->fp; /*    ,    */ /* restore the registers from the stack frame */ frame->fp = *(unsigned long *)(fp - 12); frame->sp = *(unsigned long *)(fp - 8); frame->pc = *(unsigned long *)(fp - 4); return 0; } #endif 

但是在这里我想用defined(CONFIG_ARM_UNWIND)标记行。 她暗示Linux内核还使用了unwind_frame的另一种实现,我们稍后再讨论。

-mapcs-frame选项仅对ARM指令集有效。 但是众所周知,ARM微控制器还有另一组指令-Thumb(更精确地说是Thumb-1和Thumb-2),它主要用于Cortex-M系列。 要为Thumb模式启用帧生成,请使用-mtpcs-frame-mtpcs-leaf-frame标志 本质上,它是-mapcs-frame的类似物。 有趣的是,这些选项当前仅适用于Cortex-M0 / M1。 一段时间以来,我无法弄清楚为什么我不能为Cortex-M3 / M4 / ...编译所需的图像。 在重新阅读了ARM的所有gcc选项并搜索了Internet之后,我意识到这可能是一个错误。 因此,我直接进入了arm-none-eabi-gcc编译器的源代码。 在研究了编译器如何为ARM,Thumb-1和Thumb-2生成帧之后,我得出的结论是它们绕过了Thumb-2,即目前仅为Thumb-1和ARM生成了帧。 在创建了这些错误之后 ,GCC开发人员解释说ARM的标准已经改变了好几次,并且这些标志已经过时了,但是由于某些原因它们仍然存在于编译器中。 下面是为其生成框架的函数的反汇编程序。

 static int my_func(int a) { my_func2(7); return 0; } 

 00008134 <my_func>: 8134: b084 sub sp, #16 8136: b580 push {r7, lr} 8138: aa06 add r2, sp, #24 813a: 9203 str r2, [sp, #12] 813c: 467a mov r2, pc 813e: 9205 str r2, [sp, #20] 8140: 465a mov r2, fp 8142: 9202 str r2, [sp, #8] 8144: 4672 mov r2, lr 8146: 9204 str r2, [sp, #16] 8148: aa05 add r2, sp, #20 814a: 4693 mov fp, r2 814c: b082 sub sp, #8 814e: af00 add r7, sp, #0 

相比之下,ARM指令具有相同功能的反汇编程序

 000081f8 <my_func>: 81f8: e1a0c00d mov ip, sp 81fc: e92dd800 push {fp, ip, lr, pc} 8200: e24cb004 sub fp, ip, #4 8204: e24dd008 sub sp, sp, #8 

乍一看,这些似乎完全不同。 但实际上,帧是完全相同的,事实是在Thumb模式下,推指令仅允许将低位寄存器(r0-r7)和lr寄存器进行堆栈。 对于所有其他寄存器,必须通过mov和str指令分两个阶段完成,如上例所示。

通过异常展开堆栈


一种替代方法是基于ARM体系结构( EHABI标准的异常处理ABI展开堆栈。 实际上,使用此标准的主要示例是C ++等语言中的异常处理。 编译器为异常处理准备的信息也可以用来跟踪堆栈。 通过选项GCC -fexceptions (或-funwind-frames )启用此模式。

让我们仔细看看它是如何完成的。 首先,本文档(EHABI)对编译器施加了某些要求,以生成辅助表.ARM.exidx和.ARM.extab。 这就是在Linux内核源代码中定义此.ARM.exidx部分的方式。 从文件arch / arm / kernel / vmlinux.lds.h

 /* Stack unwinding tables */ #define ARM_UNWIND_SECTIONS \ . = ALIGN(8); \ .ARM.unwind_idx : { \ __start_unwind_idx = .; \ *(.ARM.exidx*) \ __stop_unwind_idx = .; \ } \ 

“ ARM体系结构的异常处理ABI”标准将.ARM.exidx表的每个元素定义为以下结构:

 struct unwind_idx { unsigned long addr_offset; unsigned long insn; }; 

第一个元素是距函数开头的偏移量,第二个元素是指令表中的地址,需要以特殊方式对其进行解释,以便进一步旋转堆栈。 换句话说,该表的每个元素只是一个单词和半单词的序列,它们是指令序列。 第一个字表示将堆栈旋转到下一帧必须完成的指令数。

这些说明在已经提到的EHABI标准中进行了描述:



此外,此解释器在Linux上的主要实现在arch / arm / kernel / unwind.c文件中

unwind_frame函数的实现
 int unwind_frame(struct stackframe *frame) { unsigned long low; const struct unwind_idx *idx; struct unwind_ctrl_block ctrl; /*   ,   */ /*   ARM.exidx    ,   PC */ idx = unwind_find_idx(frame->pc); if (!idx) { pr_warn("unwind: Index not found %08lx\n", frame->pc); return -URC_FAILURE; } ctrl.vrs[FP] = frame->fp; ctrl.vrs[SP] = frame->sp; ctrl.vrs[LR] = frame->lr; ctrl.vrs[PC] = 0; if (idx->insn == 1) /* can't unwind */ return -URC_FAILURE; else if ((idx->insn & 0x80000000) == 0) /* prel31 to the unwind table */ ctrl.insn = (unsigned long *)prel31_to_addr(&idx->insn); else if ((idx->insn & 0xff000000) == 0x80000000) /* only personality routine 0 supported in the index */ ctrl.insn = &idx->insn; else { pr_warn("unwind: Unsupported personality routine %08lx in the index at %p\n", idx->insn, idx); return -URC_FAILURE; } /*       ,    - * ,       */ /* check the personality routine */ if ((*ctrl.insn & 0xff000000) == 0x80000000) { ctrl.byte = 2; ctrl.entries = 1; } else if ((*ctrl.insn & 0xff000000) == 0x81000000) { ctrl.byte = 1; ctrl.entries = 1 + ((*ctrl.insn & 0x00ff0000) >> 16); } else { pr_warn("unwind: Unsupported personality routine %08lx at %p\n", *ctrl.insn, ctrl.insn); return -URC_FAILURE; } ctrl.check_each_pop = 0; /* ,      */ while (ctrl.entries > 0) { int urc; if ((ctrl.sp_high - ctrl.vrs[SP]) < sizeof(ctrl.vrs)) ctrl.check_each_pop = 1; urc = unwind_exec_insn(&ctrl); if (urc < 0) return urc; if (ctrl.vrs[SP] < low || ctrl.vrs[SP] >= ctrl.sp_high) return -URC_FAILURE; } /*   */ /* ,       */ frame->fp = ctrl.vrs[FP]; frame->sp = ctrl.vrs[SP]; frame->lr = ctrl.vrs[LR]; frame->pc = ctrl.vrs[PC]; return URC_OK; } 


这是unwind_frame函数的实现,如果启用了CONFIG_ARM_UNWIND选项,则将使用该函数。 我将带有俄语解释的注释直接插入源文本中。

以下是.ARM.exidx表元素如何在Embox中查找kernel_start函数的示例:

 $ arm-none-eabi-readelf -u build/base/bin/embox Unwind table index '.ARM.exidx' at offset 0xaa6d4 contains 2806 entries: <...> 0x1c3c <kernel_start>: @0xafe40 Compact model index: 1 0x9b vsp = r11 0x40 vsp = vsp - 4 0x84 0x80 pop {r11, r14} 0xb0 finish 0xb0 finish <...> 

这是她的反汇编程序:

 00001c3c <kernel_start>: void kernel_start(void) { 1c3c: e92d4800 push {fp, lr} 1c40: e28db004 add fp, sp, #4 <...> 

让我们逐步进行。 我们看到分配vps = r11 。 (R11这是FP),然后vps = vps - 4 。 这对应于add fp, sp, #4指令。 接下来是弹出{r11,r14},它对应于push {fp, lr}指令。 最后一条finish指令报告执行的结束(老实说,我仍然不明白为什么那里有两条完成指令)。

现在,让我们看看带有-funwind-frames标志的程序集占用了多少内存
对于实验,我为STM32F4-Discovery平台编译了Embox。 这是objdump结果:

使用-funwind-frames标志:
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 0005a600 08000000 08000000 00004000 2**14
CONTENTS, ALLOC, LOAD, CODE
1 .ARM.exidx 00003fd8 0805a600 0805a600 0005e600 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .ARM.extab 000049d0 0805e5d8 0805e5d8 000625d8 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
3 .rodata 0003e380 08062fc0 08062fc0 00066fc0 2**5


没有标志:
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00058b1c 08000000 08000000 00004000 2**14
CONTENTS, ALLOC, LOAD, CODE
1 .ARM.exidx 00000008 08058b1c 08058b1c 0005cb1c 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .rodata 0003e380 08058b40 08058b40 0005cb40 2**5


可以很容易地计算出.ARM.exidx和.ARM.extab节大约占据.text大小的1/10。 之后,我为基于ARM9的ARM Integrator CP收集了更大的图像,这些部分的大小是.text部分的1/12。 但是很明显,该比率可能因项目而异。 事实证明,添加-macps-frame标志的图像大小小于exception选项(这是预期的)。 因此,例如,当.text节大小为600 Kb时,.ARM.exidx + .ARM.extab的总大小为50 Kb,带有-mapcs-frame标志的附加代码的大小仅为10 Kb。 但是,如果我们从上面看,Cortex-M1产生了很大的序幕(还记得通过mov / str吗?),那么很明显,在这种情况下几乎没有区别,这意味着在Thumb模式下不太可能使用-mtpcs-frame至少有道理。

现在ARM需要这样的堆栈跟踪吗? 有哪些选择?


第三种方法是使用调试器跟踪堆栈。 似乎许多用于FreeRTOS的操作系统,NuttX微控制器当前都建议使用此特定的跟踪选项,或提供监视反汇编程序的功能。

结果,我们得出的结论是,运行时手臂的堆栈跟踪实际上没有在任何地方使用。 这可能是由于需要在工作时制作最高效的代码并脱机执行调试动作(包括升级堆栈)的结果。 另一方面,如果操作系统已经使用C ++代码,则很有可能使用通过.ARM.exidx进行跟踪的实现。

好了,是的,非常简单地解决了Embox中中断中不正确的堆栈输出问题,事实证明足以将LR寄存器保存在堆栈中。

Source: https://habr.com/ru/post/zh-CN424365/


All Articles