【问题标题】:Stack frame appearance during recursion. C vs assembly递归期间的堆栈帧外观。 C 与汇编
【发布时间】:2018-09-07 18:28:46
【问题描述】:

我只是在学习汇编中的函数和堆栈框架等等,所以我一直在查看 gdb 中的堆栈框架,因为我运行了一个递归算法来看看会发生什么。

如果我在 C 中运行一些递归代码,堆栈看起来就像我期望的那样 - 每次调用函数时堆栈上的一个对象。在递归阶乘函数的最低递归级别,堆栈帧如下所示:(这是 gdb 中的回溯,在函数的第一行有一个断点。)

(gdb) bt
#0  factorial (n=1) at recursion.c:20
#1  0x00005555555551c7 in factorial (n=2) at recursion.c:21
#2  0x00005555555551c7 in factorial (n=3) at recursion.c:21
#3  0x00005555555551c7 in factorial (n=4) at recursion.c:21
#4  0x00005555555551c7 in factorial (n=5) at recursion.c:21
#5  0x00005555555551c7 in factorial (n=6) at recursion.c:21
#6  0x00005555555551c7 in factorial (n=7) at recursion.c:21
#7  0x00005555555551c7 in factorial (n=8) at recursion.c:21
#8  0x00005555555551c7 in factorial (n=9) at recursion.c:21
#9  0x00005555555551c7 in factorial (n=10) at recursion.c:21
#10 0x000055555555517f in main (argc=2, args=0x7fffffffe768) at recursion.c:13

我的 C 代码是这样的:

int factorial (int n)
{   
    if (n <= 1) return 1;
    return n * factorial(n-1);
}

现在我在汇编中做同样的事情(我从 Rey Seyfarth 的“64 位汇编编程简介”一书中复制了这段代码,所以我假设它是正确的)并且,无论递归的深度如何,堆栈帧看起来像这个:(第 50 行是call fact 行)。

(gdb) bt
#0  fact () at fact.asm:40
#1  0x00000000004011a8 in greater () at fact.asm:50
#2  0x0000000000000000 in ?? ()

阶乘函数的代码是这样的——这种情况下的断点在sub rsp, 16这一行:

fact:                                   ; recursive function
n       equ     8

        push    rbp
        mov     rbp, rsp
        sub     rsp, 16                 ; make room for n
        cmp     rdi, 1                  ; end recursion if n=1
        jg      greater
        mov     eax, 1
        leave
        ret

greater:
        mov     [rsp+n], rdi            ; save n
        dec     rdi                     ; call fact with n-1
        call    fact
        mov     rdi, [rsp+n]            ; restore original n
        imul    rax, rdi
        leave
        ret

事实上,在这种情况下,回溯的输出真的让我很困惑。如果我在调用事实函数(dec rdi)之前将断点放在行上,那么结果通常是这样的:

(gdb) bt
#0  greater () at fact.asm:49
#1  0x0000000000000000 in ?? ()

但事实上,第五次调用是这样的:

(gdb) bt
#0  greater () at fact.asm:49
#1  0x00007ffff7f94be0 in ?? () from /usr/lib/libc.so.6
#2  0x0000000000000006 in ?? ()
#3  0x00007fffffffe5f0 in ?? ()
#4  0x00000000004011a8 in greater () at fact.asm:50
#5  0x0000000000000000 in ?? ()

然后在第七次通话中,这个:

(gdb) bt
#0  greater () at fact.asm:49
#1  0x0000003000000008 in ?? ()
#2  0x0000000000000004 in ?? ()
#3  0x00007fffffffe5b0 in ?? ()
#4  0x00000000004011a8 in greater () at fact.asm:50
#5  0x0000000000000000 in ?? ()

我的问题:

  1. 为什么堆栈的行为与 C 中的不同?

  2. 为什么我偶尔会得到最后一个看似垃圾的输出?

谢谢!

【问题讨论】:

  • 您没有提供调试信息。即便如此,由于您使用的是标准堆栈帧,我希望 gdb 能够弄清楚。显然它没有。只需自己检查堆栈即可。
  • 有没有办法提供调试信息?我正在用 yasm -g dwarf2 ... 编译
  • 根据this question,显然不是。
  • 好的,谢谢你的帮助
  • 如果您有使用rbp 的常规堆栈帧,使用它(例如mov [rbp-n], rdi)而不是相对于RSP 访问会稍微更有效。代码大小(base=RSP 的额外 SIB 字节)和来自堆栈引擎的堆栈同步微指令。您可以通过多种方式收紧此代码,例如imul rax, [rbp-n],并且只在greater 中保留额外的堆栈空间,而不是在进入之后立即保留。同样对于私有递归,不要费心维护 16 字节堆栈对齐,因为您从不调用其他函数。所以只需push rdi 来保存它。

标签: assembly gdb x86-64 yasm debug-information


【解决方案1】:

为什么堆栈的行为与 C 中的不同?

堆栈本身的行为完全相同相同 -- 处理器并不关心程序是编译的 C 程序还是手写的程序集。

表现相同的是 GDB 对堆栈的解释

x86_64(不同于SPARC)上,不可能正确展开堆栈,除非您知道当前调用堆栈链中的每个函数是如何调整它的。

GDB 使用展开描述符,编译器正是为此目的将其写入输出。这是一个blog post 解释展开过程。

您的 C 程序有展开描述符(使用 readelf -wf a.out 查看它们),但您的汇编程序没有。

为什么我偶尔会得到最后一个看似垃圾的输出?

在没有展开描述符的情况下,GDB 会尝试应用启发式方法尽可能地做到最好,并在遇到无法向上移动的堆栈级别时放弃。发生这种情况的确切位置取决于堆栈内容,但实际上并不重要:GDB 正在有效地查看垃圾数据(因为它不知道在哪里正确查看)。

附:你可以用少量的CFI directives 来扩充你的汇编程序来创建适当的展开描述符,然后 GDB 会很高兴地处理它,除了它看起来像 YASM doesn't support CFI。将程序集重写为 GAS 语法,然后在其中添加 CFI 指令当然是微不足道的。

【讨论】:

  • OP 的汇编程序是用 YASM(不是 GAS)编写的,因此(根据问题上的 cmets),该汇编程序没有 CFI 指令。 (也可能不是 NASM,它使用相同的语法/指令。)
  • 这个程序使用标准堆栈帧,所以 gdb 的启发式算法不是非常好。生成回溯应该很简单。具有讽刺意味的是,gdb 无法做到这一点,因为标准堆栈帧的一个常见参数是更好的调试。
猜你喜欢
  • 2020-09-23
  • 1970-01-01
  • 1970-01-01
  • 2020-12-17
  • 2015-02-08
  • 2015-09-12
  • 2013-05-17
  • 1970-01-01
  • 2017-01-20
相关资源
最近更新 更多