C++虚関数の2:虚関数テーブルと虚関数呼び出し
前回の「C++虚関数の1つ:オブジェクトメモリレイアウト」に続き、今回は虚関数テーブルの構造と虚関数の呼び出し手順を分析します.
虚関数テーブル構造
虚関数テーブルの構造を表示するにはどうすればいいですか?gdbを使ってメモリを直接見るのはいいですが、直感的ではありません.では、もっと良い方法はありませんか.gccの-fdump-class-hierarchyオプションを使用するのは良い選択で、gccマニュアルではこのオプションの部分を以下のように説明しています.
-fdump-class-hierarchy-options (C++ only) Dump a representation of each class’s hierarchy and virtual function table layout to a file.
クラスの継承階層と虚関数テーブルのレイアウトを生成できます.前の文章はすでに一部のコードを貼ったことがあります.今、ソースファイル全体を以下に貼ります.
gccの-fdump-class-hierarchy-optionsオプションを使用してソースファイルを分析します.
$ g++ -c -fdump-class-hierarchy call_function.cpp
call_が生成されましたfunction.cpp.002t.classファイル(外部ヘッダファイルで定義されたクラスを削除):
各クラスの継承階層と虚関数テーブルレイアウトを得た.この3つのクラスには各クラスに虚関数テーブルがあることがわかります.Derivedクラスを例にとると、虚関数テーブルには9つのエントリがあり、そのレイアウトは次のとおりです.
エントリ1および6は整形定数であり、エントリ2および7はtypeinfo for Derivedであり、エントリ3、4、5および9はクラスメンバー関数へのポインタであり、エントリ8 demangleの後の名前はnon-virtual thunk to Derived::C()である.虚関数呼び出しを行う際にgdbを用いて,これらのエントリをどのように使用するかを解析した.
ダミー関数呼び出し
gdb逆アセンブリを使用して、各関数呼び出しの具体的なプロセスを分析します.
x->A():
x->B():
y->C()およびy->D()のプロシージャは、x->A()およびx->B()のプロシージャとほぼ同じであり、Derivedダミー関数テーブルエントリの内容、すなわち関数アドレスを取得して呼び出されます.エントリ8「non-virtual thunk to Derived::C()」,y->C()の実行時にこの関数が呼び出されたことを覚えていますか.この関数が何なのか見てみましょう.
non-virtual thunk to Derived::C()は2つのことしかしていません.まず、thisポインタを16バイト前に調整し、Derivedオブジェクトのヘッダアドレスに調整し、Derived::C()実行にジャンプします.その理由も理解できるように、yポインタはDerivedオブジェクトの中間部分を指し、Derived::C()に渡されるthisポインタは必ずDerivedオブジェクトのヘッダアドレスを指すポインタである必要があります.そうしないと、データメンバーにアクセスしてオフセット量を計算する際に問題が発生します.z->A()、z->B()、z->C()は、x->A()、x->B()呼び出しと似ていますが、z->D()は少し違いますので、異なる点に注意してみました.
このプロセスは,まずzポインタをBase 2タイプに変換し,Base 2タイプの呼び出しプロセスに従って関数呼び出しを行うことに相当する.これは、y->C()がnon-virtual thunk to Derived::C()を呼び出すのと同様に、メンバー関数を呼び出すときに使用するオブジェクトポインタがメンバー関数に渡されるthisポインタのタイプとは異なるため、データ・メンバーにアクセスするときに予期せぬコンテンツにアクセスしないように調整する必要があります.
虚関数テーブルのその他の内容
Derived虚関数テーブルには9つのエントリがあることがわかりますが、私たちは現在5つしか言及していません.残りの4つは何ですか.
残りの4つのエントリは、実行時にオブジェクトポインタのタイプ情報を取得するために使用され、例えば、前のyポインタのヘッダアドレスが虚表ポインタである.虚表ポインタを8バイト前に調整すると,エントリ7:typeinfo for Derivedを指し,このポインタが実際にオブジェクトを指すタイプを得ることができる.虚表ポインタを16バイト前に調整すると、エントリ6:-16を指す.この-16の意味は、yポインタを16バイト前に調整することであり、その真の指向対象のヘッダアドレスであり、xポインタも同様である.C++dynamic_castの実装はこのいくつかの追加のエントリに依存し、興味のある人は自分でgccソースコードを研究することができます.
虚関数テーブル構造
虚関数テーブルの構造を表示するにはどうすればいいですか?gdbを使ってメモリを直接見るのはいいですが、直感的ではありません.では、もっと良い方法はありませんか.gccの-fdump-class-hierarchyオプションを使用するのは良い選択で、gccマニュアルではこのオプションの部分を以下のように説明しています.
-fdump-class-hierarchy-options (C++ only) Dump a representation of each class’s hierarchy and virtual function table layout to a file.
クラスの継承階層と虚関数テーブルのレイアウトを生成できます.前の文章はすでに一部のコードを貼ったことがあります.今、ソースファイル全体を以下に貼ります.
#include
#include
class Base1
{
public:
Base1() { memset(&Base1Data, 0x11, sizeof(Base1Data)); }
virtual void A() {};
virtual void B() {};
uint64_t Base1Data;
};
class Base2
{
public:
Base2() { memset(&Base2Data, 0x22, sizeof(Base2Data)); }
virtual void C() {};
virtual void D() {};
uint64_t Base2Data;
};
class Derived : public Base1, public Base2
{
public:
Derived() { memset(&DerivedData, 0x33, sizeof(DerivedData)); }
virtual void A() {};
virtual void C() {};
uint64_t DerivedData;
};
int main()
{
Base1 *x = new Derived;
x->A();
x->B();
Base2 *y = new Derived;
y->C();
y->D();
Derived *z = new Derived;
z->A();
z->B();
z->C();
z->D();
return 0;
}
gccの-fdump-class-hierarchy-optionsオプションを使用してソースファイルを分析します.
$ g++ -c -fdump-class-hierarchy call_function.cpp
call_が生成されましたfunction.cpp.002t.classファイル(外部ヘッダファイルで定義されたクラスを削除):
Vtable for Base1
Base1::_ZTV5Base1: 4 entries
0 (int (*)(...))0
8 (int (*)(...))(& _ZTI5Base1)
16 (int (*)(...))Base1::A
24 (int (*)(...))Base1::B
Class Base1
size=16 align=8
base size=16 base align=8
Base1 (0x0x7ff358c9bc00) 0
vptr=((& Base1::_ZTV5Base1) + 16)
Vtable for Base2
Base2::_ZTV5Base2: 4 entries
0 (int (*)(...))0
8 (int (*)(...))(& _ZTI5Base2)
16 (int (*)(...))Base2::C
24 (int (*)(...))Base2::D
Class Base2
size=16 align=8
base size=16 base align=8
Base2 (0x0x7ff358c9bea0) 0
vptr=((& Base2::_ZTV5Base2) + 16)
Vtable for Derived
Derived::_ZTV7Derived: 9 entries
0 (int (*)(...))0
8 (int (*)(...))(& _ZTI7Derived)
16 (int (*)(...))Derived::A
24 (int (*)(...))Base1::B
32 (int (*)(...))Derived::C
40 (int (*)(...))-16
48 (int (*)(...))(& _ZTI7Derived)
56 (int (*)(...))Derived::_ZThn16_N7Derived1CEv
64 (int (*)(...))Base2::D
Class Derived
size=40 align=8
base size=40 base align=8
Derived (0x0x7ff358b424d0) 0
vptr=((& Derived::_ZTV7Derived) + 16)
Base1 (0x0x7ff358cfa180) 0
primary-for Derived (0x0x7ff358b424d0)
Base2 (0x0x7ff358cfa1e0) 16
vptr=((& Derived::_ZTV7Derived) + 56)
各クラスの継承階層と虚関数テーブルレイアウトを得た.この3つのクラスには各クラスに虚関数テーブルがあることがわかります.Derivedクラスを例にとると、虚関数テーブルには9つのエントリがあり、そのレイアウトは次のとおりです.
Vtable for Derived
Derived::_ZTV7Derived: 9 entries
0 (int (*)(...))0 #1
8 (int (*)(...))(& _ZTI7Derived) #2
16 (int (*)(...))Derived::A #3
24 (int (*)(...))Base1::B #4
32 (int (*)(...))Derived::C #5
40 (int (*)(...))-16 #6
48 (int (*)(...))(& _ZTI7Derived) #7
56 (int (*)(...))Derived::_ZThn16_N7Derived1CEv #8
64 (int (*)(...))Base2::D #9
エントリ1および6は整形定数であり、エントリ2および7はtypeinfo for Derivedであり、エントリ3、4、5および9はクラスメンバー関数へのポインタであり、エントリ8 demangleの後の名前はnon-virtual thunk to Derived::C()である.虚関数呼び出しを行う際にgdbを用いて,これらのエントリをどのように使用するかを解析した.
ダミー関数呼び出し
gdb逆アセンブリを使用して、各関数呼び出しの具体的なプロセスを分析します.
x->A():
# x $rbp-0x28 , x , rax
0x000055555555497c 34>: mov -0x28(%rbp),%rax
# x 8 rax, 8 ?
# , Derived , +16 , 3
0x0000555555554980 38>: mov (%rax),%rax
# 3 rax, 3 Derived::A , rax Derived::A
0x0000555555554983 41>: mov (%rax),%rax
# x rdx
0x0000555555554986 44>: mov -0x28(%rbp),%rdx
# x rdi,rdi , Derived::A , 1 this
0x000055555555498a 48>: mov %rdx,%rdi
# Derived::A
0x000055555555498d 51>: callq *%rax
x->B():
# x->A()
0x000055555555498f 53>: mov -0x28(%rbp),%rax
# x->A()
0x0000555555554993 57>: mov (%rax),%rax
# rax 4 :Base1::B
0x0000555555554996 60>: add $0x8,%rax
# 4 rax,Base1::B
0x000055555555499a 64>: mov (%rax),%rax
# x->A()
0x000055555555499d 67>: mov -0x28(%rbp),%rdx
# x->A()
0x00005555555549a1 71>: mov %rdx,%rdi
# Base1::B
0x00005555555549a4 74>: callq *%rax
y->C()およびy->D()のプロシージャは、x->A()およびx->B()のプロシージャとほぼ同じであり、Derivedダミー関数テーブルエントリの内容、すなわち関数アドレスを取得して呼び出されます.エントリ8「non-virtual thunk to Derived::C()」,y->C()の実行時にこの関数が呼び出されたことを覚えていますか.この関数が何なのか見てみましょう.
(gdb) x/2g y # y , 8 Derived
0x555555768eb0: 0x0000555555755d08 0x2222222222222222
(gdb) x/g 0x0000555555755d08 # 8
0x555555755d08 <_ztv7derived class="hljs-number">56>: 0x0000555555554b95
(gdb) disassemble 0x0000555555554b95 #
Dump of assembler code for function _ZThn16_N7Derived1CEv:
0x0000555555554b95 0>: sub $0x10,%rdi
0x0000555555554b99 4>: jmp 0x555555554b8a <:c>
End of assembler dump.
non-virtual thunk to Derived::C()は2つのことしかしていません.まず、thisポインタを16バイト前に調整し、Derivedオブジェクトのヘッダアドレスに調整し、Derived::C()実行にジャンプします.その理由も理解できるように、yポインタはDerivedオブジェクトの中間部分を指し、Derived::C()に渡されるthisポインタは必ずDerivedオブジェクトのヘッダアドレスを指すポインタである必要があります.そうしないと、データメンバーにアクセスしてオフセット量を計算する際に問題が発生します.z->A()、z->B()、z->C()は、x->A()、x->B()呼び出しと似ていますが、z->D()は少し違いますので、異なる点に注意してみました.
0x0000555555554a53 249>: mov -0x18(%rbp),%rax
# $rax+16 rdx,rax z , $rax+16 Derived Base2
0x0000555555554a57 253>: lea 0x10(%rax),%rdx
# y->D()
0x0000555555554a5b 257>: mov -0x18(%rbp),%rax
0x0000555555554a5f 261>: mov 0x10(%rax),%rax
0x0000555555554a63 265>: add $0x8,%rax
0x0000555555554a67 269>: mov (%rax),%rax
0x0000555555554a6a 272>: mov %rdx,%rdi
0x0000555555554a6d 275>: callq *%rax
このプロセスは,まずzポインタをBase 2タイプに変換し,Base 2タイプの呼び出しプロセスに従って関数呼び出しを行うことに相当する.これは、y->C()がnon-virtual thunk to Derived::C()を呼び出すのと同様に、メンバー関数を呼び出すときに使用するオブジェクトポインタがメンバー関数に渡されるthisポインタのタイプとは異なるため、データ・メンバーにアクセスするときに予期せぬコンテンツにアクセスしないように調整する必要があります.
虚関数テーブルのその他の内容
Derived虚関数テーブルには9つのエントリがあることがわかりますが、私たちは現在5つしか言及していません.残りの4つは何ですか.
0 (int (*)(...))0 #1
8 (int (*)(...))(& _ZTI7Derived) #2
40 (int (*)(...))-16 #6
48 (int (*)(...))(& _ZTI7Derived) #7
残りの4つのエントリは、実行時にオブジェクトポインタのタイプ情報を取得するために使用され、例えば、前のyポインタのヘッダアドレスが虚表ポインタである.虚表ポインタを8バイト前に調整すると,エントリ7:typeinfo for Derivedを指し,このポインタが実際にオブジェクトを指すタイプを得ることができる.虚表ポインタを16バイト前に調整すると、エントリ6:-16を指す.この-16の意味は、yポインタを16バイト前に調整することであり、その真の指向対象のヘッダアドレスであり、xポインタも同様である.C++dynamic_castの実装はこのいくつかの追加のエントリに依存し、興味のある人は自分でgccソースコードを研究することができます.