Dynamiccast 文字列比較(しなさい)

4.9K Views

June 21, 25

スライド概要

profile-image

きりんさんがすきです。でもC++さんのほうがもーっとすきです。

シェア

またはPlayer版

埋め込む »CMSなどでJSが使えない場合

ダウンロード

関連スライド

各ページのテキスト
1.

Dynamiccast ⽂字列⽐較(しなさい) 1 / 55

2.

概要3⾏ • C++でプラグインシステムを作っていたら • dynamic_castがstrcmpしなかったので • 王朝が分裂した 2 / 55

3.

プラグイン機構 • ユーザー側でアプリを拡張可能にする。 ◦ ゲームのmod ◦ 画像編集アプリのフィルターなど • C++でこれを作りたい。 → 実⾏時に動的ライブラリをロードするシステム関数 (POSIXのdlopenなど)を使って実現する。 3 / 55

4.
[beta]
アプリ
// 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;
}

4 / 55

5.
[beta]
プラグインのインターフェース
// 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;
};

5 / 55

6.
[beta]
プラグインのインターフェース
// plugin.hpp
template<typename T>
std::shared_ptr<T> plugin<T>::create() {
return std::shared_ptr<T> {
create_impl(),
[this] (T * p) { destroy(p); }
};
}

6 / 55

7.
[beta]
プラグインローダ
// 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();
}

7 / 55

8.
[beta]
ある機能のインターフェース
// 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() {}

8 / 55

9.

プラグインの実装 // plugin_greeting_hello.cpp struct greeting_hello: greeting { std::string say() override { return "Hello"; } ~greeting_hello() override {} }; 9 / 55

10.
[beta]
プラグインの実装
// 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;
}

10 / 55

11.

コンパイル・実⾏ (libstdc++) g++ -o app app.cpp plugin.cpp g++ -shared -fPIC -o plugin.so \ plugin_greeting_hello.cpp ./app plugin.so Hello 11 / 55

12.

コンパイル・実⾏ (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 12 / 55

13.

んんん? 13 / 55

14.

デバッグ • どうやらlibc++だとdynamic_castのところで失敗してい る。 • なんでや継承関係あるやろ static_assert(std::derived_from<plugin<T>, plugin_base>); 14 / 55

15.
[beta]
アプリ再掲
// 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;
}

15 / 55

16.
[beta]
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;
}
}

16 / 55

17.

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; } 17 / 55

18.

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(); } 18 / 55

19.
[beta]
動作検証
• 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; // ここに来た
}

19 / 55

20.
[beta]
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();
}

20 / 55

21.
[beta]
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();
}
21 / 55

22.

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つある。 22 / 55

23.

type_infoの出所を探る • 作ったバイナリ(appとplugin.so)でtype_infoがどうなって いるのか調べる。 • type_infoもバイナリ内ではシンボルを持っている (_ZTI6pluginI14basic_greetingIcEE)。 • シンボル: 関数や変数の名前に対応するバイナリ内での識別 ⼦。(余談で⾒たようにC++だと型名などを埋め込んでエン コードされる) 23 / 55

24.

余談: _ZTI6pluginI14basic_greetingIcEE 読み下し using greeting = basic_greeting<char>; 記号 意味 記号 意味 _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") 24 / 55

25.

余談: _ZTI6pluginI14basic_greetingIcEE 読み下し using greeting = basic_greeting<char>; 記号 意味 記号 意味 _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") これでみなさんはItanium C++ ABIの名前エンコード規則を完 全に理解した。もうc++filtについて考える時間は1秒たりとも必 要ない、そうですね? 25 / 55

26.

type_infoの出所を探る readelf -Wrs app の結果 (適宜省略): Symbol table '.symtab' contains 198 entries: Num: Value Size Type Bind 156: 000000000001dd30 24 OBJECT WEAK 26 / 55 Vis DEFAULT Ndx Name 22 _ZTI(略)

27.

type_infoの出所を探る readelf -Wrs plugin.so の結果 (適宜省略): 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(略) 27 / 55

28.

診断 • appとplugin.so両⽅にtype_infoがある。 readelf -Wrs app の結果 (適宜省略): 156: 000000000001dd30 24 OBJECT WEAK DEFAULT 22 _ZTI(略) readelf -Wrs plugin.so の結果 (適宜省略): 28: 0000000000003d38 24 OBJECT 28 / 55 WEAK DEFAULT 19 _ZTI(略)

29.

