3.7.2 转移控制

3.7.2 转移控制

将控制从函数 P 转移到函数 Q 只需要简单地把程序计数器(PC)设置为 Q 的代码的起始位置。不过,当稍后从 Q 返回的时候,处理器必须记录好它需要继续 P 的执行的代码位置。在 x86-64 机器中,这个信息是用指令 call Q 调用过程 Q 来记录的。该指令会把地址 A 压入栈中,并将 PC 设置为 Q 的起始地址。压入的地址 A 被称为返回地址,是紧跟在 call 指令后面的那条指令的地址。对应的指令 ret 会从栈中弹出地址 A,并把 PC 设置为 A。

下表给出的是 call 和 ret 指令的一般形式:

指令 描述
call Label 过程调用
call *Operand 过程调用
ret 从过程调用中返回

(这些指令在程序 OBJDUMP 产生的反汇编输出中被称为 callq 和 retq。添加的后缀“q”只是为了强调这些是 x86-64 版本的调用和返回,而不是 IA32 的。在 x86-64 汇编代码中,这两种版本可以互换。)

call 指令有一个目标,即指明被调用过程起始的指令地址。同跳转一样,调用可以是直接的,也可以是间接的。在汇编代码中,直接调用的目标是一个标号,而间接调用的目标是 * 后面跟一个操作数指示符,使用的是图 3-3 中描述的格式之一。

图 3-26 说明了 3.2.2 节中介绍的 multstore 和 main 函数的 call 和 ret 指令的执行情况。下面是这两个函数的反汇编代码的节选:

Beginning of function multstore
1  0000000000400540 <multstore>:
2    400540: 53                 push  %rbx
3    400541: 48 89 d3           mov   %rdx,%rbx
     ...
Return from function multstore
4    40054d: c3                 retq
     ...

Call to multstore from main
5    400563: e8 d8 ff ff ff     callq 400540 <multstore>
6    400568: 48 8b 54 24 08     mov   0x8(%rsp),%rdx

在这段代码中我们可以看到,在 main 函数中,地址为 0x400563 的 call 指令调用函数 multstore。此时的状态如图 3-26a 所示,指明了栈指针 %rsp 和程序计数器 %rip 的值。call 的效果是将返回地址 0x400568 压入栈中,并跳到函数 multstore 的第一条指令,地址为 0x400540(图 3-26b)。函数 multstore 继续执行,直到遇到地址 0x40054d 处的 ret 指令。这条指令从栈中弹出值 0x400568,然后跳转到这个地址,就在 call 指令之后,继续 main 函数的执行。

图 3-26 call 和 ret 函数的说明

再来看一个更详细说明在过程间传递控制的例子,图 3-27a 给出了两个函数 top 和 leaf 的反汇编代码,以及 main 函数中调用 top 处的代码。每条指令都以标号标出:L1~L2(leaf 中),T1~T4(top 中)和 M1~M2(main 中)。该图的 b 部分给出了这段代码执行的详细过程。

Disassembly of leaf(long y)
y in %rdi
1  0000000000400540 <leaf>:
2    400540: 48 8d 47 02        lea   0x2(%rdi),%rax    L1: y+2
3    400544: c3                 retq                    L2: Return

4  0000000000400545 <top>:
Disassembly of top(long x)
x in %rdi
5    400545: 48 83 ef 05        sub   $0x5,%rdi         T1: x-5
6    400549: e8 f2 ff ff ff     callq 400540 <leaf>     T2: Call leaf(x-5)
7    40054e: 48 01 c0           add   %rax,%rax         T3: Double result
8    400551: c3                 retq                    T4: Return

Call to top from function main
9    40055b: e8 e5 ff ff ff     callq 400545 <top>      M1: Call top(100)
10   400560: 48 89 c2           mov   %rax,%rdx         M2: Resume

a) 说明过程调用和返回的反汇编代码

图 3-27 包含过程调用和返回的程序的执行细节。使用栈来存储返回地址使得能够返回到过程中正确的位置

标号 PC 指令 %rdi %rax %rsp *%rsp 描述
M1 0x40055b callq 100 — 0x7fffffffe820 — 调用 top(100)
T1 0x400545 sub 100 — 0x7fffffffe818 0x400560 进入 top
T2 0x400549 callq 95 — 0x7fffffffe818 0x400560 调用 leaf(95)
L1 0x400540 lea 95 — 0x7fffffffe810 0x40054e 进入 leaf
L2 0x400544 retq — 97 0x7fffffffe810 0x40054e 从 leaf 返回 97
T3 0x40054e add — 97 0x7fffffffe818 0x400560 继续 top
T4 0x400551 retq — 194 0x7fffffffe818 0x400560 从 top 返回 194
M2 0x400560 mov — 194 0x7fffffffe820 — 继续 main

b) 示例代码的执行过程

图 3-27(续)

main 调用 top(100),然后 top 调用 leaf(95)。函数 leaf 向 top 返回 97,然后 top 向 main 返回 194。前三列描述了被执行的指令,包括指令标号、地址和指令类型。后面四列给出了在该指令执行前程序的状态,包括寄存器 %rdi、%rax 和 %rsp 的内容,以及位于栈顶的值。仔细研究这张表的内容,它们说明了运行时栈在管理支持过程调用和返回所需的存储空间中的重要作用。

leaf 的指令 L1 将 %rax 设置为 97,也就是要返回的值。然后指令 L2 返回,它从栈中弹出 0x40054e。通过将 PC 设置为这个弹出的值,控制转移回 top 的 T3 指令。程序成功完成对 leaf 的调用,返回到 top。

指令 T3 将 %rax 设置为 194,也就是要从 top 返回的值。然后指令 T4 返回,它从栈中弹出 0x400560,因此将 PC 设置为 main 的 M2 指令。程序成功完成对 top 的调用,返回到 main。可以看到,此时栈指针也恢复成了 0x7fffffffe820,即调用 top 之前的值。

可以看到,这种把返回地址压入栈的简单的机制能够让函数在稍后返回到程序中正确的点。C 语言(以及大多数程序语言)标准的调用/返回机制刚好与栈提供的后进先出的内存管理方法吻合。

练习题 3.32 下面列出的是两个函数 first 和 last 的反汇编代码,以及 main 函数调用 first 的代码:

Disassembly of last(long u, long v)
u in %rdi, v in %rsi
1  0000000000400540 <last>:
2    400540: 48 89 f8           mov   %rdi,%rax         L1: u
3    400543: 48 0f af c6        imul  %rsi,%rax         L2: u*v
4    400547: c3                 retq                    L3: Return

Disassembly of first(long x)
x in %rdi
5  0000000000400548 <first>:
6    400548: 48 8d 77 01        lea   0x1(%rdi),%rsi    F1: x+1
7    40054c: 48 83 ef 01        sub   $0x1,%rdi         F2: x-1
8    400550: e8 eb ff ff ff     callq 400540 <last>     F3: Call last(x-1,x+1)
9    400555: f3 c3              repz retq               F4: Return
     ...
10   400560: e8 e3 ff ff ff     callq 400548 <first>    M1: Call first(10)
11   400565: 48 89 c2           mov   %rax,%rdx         M2: Resume

每条指令都有一个标号,类似于图 3-27a。从 main 调用 first(10) 开始,到程序返回 main 时为止,填写下表记录指令执行的过程。

标号 PC 指令 %rdi %rsi %rax %rsp *%rsp 描述
M1 0x400560 callq 10 — — 0x7fffffffe820 — 调用 first(10)
F1
F2
F3
L1
L2
L3
F4
M2