# 4.1.6 一些 Y86-64 指令的详情

大多数 Y86-64 指令是以一种直接明了的方式修改程序状态的,所以定义每条指令想要达到的结果并不困难。不过,两个特别的指令的组合需要特别注意一下。

pushq 指令会把栈指针减 8,并且将一个寄存器值写入内存中。因此,当执行 pushq %rsp 指令时,处理器的行为是不确定的,因为要入栈的寄存器会被同一条指令修改。通常有两种不同的约定:1)压入 %rsp 的原始值,2)压入减去 8 的 %rsp 的值。

对于 Y86-64 处理器来说,我们采用和 x86-64 一样的做法,就像下面这个练习题确定出的那样。

练习题 4.7 确定 x86-64 处理器上指令 pushq %rsp 的行为。我们可以通过阅读 Intel 关于这条指令的文档来了解它们的做法,但更简单的方法是在实际的机器上做个实验。C 编译器正常情况下是不会产生这条指令的,所以我们必须用手工生成的汇编代码来完成这一任务。下面是我们写的一个测试程序(网络旁注 ASM:EASM,描述如何编写 C 代码和手写汇编代码结合的程序):

    .text
    .globl pushtest
pushtest:
    movq    %rsp, %rax       # Copy stack pointer
    pushq   %rsp             # Push stack pointer
    popq    %rdx             # Pop it back
    subq    %rdx, %rax       # Return 0 or 8
    ret

在实验中,我们发现函数 pushtest 总是返回 0,这表示在 x86-64 中 pushq %rsp 指令的行为是怎样的呢?

popq %rsp 指令也有类似的歧义。可以将 %rsp 置为从内存中读出的值,也可以置为加了增量后的栈指针。同练习题 4.7 一样,让我们做个实验来确定 x86-64 机器是怎么处理这条指令的,然后 Y86-64 机器就采用同样的方法。

练习题 4.8 下面这个汇编函数让我们确定 x86-64 上指令 popq %rsp 的行为:

    .text
    .globl poptest
poptest:
    movq    %rsp, %rdi       # Save stack pointer
    pushq   $0xabcd          # Push test value
    popq    %rsp             # Pop to stack pointer
    movq    %rsp, %rax       # Set popped value as return value
    movq    %rdi, %rsp       # Restore stack pointer
    ret

我们发现函数总是返回 0xabcd。这表示 popq %rsp 的行为是怎样的?还有什么其他 Y86-64 指令也会有相同的行为吗?

旁注 正确了解细节:x86 模型间的不一致

练习题 4.7 和练习题 4.8 可以帮助我们确定对于压入和弹出栈指针指令的一致惯例。看上去似乎没有理由会执行这样两种操作,那么一个很自然的问题就是“为什么要担心这样一些吹毛求疵的细节呢?”

从下面 Intel 关于 PUSH 指令的文档 [51] 的节选中,可以学到关于这个一致性的重要性的有用的教训:

对于 IA-32 处理器,从 Intel 286 开始,PUSH ESP 指令将 ESP 寄存器的值压入栈中,就好像它存在于这条指令被执行之前。(对于 Intel 64 体系结构、IA-32 体系结构的实地址模式和虚 8086 模式来说也是这样。)对于 Intel® 8086 处理器,PUSH SP 将 SP 寄存器的新值压入栈中(也就是减去 2 之后的值)。(PUSH ESP 指令。Intel 公司。50。)

虽然这个说明的具体细节可能难以理解,但是我们可以看到这条注释说明的是当执行压入栈指针寄存器指令时,不同型号的 x86 处理器会做不同的事情。有些会压入原始的值,而有些会压入减去后的值。(有趣的是,对于弹出栈指针寄存器没有类似的歧义。)这种不一致有两个缺点:

  • 它降低了代码的可移植性。取决于处理器模型,程序可能会有不同的行为。虽然这样特殊的指令并不常见,但是即使是潜在的不兼容也可能带来严重的后果。
  • 它增加了文档的复杂性。正如在这里我们看到的那样,需要一个特别的说明来澄清这些不同之处。即使没有这样的特殊情况,x86 文档就已经够复杂的了。

因此我们的结论是,从长远来看,提前了解细节,力争保持完全的一致能够节省很多的麻烦。