在递归期间堆栈框架外观。 C vs汇编

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了在递归期间堆栈框架外观。 C vs汇编相关的知识,希望对你有一定的参考价值。

我只是在学习汇编和堆栈框架中的函数等等,所以我一直在查看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. 为什么我偶尔会看到最后的,看似垃圾,输出?

谢谢!

答案

为什么堆栈的行为与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指令当然是微不足道的。

以上是关于在递归期间堆栈框架外观。 C vs汇编的主要内容,如果未能解决你的问题,请参考以下文章

汇编语言递归法求阶乘

逆向——C语言的汇编表示之堆栈图 手把手示例 可以见后面在函数内部加一个局部变量以及嵌套调用的例子来综合理解

VS反汇编分析

递归与堆栈

C#:递归方法期间的堆栈溢出异常

GDB调试汇编堆栈