变参在GCC(PowerPC)上的实现

Keywords: 变参,variable arguments, va_start, va_arg, va_end, GCC, PowerPC, ppc

  花了两天时间定位的一个变参死机问题,结果查出来是传递变参的两个模块所用编译环境的Tornado版本不一致。虽然结果让我很无语,不过此间倒是第一次透彻的了解了变参在GCC(PowerPC)上的实现,一点心得暂记于此。

  因为PowerPC在函数参数传递的机制上与ARM类似,都采用了一定数目的寄存器来传递参数,以提高执行效率。显然这是与va_list通常的定义和实现方式相矛盾的。所以,GCC采用了另一个途径来实现与原有的ANSI C变参机制的兼容:va_start首先将寄存器中的参数保存在栈区(实际上,为了避免寄存器被使用,这个操作会被提前到函数的入口处,而无视va_start的书写位置),当然,用于传递参数的寄存器数目是有限的,所以超出这个数目的参数仍是采用压栈的方式传递的。

  注:va_start会将所有可能用于变参传递的寄存器均保存在栈中,因为它并不知道实际上传递来的参数个数。在PowerPC上,这些寄存器包括:r3 – r10, f1 – f8(下面会解释这两组寄存器),可以传递8个常规参数和8个浮点参数(如果CPU支持硬件浮点运算)

  在ppc的变参定义头文件中,可以看到如下声明:

typedef struct __va_list_tag {

 char gpr;

 char fpr;

 char *overflow_arg_area;

 char *reg_save_area;

} __va_list[1], \__gnuc_va_list[1];

  其中grp存储的是下一个“常规类型”变参在其暂存数组中的下标;fpr存储的是下一个“浮点类型”变参在其暂存数组中的下标;overflow_arg_area所指向的就是前面提到的超出数目的那些参数在堆栈中的首地址;而reg_save_area的指向正是va_start在栈区所分配的“寄存器暂存区”。

  为什么要分为两种类型来存储变参呢?那是因为某些PowerPC芯片中有一组64位的浮点寄存器(fpr),它们可以用于存储浮点类型的参数;而其它类型的变参(即“常规类型”)均存储在通用寄存器(gpr)中。需要注意的是,对于聚合类型(结构体、联合等)的参数,实际保存在gpr中的是指向它的指针(传递引用)。

  下面我们来看看va_arg的实现:(伪码书写,仅反映主要行为)

if ( 需要获取的参数类型是“浮点类型” && fpr < 8 ) {  从reg_save_area的第二个数组中取下标为fpr的8字节量作为浮点数返回  fpr++ } else if ( 需要获取的参数类型是“聚合类型” && gpr < 8 ) {  从reg_save_area的第一个数组中取下标为gpr的4字节量作为地址,并返回该所指向的聚合类型的内容  gpr++ } else if ( 既不是“浮点类型”,也不是“聚合类型” && gpr < 8 ) {  从reg_save_area的第一个数组中取下标为gpr的4字节量,转换为所需的类型并返回  gpr++ } else // 其它情况,也就是超出gpr和fpr所能存储的参数个数时 {  从overflow_arg_area中读取所需的参数  overflow_arg_area += 参数长度 }

  注:上面省略了longlong类型参数的传递过程,其实longlong的参数在gpr剩余个数 >= 2时,仍然是通过寄存器传递的,只不过被分割在两个gpr寄存器中传递而已。

  从如此复杂的一个流程来看,其实在PowerPC上使用变参的代价是比i386多出很多的。光是va_arg一个宏展开的代码长度就超过了1k字节。而实现的复杂性也必然导致问题的多样化,从GCC的更新记录来看,变参这一部分也是频繁出现bug的。在文章开头所提到的死机问题也是由于va_list结构在两个Tornado版本中对齐方式不一致而造成的。所以,PowerPC下使用变参真的是要尤其小心,千万别试图挑战GCC的智商哦。

Written on September 8, 2006