5.4K Views
June 21, 25
スライド概要
Dynamiccast ⽂字列⽐較(しなさい) �/��
概要3⾏ • C++でプラグインシステムを作っていたら • dynamic_castがstrcmpしなかったので • 王朝が分裂した �/��
プラグイン機構 • ユーザー側でアプリを拡張可能にする。 ◦ ゲームのmod ◦ 画像編集アプリのフィルターなど • C++でこれを作りたい。 → 実⾏時に動的ライブラリをロードするシステム関数 (POSIXのdlopenなど)を使って実現する。 �/��
アプリ
// app.cpp
int main(int argc, char * argv[]) {
char const * file_path = argv[1];
plugin_base * pb = load_plugin(file_path);
if (!pb) return 1;
auto * pg = dynamic_cast<plugin<greeting> *>(pb);
if (!pg) return 1;
std::shared_ptr<greeting> g = pg->create();
if (!g) return 1;
std::puts(g->say().c_str());
return 0;
}
�/��
プラグインのインターフェース
// plugin.hpp
struct plugin_base {
// プラグインの名前
virtual char const * name() const = 0;
// このクラスのオブジェクトの生存管理はライブラリに
// 任せるため、仮想デストラクタを持たない
// (典型的にはライブラリ内に静的配置される想定)
};
template<typename T>
struct plugin: plugin_base {
std::shared_ptr<T> create();
private:
virtual T * create_impl() = 0;
virtual void destroy(T *) = 0;
};
�/��
プラグインのインターフェース
// plugin.hpp
template<typename T>
std::shared_ptr<T> plugin<T>::create() {
return std::shared_ptr<T> {
create_impl(),
[this] (T * p) { destroy(p); }
};
}
�/��
プラグインローダ
// plugin.cpp
plugin_base * load_plugin(char const * path) {
// ライブラリをロードする (ハンドル管理は省略)
void * dl = dlopen(path, RTLD_LOCAL | RTLD_LAZY);
if (!dl) return nullptr;
// プラグインオブジェクト取得関数を探す
auto get = reinterpret_cast<plugin_base * (*)()>(
dlsym(dl, "plugin_get")
);
if (!get) return nullptr;
// プラグインオブジェクトを得る
return get();
}
�/��
ある機能のインターフェース
// greeting.hpp
template<typename CharT>
struct basic_greeting {
virtual ~basic_greeting() = 0;
virtual std::basic_string<CharT> say() = 0;
};
using greeting = basic_greeting<char>;
template<typename CharT>
basic_greeting<CharT>::~basic_greeting() {}
�/��
プラグインの実装 // plugin_greeting_hello.cpp struct greeting_hello: greeting { std::string say() override { return "Hello"; } ~greeting_hello() override {} }; �/��
プラグインの実装
// plugin_greeting_hello.cpp
struct plugin_greeting_hello: plugin<greeting> {
char const * name() const override {
return "greeting-hello";
}
greeting * create_impl() override {
return new greeting_hello();
}
void destroy(greeting * g) override { delete g; }
};
extern "C" plugin_base * plugin_get() {
static plugin_greeting_hello pg;
return &pg;
}
��/��
コンパイル・実⾏ (libstdc++) g++ -o app app.cpp plugin.cpp g++ -shared -fPIC -o plugin.so \ plugin_greeting_hello.cpp ./app plugin.so Hello ��/��
コンパイル・実⾏ (libc++) clang++ -stdlib=libc++ -o app app.cpp plugin.cpp clang++ -stdlib=libc++ -shared -fPIC -o plugin.so \ plugin_greeting_hello.cpp ./app plugin.so ��/��
んんん? ��/��
デバッグ • どうやらlibc++だとdynamic_castのところで失敗してい る。 • なんでや継承関係あるやろ static_assert(std::derived_from<plugin<T>, plugin_base>); ��/��
dynamic_castの実装
• from_ptrの指しているオブジェクトとToのtype_infoを調
べ、from_ptrが指しているオブジェクトのクラスの継承階
層上にToが存在すれば、from_ptrからToへのポインタを計
算する。
template<typename To, typename From>
To * _dynamic_cast(From * from_ptr) {
std::type_info const & from = typeid(*from_ptr);
std::type_info const & to = typeid(To);
if (is_convertible_from(to, from)) {
return compute_to_ptr(to, from_ptr, from);
} else {
return nullptr;
}
}
��/��
dynamic_castの実装 • from_ptrの指しているオブジェクトとToのtype_infoを調 べ、from_ptrが指しているオブジェクトのクラスの継承階 層上にToが存在すれば、from_ptrからToへのポインタを計 算する。 ◦ *from_ptr のクラス階層から to を探す bool is_convertible_from( type_info const & to, type_info const & from ) { if (to == from) return true; for (type_info const & from_base: from._bases()) { if (to == from_base) return true; } // ... return false; } ��/��
dynamic_castの実装 • from_ptrの指しているオブジェクトとToのtype_infoを調 べ、from_ptrが指しているオブジェクトのクラスの継承階 層上にToが存在すれば、from_ptrからToへのポインタを計 算する。 ◦ *from_ptr のクラス階層から to を探す ▪ type_infoの等価⽐較 bool operator==( std::type_info const & ta, std::type_info const & tb ) { /* libstdc++の場合 */ return strcmp(ta.name(), tb.name()); /* libc++の場合 */ return ta.name() == tb.name(); } ��/��
動作検証
• from_ptrの継承階層のtype_info集合の中に
typeid(plugin<greeting>) に⼀致するオブジェクトがあるはず
だが、libc++のdynamic_castでは⾒つからなかった。
bool is_convertible_from(
type_info const & to, type_info const & from
) {
if (to == from) return true;
for (type_info const & from_base: from._bases()) {
if (to == from_base) return true;
}
// ...
return false; // ここに来た
}
��/��
dynamic_castの動作の検証
• from_ptrの継承階層のtype_info集合の中に
typeid(plugin<greeting>) に⼀致するオブジェクトがあるはず
だが、libc++のdynamic_castでは⾒つからなかった。
• type_info⽐較に違いがあり、2つの型名の⽂字列が同じ内容
かつ別オブジェクトである場合に影響する。
bool operator==(
std::type_info const & ta, std::type_info const & tb
) {
/* libstdc++の場合 */ return strcmp(ta.name(), tb.name());
/* libc++の場合 */
return ta.name() == tb.name();
}
��/��
dynamic_castの動作の検証
• from_ptrの継承階層のtype_info集合の中に
typeid(plugin<greeting>) に⼀致するオブジェクトがあるはず
だが、libc++のdynamic_castでは⾒つからなかった。
• type_info⽐較に違いがあり、2つの型名の⽂字列が同じ内容
かつ別オブジェクトである場合に影響する。
• もし、ある type_info::name() が返す⽂字列オブジェクトが複
数ある場合、↑の違いによってlibc++でだけキャスト可能性
判定に失敗する。
bool operator==(
std::type_info const & ta, std::type_info const & tb
) {
/* libstdc++の場合 */ return strcmp(ta.name(), tb.name());
/* libc++の場合 */
return ta.name() == tb.name();
}
��/��
dynamic_castの動作の検証 • from_ptrの継承階層のtype_info集合の中に typeid(plugin<greeting>) に⼀致するオブジェクトがあるはず だが、libc++のdynamic_castでは⾒つからなかった。 • type_info⽐較に違いがあり、2つの型名の⽂字列が同じ内容 かつ別オブジェクトである場合に影響する。 • もし、ある type_info::name() が返す⽂字列オブジェクトが複 数ある場合、↑の違いによってlibc++でだけキャスト可能性 判定に失敗する。 • type_infoも name() の返す⽂字列も静的な寿命のオブジェク トなので(決め付け)、同じ型に対する型名⽂字列が2つある なら、それを保持するtype_infoも2つある。 ��/��
type_infoの出所を探る • 作ったバイナリ(appとplugin.so)でtype_infoがどうなって いるのか調べる。 • type_infoもバイナリ内ではシンボルを持っている ( _ZTI6pluginI14basic_greetingIcEE )。 • ※シンボル: 関数や変数の名前に対応するバイナリ内での識 別⼦。(余談で⾒たようにC++だと型名などを埋め込んでエ ンコードされる) ��/��
余談: _ZTI6pluginI14basic_greetingIcEE 読み下し 記号 意味 記号 意味 _Z (Itanium C++ ABI) basic_greeting basic_greeting TI std::type_info for I < � sizeof("plugin") c char plugin plugin E > I < E > �� sizeof("basic_greeting") using greeting = basic_greeting<char>; これでみなさんはItanium C++ ABIの名前エンコード規則を完 全に理解した。もうc++filtについて考える時間は1秒たりとも必 要ない、そうですね? ��/��
type_infoの出所を探る readelf -Wrs app の結果 (適宜省略): Symbol table '.symtab' contains 198 entries: Num: Value Size Type Bind 156: 000000000001dd30 24 OBJECT WEAK Vis Ndx Name DEFAULT 22 _ZTI(略) Relocation section '.rela.dyn' at offset 0xa60 contains 31 entries: Offset Info Type … Symbol's Name + Addend 0000000000003d60 0000000b00000001 R_X86_64_64 … _ZTI(略) + 0 Symbol table '.dynsym' contains 29 entries: Num: Value Size Type Bind Vis Ndx Name 11: 0000000000003d38 24 OBJECT WEAK DEFAULT 19 _ZTI(略) Symbol table '.symtab' contains 39 entries: Num: Value Size Type Bind Vis Ndx Name 28: 0000000000003d38 24 OBJECT WEAK DEFAULT 19 _ZTI(略) ��/��
診断1 • appとplugin.so両⽅にtype_infoがある。 readelf -Wrs app の結果 (適宜省略): 156: 000000000001dd30 24 OBJECT WEAK DEFAULT 22 _ZTI(略) readelf -Wrs plugin.so の結果 (適宜省略): 28: 0000000000003d38 24 OBJECT WEAK DEFAULT 19 _ZTI(略) ��/��
Q�. なんでどっちのバイナリにもある の? • type_infoの実体化の可否を翻訳単位ごとに制御できない。 ◦ クラスCを使⽤している翻訳単位であればCのtype_info が置かれる。 • リンク時に1つにまとめられるが、依然としてリンク後のバ イナリには重複して存在する。 ◦ 通常は実⾏時リンクの際にも重複実体の処理を⾏って るので問題ない。 (Q�にて後述) ��/��
Q�. なんでどっちのバイナリにもある の? 例 a.cpp _ZTI4hoge b.cpp c.cpp _ZTI4fuga _ZTI4piyo _ZTI4hoge _ZTI4fuga link link link process b.so exe _ZTI4hoge 別バイナリ同士では 重複したまま _ZTI4fuga _ZTI4piyo _ZTI4hoge リンク時に 重複実体を 併合する ��/��
診断2 • dynamic_cast中で本来⼀致すべき2つのtype_infoのアドレ スの下3桁はそれぞれ両バイナリのものと⼀致している。 (この資料内では初出情報。本当かどうかはdynamic_castの デバッグ中に表⽰して確認してみてね) readelf -Wrs app の結果 (適宜省略): 156: 000000000001dd30 24 OBJECT WEAK DEFAULT 22 _ZTI(略) readelf -Wrs plugin.so の結果 (適宜省略): 28: 0000000000003d38 24 OBJECT WEAK DEFAULT 19 _ZTI(略) ��/��
コード再掲
• dynamic_cast<To*>(from_ptr)
• *from_ptr のクラス階層から To を探す
bool is_convertible_from(
type_info const & to, type_info const & from
) {
if (to == from) return true;
for (type_info const & from_base: from._bases()) {
// libc++だとここで本来trueになるはずの
// toとfrom_baseのアドレスが違っている
if (to == from_base) return true;
}
// ...
return false;
}
��/��
Q�. アドレスの下3桁についてくわしく • 動的ライブラリがプロセスのアドレス空間に配置される際、 ローダーは4kバイト境界(ページ境界)で位置調整する。 ◦ どう配置されたとしても、4kより下の桁(16進で下3桁) は変化しない。 ◦ 同じシンボルが異なるバイナリ間で同じアドレス下3桁 を持つことはそうそうないので、だいたいこれで判別 可能。 • dynamic_cast中の plugin<greeting> のtype_infoはapp由来 • 同 *fron_ptr のtype_infoはplugin.so由来 ��/��
診断3 readelf -Wrs plugin.so の結果 (適宜省略): Symbol table '.dynsym' contains 29 entries: 11: 0000000000003d38 24 OBJECT WEAK Symbol table '.symtab' contains 39 entries: 28: 0000000000003d38 24 OBJECT WEAK DEFAULT 19 _ZTI(略) DEFAULT 19 _ZTI(略) • plugin.soでは2つのシンボルテーブルに出現している。 • ※シンボルテーブル: バイナリに含まれる関数や変数の情報 を集めたもの。 ��/��
Q�. なんでplugin.soでは2つのシンボル テーブルに現われるの? • .symtabテーブルと.dynsymテーブル ◦ .symtabテーブル:リンクした全てのオブジェクトファ イルのシンボル情報を集めたもの → 実⾏時には不要なので削除してもよい。 ◦ .dynsymテーブル:バイナリ外部に公開するシンボル を集めたもの → 実⾏時リンクではこれを使う。 • .dynsymテーブルに登場する ⇔ バイナリ外部に公開されて いる → appはtype_infoを公開していない ��/��
診断4 • plugin.soでは再配置テーブルにもtype_infoが出現してい る。 • ※再配置テーブル: 再配置に関する情報(どこに何のアドレス を埋めるのか)を集めたテーブル readelf -Wrs plugin.so の結果 (適宜省略): Relocation section '.rela.dyn' at offset 0xa60 contains 31 entries: 0000000000003d60 0000000b00000001 R_X86_64_64 … _ZTI(略) + 0 ��/��
再配置 未解決の関数や変数への参照を、それらの実体のアドレスで埋 める処理。 例 extern char const * x; puts(x); というコードは、 static void ** tab[N]; puts((char const *)tab[x_index]); みたいに解釈される。実⾏時再配置では、 x のアドレスを探し てきて tab[x_index] に格納する。 ��/��
Q�. plugin.soは⾃分でtype_infoオブ ジェクトを持っているのになんで再配置 が必要なの? • 実⾏時リンカーがプロセス内のどのバイナリでも同じ実体を 参照するように再配置するため。 • なぜかappのリンク時には再配置が不要と判断されている。 ��/��
例 ※下図exeの _ZTI4hoge は外部に公開されているとする。 a.cpp _ZTI4hoge link b.cpp c.cpp _ZTI4fuga _ZTI4piyo _ZTI4hoge _ZTI4fuga link link d.cpp _ZTI4piyo link process b.so exe typeid(hoge) ↓ _ZTI4hoge (実行時リンカー は常に↑を選択) typeid(hoge) typeid(fuga) ↓ _ZTI4fuga (実行時リンカーにより選択) _ZTI4piyo _ZTI4hoge d.so typeid(fuga) _ZTI4fuga ��/��
Q�. なんでappのtype_infoは公開されて いないの? • 実⾏バイナリをリンクする時、依存関係として指定された他 の動的ライブラリから参照される実体のみが公開され、それ 以外は実⾏バイナリ内で静的に解決される。 → そのほうが実⾏が速くなるから。 ◦ 実⾏時に解決する場合、解決後のアドレスはメモリに 格納されるため、そこにアクセスしてアドレスを取得 する必要がある。 ◦ 静的に解決した場合、type_infoの位置は既に決まって いるので、相対アドレス(や絶対アドレス)をコード中に 埋め込むことができる。 → type_infoのアドレスを知るためにメモリにアクセス する必要がない。つまり速い。速さは正義。 ��/��
例 // a.cpp -> exe int x; int y; int main() { /* c.soを実行時ロードする */ } // b.cpp -> b.so extern int x; void f() { /* xを使用する処理 */ } // c.cpp -> c.so extern int y; void f() { /* yを使用する処理 */ } c++ -shared -fPIC -o b.so b.cpp c++ -shared -fPIC -o c.so c.cpp c++ -o exe a.cpp b.so ./exe # 何が起こる? ��/��
何が起こる? process xはリンク時にb.soから参照されていると 分かっているため公開される yはリンク時には外部のどこからも 参照されていなかったため、非公開 exe x : int y : int b.so x : extern int c.so y : extern int 凡例 公開 非公開 exeのyが公開されていないので ロードに失敗する ��/��
まとめると • appのリンク時にはplugin.soのことを知らないので、appは ⾃分で持ってるtype_infoを外部に公開しない。 • 実⾏時、plugin.so以外にtype_infoを公開している者がいな い。 → plugin.soは⾃分の持っている実体で再配置を⾏う。 → type_infoが分裂し、app朝type_infoとplugin.so朝 type_infoが誕⽣する。 • libstdc++はstrcmpでtype_infoの⽐較を⾏うため、appと plugin.soが⽴てたtype_infoは同⼀⼈物であると解釈してう まいこと(?)乗り切る。 → 摂関政治がお上⼿ • ⼀⽅libc++の采配は王朝の分裂を決定的なものとし、武家の 介⼊と権力拡⼤につながった⋯⋯ ��/��
図⽰ app typeid(plugin<greeting>) ↓ type_info for plugin<greeting> plugin.so pg : plugin_greeting_hello .vptr : vtable * ↓ type_info for plugin<greeting> ��/��
天下のlibc++様が間違っているはずがな い • もしもappのリンクにおいてplugin.soもリンク対象にして いたらlibc++の実装でも問題なかった。 • libc++の実装を正とすると、appのtype_infoが外部に公開さ れていないことが問題となる。 app typeid(plugin<greeting>) ↓ type_info for plugin<greeting> plugin.so pg : plugin_greeting_hello .vptr : vtable * ↓ type_info for plugin<greeting> ��/��
appのtype_infoを公開する⽅法 -rdynamic オプション (gcc/clang) -export-dynamic オプション (ld) • 公開しないように指⽰されているシンボル以外は全て公開す る。 ◦ 公開しないものの例: ▪ static変数・関数 ▪ 属性で⾮公開(hidden)となっている • -rdynamicは-Wl,-export-dynamicと同じ • それ(速さ)を すてるなんて とんでもない! ��/��
appのtype_infoを公開する⽅法 -dynamic-list=list-file オプション (ld) • 公開したいシンボルをテキストファイルで指定する。 • C++のシンボルを書くのはめんどくさいので不向き。 ◦ 特に今回のようにテンプレートの実体をABIに含める場 合 ��/��
appのtype_infoを公開する⽅法 -dynamic-list-cpp-typeinfo オプション (ld) • type_infoのシンボルを全て公開する。これや!!! ��/��
再チャレンジ(libc++) clang++ -stdlib=libc++ -o app app.cpp plugin.cpp \ -Wl,-dynamic-list-cpp-typeinfo clang++ -stdlib=libc++ -shared -fPIC -o plugin.so \ plugin_greeting_hello.cpp ./app plugin.so Hello やったね ��/��
readelf -Ws appの結果 File: ../build/src/app Symbol table '.dynsym' contains 34 entries: 25: 0000000000004d18 24 OBJECT WEAK Symbol table '.symtab' contains 66 entries: 54: 0000000000004d18 24 OBJECT WEAK DEFAULT 22 _ZTI(略) DEFAULT 22 _ZTI(略) ��/��
他の解決策 • libc++のビルドオプションを変更し、type_infoの⽐較のと きにstrcmpを使うようにする。 ◦ 基本的なランタイムやアプリを含むユーザーランドシ ステム全体を⾃力でセットアップ(cf. Linux from Scratch)する際にどうぞ ��/��
結局libstdc++とlibc++のどっちがおかし いの? • ELFの実⾏バイナリおよび共有オブジェクト間でvague linkageを持つC++ entityをどう扱うかについては、Itanium C++ ABIやELFの仕様に明確な記述は⾒つけられなかった。 • 実⾏時リンカーもそれ⽤の特別な仕組みを持たず、他のシン ボルと同様に扱っている。 • 処理系の実装上でうまいこと扱えるようにABIを調整すれば 問題なく動く。 • よってlibstdc++とlibc++の実装⽅針の違いでしかなく、どっ ちのバグという話ではない。 • 多分。 • 知らんけど。 ��/��
総括 • 迂闊にシンボルを公開したり⾮公開にして⼤変な⽬に遭うこ とでC++ ABI筋が鍛えられ、健康になる。 ��/��
実は • 今回のプラグインシステムは仮想デストラクタのシンボルの 問題について迂回してしまっている。 • plugin<T> : 仮想デストラクタを持たない • basic_greeting<T> : 仮想デストラクタを持つが、実体化した関 数テンプレートなので各翻訳単位に複製が存在する。 ◦ type_infoと同様の状況だが、デストラクタのアドレス を⽐較することはほとんどないので問題ない。 • もし plugin<T> が(仮想)デストラクタを持っていれば、あるい は greeting がテンプレートの実体化でなければ、プラグイン を動的ロードしようとしたときにこれらのデストラクタのシ ンボル解決に失敗し、動的ロードできなかった。 ��/��
真の総括 • C++ テンプレート • 動的ライブラリ • 実⾏時ロード を組み合わせるときは、 • 処理系への造詣を明るくして、 • (ツールチェインと実⾏時の全体を⼤局的に⾒るために)離れ て⾒てね! ��/��