[C++] 参照で例外を捕まえる
More Effective C++ 日本語版の 65 ページに参照で例外を捕まえる
という項があります.初めてここを読んだときは驚きました.この本に書かれている通り,例外オブジェクトを値渡しすると,スライシングが発生する上に 2 度コピーコンストラクタが呼び出されるので,できれば参照で渡したいと思っていました.しかし,関数ローカルな例外オブジェクトを参照で渡してもいいのだろうか,という疑問も同時に浮上.
夜も眠れぬくらい非常に気になったので,その分の睡眠時間を削って,手近な環境(g++-4.1.1, i686-pc-linux-gnu)でテストしてみることに.
以下の簡単なコードでテストします.
class E {
public:
int a;
E() { a = 0x64; }
};
int func1() { throw E(); }
int main() {
int a = 0;
try { func1(); }
catch (E& e) { a = e.a; }
return 0;
}
これを g++ -O0 でコンパイルした後にリンクして,でき上がった ELF を逆アセンブルした結果が次の通りです.ただし,_fini などの余分な関数は除いています.
080485c0 <_Z5func1v>:
80485c0: 55 push ebp
80485c1: 89 e5 mov ebp,esp
80485c3: 53 push ebx
80485c4: 83 ec 14 sub esp,0x14
80485c7: c7 04 24 04 00 00 00 mov DWORD PTR [esp],0x4
80485ce: e8 dd fe ff ff call 80484b0 <__cxa_allocate_exception@plt>
80485d3: 89 c3 mov ebx,eax
80485d5: 89 d8 mov eax,ebx
80485d7: 89 04 24 mov DWORD PTR [esp],eax
80485da: e8 75 00 00 00 call 8048654 <_ZN1EC1Ev>
80485df: c7 44 24 08 00 00 00 mov DWORD PTR [esp+8],0x0
80485e6: 00
80485e7: c7 44 24 04 1c 87 04 mov DWORD PTR [esp+4],0x804871c
80485ee: 08
80485ef: 89 1c 24 mov DWORD PTR [esp],ebx
80485f2: e8 f9 fe ff ff call 80484f0 <__cxa_throw@plt>
80485f7: 90 nop
080485f8 <main>:
80485f8: 8d 4c 24 04 lea ecx,[esp+4]
80485fc: 83 e4 f0 and esp,0xfffffff0
80485ff: ff 71 fc push DWORD PTR [ecx-4]
8048602: 55 push ebp
8048603: 89 e5 mov ebp,esp
8048605: 51 push ecx
8048606: 83 ec 24 sub esp,0x24
8048609: c7 45 f4 00 00 00 00 mov DWORD PTR [ebp-12],0x0
8048610: e8 ab ff ff ff call 80485c0 <_Z5func1v>
8048615: eb 2e jmp 8048645 <main+0x4d>
8048617: 89 45 e8 mov DWORD PTR [ebp-24],eax
804861a: 83 fa 01 cmp edx,0x1
804861d: 74 0b je 804862a <main+0x32>
804861f: 8b 45 e8 mov eax,DWORD PTR [ebp-24]
8048622: 89 04 24 mov DWORD PTR [esp],eax
8048625: e8 b6 fe ff ff call 80484e0 <_Unwind_Resume@plt>
804862a: 8b 45 e8 mov eax,DWORD PTR [ebp-24]
804862d: 89 04 24 mov DWORD PTR [esp],eax
8048630: e8 8b fe ff ff call 80484c0 <__cxa_begin_catch@plt>
8048635: 89 45 f8 mov DWORD PTR [ebp-8],eax
8048638: 8b 45 f8 mov eax,DWORD PTR [ebp-8]
804863b: 8b 00 mov eax,DWORD PTR [eax]
804863d: 89 45 f4 mov DWORD PTR [ebp-12],eax
8048640: e8 5b fe ff ff call 80484a0 <__cxa_end_catch@plt>
8048645: b8 00 00 00 00 mov eax,0x0
804864a: 83 c4 24 add esp,0x24
804864d: 59 pop ecx
804864e: 5d pop ebp
804864f: 8d 61 fc lea esp,[ecx-4]
8048652: c3 ret
8048653: 90 nop
MASM ユーザな私には AT&T 記法は読みづらいので,ここでは逆アセンブル結果を得るために objdump -m i386:intel としています.
さて,main 関数から見てみます.まず最初にスタックフレームの作成とローカル変数の初期化をやってますね.その直後に _Z5func1v (func1 関数) をコールしてます.
_Z5func1v に移って,スタックフレームを作り... その直後に (アドレス 0x80485c7で) GOT 経由で __cxa_allocate_exception を呼び出してます.
この関数は何かというと... libstdc++ に含まれている,以下のような関数です.ここでは関数名を一部改変し,エラー処理を全て省略して示しています.
void* __cxa_allocate_exception(std::size_t thrown_size) throw()
{
void *ret;
thrown_size += sizeof (__cxa_exception);
ret = malloc (thrown_size);
__cxa_eh_globals *globals = __cxa_get_globals ();
globals->uncaughtExceptions += 1;
memset (ret, 0, sizeof (__cxa_exception));
return (void *)((char *)ret + sizeof (__cxa_exception));
}
見ての通り,例外オブジェクト用のメモリ(投げるオブジェクトのサイズ + sizeof(__cxa_exception))をヒープ上に確保しています.
その後で __cxa_throw を call しています.この call 命令の後ろには func1 関数に対応する ret 命令がないことから,__cxa_throw を呼び出した時点で main 関数の catch 節に制御が移るということになります.
その main 関数では他にもいろいろと例外処理に関する関数が呼ばれていますが,今回の目的は例外処理の全貌を解析することではなく,例外オブジェクトを参照で受け取ってもよいかどうかの調査ですので,これ以上調べなくても大丈夫でしょう.念のため,class E のコンストラクタと main 関数の catch 節の両方でそれぞれ例外オブジェクトのポインタの値を出力し,両者が一致していることを確認しました.
結局,ソースコードの上では関数ローカルに見えるオブジェクトが,実はヒープ上に確保されている,ということです.謎は解けました.これで安心して眠り... もとい,参照で catch ができます.めでたしめでたし.
VC の場合はどうなってるのかも気になるところです.また機会があればそのうち...
トラックバック
トラックバック URI: https://www.pakunet.jp/hoge/trackback/2006111201
トラックバックはありません.