Arch64中va_list/va_start/va_arg/…的实现

一、背景

1. 可变参函数

        可变参函数指的是一个可以接受可变个参数的函数, 调用此函数时只有caller知道为此函数传入了多少个参数, 可变参函数callee只知道caller最少传入了多少个参数:

  • callee中可以确定的参数称为命名参数(Named arguments)
  • callee中不确定的参数称为匿名参数(Anonymous arguments)

对于可变参函数(callee), 其需要:

  • 在其函数栈中预留空间存储所有可能的匿名寄存器参数(否则由于不确定传入了多少个寄存器参数,不保存则AAPCS64标准中所有传参寄存器默认在callee中不能用)
  • 在运行时通常需要通过已知的某个命名参数来确定caller到底传入了多少个参数(caller在调用可变参数函数时也需要显式或隐式的告知callee自己传入了多少个参数), 如:
/* 常见的处理可变参的宏定义如下,后续代码中直接使用内联函数
	#define va_start(v,l)	__builtin_va_start(v,l)
	#define va_end(v)		__builtin_va_end(v)
	#define va_arg(v,l)		__builtin_va_arg(v,l)
	#define va_copy(d,s)	__builtin_va_copy(d,s)
*/
 
//int printf (const char * format, ... );
#define weak __attribute__((weak))
weak int func1(int x, ...)
{
        int i = x;
    
        __builtin_va_list vl;
        __builtin_va_start (vl, x);
        while(i > 0)
        {
                printf("%d
", va_arg(vl, int));					    // callee根据caller传入的参数个数逐个获取参数
                i --;
        }
        __builtin_va_end (vl);
 
        return 0;
}
 
int main(void)
{
   register void * sp asm("sp");
   printf("main1: sp:%p, fp:%p
", sp, __builtin_frame_address(0));		//printf通过参数R0隐式确定参数个数
   
   func1(3, 1, 2, 3);													//一般情况下调用可变参数需通过命名参数显式告知参数个数
}  
 
## 输出
tangyuan@ubuntu:~/tests/gcc/aarch64_stack/caller_callee_parm_test$ ./main 
main1: sp:0x40007ff860, fp:0x40007ff860
1
2
3

2. 可变参函数的参数

        可变参函数的参数分为四类, 即寄存器命名参数, 寄存器匿名参数, 栈命名参数, 栈匿名参数, 举例如下:

#define REP7(X) X,X,X,X,X,X,X
#define REP8(X) X,X,X,X,X,X,X,X
#define weak __attribute__((weak))
/*
   func1的:
   * 第1个参数(x0,必须指定)为命名寄存器参数
   * 第2-8个参数(若有)为匿名寄存器参数
   * 第9-n个参数(若有)为匿名栈参数
*/
weak int func1(int x0, ...) { ... }
/*
   func2的:
   * 前8个参数(x0-x7,必须指定)为命名寄存器参数
   * 第9个参数(x8,必须指定)为命名栈参数
   * 第10-n个参数(若有)为匿名栈参数
*/
weak int func2(int x0, int x1, int x2, int x3, int x4, int x5, int x6, int x7, int x8, ...) { ... }

        可变参函数的参数传递同样满足AAPCS64:

* 对caller来说, 编译期间即可确定其向可变参callee传入了多少个参数,故其直接按照AAPCS64标准布局参数(寄存器参数直接赋值给硬件寄存器,栈参数保存到outgoing栈对应位置).

* 对callee来说, 编译期间只能确定有多少个命名参数:

  • 命名寄存器参数直接通过硬件寄存器获取
  • 命名栈参数通过incoming栈获取

* 对callee来说, 匿名参数的个数通常需直接或间接通过某个命名参数得知:

  • 匿名栈参数同样继续通过incoming区域获取
  • 匿名寄存器参数则是从匿名寄存器存储区域(callee-allocated save area for register varargs)获取
     

二、va_xxx系列函数

         在可变参函数中通常会使用va_xxx系列函数来获取匿名参数,va_xxx函数在gcc中会对应到其对应的一个builtin函数上:

//stdarg.h
#define va_start(v,l)	__builtin_va_start(v,l)
#define va_end(v)		__builtin_va_end(v)
#define va_arg(v,l)		__builtin_va_arg(v,l)
#define va_copy(d,s)	__builtin_va_copy(d,s)

        gcc中定义的此系列内联函数包括:

__builtin_va_list
__builtin_va_start
__builtin_va_end
__builtin_va_copy
__builtin_next_arg
__builtin_saveregs
__builtin_va_arg

这里只介绍其中部分,其余类似的函数见源码.

三、__builtin_va_list

     __builtin_va_list是gcc内置的一个结构体类型, 在gcc内部定义如下:

/* 关于函数栈的构成可参考[3], AAPCS64中可变参函数的说明可参考[1] */
typedef struct {
       void *__stack;					/* __stack 记录下一个匿名栈参数的存储位置, 随着va_arg的调用可能变化 */
       void *__gr_top;					/* __gr_top 记录最后一个匿名通用寄存器参数的尾地址, 其不随va_arg调用变化 */
       void *__vr_top;					/* __vr_top 记录最后一个匿名浮点寄存器参数的尾地址, 其不随va_arg调用变化 */
       int   __gr_offs;				    /* __gr_offs 记录下一个匿名通用寄存器参数到__gr_top的偏移(负数),随着va_arg的调用可能变化 */
       int   __vr_offs;					/* __vr_offs 记录下一个匿名浮点寄存器参数到__vr_top的偏移(负数),随着va_arg的调用可能变化 */
} __builtin_va_list;

        各字段在函数栈中的位置如下:

/*
 *             high address
 *                 ---+--++------------------------------+
 *                    |  ||incoming stack arguments      |
 *                    |  ||------------------------------|
 *                    |  ||  ......                      |
 *                    |  ||------------------------------|
 *                    |  ||  param 9                     |
 *                    |  ||------------------------------|-- <-- vl->stack (指向incoming栈中第一个匿名栈参数的位置)
 *                    |  ||  param 8                     |
 *                    |  ||==============================+-+ <-- virtual_incoming_args_rtx/vl->grtop (指向最后一个匿名通用寄存器参数的尾地址)
 *                    |  ||callee-allocated save area    | |
 *                    |  ||for register varargs          | |
 *   stack for        |  ||------------------------------| |
 * variable arguments |  ||  param 7                     | | vl->groff		(记录下一个匿名通用寄存器参数到grtop的偏移)
 *                    |  ||------------------------------| |
 *                    |  ||  param 6                     | |
 *                    |  ||------------------------------+-+
 *                    |  ||  .......(aligned)            |
 *                    |  ||------------------------------+-+ <-- vl->vrtop	(指向最后一个匿名浮点寄存器参数的尾地址)
 *                    |  ||  VPRs(max is 8*16)           | |
 *                    |  ||                              | |
 *                    |  ||  ......                      | |
 *                    |  ||------------------------------| | vl->vroff		(记录下一个匿名浮点寄存器参数到vrtop的偏移)
 *                    |  ||  vparam1                     | |
 *                    |  ||------------------------------| |
 *                    |  ||  vparam0                     | |
 *                 ---+--+|==============================+-+
 *                       ||  .......                     |
 *                       ||                              |
 *                       ++------------------------------+-- <- stack_pointer_rtx
 *              low address
 */

        一个经过初始化后的__builtin_va_list结构体(vl)中:

  • 可以通过vl->__stack获取下一匿名栈参数, [vl->__stack, ......] 为匿名栈参数区
  • 可以通过vl->__gr_top + vl->__gr_offs 获取下一个匿名通用寄存器参数, [vl->__gr_top + vl->__gr_offs , vl->__gr_top]为匿名通用寄存器参数区
  • 可以通过vl->__vr_top + vl->__vr_offs 获取下一个匿名浮点寄存器参数, [vl->__vr_top + vl->__vr_offs , vl->__vr_top]为匿名浮点寄存器参数区
     

四、__builtin_va_start(vl, x)

         __builtin_va_start(vl, x)的作用是初始化vl结构体,在aarch64中这里的x没有实际的作用,但调用此函数时x必须是此函数的最后一个命名参数,否则会编译报错。__builtin_va_list vl; 就是一个简单的结构体局部变量定义,故在使用vl之前必须调用__builtin_va_start函数对其初始化, __builtin_va_start的作用为:

  • 设置vl->__stack 指向此函数第一个匿名栈参数可能出现的位置
  • 设置vl->__gr_top 指向最后一个匿名通用寄存器参数尾地址
  • 设置vl->__gr_offs 为vl->__gr_top 到 第一个匿名通用寄存器参数之间的偏移(负数)
  • 设置vl->__vr_top指向最后一个匿名浮点寄存器参数尾地址
  • 设置vl->__vr_offs 为vl->__vr_top 到 第一个匿名浮点寄存器参数之间的偏移(负数)

        需要注意的是, 编译器按照AAPCS64布局函数栈,一个函数的第一个匿名栈参数/通用/浮点寄存器参数的位置(基于sp的偏移)只与当前函数声明的(命名)参数列表有关,故这些偏移在编译阶段即可确定, __builtin_va_start 基于当前sp + offset计算此次运行时这些参数的位置。
 

五、__builtin_va_arg(vl, type)

       __builtin_va_arg(vl, type) 的作用是返回当前尚未访问的一个类型为type的匿名参数的值(注意返回的是值而不是地址), 并调整vl中的__stack/__gr_offset/__vr_offset指向下一个匿名参数位置。__builtin_va_arg从哪个区域返回匿名参数是由当前vl中的信息和此次要获取的参数类型type共同决定的:

  • type为浮点类型时,__builtin_va_arg会先尝试在匿名浮点寄存器参数区域查找,若没有则在incoming栈中查找下一个参数
  • type为整型时, __builtin_va_arg会先尝试在匿名通用寄存器参数区查找,若没有则在incoming栈中查找下一个参数

       以通用寄存器为例, 判断匿名通用寄存器参数区是否还有参数的标准为:

  • [vl->__gr_top + vl->__gr_offs , vl->__gr_top] 区域是否还能存下一个类型为type的参数
  • 若能存下则按照type类型大小返回 vl->__gr_top + vl->__gr_offs 中存储的值作为下一个匿名参数 (此函数总是返回值,无法get到地址)
  • vl->__gr_offs += sizeof(type); offset指向下一个匿名通用寄存器参数(未考虑对齐)

其伪代码[1]简化后如下:

type __builtin_va_arg (va_list ap, type)
{
      int nreg, offs;                                                                                                                                                                                     
      /* 若这是个浮点类型的参数(根据type判断) 则先尝试从VPRs 匿名浮点寄存器参数区域中获取参数 */
      if (type passed in simd/fp registers) 
      {
        offs = ap.__vr_offs;
        if (offs >= 0)         		   			/* 若匿名浮点寄存器参数区域已没有参数, 则直接去incoming栈区域获取下一个参数 */
          goto on_stack; 	
        
        nreg = (sizeof(type) + 15) / 16;		/* 否则看当前匿名浮点寄存器参数区域中是否能存下此类型的参数 */
        ap.__vr_offs = offs + (nreg * 16);  	/* 如果存不下则说明此参数只能是栈参数,跳转到on_stack从incoming栈获取 */ 	
        if (ap.__vr_offs > 0)		   	
          goto on_stack;
          
        return *(type *)(ap.__vr_top + offs);   /* 如果匿名浮点寄存器参数区域还有参数,则直接从此区域中获取并返回(返回"值"而不是地址) */	 
          
      } else {									/* 否则尝试从GPRs匿名通用寄存器参数区域获取参数 */
        offs = ap.__gr_offs;
        if (offs >= 0)          				/* 若匿名通用寄存器参数区域为空, 则直接去incoming栈区域获取参数 */
          goto on_stack;
          
        if (alignof(type) > 8) offs = (offs + 15) & -16; 
        nreg = (sizeof(type) + 7) / 8;           /* 计算此类型占用几个标准寄存器空间 */
        ap.__gr_offs = offs + (nreg * 8);        /* 根据类型确定匿名通用寄存器区域是否还能存下一个此类型的匿名参数 */
        if (ap.__gr_offs > 0)                    /* 如果不能(offs > 0),则去incoming栈区域获取下一个参数 */
          goto on_stack;
          
        return *(type *)(ap.__gr_top + offs);   /* 若参数在GPRs匿名通用寄存器区域,则直接返回参数值(而不是地址) */
      }
    
/* 若此匿名参数不在匿名寄存器区域则其只能保存在incoming栈中, 这里从incoming栈中获取此匿名参数 */
on_stack:
      intptr_t arg = ap.__stack;              /* 获取当前incoming栈中尚未被解析到的匿名栈参数地址 */
    
      if (alignof(type) > 8)			      /* 按需对齐 */
              arg = (arg + 15) & -16;
    
      ap.__stack = (void *)((arg + sizeof(type) + 7) & -8);		/* 修改ap.__stack指向下一个可能出现的匿名栈参数的位置 */
      return *(type *)arg;			      	  /* 返回此匿名栈参数的值 */
}

 举例如下:

#include <stdio.h>
#include <stdarg.h>
 
struct __va_list			/* 自定义的__va_list结构体 */
{
        void *__stack;
        void *__gr_top;
        void *__vr_top;
        int   __gr_offs;
        int   __vr_offs;
};
 
int func(int p0, int p1, int p2, ...)
{
        register unsigned long sp asm("sp");
        unsigned long  var0 = (unsigned long)&var0 - sp;		/* 第一个局部变量到callee入口sp的offset */
        unsigned long  var1 = (unsigned long)&var1 - sp;	    /* 第二个局部变量到callee入口sp的offset */
        __builtin_va_list vl;
 
        p0 = (unsigned long)&p0 - sp;							/* 第一个命名寄存器参数的存储位置到callee入口sp的offset
                                                                 * (解引用会导致callee在局部变量中为命名寄存器参数分配存储空间,
                                                                 *  这里顺便解释局部变量的内存分配)
                                                                 */
        p1 = (unsigned long)&p1 - sp;							/* 第二个命名寄存器参数的存储位置到callee入口sp的offset */
        printf("[+] sp:%p
", sp);
        printf("[+] var0 in addr %p (offset %p)
", &var0, var0);
        printf("[+] var1 in addr %p (offset %p)
", &var1, var1);
        printf("[+] vl in addr %p, size:%d
", &vl, sizeof(vl));	/* 局部变量vl的地址及大小 */
 
        printf("[+] p0 in addr %p (offset %p)
", &p0, p0);
        printf("[+] p1 in addr %p (offset %p)
", &p1, p1);
 
        var0 = 0;
        __builtin_va_start (vl, p2);	/* p2记录此函数匿名寄存器个数 */
        while(var0 < p2) {
                var0 ++;
                if( var0 < 6) {
                        /* 输出剩余匿名通用寄存器参数的存储首地址和offset */
                        var1 = ((struct __va_list *)&vl)->__gr_top + ((struct __va_list *)&vl)->__gr_offs;
                } else {
                        /* 输出下一个匿名栈参数的存储位置 */
                        var1 = ((struct __va_list *)&vl)->__stack;
                }
                printf("[+] p%d in addr %p (offset %p) with value %d
", var0 + 2, var1, var1 - sp, va_arg(vl, int));
        }
}
 
int main(void)
{
        func(0, 0, 7, 1, 2, 3, 4, 5, 6, 7);
}

输出结果:

##aarch64-linux-gnu-gcc main.c -O2 -o main
## 输出结果
tangyuan@ubuntu:~/tests/gcc/aarch64_stack/caller_callee_parm_test$ ./main
[+] sp:0x40007ffdf0
[+] var0 in addr 0x40007ffe38 (offset 0x48)
[+] var1 in addr 0x40007ffe30 (offset 0x40)
[+] vl in addr 0x40007ffe10, size:32
[+] p0 in addr 0x40007ffe0c (offset 0x1c)
[+] p1 in addr 0x40007ffe08 (offset 0x18)
[+] p3 in addr 0x40007ffec8 (offset 0xd8) with value 1
[+] p4 in addr 0x40007ffed0 (offset 0xe0) with value 2
[+] p5 in addr 0x40007ffed8 (offset 0xe8) with value 3
[+] p6 in addr 0x40007ffee0 (offset 0xf0) with value 4
[+] p7 in addr 0x40007ffee8 (offset 0xf8) with value 5
[+] p8 in addr 0x40007ffef0 (offset 0x100) with value 6
[+] p9 in addr 0x40007ffef8 (offset 0x108) with value 7

其函数栈分布如图:

六、__builtin_va_end(vl)

       在man 手册中对于va_end的描述是:所有vl结构体在使用完毕后都要调用va_end, 以标记在此后的代码中vl结构体不再可用.

        未开启编译优化时,__builtin_va_end(vl)语句并没有什么实质性作用; 但当开启编译优化时若在 __builtin_va_end(vl)之后再使用 vl,则可能因优化导致不可预期的错误,如:
 

#include <stdio.h>
#include <alloca.h>
#include <stdarg.h>
 
#define weak __attribute__((weak))
struct __va_list
{
        void *__stack;     //下一个栈参数地址
        void *__gr_top;    //GPR参数保存的位置
        void *__vr_top;    //FP/SIMD参数保存的位置
        int   __gr_offs;   //从__gr_top到下一个GP参数的offset
        int   __vr_offs;   //从__vr_top到下一个FP/SIMD寄存器参数的位置
};
 
weak int func1(int p0, int p1, int p2, int p3, int p4, int p5, int p6, int p7, int p8, ...)
{
      __builtin_va_list vl; 
      __builtin_va_start (vl, p8);
      printf("%d
", va_arg(vl, int));
        __builtin_va_end (vl);
 
      printf("%d
", va_arg(vl, int));
      return 0;
}
 
int main(void)
{
        func1(3, 1, 2, 3, 4, 5, 6, 7, 8, 1, 2); 
}
 
 
##aarch64-linux-gnu-gcc main.c -O2 -o main
##aarch64-linux-gnu-gcc main.c -O2 -o main1 -fno-tree-dse
tangyuan@ubuntu:~/tests/gcc/aarch64_stack/varargs_test$ ./main1 
1
2							## 正确
tangyuan@ubuntu:~/tests/gcc/aarch64_stack/varargs_test$ ./main
1
1							## 错误

七、源码分析(TLDR)

1. __builtin_va_list

        __builtin_va_list 是gcc内部定义的一个结构体类型, 在源码解析前 __builtin_va_list 类型声明结点已经定义好了,当语法分析解析到 符号 "__builtin_va_list"时会直接将其识别为一个结构体类型,其转换为c代码类似:

typedef struct {
       void *__stack;
       void *__gr_top;
       void *__vr_top;
       int   __gr_offs;
       int   __vr_offs;
} __builtin_va_list;

此结构体在cc1初始化阶段定义:

//toplev::main => lang_dependent_init => lang_hooks.init => c_init_decl_processing => c_common_nodes_and_builtins
// => build_common_tree_nodes 
void build_common_tree_nodes (bool signed_char)
{
  ......
  tree t = targetm.build_builtin_va_list ();	/* aarch64中为 aarch64_build_builtin_va_list */
  va_list_type_node = t;
}
 
/* 定义结构体类型结点(RECORD_TYPE) __va_list */
static tree aarch64_build_builtin_va_list (void)
{
  tree va_list_name;
  tree f_stack, f_grtop, f_vrtop, f_groff, f_vroff;
 
  /* 创建名为 __va_list的结构体类型 */
  va_list_type = lang_hooks.types.make_type (RECORD_TYPE);
  va_list_name = build_decl (BUILTINS_LOCATION, TYPE_DECL, get_identifier ("__va_list"), va_list_type);
  DECL_ARTIFICIAL (va_list_name) = 1;			/* 标记这是编译器生成的一个类型声明结点 */
  TYPE_NAME (va_list_type) = va_list_name;		/* 设置类型结点对应的名字 */
  TYPE_STUB_DECL (va_list_type) = va_list_name;
 
  /* 定义此结构体中的每个字段, 结果类似c代码:
     struct
     {
        void *__stack;
        void *__gr_top;
        void *__vr_top;
        int   __gr_offs;
        int   __vr_offs;
     };
  */
  f_stack = build_decl (BUILTINS_LOCATION, FIELD_DECL, get_identifier ("__stack"), ptr_type_node);
  f_grtop = build_decl (BUILTINS_LOCATION, FIELD_DECL, get_identifier ("__gr_top"), ptr_type_node);
  f_vrtop = build_decl (BUILTINS_LOCATION, FIELD_DECL, get_identifier ("__vr_top"), ptr_type_node);
  f_groff = build_decl (BUILTINS_LOCATION, FIELD_DECL, get_identifier ("__gr_offs"), integer_type_node);
  f_vroff = build_decl (BUILTINS_LOCATION, FIELD_DECL, get_identifier ("__vr_offs"), integer_type_node);
  ......
  
  /* 设置成员所在的结构体 */
  DECL_FIELD_CONTEXT (f_stack) = va_list_type;
  DECL_FIELD_CONTEXT (f_grtop) = va_list_type;
  DECL_FIELD_CONTEXT (f_vrtop) = va_list_type;
  DECL_FIELD_CONTEXT (f_groff) = va_list_type;
  DECL_FIELD_CONTEXT (f_vroff) = va_list_type;
 
  /* 设置结构体成员顺序 */
  TYPE_FIELDS (va_list_type) = f_stack;
  DECL_CHAIN (f_stack) = f_grtop;
  DECL_CHAIN (f_grtop) = f_vrtop;
  DECL_CHAIN (f_vrtop) = f_groff;
  DECL_CHAIN (f_groff) = f_vroff;
 
  layout_type (va_list_type);			/* 计算此类型布局(各成员占空间大小) */
  return va_list_type;
}
 
/* 定义类型声明结点(TYPE_DECL) __builtin_va_list */
void c_common_nodes_and_builtins (void)
{
  lang_hooks.decls.pushdecl(build_decl (UNKNOWN_LOCATION,
		 TYPE_DECL, get_identifier ("__builtin_va_list"), va_list_type_node));
}

2. __builtin_va_xxx的定义

  __builtin_va_xxx系列函数包括:

__builtin_va_list
__builtin_va_start
__builtin_va_end
__builtin_va_copy
__builtin_next_arg
__builtin_saveregs
2.1 gcc中内联函数的定义

  gcc中所有的内联函数都记录在 "builtins.def"文件中,如:

./gcc/builtins.def
DEF_GCC_BUILTIN        (BUILT_IN_VA_COPY, "va_copy", BT_FN_VOID_VALIST_REF_VALIST_ARG, ATTR_NOTHROW_LEAF_LIST)
DEF_GCC_BUILTIN        (BUILT_IN_VA_END, "va_end", BT_FN_VOID_VALIST_REF, ATTR_NOTHROW_LEAF_LIST)
DEF_GCC_BUILTIN        (BUILT_IN_VA_START, "va_start", BT_FN_VOID_VALIST_REF_VAR, ATTR_NOTHROW_LEAF_LIST)
DEF_GCC_BUILTIN        (BUILT_IN_NEXT_ARG, "next_arg", BT_FN_PTR_VAR, ATTR_LEAF_LIST)
DEF_GCC_BUILTIN        (BUILT_IN_SAVEREGS, "saveregs", BT_FN_PTR_VAR, ATTR_NULL)

在cc1初始化阶段会先为这些内联函数定义声明树节点(FUNCTION_DECL):

//toplev::main => lang_dependent_init => lang_hooks.init => c_init_decl_processing => c_common_nodes_and_builtins
// => c_define_builtins
static void
c_define_builtins (tree va_list_ref_type_node, tree va_list_arg_type_node)
{
    ......
    #define DEF_BUILTIN(ENUM, NAME, CLASS, TYPE, LIBTYPE, BOTH_P, FALLBACK_P, 
		    NONANSI_P, ATTRS, IMPLICIT, COND)			
    if (NAME && COND)							
    def_builtin_1 (ENUM, NAME, CLASS,                                   
		   builtin_types[(int) TYPE],                           
		   builtin_types[(int) LIBTYPE],                        
		   BOTH_P, FALLBACK_P, NONANSI_P,                       
		   built_in_attributes[(int) ATTRS], IMPLICIT);
    
    #include "builtins.def"				/* 对每个builtin函数都调用def_builtin_1 */
}
 
static void
def_builtin_1 (enum built_in_function fncode, const char *name, ......
{
    .......
    /* 为此builtin函数构建函数声明节点(FUNCTION_DECL) */
    decl = add_builtin_function (name, fntype, fncode, fnclass,
			       (fallback_p ? libname : NULL), fnattrs);
    .......
    /* 将函数声明结点添加到全局 visible_builtins 队列中 */
    add_builtin_function (libname, libtype, fncode, fnclass, NULL, fnattrs);
}
2.2 gcc解析源码中的内联函数

  内联函数声明结点定义后,当词法分析中发现一个内联函数名token时会将其自动转换为对应的内联函数声明树结点,此过程发生在:

toplev::main => do_compile => compile_file => c_common_parse_file => c_parser_translation_unit => c_parser_declaration_or_fndef

 细节参考[2], 这里不再做展开.

2.3 内联函数的展开

  在rtl_expand阶段, 源码中调用的所有内联函数都会被展开:

//pass_expand::execute => expand_gimple_basic_block => expand_call_stmt ... => expand_builtin
rtx expand_builtin (tree exp, rtx target, rtx subtarget, machine_mode mode, int ignore)
{
    enum built_in_function fcode = DECL_FUNCTION_CODE (fndecl);
    .......
    switch (fcode)
    {
     case BUILT_IN_VA_START:
        return expand_builtin_va_start (exp);
     case BUILT_IN_VA_END:
        return expand_builtin_va_end (exp);
     case BUILT_IN_VA_COPY:
        return expand_builtin_va_copy (exp);
     case BUILT_IN_NEXT_ARG:
        if (fold_builtin_next_arg (exp, false))
            return const0_rtx;
        return expand_builtin_next_arg ();
     case BUILT_IN_SAVEREGS:
          return expand_builtin_saveregs ();
     .......
     }
}

       需要注意的是,这里介绍的只是这些内联函数自身代码的展开,当调用某些函数时可能本身存在副总用(如BUILT_IN_VA_END会影响编译优化), 相关话题不在本文讨论范围内。

3. __builtin_va_start 的展开

  源码中调用的__builtin_va_start函数最终通过 expand_builtin_va_start 函数展开:

/*
   此函数负责展开对 __builtin_va_start 内联函数的调用,__builtin_va_start(vl, x)的作用是初始化vl结构体.
   此函数中会额外检查__builtin_va_start是否传入了>=2个参数, x是否为__builtin_va_start caller中最后一个定参.
   最终发射指令将vl结构体初始化,如下:
      vl->stack = virtual_incoming_args_rtx + cum->aapcs_stack_size * UNITS_PER_WORD;
      vl->grtop = virtual_incoming_args_rtx
      vl->vrtop = virtual_incoming_args_rtx - gr_save_area_size * UNITS_PER_WORD
      vl->groff = -gr_save_area_size
      vl->vroff = -vr_save_area_size
*/
static rtx expand_builtin_va_start (tree exp)
{
  rtx nextarg;
  tree valist;
 
  /* 这里检查了va_start是否传入了>=2个参数,同时也检查了第二个参数必须是此函数的最后一个定参 */
  ......
      
  /*
     返回一个rtx表达式,代表当前函数栈中可能出现的第一个匿名栈参数的位置, 即:
     virtual_incoming_args_rtx + crtl->args.arg_offset_rtx
     其中arg_offset_rtx是此函数中命名栈参数占用空间大小,在pass_expand::execute展开当前函数栈时确定的.
  */
  nextarg = expand_builtin_next_arg ();`
  
  /* 以 __builtin_va_start(vl,x)为例, 这里获取变量vl的左值结点 */
  valist = stabilize_va_list_loc (loc, CALL_EXPR_ARG (exp, 0), 1);
 
  /* 对于AArch64平台这里调用 aarch64_expand_builtin_va_start, 此函数中实际上没用上nextarg参数,
     此函数逻辑简单代码复杂,这里不做展开,此函数的作用是发射rtl指令初始化vl结构体, 伪代码如下:
      vl->stack = virtual_incoming_args_rtx + cum->aapcs_stack_size * UNITS_PER_WORD;
      vl->grtop = virtual_incoming_args_rtx
      vl->vrtop = virtual_incoming_args_rtx - gr_save_area_size * UNITS_PER_WORD
      vl->groff = -gr_save_area_size
      vl->vroff = -vr_save_area_size
  */
  if (targetm.expand_builtin_va_start)
    targetm.expand_builtin_va_start (valist, nextarg);
  ......
 
  return const0_rtx;
}

4. __builtin_va_end的展开

  __builtin_va_end(vl) 通过函数expand_builtin_va_end展开, 其作用是注销变量vl, 此后的代码中vl结构体不再可用。在expand_builtin_va_end函数中本身并没有做什么:

static rtx expand_builtin_va_end (tree exp)
{
  tree valist = CALL_EXPR_ARG (exp, 0);
  /* Evaluate for side effects, if needed.  I hate macros that don't do that.  */
  if (TREE_SIDE_EFFECTS (valist))
    expand_expr (valist, const0_rtx, VOIDmode, EXPAND_NORMAL);
 
  return const0_rtx;
}

        因此在未开启优化时源码中的 __builtin_va_end(vl); 语句并没有什么实质性的作用; 但当开启优化时可能导致不确定行为,大体原因可能是(这里笔者没有深入研究) __builtin_va_end(vl)会被认为是变量vl的def, 此后若再使用变量vl则会认为此时的vl与调用__builtin_va_end之前的vl无关:
 

//pass_dse::execute => dse_optimize_stmt => dse_classify_store => stmt_kills_ref_p
bool stmt_kills_ref_p (gimple *stmt, ao_ref *ref)
{
  ......
  if (is_gimple_call (stmt))
  {
    tree callee = gimple_call_fndecl (stmt);
    if (callee != NULL_TREE && gimple_call_builtin_p (stmt, BUILT_IN_NORMAL))
	switch (DECL_FUNCTION_CODE (callee))
	{
	  case BUILT_IN_VA_END:
	  {
	    tree ptr = gimple_call_arg (stmt, 0);
	    if (TREE_CODE (ptr) == ADDR_EXPR)
		{
		  tree base = ao_ref_base (ref);
		  if (TREE_OPERAND (ptr, 0) == base)
		    return true;
		}
	      break;
	  }
    }
  return false;
}

4. __builtin_va_copy

__builtin_va_copy(dest, src) 通过函数 expand_builtin_va_copy 展开,其作用是将src这个__va_list的各个当前值复制到 dest这个__va_list中, 细节见源码.

5 __builtin_next_arg

__builtin_next_arg 通过函数expand_builtin_next_arg 展开, 用来获取此函数第一个匿名栈参数的起始地址。

static rtx expand_builtin_next_arg (void)
{
  /* 返回 virtual_incoming_args_rtx + crtl->args.arg_offset_rtx, arg_offset_rtx 是当前函数中命名栈参数的个数. */
  return expand_binop (ptr_mode, add_optab,
		       crtl->args.internal_arg_pointer,
		       crtl->args.arg_offset_rtx,
		       NULL_RTX, 0, OPTAB_LIB_WIDEN);
}

6. __builtin_saveregs

  __builtin_saveregs通过expand_builtin_saveregs展开,aarch64不支持此函数(很少有平台支持此函数), 逻辑上此函数负责确保 varargs 都存到了栈上。

7. __builtin_va_arg

7.1 源码中的解析流程

  和其他__builtin_va_xxx函数不同, 在gcc中__builtin_va_arg实际上是一个关键字:

//./gcc/c-family/c-common.c
const struct c_common_resword c_common_reswords[] =
{  
    ......
    { "__builtin_va_arg", RID_VA_ARG,	0 },
    ......
}

1. 当源码中解析到 __builtin_va_arg 关键字时会先将其转换为一个VA_ARG_EXPR表达式:

//toplev::main => do_compile => compile_file => c_common_parse_file => c_parser_translation_unit => c_parser_declaration_or_fndef
// ... => c_parser_unary_expression => c_parser_postfix_expression
static struct c_expr
c_parser_postfix_expression (c_parser *parser)
{
    switch (c_parser_peek_token (parser)->type)
    {
     case RID_VA_ARG:
     {
        ......
	    e1 = c_parser_expr_no_commas (parser, NULL);	/* 解析如 __builtin_va_arg(vl, int) 中第一个参数vl */
	    t1 = c_parser_type_name (parser);				/* 解析第二个参数, 如类型 int */
         
        /* 构建一个 VA_ARG_EXPR 表达式, 此返回类型为 t1(如int), 参数为 e1(vl) */
		expr.value = c_build_va_arg (start_loc, e1.value, loc, groktypename (t1, &type_expr, NULL));
		......
	  }
     ......
    }
}

2. gimplify阶段会将 VA_ARG_EXPR 表达式转换为一个 函数编号为 IFN_VA_ARG 的gcall指令:

//toplev::main => do_compile => compile_file => symbol_table::finalize_compilation_unit => analyze_functions
// => cgraph_node::analyze => gimplify_function_tree => ... => gimplify_expr
enum gimplify_status
gimplify_expr (tree *expr_p, gimple_seq *pre_p, gimple_seq *post_p,
	       bool (*gimple_test_f) (tr`ee), fallback_t fallback)
{
    switch (TREE_CODE (*expr_p))
	{
    case VA_ARG_EXPR:
	  ret = gimplify_va_arg_expr (expr_p, pre_p, post_p);
	  break;
    ......
    }
}
 
enum gimplify_status
gimplify_va_arg_expr (tree *expr_p, gimple_seq *pre_p, gimple_seq *post_p ATTRIBUTE_UNUSED)
{ 
  ......
  tag = build_int_cst (build_pointer_type (type), 0);
  aptag = build_int_cst (TREE_TYPE (valist), 0);
    
  /* 构建一个CALL_EXPR, ifn为 IFN_VA_ARG, 其包含三个参数分别是 valist, tag, aptag,以 __builtin_va_arg(vl, type)为例, 其中:
     * valist是指向vl的一个指针树节点
     * tag是 type类型的一个常量结点
     * aptag是 __built_va_list类型的一个常量结点
  */
  *expr_p = build_call_expr_internal_loc (loc, IFN_VA_ARG, type, 3, valist, tag, aptag);
  .....
  return GS_OK;
}

3.  在pass_stdarg中通过平台相关函数单独处理 gcall(IFN_VA_ARG):

//toplev::main => do_compile => compile_file => symbol_table::finalize_compilation_unit => symbol_table::compile
// => expand_all_functions => cgraph_node::expand => execute_pass_list (cfun, g->get_passes ()->all_passes); => pass_stdarg
unsigned int pass_stdarg::execute (function *fun)
{
  expand_ifn_va_arg (fun);
 
  /* 对于可变参函数,若指定了 -fstdarg-opt优化, 则尝试优化其 va_list_gpr_size/va_list_fpr_size大小.
     对于没有调用过va_xxx的函数实际上不需要保存匿名寄存器参数.
     这里的结果只能是确定优化与不优化, 要么设置va_lsit_gpr_size=0要么保持不变.
  */
  if (flag_stdarg_opt && fun->stdarg != 0)
    optimize_va_list_gpr_fpr_size (fun);
 
  return 0;
}
 
//expand_ifn_va_arg => expand_ifn_va_arg_1
static void expand_ifn_va_arg_1 (function *fun)
{
  basic_block bb;
  gimple_stmt_iterator i;
 
  /* 遍历指令序列中所有 gcall(IFN_VA_ARG) */
  FOR_EACH_BB_FN (bb, fun)
    for (i = gsi_start_bb (bb); !gsi_end_p (i); gsi_next (&i))
    {
	  gimple *stmt = gsi_stmt (i);
    
	  if (!gimple_call_internal_p (stmt, IFN_VA_ARG))
	    continue;
        
      /* 在aarch64平台通过调用 aarch64_gimplify_va_arg_expr 展开对 __builtin_va_arg(vl, int)的调用 */
	  expr = targetm.gimplify_va_arg_expr (ap, type, &pre, &post);
      ......
    }
}
7.2 __builtin_va_arg的展开

 在aarch64中 __builtin_va_arg最终通过 aarch64_gimplify_va_arg_expr 函数展开, 此过程中构建了一系列表达式。源码全是表达式展开较复杂,还是结合AAPCS64[1]以一个简化的伪代码说明原理:

type va_arg (va_list ap, type)
{
      int nreg, offs;                                                                                                                                                                                     
      /* 若这是个浮点类型的参数(根据type判断) 则先尝试从VPRs 匿名浮点寄存器参数区域中获取参数, 
         在gcc源码中这里通过 aarch64_vfp_is_call_or_return_candidate 函数判断
       */
      if (type passed in simd/fp registers) 
      {
        offs = ap.__vr_offs;
        if (offs >= 0)         		   			/* 若匿名浮点寄存器参数区域已没有参数, 则直接去incoming栈区域获取下一个参数 */
          goto on_stack; 	
        
        nreg = (sizeof(type) + 15) / 16;		/* 否则看当前匿名浮点寄存器参数区域中是否能存下此类型的参数 */
        ap.__vr_offs = offs + (nreg * 16);  	/* 如果存不下则说明此参数只能是栈参数,跳转到on_stack从incoming栈获取 */ 	
        if (ap.__vr_offs > 0)		   	
          goto on_stack;
          
        return *(type *)(ap.__vr_top + offs);   /* 如果匿名浮点寄存器参数区域还有参数,则直接从此区域中获取并返回(返回"值"而不是地址) */	 
          
      } else {									/* 否则尝试从GPRs匿名通用寄存器参数区域获取参数 */
        offs = ap.__gr_offs;
        if (offs >= 0)          				/* 若匿名通用寄存器参数区域为空, 则直接去incoming栈区域获取参数 */
          goto on_stack;
          
        if (alignof(type) > 8) offs = (offs + 15) & -16; 
        nreg = (sizeof(type) + 7) / 8;           /* 计算此类型占用几个标准寄存器空间 */
        ap.__gr_offs = offs + (nreg * 8);        /* 根据类型确定匿名通用寄存器区域是否还能存下一个此类型的匿名参数 */
        if (ap.__gr_offs > 0)                    /* 如果不能(offs > 0),则去incoming栈区域获取下一个参数 */
          goto on_stack;
          
        return *(type *)(ap.__gr_top + offs);   /* 若参数在GPRs匿名通用寄存器区域,则直接返回参数值(而不是地址) */
      }
    
/* 若此匿名参数不在匿名寄存器区域则其只能保存在incoming栈中, 这里从incoming栈中获取此匿名参数 */
on_stack:
      intptr_t arg = ap.__stack;              /* 获取当前incoming栈中尚未被解析到的匿名栈参数地址 */
    
      if (alignof(type) > 8)			      /* 按需对齐 */
              arg = (arg + 15) & -16;
    
      ap.__stack = (void *)((arg + sizeof(type) + 7) & -8);		/* 修改ap.__stack指向下一个可能出现的匿名栈参数的位置 */
      return *(type *)arg;			      	  /* 返回此匿名栈参数的值 */
}

__builtin_va_arg(vl, type)获取下一个参数匿名的流程受到三个因素影响:

  1. 当前函数声明中记录的此函数命名参数个数及类型
  2. 此次调用要获取的参数的类型(type)
  3. vl结构体的内容(其中记录此前已经获取了多少个参数)

其中:

1. 函数声明:

  函数声明决定了此函数中已经有多少个命名参数,同时也影响了其不定参数的存储位置,如:

/*
   此函数中存在1个命名参数x0,按照AAPCS64标准x0是通用寄存器参数,因此此函数中:
   * 第1-7个匿名通用寄存器参数将保存在匿名通用寄存器参数区域,此区域的大小为 8 * 8 = 64 byte(aligned)
   * 第0-7个匿名浮点寄存器参数将保存在匿名浮点寄存器参数区域,此区域的大小为 8 * 16 = 128 byte;
   * 若还有其他匿名参数则一定为匿名栈参数,保存在incoming栈中,起始位置为 incoming栈首地址 + arg_offset_rtx(=0)
*/
int func1(int x0, ...) { ... }
/*
   此函数中存在9个命名参数x0-x8, 按照AAPCS64标准x0-x7是命名寄存器参数, x8为栈参数,此函数中:
   * 不存在匿名通用寄存器参数,对应区域大小为0(此时gr_top虽然指向incoming栈首地址,但 gr_offs为0).
   * 第0-7个匿名浮点寄存器参数同样保存在匿名浮点寄存器参数区域,大小为 8 * 16;
   * 若还存在其他匿名参数(不论什么类型),都一律为匿名栈参数,保存在incoming栈中. 
     此时由于incoming栈中已有一个命名栈参数x8, 故匿名栈参数起始位置(vl.__stack) 为incoming栈首地址 + arg_offset_rtx(=8)
*/
int func2(int x0, int x1, int x2, int x3, int x4, int x5, int x6, int x7, int x8, ...) { ... }

2. 参数类型(type)

  源码中使用va_arg(vl, type)时,编译器即可根据类型确定运行时应先从GPRs还是VPRs中动态获取参数. 对于如没有匿名通用寄存器参数的函数(如func2), 编译期间可直接优化为从incoming栈中获取匿名参数,如:

weak int func1(int p0, int p1, int p2, int p3, int p4, int p5, int p6, int p7, int p8, ...)
{
      __builtin_va_list vl; 
      __builtin_va_start (vl, p8);
      printf("%d
", va_arg(vl, int));
      return 0;
}
 
## aarch64-linux-gnu-gcc main.c -O2 -S -o main.s
func1:
        stp     x29, x30, [sp, -48]!
        adrp    x0, .LC0
        add     x0, x0, :lo12:.LC0
        mov     x29, sp
        add     x2, sp, 56
        ldr     w1, [sp, 56]    				## 参数w1直接来自栈,因为此函数不会存在匿名通用寄存器参数
        str     x2, [sp, 16]
        add     x2, sp, 48
        stp     x2, x2, [sp, 24]
        stp     wzr, wzr, [sp, 40]
        bl      printf
        mov     w0, 0
        ldp     x29, x30, [sp], 48
        ret

3. vl结构体

  vl结构体中记录已经获取过了哪些匿名寄存器,在__builtin_va_start(vl, ...)初始化时,其记录的是第一个匿名通用寄存器/浮点寄存器/栈参数的地址, 每调用一次va_arg获取一个参数后,此结构体中对应字段会指向下一个通用寄存器/浮点寄存器/栈参数。