背景
Twitterで以下のようなポストを見て、アーキテクチャの違いかな?と思ったが確証がなかったので試してみた。
こどふぉお疲れ様でした…
— ふぉく@しうかつ (@D_A_works) 2019年6月7日
Div2のB問題なんですけど,どこが間違ってるのか教えてくださる方がいらっしゃいましたらお願い致します.
ll := long long
IN(N) := cin >> N
です pic.twitter.com/dgSMGliM2i
検証用コード
# include<stdio.h>
int main(){
long long x=1, y=2, tmp1=3, tmp2=4;
asm("nop");
printf("%d %d %d %d\n", x, y); // ポイント: format上は4変数参照だが実際に関数に与えているのは2変数
asm("nop");
printf("%lld %lld %lld %lld\n", x, y);
asm("nop");
}
32bit環境
Linux debian32 4.9.0-9-686-pae #1 SMP Debian 4.9.168-1+deb9u2 (2019-05-13) i686 GNU/Linux
での実行例
debian32:~$ ./a.out
1 0 2 0
1 2 -5228134716927016668 17184064983
debian32:~$ ./a.out
1 0 2 0
1 2 -5228768035623493692 17184490967
★lld側の後者2つは毎回変わる
64bit環境
Linux www.hogetan.net 4.9.0-8-amd64 #1 SMP Debian 4.9.130-2 (2018-10-27) x86_64 GNU/Linux
$ ./a.out
1 2 160 2103203760
1 2 2147483629 1
$ ./a.out
1 2 160 -1174779984
1 2 2147483628 1
比較
両環境でgcc -W0 -S a.c
してアセンブラを出力して比較する
予備知識
MOVQ move Quad(=通常64bit)
MOVL move Long(=通常32bit)
32bit環境
# text領域
.LC0:
.string "%d %d %d %d\n"
.LC1:
.string "%lld %lld %lld %lld\n"
# 代入部分
movl $1, -16(%ebp)
movl $0, -12(%ebp)
movl $2, -24(%ebp)
movl $0, -20(%ebp)
movl $3, -32(%ebp)
movl $0, -28(%ebp)
movl $4, -40(%ebp)
movl $0, -36(%ebp)
subl $12, %esp
# printf部分(1回目)
pushl -20(%ebp) // 4つめの%dのpop
pushl -24(%ebp) // 3つめの%dのpop
pushl -12(%ebp) // 2つめの%dのpop
pushl -16(%ebp) // 1つめの%dのpop
leal .LC0@GOTOFF(%ebx), %eax
pushl %eax
call printf@PLT
addl $32, %esp
# printf部分(2回目)
subl $12, %esp
pushl -20(%ebp) // 2つめの%lldのpop
pushl -24(%ebp) // 2つめの%lldのpop
pushl -12(%ebp) // 1つめの%lldのpop
pushl -16(%ebp) // 1つめの%lldのpop
leal .LC1@GOTOFF(%ebx), %eax
pushl %eax
call printf@PLT
addl $32, %esp
32bit環境では、long longを32bit x 2として扱っている。
そして、printfの引数として渡す際もxは-20, -24
, yは-12, -16
として渡そうとする。
だが、printfのライブラリ自身が、%dを指定された場合、32bit分しかpopせず、
%lldの場合、64bit分のpopをして表示していると推察される。
64bit環境
# text領域
.LC0:
.string "%d %d %d %d\n"
.LC1:
.string "%lld %lld %lld %lld\n"
# 代入部分
movq $1, -8(%rbp)
movq $2, -16(%rbp)
movq $3, -24(%rbp)
movq $4, -32(%rbp)
# printf部分(1回目)
movq -16(%rbp), %rdx
movq -8(%rbp), %rax
movq %rax, %rsi
leaq .LC0(%rip), %rdi
movl $0, %eax
call printf@PLT
# printf部分(2回目)
movq -16(%rbp), %rdx
movq -8(%rbp), %rax
movq %rax, %rsi
leaq .LC1(%rip), %rdi
movl $0, %eax
call printf@PLT
64bit環境では、long longの変数はmovqを使って操作が行われている。
printfのライブラリは%dでも、%lldでも1回のpop相当で表示すると推察される。
もうちょっと試した
元のポストの後にご本人がポストされている以下の状況を追う。
x=1, y=1;
— ふぉく@しうかつ (@D_A_works) 2019年6月7日
printf(“%d %d\n”, x, y)
だと出力が 1 0 でWA
printf(“%d “, x);
printf(“%d\n”, y);
だと出力が 1 1 で通った
なにこれ
# include<stdio.h>
int main(){
long long x=1, y=2, tmp1=3, tmp2=4;
asm("nop");
printf("%d %d\n", x, y);
asm("nop");
printf("%d\n", x);
asm("nop");
printf("%d\n", y);
asm("nop");
}
実行結果
$ ./a.out
1 0 2 0
1
2
アセンブラ
// テキスト領域
.LC0:
.string "%d %d\n"
.LC1:
.string "%d\n"
// 代入
movl $1, -16(%ebp)
movl $0, -12(%ebp)
movl $2, -24(%ebp)
movl $0, -20(%ebp)
subl $12, %esp
// 1回目のprintf
pushl -20(%ebp)
pushl -24(%ebp) // ほんとはここもpopしてほしいけどされない(32bit2つしかpopしないから)
pushl -12(%ebp) // ここが2つめの%dでpopされる = 0
pushl -16(%ebp) // ここが1つめの%dでpopされる = 1
leal .LC0@GOTOFF(%ebx), %eax
pushl %eax
call printf@PLT
addl $32, %esp
// 2回目のprintf
subl $4, %esp
pushl -12(%ebp)
pushl -16(%ebp) // ここが1つめの%dでpopされる = 1
leal .LC1@GOTOFF(%ebx), %eax
pushl %eax
call printf@PLT
addl $16, %esp
// 3回目のprintf
subl $4, %esp
pushl -20(%ebp)
pushl -24(%ebp) // ここが1つめの%dでpopされる = 2
leal .LC1@GOTOFF(%ebx), %eax
pushl %eax
call printf@PLT
addl $16, %esp
以上の通り、
1つ目のprintfでは%d %d
で-12
と-16
が用いられるが、
2,3つ目のprintfでは%d
で-12だけ
と-24
が用いられて、予期する表示が得られる。
まとめ
32bit環境ではprintfの%dは32bit分のpopしか行わないため、64bit環境で開発したコードを32bit環境で実行すると予期しないことが起こる。