単一継承条件での虚関数のリスト
子クラスが親クラスの虚関数を実現した場合、親クラスのポインタで子クラスの虚関数にアクセスできる鍵は、コンパイル時に子クラスの虚関数ポインタの内容を書き換えることにある.
例をみる
まず2つのクラスを定義します
関数呼び出し
アセンブリ分析
初期化コード除去後、bの定義のアセンブリコードは
ここでbのアドレスをecxに送ることに注意してください.
そしてBクラスのコンストラクタ体内に入ります
同様に初期化コードを無視した後、ここに到着します.このときecx,すなわちこのときecxの値はbのアドレスであり,コンパイラでもthisポインタの内容がbのアドレスであることが容易に得られる.
その後Aのコンストラクション関数に入るので,サブクラスオブジェクトのコンストラクションは親より先に完了する.
同様に、コンストラクション関数に入る前にecxをスタックに入れます.
初期化後、Aのコンストラクタがここに来る
まずecxが格納したbのアドレスを復元する.注意したいのは、00961 BE 3および00961 BE 6のコードです.まずthisポインタの内容をeaxに入れ、すなわちbのアドレスをeaxに送る.次に、Aクラスの虚関数リストのアドレスは、bアドレスから始まる4バイトに入る.なぜなら、最初の4バイトはいわゆる虚ポインタ、すなわち、オブジェクトbの虚ポインタがクラスAの虚関数リストを指しているからである.Aのダミー関数テーブルが存在するメモリの内容は
その後変数を初期化し,bの格納アドレスから連続的に格納されていることがわかり,&b+8に占有されていることを覚えておく.
Aのコンストラクタの実行が完了したら、Bのコンストラクタの実行を続行します.
肝心なのはどこですか.00961 EBBと00961 EBEでは,先ほどのAのコンストラクション関数とほぼ同じである.
得られた効果も同様に,オブジェクトbのダミーポインタに値を付与し,先ほどの値(Aのダミー関数リストのアドレス)を上書きし,その後Bのダミー関数リストを指す.
なお,Bクラスのメンバ変数を初期化する際に占有するアドレスは&b+0 chである.したがって、継承後の親と子のメンバー変数は連続的に格納されます.
では、呼び出し方法を見てみましょう.
まず、bのアドレスをeaxに送り、次いで、bのアドレスから始まる4バイトの内容をedx、すなわちbの虚関数リストポインタの値をedxに与える.このときの虚関数ポインタがBの虚関数リストを指していることを忘れないでください.calleaxの後に何が起こったのかよくわかります.
案の定BのName関数にジャンプしました.
c++のアセンブリコードをよく分析することはやはり収穫があり,少なくとも下層でもその理由を知ることができる.
例をみる
まず2つのクラスを定義します
class A
{
private:
int ma;
int mb;
public:
A()
{
ma = 1;
mb = 2;
}
virtual void Name()
{
cout<<"this is A"<<endl;
}
virtual void special()
{
cout<<"Hello kitty"<<endl;
}
void print()
{
int c;
printf("the address of the function is %x,class A is output ma %d
",&c,ma);
}
};
class B:public A
{
private:
int mc;
public:
B()
{
mc = 3;
}
virtual void Name()
{
cout<<"this is B"<<endl;
}
virtual void specialB()
{
cout<<"Hello Motor"<<endl;
}
void print()
{
int c;
printf("the address of the function is %x,class B is output mc %d
",&c,mc);
}
};
関数呼び出し
int experimentClassUpDownPointer()
{
B b;
A *a;
a = (A*)&b;
a->Name();
b.Name();
a->print();
return 1;
}
アセンブリ分析
初期化コード除去後、bの定義のアセンブリコードは
00961D3E lea ecx,[b]
00961D41 call B::B (9611DBh)
ここでbのアドレスをecxに送ることに注意してください.
そしてBクラスのコンストラクタ体内に入ります
同様に初期化コードを無視した後、ここに到着します.このときecx,すなわちこのときecxの値はbのアドレスであり,コンパイラでもthisポインタの内容がbのアドレスであることが容易に得られる.
00961EAF pop ecx
00961EB0 mov dword ptr [ebp-8],ecx
00961EB3 mov ecx,dword ptr [this]
00961EB6 call A::A (9611E5h)
その後Aのコンストラクション関数に入るので,サブクラスオブジェクトのコンストラクションは親より先に完了する.
同様に、コンストラクション関数に入る前にecxをスタックに入れます.
初期化後、Aのコンストラクタがここに来る
00961BDF pop ecx
00961BE0 mov dword ptr [ebp-8],ecx
00961BE3 mov eax,dword ptr [this]
00961BE6 mov dword ptr [eax],offset A::`vftable' (96A8D8h)
{
ma = 1;
00961BEC mov eax,dword ptr [this]
00961BEF mov dword ptr [eax+4],1
mb = 2;
00961BF6 mov eax,dword ptr [this]
00961BF9 mov dword ptr [eax+8],2
}
まずecxが格納したbのアドレスを復元する.注意したいのは、00961 BE 3および00961 BE 6のコードです.まずthisポインタの内容をeaxに入れ、すなわちbのアドレスをeaxに送る.次に、Aクラスの虚関数リストのアドレスは、bアドレスから始まる4バイトに入る.なぜなら、最初の4バイトはいわゆる虚ポインタ、すなわち、オブジェクトbの虚ポインタがクラスAの虚関数リストを指しているからである.Aのダミー関数テーブルが存在するメモリの内容は
0x0096A8D8 ea 11 96 00 f8 12 96 00 00 00 00 00 74 68 69 73 20 69 73 20 41 00 00 00 48 65 6c 6c 6f 20 6b ・E.・E.・E.・E.....this is A...HelloK
0x0096A8F7 69 74 74 79 00 00 00 00 00 74 68 65 20 61 64 64 72 65 73 73 20 6f 66 20 74 68 65 20 66 75 6e itty.....the address of the fun
その後変数を初期化し,bの格納アドレスから連続的に格納されていることがわかり,&b+8に占有されていることを覚えておく.
Aのコンストラクタの実行が完了したら、Bのコンストラクタの実行を続行します.
00961EBB mov eax,dword ptr [this]
00961EBE mov dword ptr [eax],offset B::`vftable' (96A94Ch)
{
mc = 3;
00961EC4 mov eax,dword ptr [this]
00961EC7 mov dword ptr [eax+0Ch],3
}
00961ECE mov eax,dword ptr [this]
00961ED1 pop edi
00961ED2 pop esi
00961ED3 pop ebx
00961ED4 add esp,0CCh
00961EDA cmp ebp,esp
00961EDC call @ILT+750(__RTC_CheckEsp) (9612F3h)
00961EE1 mov esp,ebp
00961EE3 pop ebp
00961EE4 ret
肝心なのはどこですか.00961 EBBと00961 EBEでは,先ほどのAのコンストラクション関数とほぼ同じである.
得られた効果も同様に,オブジェクトbのダミーポインタに値を付与し,先ほどの値(Aのダミー関数リストのアドレス)を上書きし,その後Bのダミー関数リストを指す.
なお,Bクラスのメンバ変数を初期化する際に占有するアドレスは&b+0 chである.したがって、継承後の親と子のメンバー変数は連続的に格納されます.
では、呼び出し方法を見てみましょう.
//experimentClassUpDownPointer ,b :
A *a;
a = (A*)&b;
00961D46 lea eax,[b]
00961D49 mov dword ptr [a],eax
a->Name();
00961D4C mov eax,dword ptr [a]
00961D4F mov edx,dword ptr [eax]
00961D51 mov esi,esp
00961D53 mov ecx,dword ptr [a]
00961D56 mov eax,dword ptr [edx]
00961D58 call eax
まず、bのアドレスをeaxに送り、次いで、bのアドレスから始まる4バイトの内容をedx、すなわちbの虚関数リストポインタの値をedxに与える.このときの虚関数ポインタがBの虚関数リストを指していることを忘れないでください.calleaxの後に何が起こったのかよくわかります.
B::Name:
00961366 jmp B::Name (961F00h)
virtual void Name()
{
01101F00 push ebp
01101F01 mov ebp,esp
01101F03 sub esp,0CCh
01101F09 push ebx
01101F0A push esi
01101F0B push edi
01101F0C push ecx
01101F0D lea edi,[ebp-0CCh]
01101F13 mov ecx,33h
01101F18 mov eax,0CCCCCCCCh
01101F1D rep stos dword ptr es:[edi]
01101F1F pop ecx
01101F20 mov dword ptr [ebp-8],ecx
cout<<"this is B"<<endl;
01101F23 mov esi,esp
01101F25 mov eax,dword ptr [__imp_std::endl (110E38Ch)]
01101F2A push eax
01101F2B push offset string "this is B" (110A95Ch)
01101F30 mov ecx,dword ptr [__imp_std::cout (110E390h)]
01101F36 push ecx
01101F37 call std::operator<<<std::char_traits<char> > (110127Bh)
01101F3C add esp,8
01101F3F mov ecx,eax
01101F41 call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (110E388h)]
01101F47 cmp esi,esp
01101F49 call @ILT+750(__RTC_CheckEsp) (11012F3h)
}
案の定BのName関数にジャンプしました.
c++のアセンブリコードをよく分析することはやはり収穫があり,少なくとも下層でもその理由を知ることができる.