vtableの中身を見てみる
C++でポリモーフィズムを実現するためにvtableと呼ばれる機構が用いられている.
だいたいの入門本でvtableという言葉は出てくるものの,実装については特に触れられていないので中身を見てみた.
検証用プログラムには以下を用いた.また,実行環境はGNU/Linux Fedora 25を用い,コンパイラはgcc 6.4.1を用いた.
#include <cstdio> class Base { public: Base() { printf("Base::Base() has called\n"); } virtual void hoge() { printf("Base::hoge() has called\n"); } virtual void fuga() { printf("Base::fuga() has called\n"); } }; class Derived : public Base { public: Derived() { printf("Derived::Derived() has called\n"); } void fuga() { printf("Derived::fuga() has called\n"); } }; void func(Base &x) { x.hoge(); x.fuga(); } int main() { Derived obj; func(obj); return 0; }
コンパイルして実行すると以下のようになる.
$ g++ vtable.cpp $ ./a.out Base::Base() has called Derived::Derived() has called Base::hoge() has called Derived::fuga() has called
ではobjdumpで逆アセンブルしてみて中身を見てみよう.
$ objdump -d a.out | c++filt a.out: file format elf64-x86-64 〜 Disassembly of section .text: 〜 00000000004006a6 <func(Base&)>: 4006a6: 55 push %rbp 4006a7: 48 89 e5 mov %rsp,%rbp 4006aa: 48 83 ec 10 sub $0x10,%rsp 4006ae: 48 89 7d f8 mov %rdi,-0x8(%rbp) 4006b2: 48 8b 45 f8 mov -0x8(%rbp),%rax 4006b6: 48 8b 00 mov (%rax),%rax 4006b9: 48 8b 00 mov (%rax),%rax 4006bc: 48 8b 55 f8 mov -0x8(%rbp),%rdx 4006c0: 48 89 d7 mov %rdx,%rdi 4006c3: ff d0 callq *%rax 4006c5: 48 8b 45 f8 mov -0x8(%rbp),%rax 4006c9: 48 8b 00 mov (%rax),%rax 4006cc: 48 83 c0 08 add $0x8,%rax 4006d0: 48 8b 00 mov (%rax),%rax 4006d3: 48 8b 55 f8 mov -0x8(%rbp),%rdx 4006d7: 48 89 d7 mov %rdx,%rdi 4006da: ff d0 callq *%rax 4006dc: 90 nop 4006dd: c9 leaveq 4006de: c3 retq 00000000004006df <main>: 4006df: 55 push %rbp 4006e0: 48 89 e5 mov %rsp,%rbp 4006e3: 48 83 ec 10 sub $0x10,%rsp 4006e7: 48 8d 45 f8 lea -0x8(%rbp),%rax 4006eb: 48 89 c7 mov %rax,%rdi 4006ee: e8 6d 00 00 00 callq 400760 <Derived::Derived()> 4006f3: 48 8d 45 f8 lea -0x8(%rbp),%rax 4006f7: 48 89 c7 mov %rax,%rdi 4006fa: e8 a7 ff ff ff callq 4006a6 <func(Base&)> 4006ff: b8 00 00 00 00 mov $0x0,%eax 400704: c9 leaveq 400705: c3 retq 0000000000400706 <Base::Base()>: 400706: 55 push %rbp 400707: 48 89 e5 mov %rsp,%rbp 40070a: 48 83 ec 10 sub $0x10,%rsp 40070e: 48 89 7d f8 mov %rdi,-0x8(%rbp) 400712: ba f8 08 40 00 mov $0x4008f8,%edx 400717: 48 8b 45 f8 mov -0x8(%rbp),%rax 40071b: 48 89 10 mov %rdx,(%rax) 40071e: bf 40 08 40 00 mov $0x400840,%edi 400723: e8 78 fe ff ff callq 4005a0 <puts@plt> 400728: 90 nop 400729: c9 leaveq 40072a: c3 retq 40072b: 90 nop 000000000040072c <Base::hoge()>: 40072c: 55 push %rbp 40072d: 48 89 e5 mov %rsp,%rbp 400730: 48 83 ec 10 sub $0x10,%rsp 400734: 48 89 7d f8 mov %rdi,-0x8(%rbp) 400738: bf 58 08 40 00 mov $0x400858,%edi 40073d: e8 5e fe ff ff callq 4005a0 <puts@plt> 400742: 90 nop 400743: c9 leaveq 400744: c3 retq 400745: 90 nop 0000000000400746 <Base::fuga()>: 400746: 55 push %rbp 400747: 48 89 e5 mov %rsp,%rbp 40074a: 48 83 ec 10 sub $0x10,%rsp 40074e: 48 89 7d f8 mov %rdi,-0x8(%rbp) 400752: bf 70 08 40 00 mov $0x400870,%edi 400757: e8 44 fe ff ff callq 4005a0 <puts@plt> 40075c: 90 nop 40075d: c9 leaveq 40075e: c3 retq 40075f: 90 nop 0000000000400760 <Derived::Derived()>: 400760: 55 push %rbp 400761: 48 89 e5 mov %rsp,%rbp 400764: 48 83 ec 10 sub $0x10,%rsp 400768: 48 89 7d f8 mov %rdi,-0x8(%rbp) 40076c: 48 8b 45 f8 mov -0x8(%rbp),%rax 400770: 48 89 c7 mov %rax,%rdi 400773: e8 8e ff ff ff callq 400706 <Base::Base()> 400778: ba d8 08 40 00 mov $0x4008d8,%edx 40077d: 48 8b 45 f8 mov -0x8(%rbp),%rax 400781: 48 89 10 mov %rdx,(%rax) 400784: bf 88 08 40 00 mov $0x400888,%edi 400789: e8 12 fe ff ff callq 4005a0 <puts@plt> 40078e: 90 nop 40078f: c9 leaveq 400790: c3 retq 400791: 90 nop 0000000000400792 <Derived::fuga()>: 400792: 55 push %rbp 400793: 48 89 e5 mov %rsp,%rbp 400796: 48 83 ec 10 sub $0x10,%rsp 40079a: 48 89 7d f8 mov %rdi,-0x8(%rbp) 40079e: bf a6 08 40 00 mov $0x4008a6,%edi 4007a3: e8 f8 fd ff ff callq 4005a0 <puts@plt> 4007a8: 90 nop 4007a9: c9 leaveq 4007aa: c3 retq 4007ab: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
まずはfunc()から見てみる.
00000000004006a6 <func(Base&)>: # スタックを掘る 4006a6: 55 push %rbp 4006a7: 48 89 e5 mov %rsp,%rbp 4006aa: 48 83 ec 10 sub $0x10,%rsp # Linux x86_64なのでrdiには1番目の引数が入る # メモリの(rbp-0x8)番地に1番目の引数(つまりBase &x)を格納 4006ae: 48 89 7d f8 mov %rdi,-0x8(%rbp) # メモリからraxにコピー 4006b2: 48 8b 45 f8 mov -0x8(%rbp),%rax # raxの指すメモリを2回デリファレンス(rax=*rax) 4006b6: 48 8b 00 mov (%rax),%rax 4006b9: 48 8b 00 mov (%rax),%rax # rdxにメモリから&xをコピーし,更にrdiにコピー 4006bc: 48 8b 55 f8 mov -0x8(%rbp),%rdx 4006c0: 48 89 d7 mov %rdx,%rdi # raxの指す関数をcall 4006c3: ff d0 callq *%rax # もう一度メモリから&xをコピーし,raxに格納 4006c5: 48 8b 45 f8 mov -0x8(%rbp),%rax # 今度はraxの指すメモリをデリファレンスし,そのアドレスに0x8を加える 4006c9: 48 8b 00 mov (%rax),%rax 4006cc: 48 83 c0 08 add $0x8,%rax # そしてそのアドレスをデリファレンス 4006d0: 48 8b 00 mov (%rax),%rax # rdxにメモリから&xをコピーし,更にrdiにコピー 4006d3: 48 8b 55 f8 mov -0x8(%rbp),%rdx 4006d7: 48 89 d7 mov %rdx,%rdi # raxの指す関数をcall 4006da: ff d0 callq *%rax # 終了処理 4006dc: 90 nop 4006dd: c9 leaveq 4006de: c3 retq
func()にはオブジェクトのアドレスが格納されているので,オブジェクトの先頭位置を一度デリファレンスし,その情報を元に呼び出す関数を決定している.
では,オブジェクトの先頭に何が格納されているかを確認する.オブジェクトの初期化部分から見ていこう.
00000000004006df <main>: # スタックを掘る 4006df: 55 push %rbp 4006e0: 48 89 e5 mov %rsp,%rbp 4006e3: 48 83 ec 10 sub $0x10,%rsp # スタックの(rbp-0x8)のアドレスをraxに格納し,それをrdiにコピー 4006e7: 48 8d 45 f8 lea -0x8(%rbp),%rax 4006eb: 48 89 c7 mov %rax,%rdi # コンストラクタの呼び出し 4006ee: e8 6d 00 00 00 callq 400760 <Derived::Derived()> 〜
スタック上にメモリを確保し,それをrdi経由でDerived::Derived()に渡していることが分かる.
では,Derived::Derived()を見ていこう.
0000000000400760 <Derived::Derived()>: # スタックを掘る 400760: 55 push %rbp 400761: 48 89 e5 mov %rsp,%rbp 400764: 48 83 ec 10 sub $0x10,%rsp # 引数をメモリにコピー 400768: 48 89 7d f8 mov %rdi,-0x8(%rbp) # メモリから引数をraxにコピーし,rdiにコピー 40076c: 48 8b 45 f8 mov -0x8(%rbp),%rax 400770: 48 89 c7 mov %rax,%rdi # Base::Base()の呼び出し 400773: e8 8e ff ff ff callq 400706 <Base::Base()> # edxに即値を代入 400778: ba d8 08 40 00 mov $0x4008d8,%edx # メモリから引数をraxにコピー 40077d: 48 8b 45 f8 mov -0x8(%rbp),%rax # 先程の即値をraxの参照先にコピー 400781: 48 89 10 mov %rdx,(%rax) # ediに即値を代入(おそらく文字列のアドレスだろう) 400784: bf 88 08 40 00 mov $0x400888,%edi # puts@pltを呼び出し 400789: e8 12 fe ff ff callq 4005a0 <puts@plt> # 終了処理 40078e: 90 nop 40078f: c9 leaveq 400790: c3 retq 400791: 90 nop
突然出てきた即値も気になるが,まずはBase::Base()を見ていこう.
0000000000400706 <Base::Base()>: # スタックを掘る 400706: 55 push %rbp 400707: 48 89 e5 mov %rsp,%rbp 40070a: 48 83 ec 10 sub $0x10,%rsp # 引数をメモリにコピー 40070e: 48 89 7d f8 mov %rdi,-0x8(%rbp) # 即値をedxに代入 400712: ba f8 08 40 00 mov $0x4008f8,%edx # メモリから引数をraxにコピー 400717: 48 8b 45 f8 mov -0x8(%rbp),%rax # 先程の即値をraxの参照先にコピー 40071b: 48 89 10 mov %rdx,(%rax) # ediに即値を代入(おそらく文字列のアドレスだろう) 40071e: bf 40 08 40 00 mov $0x400840,%edi # puts@pltを呼び出し 400723: e8 78 fe ff ff callq 4005a0 <puts@plt> # 終了処理 400728: 90 nop 400729: c9 leaveq 40072a: c3 retq 40072b: 90 nop
こちらでも同じく即値を引数の参照先にコピーしている.コンストラクタに渡される引数は未初期化オブジェクトのアドレスである.main()でスタックから確保したアドレスが格納され,その先頭位置に即値が代入されている.そして,Base::Base()で設定した$0x4008f8という値は,Derived::Derived()の以降の処理で$0x4008d8に上書きされている.
くどいようだが,もう一度C++のソースコードからコンストラクタの定義を見てみよう.
class Base { public: Base() { printf("Base::Base() has called\n"); } virtual void hoge() { printf("Base::hoge() has called\n"); } virtual void fuga() { printf("Base::fuga() has called\n"); } }; class Derived : public Base { public: Derived() { printf("Derived::Derived() has called\n"); } void fuga() { printf("Derived::fuga() has called\n"); } };
中では特に代入処理はしていない.では,Derived::Derived()の$0x4008d8とBase::Base()の$0x4008f8とは何なのだろうか.
この実行時のアドレスが位置するセクションが知りたいので,readelfコマンドでセクションヘッダを見てみよう.
(追記)今回は単一のリンク済み実行ファイルを見ているのでセクションヘッダの位置が*たまたま*実行時のアドレスに対応しています.実行時のアドレスに対応するファイル位置を確認したいのであれば,本来はプログラムヘッダを見なければなりません.(誰もこんな記事見ないと思いますが)以下の文章は間違っているので気をつけてください.
$ readelf -S a.out There are 30 section headers, starting at offset 0x1c48: Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align 〜 [15] .rodata PROGBITS 0000000000400830 00000830 0000000000000116 0000000000000000 A 0 0 8 〜 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings), l (large) I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown) O (extra OS processing required) o (OS specific), p (processor specific)
0x400830+0x116の範囲は.rodataであることが分かった.つまり,$0x4008d8と$0x4008f8は(ついでにputs@pltの前にあった$0x400888と$0x400840も).rodataに存在する.
readelfコマンドで.rodataをダンプして中身を見てみよう.
$ readelf -x .rodata a.out Hex dump of section '.rodata': 0x00400830 01000200 00000000 00000000 00000000 ................ 0x00400840 42617365 3a3a4261 73652829 20686173 Base::Base() has 0x00400850 2063616c 6c656400 42617365 3a3a686f called.Base::ho 0x00400860 67652829 20686173 2063616c 6c656400 ge() has called. 0x00400870 42617365 3a3a6675 67612829 20686173 Base::fuga() has 0x00400880 2063616c 6c656400 44657269 7665643a called.Derived: 0x00400890 3a446572 69766564 28292068 61732063 :Derived() has c 0x004008a0 616c6c65 64004465 72697665 643a3a66 alled.Derived::f 0x004008b0 75676128 29206861 73206361 6c6c6564 uga() has called 0x004008c0 00000000 00000000 00000000 00000000 ................ 0x004008d0 08094000 00000000 2c074000 00000000 ..@.....,.@..... 0x004008e0 92074000 00000000 00000000 00000000 ..@............. 0x004008f0 30094000 00000000 2c074000 00000000 0.@.....,.@..... 0x00400900 46074000 00000000 90106000 00000000 F.@.......`..... 0x00400910 20094000 00000000 30094000 00000000 .@.....0.@..... 0x00400920 37446572 69766564 00000000 00000000 7Derived........ 0x00400930 38106000 00000000 40094000 00000000 8.`.....@.@..... 0x00400940 34426173 6500 4Base.
とりあえず文字列が存在することは分かったが,見づらいので先程のreadelf -Sのアドレス情報とオフセット情報を元に,odで$0x4008d8と$0x4008f8に該当するファイル位置をダンプしてみる..rodataの開始アドレスである0x400830のオフセットは0x830であるため,実行時に0x4008d8に展開される部分は単純にファイルの0x8d8バイト目でよさそうだ.
$ od -j 0x8d8 -N 16 -tx8 -Ax a.out 0008d8 000000000040072c 0000000000400792 0008e8 $ od -j 0x8f8 -N 16 -tx8 -Ax a.out 0008f8 000000000040072c 0000000000400746 000908
何かのアドレスらしき値が格納されていることが分かる.ここで,再度objdumpの逆アセンブル結果を見てみよう.
000000000040072c <Base::hoge()>: 〜 0000000000400746 <Base::fuga()>: 〜 0000000000400792 <Derived::fuga()>: 〜
関数アドレスがodのダンプ結果と一致することが分かる.以上より,Base::Base()で格納した0x4008f8という値を元に,*0x4008f8にBase::hoge(),*(0x4008f8+0x8)にBase::fuga()のアドレスが格納されていることが分かった.また,Derived::Derived()で格納した0x4008d8という値を元に,*0x4008d8の位置にBase::hoge(),*(0x4008d8+0x8)ではオーバーライドしたDerived::fuga()のアドレスが格納されていることが分かった.
以上の機構により,C++ではポリモーフィズムを実現している.そして.rodataに存在する関数のアドレスのリストをvtableと呼んでいる.
.rodataにvtableを設置し各オブジェクトで二回デリファレンスするより,各オブジェクトの先頭に直接関数のアドレスのリストを格納すればデリファレンスの回数が減ってよいのでは?と思ったが,おそらくオブジェクトの数が多くなった場合にプログラムが無駄に肥大化するためこのような形になっているのだと思う.ちなみに,コンパイラの最適化レベルを上げるとまた異なった結果が得られるため,その辺りは注意されたい.
以下,参考サイト等.
以下のサイトではC++の擬似コードで説明してくれているのでより分かりやすい.
https://qiita.com/msmania/items/452d4fb4dec76207df87qiita.com
stackoverflow.com
また,ヒープオーバーフローを用いてvtableを上書きする攻撃も存在するらしい.
inaz2.hatenablog.com