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

本篇作为上一篇文章 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 单元,这点尤其重要。

以下是代码模板:

     movq %rdi, offset(%rbp)
     movq %rsi, offset+8(%rbp)
     movq %rdx, offset+16(%rbp)
     movq %rcx, offset+24(%rbp)
     movq %r8, offset+32(%rbp)
     movq %r9, offset+40(%rbp)
     testb %al, %al
     je .L1
     movaps %xmm0, offset+48(%rbp)
     movaps %xmm1, offset+64(%rbp)
     movaps %xmm2, offset+80(%rbp)
     movaps %xmm3, offset+96(%rbp)
     movaps %xmm4, offset+112(%rbp)
     movaps %xmm5, offset+128(%rbp)
     movaps %xmm6, offset+144(%rbp)
     movaps %xmm7, offset+160(%rbp)
.L1:

其中,offset 代表第一个参数的偏移量。实际实现中,可以由函数参数列表略做修改,例如把已知的参数类型大小指令修改下。

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)。

以下是代码模板:

movl $gp_offset, ap->gp_offset
movl $fp_offset, ap->fp_offset
leaq 16(%rbp), %rax
movq %rax, ap->overflow_arg_area
leaq offset(%rbp), %rax
movq %rax, ap->reg_save_area

其中,gp_offset, fp_offset 根据函数原型的参数推算。offset 即上面所述 Register Save Area 的偏移量。

va_arg(ap, type)


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

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

以下按照步骤来描述:

  1. 判断 type 是不是通过寄存器来传递,如果不是,跳到 7.
  2. 计算:
  num_gp = 传递 type 需要用到的通用寄存器个数
  num_fp = 传递 type 需要用到的浮点寄存器个数
  1. 如果:
  ap->gp_offset > 48 - num_gp * 8
   	或
   ap->fp_offset > 176 - num_fp * 16

跳到 7.

  1. 根据 ap->gp_offset, ap->fp_offsetap->reg_save_area 获取出参数。这可能需要临时变量来保存,比如参数是保存在不同的寄存器中。
  2. 设:
  ap->gp_offset = ap->gp_offset + num_gp * 8
  ap->fp_offset = ap->fp_offset + num_fp * 16
  1. 返回。
  2. 首先 ap->overflow_arg_area 是根据 8 字节对齐的。
  3. ap-> overflow_arg_area 获取参数。
  4. 设:
  ap->overflow_arg_area += sizeof(type)
  1. ap->overflow_arg_area 按照 8 字节对齐。
  2. 返回。

一个实现示例: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_arg(ap, type) 中 type 的类型和大小各写一个模板:

  1. 如果 type size > 16 字节,使用 va_arg_stack 模板;
  2. 如果 type 是 struct/union,使用 va_arg_record 模板;
  3. 如果 type 是 整数或指针,使用 va_arg_integer 模板;
  4. 如果 type 是 浮点数,使用 va_arg_floating 模板;
  5. 其他情况,断言失败。

va_arg_integer

     movl ap->gp_offset, %eax
     cmpl $48, %eax
     jnb .L1
     movq ap->reg_save_area, %rax
     movl ap->gp_offset, %edx
     addq %rdx, %rax
     addl $8, %edx
     movl %edx, ap->gp_offset
     jmp .L2
.L1:
     movq ap->overflow_arg_area, %rax
     leaq 8(%rax), %rdx
     movq %rdx, ap->overflow_arg_area
.L2:
     # (%rax) now contains the result

上面代码模板 需要两个整数寄存器(%rax, %rdx)。最后的结果可以重复使用 rax 寄存器。

va_arg_floating

     movl ap->fp_offset, %eax
     cmpl $176, %eax
     jnb .L1
     movq ap->reg_save_area, %rax
     movl ap->fp_offset, %edx
     addq %rdx, %rax
     addl $16, %edx
     movl %edx, ap->fp_offset
     jmp .L2
.L1:
     movq ap->overflow_arg_area, %rax
     leaq 8(%rax), %rdx
     movq %rdx, ap->overflow_arg_area
.L2:
     # (%rax) now contains the result

这个模板与上面的模板基本类似,除了换成 fp_offset、以及 16 字节增量。同样需要两个整数寄存器,另外还需要一个 xmm 寄存器来保存最后的浮点数结果。

va_arg_stack

movq ap->overflow_arg_area, %rax
leaq 8(%rax), %rdx
movq %rdx, ap->overflow_arg_area
# (%rax) now contains the content base

这个模板很简单,需要两个整数寄存器。后续一般附带结构体复制指令,可以继续使用 rax 寄存器。

va_arg_record

这种情况最复杂,模板也一般是动态的。首先是根据 type 的 classification 知道需要哪两个寄存器,根据整数和浮点数排列组合,共有四种寄存器组合,都处理了便是。有点综合了 va_arg_integerva_arg_floating 的逻辑。

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