虚関数テーブルから虚関数テーブルを呼び出し、虚関数テーブルを介して(アクセス権制御を迂回)


一、背景知識
虚関数を説明するとき、クラスに虚関数がある場合、そのクラスには虚関数テーブル(V-Table)が存在し、各クラスのオブジェクトにはその虚関数テーブルを指すポインタがあり、このような虚関数の関数ポインタが格納され、虚関数テーブルのアドレスはそのクラスのオブジェクトメモリの先頭に存在し、虚関数の検索を容易にすることを目的としていることがわかります.
陳浩の技術のコラムの中で1篇のC++虚関数の数表の解析に対してとても透徹している文章を書いたことがあります:C++虚関数の数表の解析
この文章では虚関数表の構造を徹底的に解析し、直感的な画像の説明を添えています.皆さんは見てから分かります.ここでは,虚関数テーブルによる虚関数呼び出しと虚関数テーブル(アクセス権限制御を迂回)によるコード実装について再説明する.
二、虚関数テーブルを通して虚関数を呼び出す
陳浩兄貴の文章では、虚関数テーブルを通じて関数を呼び出す方法について言及し、サンプルコードは以下の通りです.
#include<iostream>
#include<string>
using namespace std;

typedef void(*Fun)(void);

class Base {
 public:
  virtual void f() {
    cout << "Base::f()" << endl;
  }
  virtual void g() {
    cout << "Base::g()" << endl;
  }
  virtual void h() {
    cout << "Base::h()" << endl;
  }
};
上はベースクラスコードであり,Baseクラスにはf(),g(),h()の3つの虚関数が順に存在し,それらのポインタもBaseクラスのV−Tableに順次格納される.次のコードは、V-Tableの虚関数ポインタから虚関数を呼び出してみます.
int main() {
  Base b;
  Fun fp = NULL;
  cout << "      :" << (int*)(&b) << endl;
  cout << "              :" <<(int*)*(int*)(&b) << endl;
  for (int i = 0; i != 3; ++i) {
    fp = (Fun)*((int*)*(int*)(&b) + i);
    fp();
  }
  return 0;
}

自分のパソコン(64ビットシステム)で上記のテストコードを実行した場合、実行結果は以下の通りです.
      :0x7fff13ec2c80
               :0x400c70
Base::f()
Segmentation fault

虚関数テーブルから虚関数のポインタを取得し、虚関数実行を呼び出した結果、最初の虚関数が正しく呼び出されたこと、2番目の虚関数ポインタを取得したときにセグメントエラーが発生したことが分かった.
なぜこんなことになったのでしょうか.真剣に分析した結果、運行環境を無視したため、原因は以下の通りであることが分かった.
陳浩兄貴が与えたサンプルコードは32ビットシステムで実行されています.私が今使っているのは64ビットのシステムですが、32ビットシステムと64はシステムの違いは何ですか.ポインタはアドレス番号なので、ポインタsizeのサイズはアドレスビット数と一致します.32ビットシステムでは仮想アドレスが32ビットあるのでアドレスも32ビットなので、32ビットシステムではポインタも32ビットの4バイト、したがって、ポインタを(int*)に変換することができる.同様に、64ビットシステムでは、ポインタは64ビット、すなわち8バイトであり、このときポインタ配列を(int*)問題が発生し、int*は+1オフセットをするときにintの大きさの長さをオフセットし、64ビットシステムではintは32ビットであり、この32ビットシステムでは一致している.したがって、64ビットシステムでは、ポインタをlong*に配列化する.
long lo[] = {1.0, 2.0, 3.0};
long* p_L = &lo;
cout << *p_L;  //   lo[0] == 1.0  
cout << *++p_L;  //  lo[0] == 2.0
このとき、オフセットごとのアドレスサイズは8バイトで、ポインタサイズとちょうど同じです.ポインタ配列をint*に変換すると、+1が4バイトずれるたびにセグメントエラーが発生します.8バイト安いのが次のポインタのアドレスです.また、虚関数テーブルのアドレスと虚関数テーブルの最初の虚関数ポインタのアドレスは同じであるべきである.これは配列アドレスと配列ヘッダ要素アドレスが同じであるからである.(int*)(&b)得られるのは虚函数表アドレスのアドレスのみであるからである.
int main() {
  Base b;
  Fun fp = NULL;
  cout << "      :" << (long*)*(long*)(&b) << endl;
  cout << "              :" <<(long*)*(long*)(&b) << endl;
  for (int i = 0; i != 3; ++i) {
    fp = (Fun)*((long*)*(long*)(&b) + i);
    fp();
  }
  return 0;
}
の実行結果は次のとおりです.
      :0x400c70
              :0x400c70
Base::f()
Base::g()
Base::h()

