Open2

自作Cコンパイラ(C11)に可変長引数対応を実装する

Suguru KatoSuguru Kato

C11規格におけるstdarg.h

(当たり前だが)仕様のみ指定、実装は未指定。

va_list

可変長引数を格納する型

マクロva_arg

type va_arg(va_list ap, type);

va_list型の変数apから次の引数を型typeとして引き出す。
このマクロは次の引数を指すapの内部ポインタを1つ進める。
(※va_listの中で引数を指す内部実装がポインタとして実装されている必要はない)

マクロva_copy

void va_copy(va_list dest, va_list src);

srcdestにコピーする。

マクロva_end

void va_end(va_list ap);

apの後処理をしてapを使えなくする。
関数から戻る前にva_endを呼ばなかった場合は未定義動作を起こす。

マクロva_start

void va_start(va_list ap, paramN);

apを初期化する。paramNは可変長引数(...)の直前の引数。

Reference

http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf

Suguru KatoSuguru Kato

AMD64におけるstdarg.h

  • 可変長引数を取る関数が呼ばれるときは、ベクターレジスタに格納される浮動小数点数引数の数を%alに格納する。%alの値域は[0,7]なので、8つ以上は
  • __m256もしくは__m512が可変長引数として渡されたときは必ずスタックに載せる。一方、名前付きの変数として渡された場合はレジスターに載せられる。

Register Save Area

  • 可変長引数を受け取りva_startマクロを呼ぶことが分かっている関数のプロローグは、register save areaに引数レジスタを保存する。それぞれのレジスタのregister save area内におけるオフセットは以下の表。
Register Offset
%rdi 0
%rsi 8
%rdx 16
%rcx 24
%r8 32
%r9 40
%xmm0 48
%xmm1 64
...
%xmm15 288
  • 引数を格納するレジスタだけ保存する必要がある。
  • レジスタに格納される引数を全く使わない場合は、register save areaは必要ない。これは一度もvar_argを呼ばないか、名前付き引数が引数レジスタを使い切っている場合。
  • プロローグは%alを参照して不必要なXMMレジスタを保存することを避けなければいけない。特に整数のみのプログラムでXMMユニットの初期化を避けるために必要。

The va_list Type

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

The var_start Macro

reg_save_area

register save areaの開始位置へのポインタ。

overflow_arg_area

スタックに載ってる引数を取ってくるのに使うポインタ。
スタックに載ってる最初の引数のアドレスで初期化される。
以降は、スタックに載っている次の引数の位置へと更新されていく。

gp_offset

次の汎用引数レジスタが保存されている箇所へのreg_save_areaからのバイト単位でのオフセットを格納する。
すべてのレジスタを使い切った後は48(= 6 * 8)になる。

fp_offset

次の浮動小数点数引数レジスタが保存されている箇所へのreg_save_areaからのバイト単位でのオフセットを格納する。
すべてのレジスタを使い切った後は304 (= 6 * 8 + 16 * 16)になる。

The va_arg Macro

va_arg(l, type)
  1. typeはレジスタに渡されるかどうか判断する。違う場合は7へ。
  2. num_gpに必要な汎用レジスタの数を格納する。num_fpに必要な浮動小数点数引数レジスタの数を格納する。
  3. 引数がレジスタにフィットするかどうか確認する。もしl->gp_offset > 48 - num_gp * 8l->fp_offset > 304 - num_fp * 16であれば7へ。
  4. l->reg_save_areaからオフセットl->gp_offsetもしくはl->fp_offsetのところからtypeの値を取ってくる。引数が異なるレジスタクラスに渡される場合や、汎用レジスタのために8より大きいアライメントが必要な時、XMMのために16より大きいアライメントが必要な時は、一時的なところへコピーする必要がある。
  5. l->gp_offset += num_gp*8; l->fp_offset += num_fp*16;
  6. typeの値を返す。
  7. typeが8バイトを超える境界を必要とする場合は、l->overflow_arg_areaを16バイト境界へアラインする。
  8. l->overflow_arg_areaからtypeの値を取ってくる。
  9. l->overflow_arg_area += sizeof(type)
  10. l->overflow_arg_areaを8バイト境界へアラインする。
  11. typeの値を返す。
       movl l->gp_offset, %eax
       cmpl $48, %eax
       jae  stack
       leal $8(%rax), %edx
       addq l->reg_save_area, %rax
       movl %edx, l->gp_offset
       jmp  fetch
stack: movq l->overflow_arg_area, %rax
       leaq 8(%rax), %rdx
       movq %rdx, l->overflow_arg_area
fetch: movl (%rax), %eax

Reference

https://raw.githubusercontent.com/wiki/hjl-tools/x86-psABI/x86-64-psABI-1.0.pdf