Q�. なんでどっちのバイナリにもある の? • type_infoの実体化の可否を翻訳単位ごとに制御できない。 → クラスCを使⽤している翻訳単位であればCのtype_infoが 置かれる。 • リンク時に1つにまとめられるが、依然としてリンク後のバ イナリには重複して存在する。 ◦ 通常は実⾏時リンクの際にも重複実体の処理を⾏って るので問題ない。 29 / 55

30.

診断 • dynamic_cast中で本来⼀致すべき2つのtype_infoのアドレ スの下3桁はそれぞれ両バイナリのものと⼀致している。 (この資料内では初出) readelf -Wrs app の結果 (適宜省略): 156: 000000000001dd30 24 OBJECT WEAK DEFAULT 22 _ZTI(略) readelf -Wrs plugin.so の結果 (適宜省略): 28: 0000000000003d38 24 OBJECT 30 / 55 WEAK DEFAULT 19 _ZTI(略)

31.
[beta]
コード再掲
• 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()) {
if (to == from_base) return true;
}
// ...
return false;
}

31 / 55

32.

Q�. アドレスの下3桁についてくわしく • 動的ライブラリがプロセスのアドレス空間に配置される 際、ローダーは4kバイト境界(ページ境界)で位置調整する。 ◦ どう配置されたとしても、4kより下の桁(16進で下3桁) は変化しない。 ◦ 同じシンボルが異なるバイナリ間で同じアドレス下4桁 を持つことはそうそうないので、だいたいこれで判別 可能。 • dynamic_cast中のplugin<greeting>のtype_infoはapp由来 • 同*fron_ptrのtype_infoはplugin.so由来 32 / 55

33.

診断 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つのシンボルテーブルに出現している。 • シンボルテーブル: バイナリに含まれる関数や変数の情報を 集めたもの。 33 / 55

34.

Q�. なんでplugin.soでは2つのシンボル テーブルに現われるの? • .symtabテーブルと.dynsymテーブル ◦ .symtabテーブル:リンクした全てのオブジェクトファ イルのシンボル情報を集めたもの → 実⾏時には不要なので削除してもよい。 ◦ .dynsymテーブル:バイナリ外部に公開するシンボル を集めたもの → 実⾏時リンクではこれを使う。 • .dynsymテーブルに登場する ⇔ バイナリ外部に公開されて いる → appはtype_infoを公開していない 34 / 55

35.

診断 • 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 35 / 55

36.

再配置 未解決の関数や変数への参照を、それらの実体のアドレスで埋 める処理。 例 extern char const * x; puts(x); というコードは、 static void ** tab[N]; puts((char const *)tab[x_index]); みたいに解釈される。再配置は、xのアドレスを探してきて tab[x_index]に格納する。 36 / 55

37.

Q�. plugin.soは⾃分でtype_infoオブ ジェクトを持っているのになんで再配置 が必要なの? • ⾃⾝が実体を持っているシンボル(ここでは plugin<greeting>のtype_info)も再配置の対象にすること で、他のロードされたバイナリに重複実体があっても参照 先を統⼀できる。 ◦ それらの重複から1つだけが使⽤される。 (選ばれなかった実体はただのデッドスペースとなる) ◦ これがQ�の「実⾏時にも重複を排除するので問題な い」の理由 37 / 55

38.

Q�. plugin.soは⾃分でtype_infoオブ ジェクトを持っているのになんで再配置 が必要なの? • 実⾏バイナリは⾃分が実体を持っているグローバル変数や 関数を再配置することはないが、代わりに常に実⾏バイナ リの持つ実体が全体で使⽤される。 (ただしシンボルが外部に公開されているものとする) 38 / 55

39.

Q�. なんでappのtype_infoは公開されて いないの? • 実⾏バイナリをリンクする時、依存関係として指定された 他の動的ライブラリから参照される実体のみが公開され、 それ以外は実⾏バイナリ内で静的に解決される。 → そのほうが⽣成されたコードが速くなるから。 ◦ 実⾏時に解決する場合、解決後のアドレスはメモリに 格納されるため、そこにアクセスしてアドレスを取得 する必要がある。 ◦ 静的に解決した場合、type_infoの位置は既に決まって いるので、相対アドレス(や絶対アドレス)をコード中に 埋め込むことができる。 → type_infoのアドレスを知るためにメモリにアクセス する必要がない。つまり速い。速さは正義。 39 / 55

40.

例 c++ -shared -fPIC -o b.so b.cpp c++ -o app a.cpp b.so # b.soがappのリンク対象に含まれる // a.cpp -> app int x; // app 外部に公開される int y; // app 外部には公開されない // b.cpp -> b.so extern char const * x; void f() { puts(x); } // xを参照している 40 / 55

