3.7.6 递归过程

3.7.6 递归过程

前面已经描述的寄存器和栈的惯例使得 x86-64 过程能够递归地调用它们自身。每个过程调用在栈中都有它自己的私有空间,因此多个未完成调用的局部变量不会相互影响。此外,栈的原则很自然地就提供了适当的策略,当过程被调用时分配局部存储,当返回时释放存储。

图 3-35 给出了递归的阶乘函数的 C 代码和生成的汇编代码。可以看到汇编代码使用寄存器 %rbx 来保存参数 n,先把已有的值保存在栈上(第 2 行),随后在返回前恢复该值(第 11 行)。根据栈的使用特性和寄存器保存规则,可以保证当递归调用 rfact(n-1) 返回时(第 9 行),(1)该次调用的结果会保存在寄存器 %rax 中,(2)参数 n 的值仍然在寄存器 %rbx 中。把这两个值相乘就能得到期望的结果。

从这个例子我们可以看到,递归调用一个函数本身与调用其他函数是一样的。栈规则提供了一种机制,每次函数调用都有它自己私有的状态信息(保存的返回位置和被调用者保存寄存器的值)存储空间。如果需要,它还可以提供局部变量的存储。栈分配和释放的规则很自然地就与函数调用-返回的顺序匹配。这种实现函数调用和返回的方法甚至对更复杂的情况也适用,包括相互递归调用(例如,过程 P 调用 Q,Q 再调用 P)。

long rfact(long n)
{
    long result;
    if (n <= 1)
        result = 1;
    else
        result = n * rfact(n-1);
    return result;
}

a) C 代码

long rfact(long n)
n in %rdi
1  rfact:
2      pushq %rbx             Save %rbx
3      movq %rdi, %rbx        Store n in callee-saved register
4      movl $1, %eax          Set return value = 1
5      cmpq $1, %rdi          Compare n:1
6      jle .L35               If <=, goto done
7      leaq -1(%rdi), %rdi    Compute n-1
8      call rfact             Call rfact(n-1)
9      imulq %rbx, %rax       Multiply result by n
10 .L35:                      done:
11     popq %rbx              Restore %rbx
12     ret                    Return

b) 生成的汇编代码

图 3-35 递归的阶乘程序的代码。标准过程处理机制足够用来实现递归函数

练习题 3.35 一个具有通用结构的 C 函数如下:

long rfun(unsigned long x) {
    if (________________)
        return ________________;
    unsigned long nx = ________________;
    long rv = rfun(nx);
    return ________________;
}

GCC 产生如下汇编代码:

long rfun(unsigned long x)
x in %rdi
1  rfun:
2      pushq %rbx
3      movq %rdi, %rbx
4      movl $0, %eax
5      testq %rdi, %rdi
6      je .L2
7      shrq $2, %rdi
8      call rfun
9      addq %rbx, %rax
10 .L2:
11     popq %rbx
12     ret

A. rfun 存储在被调用者保存寄存器 %rbx 中的值是什么?

B. 填写上述 C 代码中缺失的表达式。