hoge diary - November 12, 2006

[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 の場合はどうなってるのかも気になるところです.また機会があればそのうち...

コメント

名前(何でも可):

テキスト(http:// を含む内容は投稿できません):

トラックバック

トラックバック URI: https://www.pakunet.jp/hoge/trackback/2006111201

トラックバックはありません.


Valid XHTML 1.1! Valid CSS!
© 2004-2009 ぱくちゃん.
Last modified: Fri Nov 02 08:58:03 JST 2007