41.

まとめると • appのリンク時にはplugin.soのことを知らないので、appは ⾃分で持ってるtype_infoを外部に公開しない。 41 / 55

42.

まとめると • appのリンク時にはplugin.soのことを知らないので、appは ⾃分で持ってるtype_infoを外部に公開しない。 • 実⾏時、plugin.so以外にtype_infoを公開している者がいな い。 → plugin.soは⾃分の持っている実体で再配置を⾏う。 → type_infoが分裂し、app朝type_infoとplugin.so朝 type_infoが誕⽣する。 42 / 55

43.

まとめると • 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は同⼀⼈物であると解釈してう まいこと(?)乗り切る。 → 摂関政治がお上⼿ 43 / 55

44.

まとめると • 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++の采配は王朝の分裂を決定的なものとし、武家の 介⼊と権力拡⼤につながった⋯⋯ 44 / 55

45.

天下のlibc++様が間違っているはずがな い • もしもappのリンクにおいてplugin.soもリンク対象にして いたらlibc++の実装でも問題なかった。 • libc++の実装を正とすると、appのtype_infoが外部に公開 されていないことが問題となる。 45 / 55

46.

appのtype_infoを公開する⽅法 -rdynamic オプション (gcc/clang) -export-dynamic オプション (ld) • 公開しないように指⽰されているシンボル以外は全て公開 する。 ◦ 公開しないものの例: ▪ static変数・関数 ▪ 属性で⾮公開(hidden)となっている • -rdynamicは-Wl,-export-dynamicと同じ • それ(速さ)を すてるなんて とんでもない! 46 / 55

47.

appのtype_infoを公開する⽅法 -dynamic-list=list-file オプション (ld) • 公開したいシンボルをテキストファイルで指定する。 • C++のシンボルを書くのはめんどくさいので不向き。 ◦ 特に今回のようにテンプレートの実体をABIに含める場 合 47 / 55

48.

appのtype_infoを公開する⽅法 -dynamic-list-cpp-typeinfo オプション (ld) • type_infoのシンボルを全て公開する。これや!!! 48 / 55

49.

再チャレンジ(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 やったね 49 / 55

50.

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 50 / 55 DEFAULT 22 _ZTI(略) DEFAULT 22 _ZTI(略)

51.

他の解決策 • libc++のビルドオプションを変更し、type_infoの⽐較のと きにstrcmpを使うようにする。 ◦ 基本的なランタイムやアプリを含むユーザーランドシ ステム全体を⾃力でセットアップ(cf. Linux from Scratch)する際にどうぞ 51 / 55

52.

結局libstdc++とlibc++のどっちがおかし いの? • ELFの実⾏バイナリおよび共有オブジェクト間でvague linkageを持つC++ entityをどう扱うかについては、Itanium C++ ABIやELFの仕様に明確な記述は⾒つけられなかった。 • 実⾏時リンカーもそれ⽤の特別な仕組みを持たず、他のシ ンボルと同様に扱っている。 • 処理系の実装上でうまいこと扱えるようにABIを調整すれば 問題なく動く。 • よってlibstdc++とlibc++の実装⽅針の違いでしかなく、 どっちのバグという話ではない。 • 多分。 • 知らんけど。 52 / 55

53.

総括 • 迂闊にシンボルを公開したり⾮公開にして⼤変な⽬に遭う ことでC++ ABI筋が鍛えられ、健康になる。 53 / 55

54.

実は • 今回のプラグインシステムは仮想デストラクタのシンボル の問題について迂回してしまっている。 • plugin<T>: 仮想デストラクタを持たない • basic_greeting<T>: 仮想デストラクタを持つが、実体化し た関数テンプレートなので各翻訳単位に複製が存在する。 ◦ type_infoと同様の状況だが、デストラクタのアドレス を⽐較することはほとんどないので問題ない。 • もしplugin<T>が(仮想)デストラクタを持っていれば、ある いはgreetingがテンプレートの実体化でなければ、プラグ インを動的ロードしようとしたときにこれらのデストラク タのシンボル解決に失敗し、動的ロードできなかった。 54 / 55

55.

真の総括 • C++ テンプレート • 動的ライブラリ • 実⾏時ロード を組み合わせるときは、 • 処理系への造詣を明るくして、 • (ツールチェインと実⾏時の全体が視界に⼊るように)離れて ⾒てね! 55 / 55