FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

Cの可変長引数はどのようにしてコンパイルされるのか

f:id:msyksphinz:20160522024602j:plain

xv6のcprintfに詰まってしまっているので、気分転換に可変長引数をどのようにしてGCCコンパイルしているのかについて調査してみた。 調査対象にしたアーキテクチャMIPSRISC-Vだ。

msyksphinz.hatenablog.com

テストプログラム

テストプログラムとして以下のようなものを用意した。以下は可変長引数で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となっているのに、アドレス的に続いていることが確認できた。