2018年12月14日
GNU ldのwrap機能でC++メンバ関数をwrapする例
GNU ld には wrap という機能があります。これはリンク時に、未定義(Undefined)のシンボル foo を __wrap_foo として解決し、元のシンボルを __real_foo として解決するという機能です。これを利用すると、バイナリ提供のライブラリ関数を差し替えることが可能になります。
C での使用例はネット上にたくさん存在しますが、C++ の例がほとんど見つからなかったのでメモしておきます。
C での使用例はネット上にたくさん存在しますが、C++ の例がほとんど見つからなかったのでメモしておきます。
C でも C++ でも、リンカから見ればどちらもただのシンボル(文字列)なので、C++ でも wrap 機能は使用できるのですが、C++ の場合はシンボルが名前マングリングされる点が大きく異なります。
そのため、__wrap_ 関数を C++ で定義することはできないので、C 関数、または extern "C" リンケージで定義する必要があり、実装する際には C++ の機能や型(class や template 等)は使用できないという制限があります。
また、引数が全部 C と互換の型(POD: Plain Old Data)であっても、クラスのメンバ関数の場合は先頭に暗黙の this ポインタが渡されることにも注意が必要です。
以下がメンバ関数を wrap して、元の関数を呼び出す例となります。wrap 関数だけ C ファイルで定義しています。ヘッダや同一ファイルで定義したメンバ関数は参照時に解決されてしまい Undefined ではなくなってしまうため、差し替えることはできないので、別ファイルで f を定義しています。
そのため、__wrap_ 関数を C++ で定義することはできないので、C 関数、または extern "C" リンケージで定義する必要があり、実装する際には C++ の機能や型(class や template 等)は使用できないという制限があります。
また、引数が全部 C と互換の型(POD: Plain Old Data)であっても、クラスのメンバ関数の場合は先頭に暗黙の this ポインタが渡されることにも注意が必要です。
以下がメンバ関数を wrap して、元の関数を呼び出す例となります。wrap 関数だけ C ファイルで定義しています。ヘッダや同一ファイルで定義したメンバ関数は参照時に解決されてしまい Undefined ではなくなってしまうため、差し替えることはできないので、別ファイルで f を定義しています。
$ cat a.h class C { const int n; public: C(int n_) : n(n_) {} void f(int x); }; $ cat a.cpp #include "a.h" #include <stdio.h> void C::f(int x) { printf("%d\n", x + this->n); } $ cat main.cpp #include "a.h" #include <stdio.h> int main() { C c(10); c.f(10); return 0; } $ cat wrap.c #include <stdio.h> void __real__ZN1C1fEi(void *p, int x); void __wrap__ZN1C1fEi(void *p, int x) { printf("C::f() called.\n"); __real__ZN1C1fEi(p, x); } $ g++ -Wall -c a.cpp $ g++ -Wall -c main.cpp $ gcc -Wall -c wrap.c $ g++ -Wall a.o main.o $ ./a.out 20 $ g++ -Wall a.o main.o wrap.o -Wl,-wrap=_ZN1C1fEi $ ./a.out C::f() called. 20この例での確認環境は Ubunx 64-bit 18.04 の GCC ですが、GCCや Clang(おそらく MS VC++ 以外の現役の C++ コンパイラ全て)は Itanium C++ ABI という仕様で名前マングリングのルールが決まっているので、おそらく GNU ld を使用する C++ コンパイラならばこの例は動作すると思います。弊社の製品 SOLID のツールチェーンでも動作を確認しています。(この調査はもともとは exeGCC のサポートのためのものでした。)