C
Linux

64bit環境と32bit環境でlong longの出力結果が異なる事象の検証


背景

Twitterで以下のようなポストを見て、アーキテクチャの違いかな?と思ったが確証がなかったので試してみた。


検証用コード

#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相当で表示すると推察される。


もうちょっと試した

元のポストの後にご本人がポストされている以下の状況を追う。

#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環境で実行すると予期しないことが起こる。