From 8977099ad0d195db361e78d90a43a7b41423c1c5 Mon Sep 17 00:00:00 2001 From: Chen Wang Date: Tue, 19 Jul 2022 15:59:50 +0800 Subject: [PATCH] article: stack unwinding alos added article stack unwind fp --- articles/20220717-call-stack.md | 144 +++---------- articles/20220719-stack-unwinding.md | 129 ++++++++++++ articles/20220719-stackuw-fp.md | 233 +++++++++++++++++++++ articles/code/20220717-call-stack/Makefile | 3 + 4 files changed, 392 insertions(+), 117 deletions(-) create mode 100644 articles/20220719-stack-unwinding.md create mode 100644 articles/20220719-stackuw-fp.md diff --git a/articles/20220717-call-stack.md b/articles/20220717-call-stack.md index 26e22e8..b70c614 100644 --- a/articles/20220717-call-stack.md +++ b/articles/20220717-call-stack.md @@ -52,18 +52,22 @@ 一开始 SP 指向 Call Stack 的 BOTTOM 位置: ![](./diagrams/20220717-call-stack/call-stack-0.png) +【图一】 初始状态 当 “函数 A” 被调用时,程序在 call stack 中 push “函数 A” 的 stack frame,同时 SP 的值被更新,指向当前 Call Stack 的 TOP。当然这个 stack frame 中的内容如果存在多项的话,并不是一次性 push 的,而是需要多次 push,具体会有哪些内容,这个我们在下一节讨论。 ![](./diagrams/20220717-call-stack/call-stack-1.png) +【图二】 “函数 A” 被调用 当 “函数 A” 调用 “函数 B” 时,程序在 call stack 中 push “函数 B” 的 stack frame,同时 SP 的值被更新,指向当前 Call Stack 的 TOP。 ![](./diagrams/20220717-call-stack/call-stack-2.png) +【图三】 “函数 B” 被调用 当 “函数 B” 调用 “函数 C” 时,程序在 call stack 中 push “函数 C” 的 stack frame,同时 SP 的值被更新,继续指向当前 Call Stack 的 TOP。 ![](./diagrams/20220717-call-stack/call-stack-3.png) +【图四】 “函数 C” 被调用 当函数调用逐级返回时,会逆向发生 Call Stack 的 pop 操作。在 call stack 中创建子程序的 stack frame 有时称为 "winding";相反,删除该内容叫做 "unwinding"。wind 英文有 “缠绕” 的意思,而函数调用 Call stack 中 push 过程类似于我们绕绳子的绕的过程,而 pop 过程则对应松开绕的绳子(原路返回),记住这个就比较好理解为啥叫 winding 或者 unwinding 了。 @@ -113,7 +117,10 @@ void foo(void) } ``` -我们简单编译一下 `riscv64-unknown-linux-gnu-gcc -g -c callstack.c`,然后用 objdump 看一下反汇编的结果: +我们采用最常见的方式编译一下 `riscv64-unknown-linux-gnu-gcc -g -c callstack.c`,注意我们在编译 `callstack.c` 时,没有带任何优化选项,所以编译器默认为 `-O0`,这等同于默认启用了 `-fno-omit-frame-pointer` 选项,编译器会把某个特定的寄存器当 frame pointer,用来保存当前函数 stack frame 的 BOTTOM(起始地址),前面说过,在 RISC-V 中,这个寄存器是 x8,其 ABI name 是 s0 或者 fp。同时编译器在对高级语言进行汇编处理时,针对 callee 函数的 prologue 阶段生成的指令中会插入指令,将 caller 对应的 fp 中的值保存到 callee 的 stack frame 中同时将 fp 更新为指向 caller 的 sp。 + +我们用 objdump 仔细看一下反汇编的结果,特别关注一下函数的 prologue 和 epilogue 部分生成的指令: + ```cpp extern int bar(int a, int b); @@ -148,6 +155,7 @@ void foo(void) 我们可以对照下图来理解一下这段汇编实现的效果。 ![](./diagrams/20220717-call-stack/call-stack.png) +【图五】 RISC-V 的函数 从 line 0 ~ line 6,是 `foo()` 函数的 prologue 处理, - `0: addi sp,sp,-32`:将 SP 从左边的位置向下移到右边的 SP 位置,可以认为就是为 `foo()` 函数预留了 32 个字节的 stack frame 空间。该指令执行完后,SP 指向 stack frame 的 TOP。 @@ -159,39 +167,7 @@ void foo(void) 从 line 2a ~ line 32 是 `foo()` 函数的 epilogue 处理,这里我就不赘述了。 - -**注意:以下优化代码的例子放在本文可能是不合适的,后面可能会放到有关栈回溯的文章中去。** - -上面我们在编译 `callstack.c` 时,没有带任何优化选项,默认为 `-O0`,但是如果使用优化选项,gcc 会做如下优化 : - -`-O1` 的结果如下,我们发现 fp 被优化掉了。 - -```cpp -0000000000000000 : -extern int bar(int a, int b); - -void foo(void) -{ - 0: 1141 addi sp,sp,-16 - 2: e406 sd ra,8(sp) - int a = 1; - int b = 2; - bar(a, b); - 4: 4589 li a1,2 - 6: 4505 li a0,1 - 8: 00000097 auipc ra,0x0 - c: 000080e7 jalr ra # 8 - -0000000000000010 <.LVL1>: -} - 10: 60a2 ld ra,8(sp) - 12: 0141 addi sp,sp,16 - 14: 8082 ret -``` - -如果在 `-O1` 情况下希望在 call stack 中保留 frame pointer,可以加上 `-fno-omit-frame-pointer` 告诉编译器不要把 frame pointer 优化掉 - -`-O2 -fno-omit-frame-pointer` 的结果如下: +需要注意的是: fp 是否需要其实并不是必须的,不定义 fp 并不会影响程序的执行,我们可以在上面的编译命令中加上 `-fomit-frame-pointer`, 编译器就不会生成 fp 相关的指令了。执行 `riscv64-unknown-linux-gnu-gcc -g -c callstack.c -fomit-frame-pointer` 后 objdump 如下,大家可以比较一下: ```cpp 0000000000000000 : @@ -199,95 +175,29 @@ extern int bar(int a, int b); void foo(void) { - 0: 1141 addi sp,sp,-16 - 2: e406 sd ra,8(sp) - 4: e022 sd s0,0(sp) - 6: 0800 addi s0,sp,16 - int a = 1; - int b = 2; - bar(a, b); - 8: 4589 li a1,2 - a: 4505 li a0,1 - c: 00000097 auipc ra,0x0 - 10: 000080e7 jalr ra # c - -0000000000000014 <.LVL1>: -} - 14: 60a2 ld ra,8(sp) - 16: 6402 ld s0,0(sp) - 18: 0141 addi sp,sp,16 - 1a: 8082 ret -``` - -`-O2` 的结果如下,我们发现不仅 fp 被优化掉了,ra 都被优化掉了,这主要时因为我们这里的例子代码是 “尾调用”,所以可以这么优化,而且 `jalr` + `ret` 的组合被优化成了一条 `jr`。 - -```cpp -0000000000000000 : - -void foo(void) -{ - int a = 1; - int b = 2; - bar(a, b); - 0: 4589 li a1,2 - 2: 4505 li a0,1 - 4: 00000317 auipc t1,0x0 - 8: 00030067 jr t1 # 4 -``` - -但注意在 `-O2` 的情况下,光加上 `-fno-omit-frame-pointer` 还不足以实现正常的栈回溯,见下面的反汇编结果,line 6 在调用 `bar()` 函数之前过早地恢复了 fp,这对栈回溯是有影响的。 - -```cpp -0000000000000000 : -extern int bar(int a, int b); - -void foo(void) -{ - 0: 1141 addi sp,sp,-16 - 2: e422 sd s0,8(sp) - 4: 0800 addi s0,sp,16 + 0: 1101 addi sp,sp,-32 + 2: ec06 sd ra,24(sp) int a = 1; + 4: 4785 li a5,1 + 6: c63e sw a5,12(sp) int b = 2; + 8: 4789 li a5,2 + a: c43e sw a5,8(sp) bar(a, b); + c: 4722 lw a4,8(sp) + e: 47b2 lw a5,12(sp) + 10: 85ba mv a1,a4 + 12: 853e mv a0,a5 + 14: 00000097 auipc ra,0x0 + 18: 000080e7 jalr ra # 14 } - 6: 6422 ld s0,8(sp) - bar(a, b); - 8: 4589 li a1,2 - a: 4505 li a0,1 -} - c: 0141 addi sp,sp,16 - bar(a, b); - e: 00000317 auipc t1,0x0 - 12: 00030067 jr t1 # e + 1c: 0001 nop + 1e: 60e2 ld ra,24(sp) + 20: 6105 addi sp,sp,32 + 22: 8082 ret ``` -所以如果考虑到尾调用的情况,还要加上 `-fno-optimize-sibling-calls` 选项,该选项让编译器不要优化尾部调用。`-O2 -fno-omit-frame-pointer -fno-optimize-sibling-calls` 的结果如下,这样就没有问题了。 - -```cpp -0000000000000000 : -extern int bar(int a, int b); - -void foo(void) -{ - 0: 1141 addi sp,sp,-16 - 2: e022 sd s0,0(sp) - 4: e406 sd ra,8(sp) - 6: 0800 addi s0,sp,16 - int a = 1; - int b = 2; - bar(a, b); - 8: 4589 li a1,2 - a: 4505 li a0,1 - c: 00000097 auipc ra,0x0 - 10: 000080e7 jalr ra # c - -0000000000000014 <.LVL1>: -} - 14: 60a2 ld ra,8(sp) - 16: 6402 ld s0,0(sp) - 18: 0141 addi sp,sp,16 - 1a: 8082 ret -``` +有同学问,那为啥我们要专门浪费一个寄存器来存放 fp 呢,还增加了 prologue 和 epilogue 的指令处理。这个原因涉及到 “Stack Unwinding” 的处理问题,我会专门另外写一篇笔记来总结。 另外利用 call stack 传参的例子可以见 [param.c][3],我这里就不解释了。 diff --git a/articles/20220719-stack-unwinding.md b/articles/20220719-stack-unwinding.md new file mode 100644 index 0000000..2a3b98c --- /dev/null +++ b/articles/20220719-stack-unwinding.md @@ -0,0 +1,129 @@ +![](./diagrams/linker-loader.png) + +文章标题:**Stack Unwinding - Overview** + +- 作者:汪辰 +- 联系方式: / + +文章大纲 + + + +- [1. 参考](#1-参考) +- [2. 什么是 Stack Unwinding](#2-什么是-stack-unwinding) +- [3. 哪些场景下需要 Stack Unwinding](#3-哪些场景下需要-stack-unwinding) +- [4. 实现 Stack Unwinding 的方法](#4-实现-stack-unwinding-的方法) + + + +最近一直在研究和学习有关栈回溯的问题,由于内容比较多,所以一边看一边做笔记,同时整理一下自己的思路。 + +# 1. 参考 + +本文主要参考了如下内容: + +- 【参考 1】[Call Stack (RISC-V)][1] +- 【参考 2】[Stack unwinding][2] +- 【参考 3】[Deep Wizardry: Stack Unwinding][3] +- 【参考 4】[Stack Unwinding in C++][4] + + +# 2. 什么是 Stack Unwinding + +在 [【参考 1】][1] 中我们了解到在计算机程序执行过程中,当 caller 函数调用 callee 函数时,程序会在 call stack 中为 callee 函数分配一块新的 stack frame,同时 stack pointer 也指向了 callee 函数(即 active subroutine)。[【参考 1】][1] 中的 【图一】到【图四】中 SP 的向下移动的过程我们形象地称其为 “Winding”。那么与之相反的过程自然就是 “Unwinding” 了。因为这个过程发生在 call stack 中,所以我们称其为 “Stack Unwinding”, 中文翻译为 “栈回溯”。 + +所以我们定义:在 call stack 中,从最近压栈的 callee 函数对应的 stack frame 开始向 caller 方向进行遍历的过程,称之为 “Stack Unwinding”。 + +值得注意的是,“Stack Unwinding” 这个过程的发生并不一定意味着函数一定要返回,在回溯过程中 call stack 可能是保持不变的。而且 “Stack Unwinding” 甚至可能并不一定发生在程序执行过程中,我们只要把它理解成一种逻辑上对 call stack 的信息进行反向遍历的过程行为就好了。 + +# 3. 哪些场景下需要 Stack Unwinding + +最常见的栈回溯发生在我们使用调试器(Debugger)对程序进行调试时,譬如使用 gdb 中我们将程序指令 break 住以后,执行 `backtrace` 命令打印整个 call stack,call stack 中每一个 stack frame 一行。 + +``` +(gdb) bt +#0 foo_3 () at test_backtrace.c:20 +#1 0x00005555555548e6 in foo_2 () at test_backtrace.c:24 +#2 0x00005555555548f7 in foo_1 () at test_backtrace.c:28 +#3 0x0000555555554908 in foo () at test_backtrace.c:32 +#4 0x0000555555554919 in main () at test_backtrace.c:37 +(gdb) +``` + +还有一种常见的栈回溯和类似 C++ 这种支持异常处理的程序语言有关。以下代码摘录自 [【参考 4】][4] + +```cpp +// CPP Program to demonstrate Stack Unwinding +#include +using namespace std; + +// A sample function f1() that throws an int exception +void f1() throw(int) +{ + cout << "\n f1() Start "; + throw 100; + cout << "\n f1() End "; +} + +// Another sample function f2() that calls f1() +void f2() throw(int) +{ + cout << "\n f2() Start "; + f1(); + cout << "\n f2() End "; +} + +// Another sample function f3() that calls f2() and handles +// exception thrown by f1() +void f3() +{ + cout << "\n f3() Start "; + try { + f2(); + } + catch (int i) { + cout << "\n Caught Exception: " << i; + } + cout << "\n f3() End"; +} + +// Driver Code +int main() +{ + f3(); + + getchar(); + return 0; +} +``` +程序运行输出如下: +``` + f3() Start + f2() Start + f1() Start + Caught Exception: 100 + f3() End +``` + +函数的调用关系是 main->f3->f2->f1,当 f1 抛出异常时,由于 f1 没有定义异常处理函数,所以根据 C++ 的 ABI 定义([【参考 5】][5]),将发生栈回溯,逐级回退并检查每一级函数是否可以处理该异常,直到回溯到 f3 执行完到异常处理并打印 `Caught Exception: 100`。 + +除了以上两种最常见的会涉及栈回溯的场景外,还有: +- 自己编写的程序中也可能会在 active subroutine 中尝试遍历当前 call stack 并执行一些自定义的动作。 +- 一些工具分析软件中会遍历 call stack 的信息,这些分析可能发生在 online 状态,也可能发生在 offline 状态。 + +# 4. 实现 Stack Unwinding 的方法 + +一句话总结:有很多。但常见的方法有两种,内容比较多,我打算另辟两篇文章如下单独总结。 + +- [基于 Frame Pointer 的栈回溯][6] +- 基于 Call Frame Information 的栈回溯 + +除此之外,还有一些基于以上方法的改良定制版本,以及基于 ARCH 自身自己发明的方法,我这里就不一一赘述了,感兴趣可以阅读 [【参考 2】][2]。 + + +[1]: ./20220717-call-stack.md +[2]: https://maskray.me/blog/2020-11-08-stack-unwinding +[3]: https://blog.reverberate.org/2013/05/deep-wizardry-stack-unwinding.html +[4]: https://www.geeksforgeeks.org/stack-unwinding-in-c/ +[5]: https://itanium-cxx-abi.github.io/cxx-abi/abi-eh.html +[6]: ./20220719-stackuw-fp.md diff --git a/articles/20220719-stackuw-fp.md b/articles/20220719-stackuw-fp.md new file mode 100644 index 0000000..60457db --- /dev/null +++ b/articles/20220719-stackuw-fp.md @@ -0,0 +1,233 @@ +![](./diagrams/linker-loader.png) + +文章标题:**Stack Unwinding 之基于 Frame Pointer** + +- 作者:汪辰 +- 联系方式: / + +文章大纲 + + + +- [1. 参考](#1-参考) +- [2. 基于 Frame Pointer 实现栈回溯的工作原理](#2-基于-frame-pointer-实现栈回溯的工作原理) +- [3. fp 的优化相关总结](#3-fp-的优化相关总结) + - [3.1. `-O1`](#31--o1) + - [3.2. `-O2`](#32--o2) +- [4. 基于 FP 实现 Stack Unwinding 的优势和不足](#4-基于-fp-实现-stack-unwinding-的优势和不足) + + + +# 1. 参考 + +本文主要参考了如下内容: + +- 【参考 1】[Call Stack (RISC-V)][1] +- 【参考 2】[Stack Unwinding - Overview][2] +- 【参考 3】[Stack unwinding][3] +- 【参考 4】[AARCH64平台的栈回溯][4] +- 【参考 5】[Unwind 栈回溯详解][5] + + +本文主要总结基于 Frame Pointer 实现栈回溯(Stack Unwinding)的工作原理,至于什么是 “Stack” 以及什么是 “Stack Unwinding”,请阅读我的另外两篇总结 [《Call Stack (RISC-V)》][1] 和 [《Stack Unwinding - Overview》][2]。 + +# 2. 基于 Frame Pointer 实现栈回溯的工作原理 + +我们先分析一下在 [《Call Stack (RISC-V)》][1] 中的例子中如果编译时指定了 `-fomit-frame-pointer`,也就是没有引入 FP 的情况下,是否可以实现栈回溯。下图是没有 FP 的情况的 call stack。 + +``` + | | + ? -->+--------+ + | ra' | \ high address + | ...... | | + | | caller + | | | + | | / + ? -->+--------+ + | ra | \ + | ...... | | + | | callee <- active subroutine + SP-->|--------| | + | | / low address +``` + +如果要回溯,也就是要从上图中的 active subroutine 中找到 caller 的 stack frame 的位置(包括大小),但关键问题是每个函数的 call stack 的大小并不是固定的,所以当我们程序执行到 active subroutine 时,虽然我们有 SP 指向当前 stack frame 的 TOP,却没办法知道当前 stack frame 的 BOTTOM( 即 caller 的 SP),也无从得知 caller 的 stack frame 的 BOTTOM。 + +我们想到的方法就是新引入一个寄存器 FP(以 RISC-V 为例,这个寄存器是 x8,其 ABI name 是 s0 或者 fp),和 SP 一起协同,指向当前 stack frame 的 BOTTOM,同时也是 caller 对应 stack frame 的 TOP,同时我们再在当前的 stack frame 里保存一下 caller 的 stack frame 的 BOTTOM 的位置(下图中的 fp,其值指向 caller 作为 active subroutine 时 FP 的值 FP'),如下所示。 + +``` + : | | + | +---->FP''-->+--------+ + | | | ra'' | \ high address + +---+---+--------+--fp'' | | + | | ...... | caller's caller + | | | | + | | | / + | +->FP'-->+--------+ + | | | ra' | \ + +---+--------+--fp' | | + | | ...... | caller + | | | | + | | | / + | FP-->+--------+ + | | ra | \ + +--------+--fp | | + | ...... | callee <- active subroutine + SP-->|--------| | + | | / low address +``` + +如果建立起这种联系,相当于在 call stack 中以 stack frame 为节点(node)建立起一个单向链表,每个 stack frame 中保存的 fp 值相当于就是链表节点中的单向指针,指向上一个节点。而访问 stack frame 中的 fp 非常方便,因为对于 RISC-V 这种 ARCH 来说,就是 FP 指针往低地址方向偏移固定的 offset,譬如在 RV64 上就是 16 个字节。同时我们知道基于 FP 向低地址方向偏移一个固定的 offset,譬如在 RV64 上 8 个字节就是存放的 caller 的 RA,那么我们也可以根据这个信息很容易拿到函数的信息。 + +为了实现这种数据结构,函数的 prologue 和 epilogure 指令部分会有相应的处理,这里就不赘述了,可以参考 [《Call Stack (RISC-V)》][1] 中 `-O0` 情况下的反汇编例子描述。 + +# 3. fp 的优化相关总结 + +在 [《Call Stack (RISC-V)》][1] 中我们在编译 `callstack.c` 时,没有带任何优化选项,默认为 `-O0`,但是如果使用优化选项,gcc 会做如下优化。具体的代码例子参考 [这里][6]。 + +## 3.1. `-O1` + +`-O1` 的结果如下,我们发现 fp 被优化掉了,相当于自动加上了 `-fomit-frame-pointer`(当然还包括了一些其他的优化,感兴趣可以自行对比)。 + +```cpp +0000000000000000 : +extern int bar(int a, int b); + +void foo(void) +{ + 0: 1141 addi sp,sp,-16 + 2: e406 sd ra,8(sp) + int a = 1; + int b = 2; + bar(a, b); + 4: 4589 li a1,2 + 6: 4505 li a0,1 + 8: 00000097 auipc ra,0x0 + c: 000080e7 jalr ra # 8 + +0000000000000010 <.LVL1>: +} + 10: 60a2 ld ra,8(sp) + 12: 0141 addi sp,sp,16 + 14: 8082 ret +``` + +如果在 `-O1` 情况下希望在 call stack 中保留 frame pointer,可以加上 `-fno-omit-frame-pointer` 告诉编译器不要把 frame pointer 优化掉 + +`-O1 -fno-omit-frame-pointer` 的结果如下: + +```cpp +0000000000000000 : +extern int bar(int a, int b); + +void foo(void) +{ + 0: 1141 addi sp,sp,-16 + 2: e406 sd ra,8(sp) + 4: e022 sd s0,0(sp) + 6: 0800 addi s0,sp,16 + int a = 1; + int b = 2; + bar(a, b); + 8: 4589 li a1,2 + a: 4505 li a0,1 + c: 00000097 auipc ra,0x0 + 10: 000080e7 jalr ra # c + +0000000000000014 <.LVL1>: +} + 14: 60a2 ld ra,8(sp) + 16: 6402 ld s0,0(sp) + 18: 0141 addi sp,sp,16 + 1a: 8082 ret +``` + +## 3.2. `-O2` + +`-O2` 的结果如下,我们发现不仅 fp 被优化掉了,ra 都被优化掉了,这主要时因为我们这里的例子代码是 “尾调用”,所以会被优化到这个程度,而且 `jalr` + `ret` 的组合被优化成了一条 `jr`。 + +```cpp +0000000000000000 : + +void foo(void) +{ + int a = 1; + int b = 2; + bar(a, b); + 0: 4589 li a1,2 + 2: 4505 li a0,1 + 4: 00000317 auipc t1,0x0 + 8: 00030067 jr t1 # 4 +``` + +但注意在 `-O2` 的情况下,光加上 `-fno-omit-frame-pointer` 还不足以实现正常的栈回溯,见下面 `-O2 -fno-omit-frame-pointer` 的反汇编结果,line 6 在调用 `bar()` 函数之前过早地恢复了 fp,这对栈回溯是有影响的。 + +```cpp +0000000000000000 : +extern int bar(int a, int b); + +void foo(void) +{ + 0: 1141 addi sp,sp,-16 + 2: e422 sd s0,8(sp) + 4: 0800 addi s0,sp,16 + int a = 1; + int b = 2; + bar(a, b); +} + 6: 6422 ld s0,8(sp) + bar(a, b); + 8: 4589 li a1,2 + a: 4505 li a0,1 +} + c: 0141 addi sp,sp,16 + bar(a, b); + e: 00000317 auipc t1,0x0 + 12: 00030067 jr t1 # e +``` + +所以如果考虑到尾调用的情况,还要加上 `-fno-optimize-sibling-calls` 选项,该选项让编译器不要优化尾部调用。`-O2 -fno-omit-frame-pointer -fno-optimize-sibling-calls` 的结果如下,这样就没有问题了。 + +```cpp +0000000000000000 : +extern int bar(int a, int b); + +void foo(void) +{ + 0: 1141 addi sp,sp,-16 + 2: e022 sd s0,0(sp) + 4: e406 sd ra,8(sp) + 6: 0800 addi s0,sp,16 + int a = 1; + int b = 2; + bar(a, b); + 8: 4589 li a1,2 + a: 4505 li a0,1 + c: 00000097 auipc ra,0x0 + 10: 000080e7 jalr ra # c + +0000000000000014 <.LVL1>: +} + 14: 60a2 ld ra,8(sp) + 16: 6402 ld s0,0(sp) + 18: 0141 addi sp,sp,16 + 1a: 8082 ret +``` + +# 4. 基于 FP 实现 Stack Unwinding 的优势和不足 + +基于这种方式实现 Stack Unwinding 时非常方便快捷。但是这种方法也有自己的不足: + +- 需要一个专门寄存器(在 RISC-V 中这个寄存器是 x8,其 ABI name 是 s0 或者 fp )来保存 frame poniter。 +- 在函数的 prologue 和 epilogue 中会有额外的 push fp 寄存器或者 pop fp 寄存器信息的动作,实际执行时增大了指令开销。 +- 在栈回溯过程中,除了恢复 fp 和 ra,并不能恢复其他的寄存器。 +- 依赖于 ARCH 和编译器的行为,譬如某种 ARCH 因为寄存器不够用无法专门预留一个 fp 寄存器,或者编译函数时使用了 `-O1` 及以上的优化。实践中也不能保证所有库都包含 frame pointer。 +- 没有源语言信息。 + +[1]: ./20220717-call-stack.md +[2]: ./20220719-stack-unwinding.md +[3]: https://maskray.me/blog/2020-11-08-stack-unwinding +[4]: https://bbs.pediy.com/thread-270936.htm +[5]: https://blog.csdn.net/pwl999/article/details/107569603 +[6]: ./code/20220717-call-stack/ + diff --git a/articles/code/20220717-call-stack/Makefile b/articles/code/20220717-call-stack/Makefile index 44889b3..7fad15b 100644 --- a/articles/code/20220717-call-stack/Makefile +++ b/articles/code/20220717-call-stack/Makefile @@ -8,6 +8,9 @@ callstack: clean callstack-param: clean ${CROSS_COMPILE}gcc -g -c param.c +callstack-o0-omit-fp: clean + ${CROSS_COMPILE}gcc -g -c callstack.c -fomit-frame-pointer + callstack-o1: clean ${CROSS_COMPILE}gcc -g -c callstack.c -O1 -- Gitee