3

私はvtablesとvpointersの内部動作についてもう少し学ぼうとしていたので、いくつかのトリックを使用してvtableに直接アクセスすることにしました。私は2つのクラスを作成BaseDerv、それぞれに2つのvirtual関数があります(の関数DervをオーバーライドしますBase)。

class Base
{
    int x;
    int y;

    public:
        Base(int x_, int y_) : x(x_), y(y_) {}

        virtual void foo() { cout << "Base::foo(): x = " << x << '\n'; }    
        virtual void bar() { cout << "Base::bar(): y = " << y << '\n'; }
};

class Derv: public Base
{
    int x;
    int y;

    public:
        Derv(int x_, int y_) : Base(x_, y_), x(x_), y(y_) {}

        virtual void foo() { cout << "Derived::foo(): x = " << x << '\n'; }
        virtual void bar() { cout << "Derived::bar(): y = " << y << '\n'; }
};

ここで、コンパイラは各クラスにvtableポインタを追加し、メモリの最初の4バイト(32ビット)を占有します。size_t*ポインタがサイズの別のポインタを指しているため、オブジェクトのアドレスをにキャストしてこのポインタにアクセスしましたsizeof(size_t)。これで、vpointerにインデックスを付け、その結果を適切なタイプの関数ポインターにキャストすることで、仮想関数にアクセスできます。これらのステップを関数にカプセル化しました。

template <typename T>
void call(T *ptr, size_t num)
{
    typedef void (*FunPtr)();

    size_t *vptr = *reinterpret_cast<size_t**>(ptr);
    FunPtr fun = reinterpret_cast<FunPtr>(vptr[num]);

    //setThisPtr(ptr);      added later, see below!
    fun();
}

メンバー関数の1つがこのように呼び出された場合、たとえばcall(new Base(1, 2), 0)Base :: foo()を呼び出す場合、ポインターなしで呼び出されるため、何が起こるかを予測することは困難thisです。thisg ++がレジスタに-pointerを格納することを知って、少しテンプレート化された関数を追加することでこれを解決しました(ただし、これにより、コンパイラフラグecxを使用してコンパイルする必要があります)。-m32

template <typename T>
void setThisPtr(T *ptr)
{  
    asm ( mov %0, %%ecx;" :: "r" (ptr) );
}

setThisPtr(ptr)上記のスニペットの行のコメントを解除すると、プログラムとして機能するようになります。

int main()
{
    Base* base = new Base(1, 2);
    Base* derv =  new Derv(3, 4);

    call(base, 0); // "Base::foo(): x = 1" 
    call(base, 1); // "Base::bar(): y = 2"
    call(derv, 0); // "Derv::foo(): x = 3"
    call(derv, 1); // "Derv::bar(): y = 4"
}

この小さなプログラムを書く過程で、vtablesがどのように機能するかについてより多くの洞察を得て、他の人がこの資料をもう少しよく理解するのに役立つかもしれないので、これを共有することにしました。ただし、まだいくつか質問があります
。1. 64ビットバイナリをコンパイルするときにthis-pointerを格納するために使用されるレジスタ(gcc 4.x)はどれですか。ここに記載されているように、すべての64ビットレジスタを試しました。http://developers.sun.com/solaris/articles/asmregs.html
2. this-pointerはいつ/どのように設定されますか?コンパイラは、オブジェクトを介した各関数呼び出しで、これを行ったのと同じようにthisポインタを設定していると思います。これはポリモーフィズムが実際に機能する方法ですか?(最初にthis-pointerを設定してから、vtableから仮想関数を呼び出しますか?)

4

1 に答える 1

4

Linux x86_64、および他のUNIXライクなOSでは、関数呼び出しはSystem V ABI(AMD64)に従い、それ自体はIA-64 C ++ ABI forC++に従います。メソッドのタイプに応じて、thisポインターは最初の引数または2番目の引数を介して暗黙的に渡されます(戻り値に重要なコピーコンストラクターまたはデストラクタがある場合、それは一時的なスタックとして存在する必要があり、最初の引数は暗黙的にポインターですそのスペースに); それ以外の場合、仮想メソッド呼び出しはCの関数呼び出しと同じです(、、、、、、の整数/ポインター引数、%rdiスタック%rsiにオーバーフロー%rdx、整数/ポインターの戻り値%rcx、浮動小数点数-%r8%r9%rax%xmm0%xmm7; 等。)。仮想メソッドディスパッチは、vtableでポインターを検索し、それを非仮想メソッドのように呼び出すことで機能します。

私はWindowsx64の規則にあまり詳しくありませんが、C ++メソッド呼び出しがC関数呼び出し(Linuxとは異なるレジスターを使用)とまったく同じ構造に従い、this最初に暗黙の引数があるという点で似ていると思います。

于 2012-08-15T05:58:13.513 に答える