【C++】LLDBによる動的な関数呼び出しが出来ない問題と対処【print(p/po)コマンド】

C++のクラス・オブジェクトには、LLDBによる関数の動的呼び出しが行えないケースが存在する。

struct Foo {
  int get() { return 9; }
};

Foo a;
// (lldb) p a.get()
// error: call to a function 'Foo::get()' ('_ZN3Foo3getEv') that is not present in the target
// error: 0 errors parsing expression
// error: The expression could not be prepared to run in the target

get関数の実体である_ZN3Foo3getEvシンボルが見つからないというエラーが発生している。

原因

プログラム内でget関数が一度も使われていないとこの現象が発生する。

逆にプログラム内で一度でもget関数が使用されていれば、get関数の実体がコンパイラによって生成され、LLDB側でも問題なく動的な関数呼び出しが行えるようになる。

Foo a;
a.get();
// (lldb) p a.get()
// (int) $1 = 9

これはvectorクラス、stringクラスのat関数やその他の一部の関数についても同様の方法で対処が可能である。

一部の関数に制限あり

上記クラスのsize関数や、スマートポインタ(unique_ptr, shared_ptr)のget関数についてはClangのlibc++ライブラリが__attribute__((__always_inline__))オプション(関数を常にインライン化するオプション)を指定している関係で関数の実体が作成されない制限があり、動的呼び出しは常に行えない。加えて、シンボルの可視属性が不可視__visibility__("hidden")に設定されている点にも注意したい。

コンパイル時にマクロ定義_LIBCPP_INLINE_VISIBILITYを無効にすればなんとかなるのではないか。

ちなみに、上記の制限によって動的な関数呼び出しが行えない場合は、最終手段としてプライベート・メンバ変数を直接参照する方法があるので、万が一のために覚えておくと良いかもしれない。

std::unique_ptr<int> ptr = std::make_unique<int>(99);
// (lldb) p *ptr.__ptr_.__first_
// (int) $1 = 99

std::u16string str = u"ABC";
// (lldb) p (char16_t*)str.__r_.__first_.__s.__data_
// (char16_t *) $0 = 0x00007fff5fbff482 u"ABC"

根本原因

なお、Foo::get関数の実体が生成されない根本的な原因は、get関数をクラスの宣言部で実装している所にある。

宣言部で実装された関数はインライン関数となるため、実体は存在し得ないわけだ。上記の対応方法で生成された実体はおそらくスタックトレース用のものなのであろう。最適化ビルド時やリリースビルド時には生成されない可能性がある。

対処方法

3つの対処方法がある。一番のお手頃は「 対処方法2」だが、もっとも安全でオススメの方法は「 対処方法3」である。

対処方法1

宣言部で行われていた関数の実装を分離することで対処が可能になる。

/* foo.h */
struct Foo {
  int get();
};
/* foo.cpp */
int Foo::get() { return 9; } // 分離した

Foo a;
// (lldb) p a.get()
// (int) $1 = 9

ただしinlineキーワードによる関数のインライン化は当然行えなくなる。

対処方法2

こちらは最終奥義であり、オススメはしない。私はデバッグ中に何度もお世話になったが、あまり他人に勧められるものではない。

対象の関数をvirtualキーワードによって仮想関数として宣言すれば、LLDB側でも動的な呼び出しが可能になる。

struct Foo {
  virtual int get() { return 9; }
};

Foo a;
// (lldb) p a.get()
// (int) $1 = 9

対処方法3

また実体化したい関数が複数になってしまう場合は、対象の関数を呼び出すだけのダミー関数を作成すると良い。

struct Foo {
  int get() { return 9; }
  int foo() { return 8; }
#ifdef DEBUG
private:
  virtual void OPEN_YOUR_HEART() {
    get(); /* HACK_DEFINITIONS */
    foo();
  }
#endif
};

Foo a;
// (lldb) p a.get()
// (int) $1 = 9
// (lldb) p a.foo()
// (int) $1 = 8

若干面倒ではあるが、実体化したい対象の関数を個別にvirtual修飾するよりは安全で意図も明確になる。

オーバーライド時の対応

これらのえげつないハックは非オーバーライド関数であれば派生クラス側でも問題なく機能する。

struct Bar : Foo { };

Bar b;
// (lldb) p b.get()
// (int) $0 = 9

ただ残念ながらオーバーライド時にはエラーになってしまう。

struct Foo       { virtual int get() { return 9;   } };
struct Bar : Foo { virtual int get() { return 123; } };

Bar b;
// (lldb) p b.get()
// error: Execution was interrupted, reason: EXC_BAD_ACCESS (code=2, address=0x7fff7406bb48).
// The process has been returned to the state before expression evaluation.

この場合、基底クラス側に適当な名前でブリッジ用の関数を作成し、そちらを代わりに呼び出すようにすれば良い。

struct Foo {
  virtual int get() { return 99; }
  virtual int GET() { return get(); }
};
struct Bar : Foo { virtual int get() { return 108; } };

Bar b;
// (lldb) p b.GET()
// (int) $0 = 108
広告
広告