# 3.7.3 数据传送

当调用一个过程时,除了要把控制传递给它并在过程返回时再传递回来之外,过程调用还可能包括把数据作为参数传递,而从过程返回还有可能包括返回一个值。x86-64 中,大部分过程间的数据传送是通过寄存器实现的。例如,我们已经看到无数的函数示例,参数在寄存器 %rdi%rsi 和其他寄存器中传递。当过程 P 调用过程 Q 时,P 的代码必须首先把参数复制到适当的寄存器中。类似地,当 Q 返回到 P 时,P 的代码可以访问寄存器 %rax 中的返回值。在本节中,我们更详细地探讨这些规则。

x86-64 中,可以通过寄存器最多传递 6 个整型(例如整数和指针)参数。寄存器的使用是有特殊顺序的,寄存器使用的名字取决于要传递的数据类型的大小,如图 3-28 所示。会根据参数在参数列表中的顺序为它们分配寄存器。可以通过 64 位寄存器适当的部分访问小于 64 位的参数。例如,如果第一个参数是 32 位的,那么可以用 %edi 来访问它。

操作数大小(位) 参数 1 参数 2 参数 3 参数 4 参数 5 参数 6
64 %rdi %rsi %rdx %rcx %r8 %r9
32 %edi %esi %edx %ecx %r8d %r9d
16 %di %si %dx %cx %r8w %r9w
8 %dil %sil %dl %cl %r8b %r9b

图 3-28 传递函数参数的寄存器。寄存器是按照特殊顺序来使用的,而使用的名字是根据参数的大小来确定的

如果一个函数有大于 6 个整型参数,超出 6 个的部分就要通过栈来传递。假设过程 P 调用过程 Q,有 n 个整型参数,且 n > 6。那么 P 的代码分配的栈帧必须要能容纳 7 到 n 号参数的存储空间,如图 3-25 所示。要把参数 1~6 复制到对应的寄存器,把参数 7~n 放到栈上,而参数 7 位于栈顶。通过栈传递参数时,所有的数据大小都向 8 的倍数对齐。参数到位以后,程序就可以执行 call 指令将控制转移到过程 Q 了。过程 Q 可以通过寄存器访问参数,有必要的话也可以通过栈访问。相应地,如果 Q 也调用了某个有超过 6 个参数的函数,它也需要在自己的栈帧中为超出 6 个部分的参数分配空间,如图 3-25 中标号为“参数构造区”的区域所示。

作为参数传递的示例,考虑图 3-29a 所示的 C 函数 proc。这个函数有 8 个参数,包括字节数不同的整数(8、4、2 和 1)和不同类型的指针,每个都是 8 字节的。

void proc(long a1, long *a1p,
          int a2, int *a2p,
          short a3, short *a3p,
          char a4, char *a4p)
{
    *a1p += a1;
    *a2p += a2;
    *a3p += a3;
    *a4p += a4;
}

a) C 代码

void proc(a1, a1p, a2, a2p, a3, a3p, a4, a4p)
Arguments passed as follows:
    a1  in %rdi       (64 bits)
    a1p in %rsi       (64 bits)
    a2  in %edx       (32 bits)
    a2p in %rcx       (64 bits)
    a3  in %r8w       (16 bits)
    a3p in %r9        (64 bits)
    a4  at %rsp+8     ( 8 bits)
    a4p at %rsp+16    (64 bits)

1  proc:
2      movq 16(%rsp), %rax     Fetch a4p       (64 bits)
3      addq %rdi, (%rsi)       *a1p += a1      (64 bits)
4      addl %edx, (%rcx)       *a2p += a2      (32 bits)
5      addw %r8w, (%r9)        *a3p += a3      (16 bits)
6      movl 8(%rsp), %edx      Fetch a4        ( 8 bits)
7      addb %dl, (%rax)        *a4p += a4      ( 8 bits)
8      ret                     Return

b) 生成的汇编代码

图 3-29 有多个不同类型参数的函数示例。参数 1~6 通过寄存器传递,而参数 7~8 通过栈传递

图 3-29b 中给出 proc 生成的汇编代码。前面 6 个参数通过寄存器传递,后面 2 个通过栈传递,就像图 3-30 中画出来的那样。可以看到,作为过程调用的一部分,返回地址被压入栈中。因而这两个参数位于相对于栈指针距离为 8 和 16 的位置。在这段代码中,我们可以看到根据操作数的大小,使用了 ADD 指令的不同版本:a1long)使用 addqa2int)使用 addla3short)使用 addw,而 a4char)使用 addb。请注意第 6 行的 movl 指令从内存读入 4 字节,而后面的 addb 指令只使用其中的低位一字节。

图 3-30 函数 proc 的栈帧结构

练习题 3.33 C 函数 procprob 有 4 个参数 uavb,每个参数要么是一个有符号数,要么是一个指向有符号数的指针,这里的数大小不同。该函数的函数体如下:

*u += a;
*v += b;
return sizeof(a) + sizeof(b);

编译得到如下 x86-64 代码:

1  procprob:
2      movslq %edi, %rdi
3      addq %rdi, (%rdx)
4      addb %sil, (%rcx)
5      movl $6, %eax
6      ret

确定 4 个参数的合法顺序和类型。有两种正确答案。