レバテックフリーランスのサイトに当サイトが紹介されました!

C++クラスのメモリ展開アドレス

仕事でアプリケーションクラッシュ時のメモリダンプ解析をたまにするのですが、
継承が複雑なクラスインスタンスのメンバどこにあるんだ?ってなったので、
その辺をまとめておこうと思います。

ポイントは以下です。

  • 多階層継承時
  • 多重継承時
  • vtableとメンバ変数の位置

環境はmacでclang-600.0.56。64bit環境、64bitアプリとしてコンパイルします。
(こうゆうのってコンパイラ依存ないよね・・?)

アドレス確認

実際に試して、アドレス確認するのが一番。論より証拠ってやつですね。実験していきます。

まずはわかりきってることだけど、一通りのパターン確認していきます。一応。

#include
using namespace std;

class Child
{
public:
    int m_val;
};

int main()
{
    Child child;
    cout << "&child=" << &child << ", &child.m_val=" << &child.m_val << endl;
    return 0;
}

の場合

&child=0x7fff51f71a48, &child.m_val=0x7fff51f71a48

と出力されて、インスタンスアドレスと先頭のメンバのアドレスが一致することがわかります。

次。継承が挟まる場合。

class Base0
{
public:
    void Hoge() {}
};

class Child : public Base0
{
public:
    int m_val;
};

結果
&child=0x7fff52aa4a48, &child.m_val=0x7fff52aa4a48
同じ。
Base0クラスには関数はあるが仮想化されてないのでchildインスタンスへの影響はありません。

次。継承して仮想関数がある場合。

class Base0
{
public:
    virtual void Hoge() {}
};
class Child : public Base0
{
public:
    int m_val;
};

結果
&child=0x7fff54330a38, &child.m_val=0x7fff54330a40
はい、0x8(16進数)の差が発生しました。ご存知vtable分ですね。想定通りです。

次。基底クラスにメンバ変数がある場合。

class Base0
{
public:
    virtual void Hoge() {}
    int m_hoge;
};
class Child : public Base0
{
    public:
    int m_val;
};

&child=0x7fff56497a48, &child.m_val=0x7fff56497a54, &child.m_hoge=0x7fff56497a50
はい、基底のメンバがvtableの後ろに入り、派生クラスのm_valはさらに後ろのアドレスにずれています。
つまり、この構成の場合

インスタンスアドレスから

| vtable | Baseのメンバ | 派生のメンバ |

というメモリ配置となるということです。

次。継承の階層が2つ以上の場合。

class Base0
{
public:
    virtual void Hoge() {}
    int m_hoge;
};

class Middle : public Base0
{
public:
    virtual void Hoge() { m_middle++; }
    int m_middle;
};

class Child : public Middle
{
public:
    int m_val;
};

&child=0x7fff5455aa40, &child.m_val=0x7fff5455aa50,
&child.m_hoge=0x7fff5455aa48, &child.m_middle=0x7fff5455aa4c

まぁ予想通り、MiddleのメンバがBaseのメンバの次に展開されました。

次。多重継承の場合。

class Base0
{
public:
    virtual void Hoge() {}
    int m_hoge;
};

class Base1
{
public:
    virtual void Fuga() {}
    int m_fuga;
};

class Child : public Base0, public Base1
{
public:
    int m_val;
};

&child=0x7fff5376da38, &child.m_val=0x7fff5376da54,
&child.m_hoge=0x7fff5376da40, &child.m_fuga=0x7fff5376da50

まず、childの先頭アドレスとm_valは0x1Cの差があります。つまり10進数で28byteの差があります。
この実態を突き止めていきます。
まずはBase0のvtableがあるでしょう。そこにm_hogeのアドレスが続きます。
そして次がBase1のvtableとなると予想されます。
がここで、m_fugaのアドレスから逆算するとBase1のvtableは0x7fff5376da48にあるように思われます。
つまり0x7fff5376da44からの4バイトが空いています。
これはアライメントの影響と考えられます。64bit環境なので、8byteアラインメントがかけられるためです。
これを確認するためm_hogeの後ろにさらに4byteのメンバを足して出力してみても全く同じ結果が得られました。

すなわち、インスタンスの開始アドレスから

| Base0系のvtable | Base0のメンバ | Base1系のvtable | Base1のメンバ | Childのメンバ |

となるということです。注意としてアラインメントを考慮する必要が有ることも気づけました。

最後。全部入りで、多重継承かつ継承階層2つ以上かつメンバ変数入り。

class Base0
{
public:
    virtual void Hoge() {}
    int m_hoge;
};

class Base1
{
public:
    virtual void Fuga() {}
    int m_fuga;
};

class Middle : public Base0
{
public:
    virtual void Hoge() { m_middle++; }
    int m_middle;
};

class Child : public Middle, public Base1
{
public:
    int m_val;
};

&child=0x7fff53874a38, &child.m_val=0x7fff53874a54,
&child.m_hoge=0x7fff53874a40, &child.m_middle=0x7fff53874a44,
&child.m_fuga=0x7fff53874a50

はい、まぁ予想通り以下のとおりとなるということです。
インスタンスアドレスから

| Base0系のvtable | Base0のメンバ | Middleのメンバ | Base1系のvtable | Base1のメンバ | 派生のメンバ |

基本的に想定通りの挙動となりはしましたが、この辺をちゃんと一度確認してみておかないと意外と、いざダンプを解析しようとするときに不安になってしまいます。

この結果で確信をもって、ダンプ解析に励みたいと思います。

追記
以下を参照すると、コンパイラによってはコンパイルオプションでアドレスのダンプができるようで、簡単に確認できるようです。
また、多重継承の実現方法は場合によってはコンパイラに依存する可能性があるようです。基本的には上の形となりそうですが。

仮想関数テーブル

コメント

タイトルとURLをコピーしました