28
9

More than 3 years have passed since last update.

C++ コンパイラで 1=2 を証明してみた

Last updated at Posted at 2021-05-08

はじめに

@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)
$ 

実行エラーも出てるしわやですね。

折角なので clang++ 9.0.1 でも試してみました

どうしてこうなった

みなさんお気づきの通り、関数 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;
    }
}
  1. bool の値を return する筈がそれが見当たらない
  2. 関数の中に処理らしいものは std::cout への出力処理だけなので 1. を満足するためには std::cout への出力処理を行った後、この関数に帰ってくるパスが存在しない(戻って来ない)
  3. 2. により、std::cout への出力処理より後の処理は命令を生成しなくて良い
  4. 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
$ 

こうすることで

  1. 値を返して復帰するのはa == bの条件が真となった場合に限られる
  2. 値を返すはずの関数で値を返さないのは未定義動作であるのでその条件は想定しないで良い
  3. 1. と 2. によりa == bは常に真になるに決まっている

以上のような判断がコンパイラに働くのか、g++ でも clang++ でも "1==2" が出力され異常終了もしない実行ファイルを生成することができました。勿論、未定義動作を前提としており C++ のプログラムとしては正しいものではありません。全く間違っていますがこの記事自体ネタとして書いてるものなのでこれで良しとします。

おわりに

おわりです。

28
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
28
9