情弱ログ

参考にならないので当てにしないでください

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