C++でキャッチされなかった例外がどこで発生したか調べる方法(gdbが使えない場合)

# はじめに

C++のプログラムを書いていると、次のようにどこかで投げられた例外がどこにもキャッチされずプログラムが終了してしまうことがあります。

```bash
$ ./a.out 
hello
terminate called after throwing an instance of 'std::out_of_range'
  what():  vector::_M_range_check: __n (which is 2) >= this->size() (which is 0)
Aborted (core dumped)
```

その場合に、どこで例外が投げられたのか調べる必要があるのですが、gdbが使えない(gdbでアタッチした状態だと再現しない等)の場合の調べる方法があまりなかったので調べてみました。

環境は以下のとおりです

* Ubuntu 20.04
* g++

# サンプル

以下のようなサンプルで試してみます。このプログラムははそのまま実行すると先程のような例外が発生します。

```c++
#include <iostream>
#include <vector>

int test() {
  std::cout << "hello" << std::endl;
  std::vector<int> a;
  std::cout << a.at(2) << std::endl;
  return a.at(1);
}

int main() {
  return test();
}
```

ビルド(分かりやすさのために、デバッグ情報をつけます)

```bash
g++ -g -rdynamic test.cpp
```

実行

```bash
$ ./a.out 
hello
terminate called after throwing an instance of 'std::out_of_range'
  what():  vector::_M_range_check: __n (which is 2) >= this->size() (which is 0)
Aborted (core dumped)
```

例外が発生しますが、どこで発生したかわかりません。

# 例外が発生した場所を調べる方法
## まずgdbを使う方法

例外発生時に、`bt`コマンドでbacktraceを確認し、例外場所が特定できます。`Test () at test.cpp:7` で例外が発生しているのが分かります。

```bash
$ gdb a.out 
GNU gdb (Ubuntu 9.2-0ubuntu1~20.04) 9.2
Copyright (C) 2020 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from a.out...
(gdb) r
Starting program: /home/nishino/proj/show-exception-backtrace/a.out 
hello
terminate called after throwing an instance of 'std::out_of_range'
  what():  vector::_M_range_check: __n (which is 2) >= this->size() (which is 0)

Program received signal SIGABRT, Aborted.
__GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:50
50	../sysdeps/unix/sysv/linux/raise.c: そのようなファイルやディレクトリはありません.
(gdb) bt
#0  __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:50
#1  0x00007ffff7bbd859 in __GI_abort () at abort.c:79
#2  0x00007ffff7e43911 in ?? () from /lib/x86_64-linux-gnu/libstdc++.so.6
#3  0x00007ffff7e4f38c in ?? () from /lib/x86_64-linux-gnu/libstdc++.so.6
#4  0x00007ffff7e4f3f7 in std::terminate() () from /lib/x86_64-linux-gnu/libstdc++.so.6
#5  0x00007ffff7e4f6a9 in __cxa_throw () from /lib/x86_64-linux-gnu/libstdc++.so.6
#6  0x00007ffff7e463ab in ?? () from /lib/x86_64-linux-gnu/libstdc++.so.6
#7  0x0000555555556652 in std::vector<int, std::allocator<int> >::_M_range_check (this=0x7fffffffdf10, __n=2) at /usr/include/c++/9/bits/stl_vector.h:1070
#8  0x000055555555650f in std::vector<int, std::allocator<int> >::at (this=0x7fffffffdf10, __n=2) at /usr/include/c++/9/bits/stl_vector.h:1091
#9  0x00005555555562aa in test () at test.cpp:7
#10 0x0000555555556338 in main () at test.cpp:12
(gdb) quit

```

## `__cxa_allocate_exception` をフックする方法

C++では、例外は`__cxa_allocate_exception` という関数を使ってメモリを確保されることを利用し、これをhookすることで、例外の場所を特定してみます。

以下のように要求されたメモリを確保すると同時に、stack traceを表示するような`hook.cpp`を用意し、

```hook.cpp
#include <execinfo.h>
#include <unistd.h>

#include <array>
#include <iostream>
#include <cxxabi.h>
#include <cstdlib>

extern "C" {
void* __cxa_allocate_exception(size_t thrown_size) {
  void* p = malloc(thrown_size);
  if(p == NULL) {
    printf("Could not allocate memory\n");
    exit(0);
  }
  std::array<void*, 24> stack;
  size_t size = backtrace(stack.data(), 24);
  backtrace_symbols_fd(stack.data(), size, STDOUT_FILENO);
  return p;
}
}
```

ビルドします。

```bash
g++ -shared -fPIC -o hook.so hook.cpp
```

`LD_PRELOAD` を利用し、`__cxa_allocate_exception` を上書きし、先程のプログラムを実行してみます。

```
$ LD_PRELOAD=./hook.so ./a.out 
hello
./hook.so(__cxa_allocate_exception+0x78)[0x7fc247f2c2b1]
/lib/x86_64-linux-gnu/libstdc++.so.6(_ZSt24__throw_out_of_range_fmtPKcz+0x114)[0x7fc247ddd6f4]
./a.out(_ZNKSt6vectorIiSaIiEE14_M_range_checkEm+0x52)[0x55d144f90652]
./a.out(_ZNSt6vectorIiSaIiEE2atEm+0x27)[0x55d144f9050f]
./a.out(_Z4testv+0x61)[0x55d144f902aa]
./a.out(main+0xd)[0x55d144f90338]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf3)[0x7fc247b230b3]
./a.out(_start+0x2e)[0x55d144f9018e]
terminate called after throwing an instance of 'std::out_of_range'
  what():  vector::_M_range_check: __n (which is 2) >= this->size() (which is 0)
Aborted (core dumped)
```

`test` 関数の中で、例外が発生していることが分かります。

`gdb`を使わず、例外が発生している場所を特定する方法を、`__cxa_allocate_exception`を差し替えることで実現してみました。