如需转载,请注明本文出处。

本篇作为上一篇文章 AMD64 调用约定 的续。

设置 %rax 寄存器


当调用包含可变参数的函数时,%rax 寄存器被设置成通过SSE寄存器传递的浮点类型参数的个数。因此,%rax 的取值范围是 [0, 8]。

Register Save Area


对于包含可变参数的函数,当函数中有调用 va_start 时,该函数的 prologue 就会发生变化:需要将存储在寄存器里的参数,转存到一个叫做 register save area 的地方。

所有的寄存器有固定的偏移量,如下所示:

Register Offset
%rdi 0
%rsi 8
%rdx 16
%rcx 24
%r8 32
%r9 40
%xmm0 48
%xmm1 64
%xmm7 160
%xmm15 288

根据上一篇文章的简化,我们只需要处理到 %xmm7 即可。因此我们需要在函数的栈中,预留出 176 字节 的区域。

简单粗暴的处理,就是把所有的寄存器都保存一番。除此之外,可以做点优化:如果函数中没有用到这些可变参数,就可以忽略掉 register save area,另外,根据前面提到的 %rax,我们可以根据这个值来决定要不要保存 XMM 寄存器。在仅传整数参数的情况下,避免初始化 XMM 单元,这点尤其重要。

va_list


C 标准头文件 stdarg.h 提供了几个宏来操作可变参数。

va_start(ap, param)
va_end(ap)
va_arg(ap, type)
va_copy(dst, src)

并且提供了一个类型 va_list

以上这些符号,编译器可以用宏实现,也可以用内置函数的方式实现,对使用者来说是透明的。使用者唯一要关心的是这些宏(或函数)的语义,至于它们是如何实现的,是不应该去关心的。但是,作为编译器的作者,是需要关心如何实现的细节的。

首先,va_list 的类型,用 C 语言可以表示如下:

typedef struct {
    unsigned int gp_offset;
    unsigned int fp_offset;
    void *overflow_arg_area;
    void *reg_save_area;
} va_list[1];

这个数组定义是很巧妙的,如果将它作为 va_start 等函数的参数,它就会被退化成指向结构体的指针。

接下来,我们逐个解释 stdarg.h 中各函数的语义及其实现。

va_start(ap, param)


语义:初始化 ap 的结构体。

各字段初始化如下:

reg_save_area: 初始化成 Register Save Area 的地址。

overflow_arg_area: 指向通过内存传递的参数的地址。初始化成第一个通过内存传递的参数的地址(如果有的话,$16(%rbp)),以后每次都更新为下一个通过内存传递的参数的地址。

gp_offset: 下一个通过 通用寄存器(General Purpose Registers) 传递的参数的寄存器的位移(相对于 reg_save_area,单位字节)。如果已经更新到最后一个,则它的值将是 48 (6 * 8)。

fp_offset: 下一个通过 浮点寄存器 传递的参数的寄存器的位移(相对于 reg_save_area,单位字节)。如果已经更新到最后一个,则它的值将是 176 (6 * 8 + 8 * 16)。

va_arg(ap, type)


语义:获取下一个类型为 type 的参数。

这个宏比较复杂,通常编译器使用内置方法来实现。当然,也有像 lcc,它通过增加一个 __typecode 扩展,然后使用 C 代码实现。

以下按照步骤来描述:

  1. 判断 type 是不是通过寄存器来传递,如果不是,跳到 7.
  2. 计算:

     num_gp = 传递 type 需要用到的通用寄存器个数
     num_fp = 传递 type 需要用到的浮点寄存器个数
    
  3. 如果:

    ap->gp_offset > 48 - num_gp * 8 或 ap->fp_offset > 176 - num_fp * 16

    跳到 7.

  4. 根据 ap->gp_offset, ap->fp_offsetap->reg_save_area 获取出参数。这可能需要临时变量来保存,比如参数是保存在不同的寄存器中。
  5. 设:

     ap->gp_offset = ap->gp_offset + num_gp * 8
     ap->fp_offset = ap->fp_offset + num_fp * 16
    
  6. 返回。
  7. 首先 ap->overflow_arg_area 是根据 8 字节对齐的。
  8. ap-> overflow_arg_area 获取参数。
  9. 设:

     ap->overflow_arg_area += sizeof(type)
    
  10. ap->overflow_arg_area 按照 8 字节对齐。
  11. 返回。
一个实现示例:va_arg(ap, int)
       movl ap->gp_offset, %eax
       cmpl $48, %eax                    Is register varibale?
       jae stack                         If not, use stack
       leal $8(%rax), %edx               Next available register
       addq ap->reg_save_area, %rax      Address of saved register
       movl %eax, ap->gp_offset          Update gp_offset
       jmp fetch
stack: movq ap->overflow_arg_area, %rax  Address of stack slot
       leaq $8(%rax), %rdx               Next available stack slot
       movq %rax, ap->overflow_arg_area  Update
fetch: movl (%rax), %eax                 Load argument

va_copy(dst, src)


这个宏非常简单,就是 va_list 类型的变量赋值。一般的实现如下:

#define va_copy(dst, src)  ((dst)[0] = (src)[0])

va_end(ap)


在我知道的编译器实现中,这个宏都是空的,什么事都不做:

#define va_end(ap)	/* empty */

你可以理解成它是一种未来扩展需要,也可以理解成强迫症的产物(与 va_start 对称)。

参考文献


  1. AMD64 ABI v0.90 (Dec 2, 2003): http://people.freebsd.org/~obrien/amd64-elf-abi.pdf