x86_64の呼び出し規約 | パークのソフトウエア開発者ブログ|ICT技術(Java・Android・iPhone・C・Ruby)なら株式会社パークにお任せください

パークのソフトウエア開発者ブログ|ICT技術(Java・Android・iPhone・C・Ruby)なら株式会社パークにお任せください

開発の解決方法や新しい手法の情報を、パークのエンジニアが提供します。パークのエンジニアが必要な場合は、ぜひお気軽にお問い合わせ下さい。 株式会社パーク:http://www.pa-rk.co.jp/

下記のようなCのコードがあります。
#include <stdio.h>
#include <stdarg.h>

static long sum(int n, ...)
{
    long total = 0;
    va_list ap;
    va_start(ap, n);

    int i;
    for (i=0; i < n; i++)
    {
        total += va_arg(ap, long);
    }
    va_end(ap);
    return total;
}

int main(int argc, char *argv[])
{
    printf("a) %ld\n", sum(10, -1L, -2L, -3L, -4L, -5L, -6L, -7L, -8L, -9L, -10L));
    printf("b) %ld\n", sum(5, 1, 2, 3, 4, 5));
    printf("c) %ld\n", sum(10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
    return 0;
}

このコードを64bitのLinux上でビルドします。
 $ gcc -m64 -Wall -g -o sample sample.c
実行すると以下の結果が得られます
 $ ./sample
 a) -55
 b) 15
 c) -21474836425

c)の結果が55でないのはlong(64bit)の引数にint(32bit)と解釈される1..10を渡してるからでしょ?
ちゃんと 1L とか (long)1 とかすれば?と思いますよね。ご尤もその通りです。

でも b) はなんで正しい結果が得られてるのに c)はダメなの?というのが今回の本題。

調べたところ、関数の呼び出し規約 がx86_64の場合、

  • 整数・ポインタ引数 : RDI, RSI, RDX, RCX, R8, R9 の6個のレジスタ
  • 浮動小数点引数 : XMM0 ~ XMM7 の8個のレジスタ

が使用され、これ以上の引数はスタックに積まれて渡されます。


よって b) のケースでは 6個の引数なので全てレジスタ(64bit)に格納されてセーフ
c) のケースでは 6 ~ 10 がスタックに32bitずつしかコピーされずにアウトという理由でした。

objdumpで逆アセしてみると 6~10までが movlで32bitしかコピーされていないことが判ります。

  $ objdump -S sample
    :
  printf("c) %ld\n", sum(10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
  4006fd:       c7 44 24 20 0a 00 00    movl   $0xa,0x20(%rsp)
  400704:       00
  400705:       c7 44 24 18 09 00 00    movl   $0x9,0x18(%rsp)
  40070c:       00
  40070d:       c7 44 24 10 08 00 00    movl   $0x8,0x10(%rsp)
  400714:       00
  400715:       c7 44 24 08 07 00 00    movl   $0x7,0x8(%rsp)
  40071c:       00
  40071d:       c7 04 24 06 00 00 00    movl   $0x6,(%rsp)
  400724:       41 b9 05 00 00 00       mov    $0x5,%r9d
  40072a:       41 b8 04 00 00 00       mov    $0x4,%r8d
  400730:       b9 03 00 00 00          mov    $0x3,%ecx
  400735:       ba 02 00 00 00          mov    $0x2,%edx
  40073a:       be 01 00 00 00          mov    $0x1,%esi
  40073f:       bf 0a 00 00 00          mov    $0xa,%edi
  400744:       b8 00 00 00 00          mov    $0x0,%eax
  400749:       e8 e2 fd ff ff          callq  400530 <sum>
    :

キチンとサフィックスLを付けると以下のようにmovqで64bitコピーされ正しい結果が得られます。
  printf("c) %ld\n", sum(10, 1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L));
  400760:       48 c7 44 24 20 0a 00    movq   $0xa,0x20(%rsp)
  400767:       00 00
  400769:       48 c7 44 24 18 09 00    movq   $0x9,0x18(%rsp)
  400770:       00 00
  400772:       48 c7 44 24 10 08 00    movq   $0x8,0x10(%rsp)
  400779:       00 00
  40077b:       48 c7 44 24 08 07 00    movq   $0x7,0x8(%rsp)
  400782:       00 00
  400784:       48 c7 04 24 06 00 00    movq   $0x6,(%rsp)
  40078b:       00
  40078c:       41 b9 05 00 00 00       mov    $0x5,%r9d
  400792:       41 b8 04 00 00 00       mov    $0x4,%r8d
  400798:       b9 03 00 00 00          mov    $0x3,%ecx
  40079d:       ba 02 00 00 00          mov    $0x2,%edx
  4007a2:       be 01 00 00 00          mov    $0x1,%esi
  4007a7:       bf 0a 00 00 00          mov    $0xa,%edi
  4007ac:       b8 00 00 00 00          mov    $0x0,%eax
  4007b1:       e8 7a fd ff ff          callq  400530 <sum>
    :
寧ろ、c) より b) が正しい結果を得られる事の方が恐ろしいですね。

(printfなど標準関数を除いて)可変引数は警告出ないので気を付けましょう。
そんなにたくさんの引数を使うことは希でしょうが・・・