xv6のcprintfに詰まってしまっているので、気分転換に可変長引数をどのようにしてGCCはコンパイルしているのかについて調査してみた。 調査対象にしたアーキテクチャはMIPSとRISC-Vだ。
テストプログラム
テストプログラムとして以下のようなものを用意した。以下は可変長引数でint型を読み込み、その総計を算出するプログラムだ。入力値が0ならば終了とする。
#include <stdarg.h> int sumf(int nfirst, ...) { int r = 0, n; va_list args; va_start(args, nfirst); for (n = nfirst; n != 0; n = va_arg(args, int)) r += n; va_end(args); return r; }
コンパイル後コードは以下のようになった。
MIPS
00000000 <sumf>: 0: 27bdfff8 addiu sp,sp,-8 4: 27a3000c addiu v1,sp,12 8: 00001021 move v0,zero c: afa5000c sw a1,12(sp) 10: afa60010 sw a2,16(sp) 14: afa70014 sw a3,20(sp) 18: 10800008 beqz a0,3c <sumf+0x3c> 1c: afa30000 sw v1,0(sp) 20: 24630004 addiu v1,v1,4 24: 00441021 addu v0,v0,a0 28: 8c64fffc lw a0,-4(v1) 2c: 00000000 nop 30: 1480fffc bnez a0,24 <sumf+0x24> 34: 24630004 addiu v1,v1,4 38: 2463fffc addiu v1,v1,-4 3c: 03e00008 jr ra 40: 27bd0008 addiu sp,sp,8
RISC-V
00000000 <sumf>: 0: fd010113 addi sp,sp,-48 4: 01410313 addi t1,sp,20 8: 00b12a23 sw a1,20(sp) c: 00c12c23 sw a2,24(sp) 10: 00d12e23 sw a3,28(sp) 14: 02e12023 sw a4,32(sp) 18: 02f12223 sw a5,36(sp) 1c: 03012423 sw a6,40(sp) 20: 03112623 sw a7,44(sp) 24: 00612623 sw t1,12(sp) 28: 02050463 beqz a0,50 <.L4> 2c: 00030793 mv a5,t1 30: 00000713 li a4,0 00000034 <.L3>: 34: 00478793 addi a5,a5,4 38: 00a70733 add a4,a4,a0 3c: ffc7a503 lw a0,-4(a5) 40: fe051ae3 bnez a0,34 <.L3> 00000044 <.L2>: 44: 00070513 mv a0,a4 48: 03010113 addi sp,sp,48 4c: 00008067 ret 00000050 <.L4>: 50: 00000713 li a4,0 54: ff1ff06f j 44 <.L2>
引数は最大限アーキテクチャのサポートするレジスタを利用する。そこから先はスタック
まず、MIPSは引数用に利用できるレジスタがa0からa3までの4つなので、a1からa3をスタックに貯める。さらに、そこからスタックの先には4番目の引数以降が格納されている前提となる。 一つずつロードしてき、インデックスを4ずらしていく。ロードしたデータを加算しては、0判定をしてループ脱出を決めるという具合だ。
一方でRISC-Vのアーキテクチャはインデックスが8つまで取れるため、a1からa7までをスタックに貯め、同様にそこから先のスタックには8番目以降の引数が格納されているということになる。それ以外の動作は同じだ。
呼び出し元の動作
MIPSで以下のような呼び出しを書いた場合、
int call_sumf (int a[10]) { return sumf (a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], a[9]); }
コンパイルコードは以下のようになる。
00000044 <call_sumf>: 60: 27bdffd0 addiu sp,sp,-48 64: 8c87000c lw a3,12(a0) 68: 8c860008 lw a2,8(a0) 6c: 8c850004 lw a1,4(a0) 70: 8c840000 lw a0,0(a0) 74: afab0024 sw t3,36(sp) 78: afaa0020 sw t2,32(sp) 7c: afa9001c sw t1,28(sp) 80: afa80018 sw t0,24(sp) 84: afa30014 sw v1,20(sp) 88: afa20010 sw v0,16(sp) 8c: afbf002c sw ra,44(sp) 90: 0c000000 jal 0 <sumf> ... 00000000 <sumf>: 0: 27bdfff8 addiu sp,sp,-8 4: 27a3000c addiu v1,sp,12 8: 00001021 move v0,zero c: afa5000c sw a1,12(sp) 10: afa60010 sw a2,16(sp) 14: afa70014 sw a3,20(sp)
sumfが呼び出されてからspが-8されるため、sumfの中では、4番目以降の引数は、
- v0: 16+8 = 24
- v1: 20+8 = 28 ...
となり、第一引数から第8引数が12,16,20となっているのに、アドレス的に続いていることが確認できた。