はじめに
@skytomo221さんの書かれた記事『古代C言語で1=2を証明してみた』のパクりです。
C++ コンパイラで 1=2 を証明してみました。
コードと実行
$ cat 1=2.cpp
#include <iostream>
bool compare(int a, int b)
{
if (a == b) {
std::cout << a << "==" << b << std::endl;
}
}
int main()
{
compare(1, 2);
}
$ g++ --version
g++ (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0
Copyright (C) 2019 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
$ g++ -O2 -w 1=2.cpp
$ ./a.out
1==2
Segmentation fault (core dumped)
$
実行エラーも出てるしわやですね。
どうしてこうなった
みなさんお気づきの通り、関数 compare(int, int) が bool の型を持つにも係わらず bool の値を return しないことにより未定義動作を踏んでいることが原因です。
致命的な動作不良なのでコンパイラにはエラーや警告を出て欲しいところですが、コマンドラインオプションに警告の出力を抑止するオプション`-w'を与えており警告の出力を禁止しています。これを指定しないと普通に警告は出力されるのでコンパイラに罪はありません。
$ g++ 1=2.cpp
1=2.cpp: In function ‘bool compare(int, int)’:
1=2.cpp:8:1: warning: no return statement in function returning non-void [-Wreturn-type]
8 | }
| ^
$
さて、折角なのでコンパイラがどうのようなコードを吐いているか確認してみましょう。
Compiler Explorer で確認したところでは
.LC0:
.string "=="
compare(int, int):
pushq %r12
movl %esi, %r12d
movl %edi, %esi
movl $_ZSt4cout, %edi
pushq %rbp
subq $8, %rsp
call std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
movl $2, %edx
movl $.LC0, %esi
movq %rax, %rbp
movq %rax, %rdi
call std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
movq %rbp, %rdi
movl %r12d, %esi
call std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
movq %rax, %rdi
call std::basic_ostream<char, std::char_traits<char> >& std::endl<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&)
main:
subq $8, %rsp
movl $2, %esi
movl $1, %edi
call compare(int, int)
_GLOBAL__sub_I_compare(int, int):
subq $8, %rsp
movl $_ZStL8__ioinit, %edi
call std::ios_base::Init::Init() [complete object constructor]
movl $__dso_handle, %edx
movl $_ZStL8__ioinit, %esi
movl $_ZNSt8ios_base4InitD1Ev, %edi
addq $8, %rsp
jmp __cxa_atexit
となっていました。
compare(int, int) のコードが、std::cout へ std::endl を出力する呼び出しでスッパリ終わっており、通常の関数であれば
- スタックポインタを戻す命令
- 復帰命令
辺りがあるところですがそれらが全く見当たりません。
推測ですが、コンパイラの最適化の作用により、
bool compare(int a, int b)
{
if (a == b) {
std::cout << a << "==" << b << std::endl;
}
}
- bool の値を return する筈がそれが見当たらない
- 関数の中に処理らしいものは std::cout への出力処理だけなので 1. を満足するためには std::cout への出力処理を行った後、この関数に帰ってくるパスが存在しない(戻って来ない)
- 2. により、std::cout への出力処理より後の処理は命令を生成しなくて良い
- std::cout への出力処理は絶対に行われる想定なので
a == b
の判定は常に真ということにして良い
以上のような判断が行われたのではないかと思います。
さて、この途中までしか生成されなかった compare(int, int) を実行するとどうなるか。実行ファイル a.out を逆アセンブルしてみると
$ objdump -d a.out|less
~ 略 ~
0000000000001230 <_Z7compareii>:
1230: f3 0f 1e fa endbr64
1234: 41 54 push %r12
1236: 41 89 f4 mov %esi,%r12d
1239: 89 fe mov %edi,%esi
123b: 48 8d 3d fe 2d 00 00 lea 0x2dfe(%rip),%rdi # 4040 <_ZSt4cout@@GLIBCXX_3.4>
1242: 55 push %rbp
1243: 48 83 ec 08 sub $0x8,%rsp
1247: e8 84 fe ff ff callq 10d0 <_ZNSolsEi@plt>
124c: ba 02 00 00 00 mov $0x2,%edx
1251: 48 8d 35 ac 0d 00 00 lea 0xdac(%rip),%rsi # 2004 <_IO_stdin_used+0x4>
1258: 48 89 c5 mov %rax,%rbp
125b: 48 89 c7 mov %rax,%rdi
125e: e8 4d fe ff ff callq 10b0 <_ZSt16__ostream_insertIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_PKS3_l@plt>
1263: 48 89 ef mov %rbp,%rdi
1266: 44 89 e6 mov %r12d,%esi
1269: e8 62 fe ff ff callq 10d0 <_ZNSolsEi@plt>
126e: 48 89 c7 mov %rax,%rdi
1271: e8 1a fe ff ff callq 1090 <_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_@plt>
1276: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
127d: 00 00 00
0000000000001280 <__libc_csu_init>:
1280: f3 0f 1e fa endbr64
1284: 41 57 push %r15
1286: 4c 8d 3d eb 2a 00 00 lea 0x2aeb(%rip),%r15 # 3d78 <__frame_dummy_init_array_entry>
128d: 41 56 push %r14
128f: 49 89 d6 mov %rdx,%r14
1292: 41 55 push %r13
1294: 49 89 f5 mov %rsi,%r13
1297: 41 54 push %r12
1299: 41 89 fc mov %edi,%r12d
129c: 55 push %rbp
129d: 48 8d 2d e4 2a 00 00 lea 0x2ae4(%rip),%rbp # 3d88 <__do_global_dtors_aux_fini_array_entry>
~ 略 ~
$
途中で終わっている compare(int, int) の後に配置された__libc_csu_init() というなんだかわからん関数に処理が継続するようです。__libc_csu_init() の中身は知る気になりませんが仮にこの関数が正常に処理を行えたとしても、最後の RET で参照するスタック位置に正常な戻りアドレスは積まれていないのでどっか変なところへ飛んで行ってしまいますね。"1==2" の出力の後にセグフォを吐いた理由は恐らくこれでしょう。
clang についてもついでで確認してみました。
compare(int, int) が std::cout へ std::endl を出力する処理の呼び出しでスッパリ終わっているのは gcc と同様のようです。
実行ファイル a.out を逆アセンブルしてみると
$ objdump -d a.out|less
~ 略 ~
00000000004011a0 <_Z7compareii>:
4011a0: 53 push %rbx
4011a1: 89 fb mov %edi,%ebx
4011a3: bf 80 40 40 00 mov $0x404080,%edi
4011a8: 89 de mov %ebx,%esi
4011aa: e8 c1 fe ff ff callq 401070 <_ZNSolsEi@plt>
4011af: be 04 20 40 00 mov $0x402004,%esi
4011b4: 48 89 c7 mov %rax,%rdi
4011b7: e8 94 fe ff ff callq 401050 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@plt>
4011bc: 48 89 c7 mov %rax,%rdi
4011bf: 89 de mov %ebx,%esi
4011c1: e8 aa fe ff ff callq 401070 <_ZNSolsEi@plt>
4011c6: 48 89 c7 mov %rax,%rdi
4011c9: e8 62 fe ff ff callq 401030 <_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_@plt>
4011ce: 66 90 xchg %ax,%ax
00000000004011d0 <main>:
4011d0: 50 push %rax
4011d1: bf 01 00 00 00 mov $0x1,%edi
4011d6: be 02 00 00 00 mov $0x2,%esi
4011db: e8 c0 ff ff ff callq 4011a0 <_Z7compareii>
~ 略 ~
$
途中で終わってる compare(int, int) の直後に main() が配置されています。compare(int, int) での std::cout への std::endl の出力処理の後、main() へ処理が継続することとなるため、clang では "1==2" が連続してどばどば出力されてたのはこれが原因でしょう。
プログラムの修正
正しいプログラムに修正するとしては、compare(int, int) の型を値を返さない void とするか
void compare(int a, int b)
{
if (a == b) {
std::cout << a << "==" << b << std::endl;
}
}
関数の型に従い値を返すよう関数の最後に return 値; を追加するかですが
bool compare(int a, int b)
{
if (a == b) {
std::cout << a << "==" << b << std::endl;
}
return true;
}
そうしてしまってはこの記事のテーマである 1=2 の証明ができなくなってしまいます。
https://godbolt.org/z/fqWEv4MKq
https://godbolt.org/z/nesxacMnn
仕方がないので
bool compare(int a, int b)
{
if (a == b) {
std::cout << a << "==" << b << std::endl;
return true;
}
}
a == b
の条件にあった場合のみ true を返すようしてみましょう。
$ cat 1=2.cpp
#include <iostream>
bool compare(int a, int b)
{
if (a == b) {
std::cout << a << "==" << b << std::endl;
return true;
}
}
int main()
{
compare(1, 2);
}
$ g++ -O2 -w 1=2.cpp
$ ./a.out
1==2
$
こうすることで
- 値を返して復帰するのは
a == b
の条件が真となった場合に限られる - 値を返すはずの関数で値を返さないのは未定義動作であるのでその条件は想定しないで良い
- 1. と 2. により
a == b
は常に真になるに決まっている
以上のような判断がコンパイラに働くのか、g++ でも clang++ でも "1==2" が出力され異常終了もしない実行ファイルを生成することができました。勿論、未定義動作を前提としており C++ のプログラムとしては正しいものではありません。全く間違っていますがこの記事自体ネタとして書いてるものなのでこれで良しとします。
おわりに
おわりです。