上記のコードは少し混乱しているかもしれませんが、理解しにくいので、徐々に分解してみましょう.
int main() {
  Base b;
  Fun fp = NULL;
  long* v_table_addr_addr = (long*)&b;
  long* v_table_addr =(long*)*v_table_addr_addr;
  cout << "       :" << v_table_addr << endl;
  long* first_v_func_ptr_addr = v_table_addr;
  cout << "               :" << first_v_func_ptr_addr << endl;
  fp = (Fun)*first_v_func_ptr_addr;
  fp();
  for (int i = 0; i != 3; ++i) {
    fp = (Fun)*(v_table_addr + i); 
    fp();
  }
  return 0;
}

前に述べたように、クラスに虚関数がある場合、このクラスには虚関数テーブルがあり、このクラスの各オブジェクトにはテーブルを指すポインタがあり、オブジェクトメモリアドレスの最初に保存されます.
  long* v_table_addr_addr = (long*)&b;
&bオブジェクトのアドレスを取得し、ポインタのアドレスに変換します.このアドレスには虚関数テーブルのアドレスが格納されます.
   
 long* v_table_addr =(long*)*v_table_addr_addr;
上記アドレスへの参照を解除*v_table_addr_addr;虚関数テーブルのアドレスが得られます.
つまり、虚関数テーブルのヘッダ要素のアドレス:long*first_v_func_ptr_addr = v_table_addr;
次に参照を解除し、関数ポインタに変換します:fp=(Fun)*first_v_func_ptr_addr;
関数ポインタを得ると、普通の関数と同じように呼び出すことができます:fp();
  for (int i = 0; i != 3; ++i) {
    fp = (Fun)*(v_table_addr + i); 
    fp();
  }

上記のコードはforループを介して、仮想関数テーブルの仮想関数を順次呼び出し、配列要素にアクセスするのと同じです.
v_table_addrは、ポインタ配列(虚関数テーブル)の先頭アドレスであり、+iを1つ加算するたびに次の関数ポインタを指す.ここでは、v_table_addrをlong*タイプとして定義しているため、ポインタタイプと同様に8バイトを占めるため、ちょうど8バイトのメモリスペースが安くなり、次の関数ポインタを指す.
実行結果は次のとおりです.
       :0x400c90
               :0x400c90
Base::f()
Base::f()
Base::g()
Base::h()

より簡潔な呼び出し方法:
int main() {
Base b;
long** p = (long**)&b;  //(long*)&b  v_table, (long**)&b   v_table      
for(int i = 0; i !=3; ++i){
((Fun)*(*p+i))();  // *p  v_table   , *p+i   v_table i   ,*(*p+i)   i     ,((Fun)*(*p+i))();      。
}
return 0;  
}
実行結果:
Base::f()
Base::g()
Base::h()

三、虚関数表を通してアクセス権限制御を迂回する
虚関数をprivateタイプとして定義することで、クラスの外でprivateタイプの関数をオブジェクトで呼び出すことはできません.
ただし、クラスの虚関数テーブルでは、publicまたはprivateのアクセスタイプにかかわらず、すべての虚関数が格納されます.
これにより、オブジェクトのアクセス権制御を迂回し、オブジェクトではなくクラスの虚関数テーブルから対応する関数ポインタを取得し、その関数を呼び出すことができます.
class Base {
 public:
  virtual void f() {
    cout << "Base::f()" << endl;
  }
  virtual void g() {
    cout << "Base::g()" << endl;
  }
private:
  virtual void h() {
    cout << "Base::h()" << endl;
  }
};
クラスBaseのオブジェクトbについて:
Base b;
b.f();  
b.g();
b.h();  // error! private!
bオブジェクトを介して、f()とg()を直接呼び出すことができますが、h()を呼び出すことはできません.h()はprivateであり、クラス内部とその友元を除いて、他の場所では直接呼び出すことはできません.
しかし、クラスの虚関数テーブルを使用すると、アクセス制御権限を迂回してprivateタイプの虚関数を呼び出すことができます.虚関数でないとだめです.虚関数テーブルには虚関数のポインタしかないからです.
サンプルコード:
int main() {
  Base b;
  b.f();
  b.g();
  Fun fp = NULL;
  fp = (Fun)*((long*)*(long*)(&b) + 2);
  fp();
  //          
  long** p = (long**)&b;  //(long*)&b  v_table, (long**)&b   v_table      
  ((Fun)*(*p+2))();  // *p  v_table   , *p+2   v_table     ,*(*p+2)         ,(Fun)*(*p+2)();      。
  return 0;
}
実行結果:
Base::f()
Base::g()
Base::h()
Base::h()

オリジナル文章、転載は明記してください:転載自
IIcyZhao's Road
リンク先:http://blog.csdn.net/iicy266/article/details/11906807