追記: ほんとは怖くないよ!って話を追記してます。
夏だし怖い話しようぜ!
訳あって一部だけ C++ のコードです。
#include <stdio.h>
#include <iostream>
int
main() {
char str[256];
double a = 123.45;
int b = 57;
int c = 89;
sprintf(str, "a=%f b=%d c=%d", a, b, c);
std::cout << str << std::endl;
sprintf(str, "a=%d b=%f c=%d", a, b, c);
std::cout << str << std::endl;
std::cout << "a=" << a << std::endl;
std::cout << "b=" << b << std::endl;
std::cout << "c=" << c << std::endl;
return 0;
}
コンパイル時に警告は出るだろうけど、ちゃんと動くよね。
$ g++ -Wformat-nonliteral aaaa.cxx && ./a.out
aaaa.cxx: In function ‘int main()’:
aaaa.cxx:16:43: warning: format ‘%d’ expects argument of type ‘int’, but argument 3 has type ‘double’ [-Wformat]
aaaa.cxx:16:43: warning: format ‘%f’ expects argument of type ‘double’, but argument 4 has type ‘int’ [-Wformat]
a=123.450000 b=57 c=89
a=57 b=123.450000 c=89
a=123.45
b=57
c=89
きぇーーーーーーーーーーーーーーー!
C++ でなく C++ の場合だけ発生します。(Cでも発生しました)
ちなみに 32bit Windows だと gcc4.8, gcc4.9, clang3.4.2, VS2010, VS2012, VS2013 では
123.450000 57 89
-858993459 0.000000 89
こう出力されました。64bit Linux の gcc4.6.3, gcc4.9.1 だと上記の結果が出力されました。wandbox でもこんな結果になります。
だれか理由を教えて下さい...
追記
Cの呼び出し規約の varargs で、期待する型と与えられた型のサイズ違い(sizeof(double)-sizeof(int))で発生する様です。(参考)
さらに勘違い。64bit OS の場合は int と double で格納されるレジスタが違うらしく(XMMレジスタ)、その取り出し順序が期待と異なるのが原因らしいです。(yohhoyさんありがとうございます)
さらに 32bit Windows の場合は単に順番にstackに積むだけなのでa[31:0]をintと、b:a[63:32]をdoubleと見做して解釈する事で発生するらしいです。(kikairoyaさんありがとうございます)
#include <stdio.h>
int
main() {
double a = 123.45;
int b = 57;
int c = 89;
printf("%d %f %d\n", a, b, c);
}
このコードを Windows 32bit OS で -S
出力してみました。
.file "aaaa.c"
.def ___main; .scl 2; .type 32; .endef
.section .rdata,"dr"
LC1:
.ascii "%d %f %d\12\0"
.text
.globl _main
.def _main; .scl 2; .type 32; .endef
_main:
pushl %ebp
movl %esp, %ebp
andl $-16, %esp
subl $48, %esp
call ___main
fldl LC0
fstpl 40(%esp)
movl $57, 36(%esp)
movl $89, 32(%esp)
movl 32(%esp), %eax
movl %eax, 16(%esp)
movl 36(%esp), %eax
movl %eax, 12(%esp)
fldl 40(%esp)
fstpl 4(%esp)
movl $LC1, (%esp)
call _printf
leave
ret
.section .rdata,"dr"
.align 8
LC0:
.long -858993459
.long 1079958732
.ident "GCC: (i686-posix-sjlj, built by strawberryperl.com project) 4.8.2"
.def _printf; .scl 2; .type 32; .endef
esp レジスタに 57 と 89 の順で格納され、処理順は異なりますが最後にはコードと同じ順に esp レジスタに格納されています。かたや Linux 64bit OS だと
.file "aaaa.c"
.section .rodata
.LC1:
.string "%d %f %d\n"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $32, %rsp
movabsq $4638387438405602509, %rax
movq %rax, -8(%rbp)
movl $57, -12(%rbp)
movl $89, -16(%rbp)
movl -16(%rbp), %edx
movl -12(%rbp), %ecx
movq -8(%rbp), %rax
movl %ecx, %esi
movq %rax, -24(%rbp)
movsd -24(%rbp), %xmm0
movl $.LC1, %edi
movl $1, %eax
call printf
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (GNU) 4.9.1"
.section .note.GNU-stack,"",@progbits
XMM レジスタが使われている様です。printf がフォーマット %f
や %d
を見て取り出すべきレジスタに異なる値が入っているのだから起きて当たり前という訳でした。