C++の仮想関数の実現方法をアセンブリ言語で解説

<h1>C++の仮想関数の実現方法をアセンブリ言語で解説</h1>

仮想関数はオーバーヘッドがかかると知られています。また、オブジェクトのサイズが増大することも知られています。今回は、コンパイラによってC++で書かれたソースコードをアセンブリ言語で書かれたソースコードに変換して、仮想関数を解説することにしました。

オブジェクト指向では、ポリモーフィズムという仕組みが存在します。ポリモーフィズムとは、呼び出し元が複数の派生クラスを同じインターフェースで取り扱えるようにするための仕組みです。C++では、基底クラスのオブジェクトへのポインタや参照に派生クラスのオブジェクトへの番地を入れることで、ポリモーフィズムを実現させます。C++では、同じインターフェースでも本来のオブジェクトのインターフェースが呼び出せるようにするために、virtualを基底クラスの関数の頭に付け足す必要があります。virtualが頭についた関数のことを仮想関数と言います。

※ここでいうインターフェースとは、関数名やデータメンバのことです。特にpublicメンバのことを指しており、外部から見える振る舞いのことを指しています。

<h2>使用するコード</h2>

```hoge.h

class Object{
    public:
        Object(){

        }
        virtual ~Object(){

        }
        virtual void Show(){
        
        }

};

class A:public Object{
    public:
        void Show(){

        }
};
```

基底クラスObjectを用意し、派生クラスAを用意しました。基底クラスObjectのデストラクタとShow関数はvirtualにしました。あと、上に書いたコードをhoge.hにしておきます。

<h2>普通に派生クラスAからインスタンスを生成した場合のアセンブリコード</h2>

```
#include "hoge.h"

int main(){
    A a;
    a.Show();
    return 0;
}
```

単純に派生クラスAからインスタンスaを生成して、Show関数を呼び出しています。この場合は、派生クラスAのShow関数を普通にcall命令で呼び出していそうです。以下は上のコードに対応するアセンブリコードです。<a href="https://godbolt.org/">Compiler Explorer</a>というサイトで、アセンブリコードを生成しました。

```
Object::~Object() [base object destructor]:
        push    rbp
        mov     rbp, rsp
        mov     QWORD PTR [rbp-8], rdi
        mov     edx, OFFSET FLAT:vtable for Object+16
        mov     rax, QWORD PTR [rbp-8]
        mov     QWORD PTR [rax], rdx
        nop
        pop     rbp
        ret
Object::~Object() [deleting destructor]:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     QWORD PTR [rbp-8], rdi
        mov     rax, QWORD PTR [rbp-8]
        mov     rdi, rax
        call    Object::~Object() [complete object destructor]
        mov     rax, QWORD PTR [rbp-8]
        mov     esi, 8
        mov     rdi, rax
        call    operator delete(void*, unsigned long)
        leave
        ret
Object::Show():
        push    rbp
        mov     rbp, rsp
        mov     QWORD PTR [rbp-8], rdi
        nop
        pop     rbp
        ret
A::Show():
        push    rbp
        mov     rbp, rsp
        mov     QWORD PTR [rbp-8], rdi
        nop
        pop     rbp
        ret
main:
        push    rbp
        mov     rbp, rsp
        push    rbx
        sub     rsp, 24
        mov     eax, OFFSET FLAT:vtable for A+16
        mov     QWORD PTR [rbp-24], rax
        lea     rax, [rbp-24]
        mov     rdi, rax
        call    A::Show()
        mov     ebx, 0
        lea     rax, [rbp-24]
        mov     rdi, rax
        call    A::~A() [complete object destructor]
        mov     eax, ebx
        mov     rbx, QWORD PTR [rbp-8]
        leave
        ret
vtable for A:
        .quad   0
        .quad   typeinfo for A
        .quad   A::~A() [complete object destructor]
        .quad   A::~A() [deleting destructor]
        .quad   A::Show()
A::~A() [base object destructor]:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     QWORD PTR [rbp-8], rdi
        mov     edx, OFFSET FLAT:vtable for A+16
        mov     rax, QWORD PTR [rbp-8]
        mov     QWORD PTR [rax], rdx
        mov     rax, QWORD PTR [rbp-8]
        mov     rdi, rax
        call    Object::~Object() [base object destructor]
        nop
        leave
        ret
A::~A() [deleting destructor]:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     QWORD PTR [rbp-8], rdi
        mov     rax, QWORD PTR [rbp-8]
        mov     rdi, rax
        call    A::~A() [complete object destructor]
        mov     rax, QWORD PTR [rbp-8]
        mov     esi, 8
        mov     rdi, rax
        call    operator delete(void*, unsigned long)
        leave
        ret
vtable for Object:
        .quad   0
        .quad   typeinfo for Object
        .quad   Object::~Object() [complete object destructor]
        .quad   Object::~Object() [deleting destructor]
        .quad   Object::Show()
typeinfo for A:
        .quad   vtable for __cxxabiv1::__si_class_type_info+16
        .quad   typeinfo name for A
        .quad   typeinfo for Object
typeinfo name for A:
        .string "1A"
typeinfo for Object:
        .quad   vtable for __cxxabiv1::__class_type_info+16
        .quad   typeinfo name for Object
typeinfo name for Object:
        .string "6Object"
```

ラベルmainの部分を抽出して、観察してみます。

```
main:
        push    rbp
        mov     rbp, rsp
        push    rbx
        sub     rsp, 24
        mov     eax, OFFSET FLAT:vtable for A+16
        mov     QWORD PTR [rbp-24], rax
        lea     rax, [rbp-24]
        mov     rdi, rax
        call    A::Show()
        mov     ebx, 0
        lea     rax, [rbp-24]
        mov     rdi, rax
        call    A::~A() [complete object destructor]
        mov     eax, ebx
        mov     rbx, QWORD PTR [rbp-8]
        leave
        ret
```

中央付近に、A::Show()と書かれていますね。このことから、普通に派生クラスAを生成する場合は、仮想関数の仕組みが利用されていないことが分かります。分かりきっていたことですが。ちなみにcall A::Showを実行する前に、mov rdi, raxという命令を実行していることが分かります。これは、rdiにthisを格納しています。隠し引数ですね。

<h2>基底クラスObjectへのポインタに派生クラスAのオブジェクトの番地を格納した場合</h2>


```
#include "hoge.h"

int main(){
    Object* obj = new A();
    obj->Show();
    return 0;
}
```
基底クラスObjectへのポインタに派生クラスAのオブジェクトの番地を格納し、派生クラスAのShow関数を呼び出しています。これに対応するアセンブリ言語で書かれたコードに変換してみます。ちなみに、deleteは書いてません。アセンブリコードが読みづらくなります。仮想関数が呼ばれる仕組みを知りたいだけなので。

```sample.asm
Object::~Object() [base object destructor]:
        push    rbp
        mov     rbp, rsp
        mov     QWORD PTR [rbp-8], rdi
        mov     edx, OFFSET FLAT:vtable for Object+16
        mov     rax, QWORD PTR [rbp-8]
        mov     QWORD PTR [rax], rdx
        nop
        pop     rbp
        ret
Object::~Object() [deleting destructor]:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     QWORD PTR [rbp-8], rdi
        mov     rax, QWORD PTR [rbp-8]
        mov     rdi, rax
        call    Object::~Object() [complete object destructor]
        mov     rax, QWORD PTR [rbp-8]
        mov     esi, 8
        mov     rdi, rax
        call    operator delete(void*, unsigned long)
        leave
        ret
Object::Show():
        push    rbp
        mov     rbp, rsp
        mov     QWORD PTR [rbp-8], rdi
        nop
        pop     rbp
        ret
A::Show():
        push    rbp
        mov     rbp, rsp
        mov     QWORD PTR [rbp-8], rdi
        nop
        pop     rbp
        ret
Object::Object() [base object constructor]:
        push    rbp
        mov     rbp, rsp
        mov     QWORD PTR [rbp-8], rdi
        mov     edx, OFFSET FLAT:vtable for Object+16
        mov     rax, QWORD PTR [rbp-8]
        mov     QWORD PTR [rax], rdx
        nop
        pop     rbp
        ret
A::A() [base object constructor]:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     QWORD PTR [rbp-8], rdi
        mov     rax, QWORD PTR [rbp-8]
        mov     rdi, rax
        call    Object::Object() [base object constructor]
        mov     edx, OFFSET FLAT:vtable for A+16
        mov     rax, QWORD PTR [rbp-8]
        mov     QWORD PTR [rax], rdx
        nop
        leave
        ret
main:
        push    rbp
        mov     rbp, rsp
        push    rbx
        sub     rsp, 24
        mov     edi, 8
        call    operator new(unsigned long)
        mov     rbx, rax
        mov     QWORD PTR [rbx], 0
        mov     rdi, rbx
        call    A::A() [complete object constructor]
        mov     QWORD PTR [rbp-24], rbx
        mov     rax, QWORD PTR [rbp-24]
        mov     rax, QWORD PTR [rax]
        add     rax, 16
        mov     rdx, QWORD PTR [rax]
        mov     rax, QWORD PTR [rbp-24]
        mov     rdi, rax
        call    rdx
        mov     eax, 0
        mov     rbx, QWORD PTR [rbp-8]
        leave
        ret
vtable for A:
        .quad   0
        .quad   typeinfo for A
        .quad   A::~A() [complete object destructor]
        .quad   A::~A() [deleting destructor]
        .quad   A::Show()
A::~A() [base object destructor]:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     QWORD PTR [rbp-8], rdi
        mov     edx, OFFSET FLAT:vtable for A+16
        mov     rax, QWORD PTR [rbp-8]
        mov     QWORD PTR [rax], rdx
        mov     rax, QWORD PTR [rbp-8]
        mov     rdi, rax
        call    Object::~Object() [base object destructor]
        nop
        leave
        ret
A::~A() [deleting destructor]:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     QWORD PTR [rbp-8], rdi
        mov     rax, QWORD PTR [rbp-8]
        mov     rdi, rax
        call    A::~A() [complete object destructor]
        mov     rax, QWORD PTR [rbp-8]
        mov     esi, 8
        mov     rdi, rax
        call    operator delete(void*, unsigned long)
        leave
        ret
vtable for Object:
        .quad   0
        .quad   typeinfo for Object
        .quad   Object::~Object() [complete object destructor]
        .quad   Object::~Object() [deleting destructor]
        .quad   Object::Show()
typeinfo for A:
        .quad   vtable for __cxxabiv1::__si_class_type_info+16
        .quad   typeinfo name for A
        .quad   typeinfo for Object
typeinfo name for A:
        .string "1A"
typeinfo for Object:
        .quad   vtable for __cxxabiv1::__class_type_info+16
        .quad   typeinfo name for Object
typeinfo name for Object:
        .string "6Object"
```
<strong>この長々しく記述されたコードをsample.asmとして、これ以降も使っていきます。注意してください。</strong>

sample.asmのラベルmainの部分を抽出してみます。

```
main:
        push    rbp
        mov     rbp, rsp
        push    rbx
        sub     rsp, 24
        mov     edi, 8
        call    operator new(unsigned long)
        mov     rbx, rax
        mov     QWORD PTR [rbx], 0
        mov     rdi, rbx
        call    A::A() [complete object constructor]
        mov     QWORD PTR [rbp-24], rbx
        mov     rax, QWORD PTR [rbp-24]
        mov     rax, QWORD PTR [rax]
        add     rax, 16
        mov     rdx, QWORD PTR [rax]
        mov     rax, QWORD PTR [rbp-24]
        mov     rdi, rax
        call    rdx
        mov     eax, 0
        mov     rbx, QWORD PTR [rbp-8]
        leave
        ret
```

call Show命令がどこにもありません。call Object::Showやcall A::Showがどこにも書かれていないのに、派生クラスAのShow関数が呼ばれるわけです。callはされないとShow関数は呼ばれないわけです。inline化されていれば、話は別だけど。mainラベル内ではcall命令が三回呼ばれています。どのcallがShow関数に対応しているかを考えます。最初のcall命令は、

```
        call    operator new(unsigned long)
```

です。これは明らかにnew演算を実行していると分かります。これはShow関数には対応していないでしょう。

次のcall命令は、

```
        call    A::A() [complete object constructor]
```
です。これはコンストラクタを呼び出しているだけですね。これも明らかにShow関数とは無関係ですね。C++入門書では関数みたいなものと説明されることが多いですが、アセンブリコードだと、コンストラクタも普通の関数もcallで呼び出しているようです。アセンブリコードから見た、コンストラクタは戻り値無しの関数ということになりそうです。

最後のcall命令は、

```
        call    rdx
```

です。これ以降、call命令は存在しません。これがShow関数に対応していそうです。推測するに、rdxに格納された値が派生クラスAのShow関数の先頭の番地なのでしょう。call rdxが実行されるまでのコードを観察してみます。行番号を今回は付け足しました。

```
main:
1        push    rbp
2        mov     rbp, rsp
3        push    rbx
4        sub     rsp, 24
5        mov     edi, 8
6        call    operator new(unsigned long)
7        mov     rbx, rax
8        mov     QWORD PTR [rbx], 0
9        mov     rdi, rbx
10       call    A::A() [complete object constructor]
11       mov     QWORD PTR [rbp-24], rbx
12       mov     rax, QWORD PTR [rbp-24]
13       mov     rax, QWORD PTR [rax]
14       add     rax, 16
15       mov     rdx, QWORD PTR [rax]
16       mov     rax, QWORD PTR [rbp-24]
17       mov     rdi, rax
18       call    rdx
```

最後の行(18行目)がcall rdxでShow関数を呼び出しており、それまでの機械語命令で、rdxに派生クラスAのShow関数の先頭の番地が格納されているはずです。1行目から何をやっているかをみていきましょう。

<h3>1~6行目</h3>
rbpを今のスタックポインタの値に設定したり、operator new関数で派生クラスAのオブジェクト用の番地を確保したりしています。


<h3>7~10行目</h3>
7行目の

```
mov     rbx, rax
```
では、operator newで確保した番地がraxに格納されており、それをrbxに格納しています。関数からの戻り値は一般にraxに格納されます。それをrbxに退避しただけです。

8行目の

```
mov     QWORD PTR [rbx], 0
```

rbxは派生クラスAのオブジェクトの番地を示している状態です。rbxの参照先を0に初期化しているだけです。オブジェクト内部を0で埋めています。今の派生クラスAはサイズが8バイトなので、QWORD PTR[rbx]で初期化するだけで十分なのです。コンストラクタ呼び出しの前に、データメンバは初期化されるというのがC++の仕様です。基底クラスObjectにも派生クラスAにもデータメンバは存在しません。何が存在してるのでしょうか?<strong>仮想テーブルへのポインタが格納されているのです。仮想テーブルとは、親クラスへのポインタに派生クラスの番地を代入しても、派生クラスの関数が呼び出されるようにするためのテーブルです。</stong>基底クラスObjectのShow関数やvirtualを外してみると、8行目の処理は消えます。<strong>virtualを用意すると、オブジェクトのサイズが増大するのです。</strong>仮想テーブルは、sample.asmのどこに存在しているのでしょうか?答えは、ラベルvtable for Aと、ラベルvtable for Objectに存在します。


9行目の

```
mov     rdi, rbx
```
何度も書いて申し訳ないのですが、rbxは派生クラスAのオブジェクトの番地を示している状態です。それをrdiに値をコピーしています。この後の派生クラスAのコンストラクタ呼び出しで、rdiは第一引数として利用されます。C++では、隠れ引数としてthisが存在します。なので、rdiに派生クラスAのオブジェクトの番地を設定しているわけです。

10行目の

```
call    A::A() [complete object constructor]
```
隠れ引数を設定したのですから、あとは派生クラスAのコンストラクタを呼び出すだけです。10行目はそれをやっているだけです。

<h3>11~18行目</h3>
10行目までの処理では、派生クラスAのShow関数とはあまり関係のない処理であったことが分かりました。恐らく11行目以降が派生クラスAのShow関数呼び出しと関係していそうです。
<strong>※派生クラスAのコンストラクタ内で仮想テーブルへのポインタの初期化が行われているのですが、一旦後回しにします。</strong>

11行目の

```
       mov     QWORD PTR [rbp-24], rbx
```
何度も書いて申し訳ないのですが、rbxは派生クラスAのオブジェクトの番地を示している状態です。rbp-24が指し示す先に、コピーしています。

12行目の

```
       mov     rax, QWORD PTR [rbp-24]
```

では、raxに11行目でrbxからコピーした派生クラスAのオブジェクトの番地をraxにコピーしています。

13行目では、

```
mov     rax, QWORD PTR [rax]
```
QWORD PTR [rax]は派生クラスAのオブジェクトの中身を示しています。今現在、派生クラスAのサイズは８バイトです。その正体は仮想テーブルへのポインタであると、7~10行目の解説の時に話しました。つまり、QWORD PTR [rax]からraxにコピーされている値の正体は、仮想テーブルの番地です
<strong>※厳密には仮想テーブルの先頭の番地から16バイト加算された状態がraxに格納されています。いつ16バイト加算したのかと言いますと、コンストラクタの中で行われています。何度も申し訳ないのですが、18行目の解説を終えてから、その話をします。</strong>

14行目では、

```
add     rax, 16
```
仮想テーブルの先頭の番地に16バイト加算しています。16バイト加算した先に何が存在しているのかは、仮想テーブルを観察すればよく分かります。7~10行目でも話した通り、sample.asmの内のラベルvtable for Aとラベルvtable for Objectが仮想テーブルです。それぞれが仮想テーブルです。では、raxには、どちらの仮想テーブルの番地が格納されているのでしょうか?答えは、ラベルvtable for Aの方です。決め方の詳細は18行目の解説を終えた後に話します。話を戻すと、仮想テーブルの観察をすれば、16バイト加算される理由がわかるといいました。ラベルvtable for Aを観察しましょう。

```
vtable for A:
        .quad   0                                    8バイト
        .quad   typeinfo for A                       8バイト
        .quad   A::~A() [complete object destructor] 8バイト
        .quad   A::~A() [deleting destructor]        8バイト
        .quad   A::Show()                            8バイト
```

.quadは8バイトそこに置くぐらいの意味で捉えるといいです。14行目でraxには仮想テーブルの16バイトが既に加算された番地が格納されていると言いました。つまり、そこから更に16バイト加算するわけですから、、、

```
        .quad   A::Show()
```
<strong>A::Show()の番地の番地をraxは示すことになります。!!</strong>

15行目では、

```
mov     rdx, QWORD PTR [rax]
```

rdxにA::Showの番地を格納しています。raxは仮想テーブルのA::Showの番地の番地を保持していますので、QWORD PTR [rax]とは、A::Showの番地になるわけです。

16行目では、

```
mov     rax, QWORD PTR [rbp-24]
```
raxに派生クラスAのオブジェクトの番地を格納しています。

17行目では、

```
mov     rdi, rax
```
rdiは関数の第一引数として利用されます。A::Show呼び出しの前に、隠れ引数thisを設定するために、rdiに派生クラスAのオブジェクトの番地を格納しています。

18行目では、

```
call    rdx
```

rdxにはA::Showの番地が格納されているので、call命令によって、A::Show関数が呼び出されるのです!!

<h2>仮想テーブルへのポインタの初期化はいつやっているのか?</h2>
前章では、仮想テーブルへのポインタの初期化や、既に16バイト加算されている状態の理由をうやむやにしていました。今回の章で、それらの理由を解説していきます。前章の行番号をつけたコードの、

```
call    A::A() [complete object constructor]
```

で、派生クラスAのコンストラクタを呼び出しています。派生クラスAのコンストラクタ内部を観察していきましょう。sample.asm内のA::A()ラベルを抜き出します。

```
A::A() [base object constructor]:
1        push    rbp
2        mov     rbp, rsp
3        sub     rsp, 16
4        mov     QWORD PTR [rbp-8], rdi
5        mov     rax, QWORD PTR [rbp-8]
6        mov     rdi, rax
8        call    Object::Object() [base object constructor]
9        mov     edx, OFFSET FLAT:vtable for A+16
10       mov     rax, QWORD PTR [rbp-8]
11       mov     QWORD PTR [rax], rdx
12       nop
13       leave
14       ret
```

こうしてみると、普通の関数と同じですね。push rbp, mov rbp, rspとか、最後にleaveしてretするところとか普通の関数と同じことしてます。派生クラスAのコンストラクタで気になるところは、9行目~11行目でしょう。ここで仮想テーブルへのポインタの数値を設定されていることが予想されます。

9行目では、

```
mov     edx, OFFSET FLAT:vtable for A+16
```
ラベルvtable for Aに16バイト加算した状態(つまりA::A()の番地の番地)を行なって、edxにそれを格納しています。16バイト加算された状態で、前章の行番号を用意したコード内の14行目のadd rax, 16によって、計32バイト加算されることになったわけですね。

10行目では、

```
mov     rax, QWORD PTR [rbp-8]
```
派生クラスAのオブジェクトの番地を格納しています。

11行目では、

```
mov     QWORD PTR [rax], rdx
```
9行目で、rdxはA::A()の番地の番地が格納されている状態です。仮想テーブルからのオフセット16バイト加算された状態での番地がQWORD PTR [rax]に格納されます。ちなみに、8行目の

```
call    Object::Object() [base object constructor]
```
では、Object::Object()が呼び出されています。この先では、派生クラスAのデータメンバである仮想テーブルへのポインタにラベルvtable for Objectが格納されるわけですが、<strong>基底クラスObjectのコンストラクタ呼び出し後</strong>に、ラベルvtable for Aがデータメンバである仮想テーブルへのポインタに上書きされることになります。なので、派生クラスAのShow関数がしっかりと呼び出されているわけです。

グダグダ上に書きましたが、virtualを付けた場合は関数の番地を並べた仮想テーブルに書かれた関数が実行されるようになるわけです。その仮想テーブルへのポインタはコンストラクタによって初期化されるのです。まずは基底クラスで基底クラスに関する仮想テーブルへのポインタが初期化されますが、その後の派生クラスのコンストラクタ内で派生クラスに関する仮想テーブルへのポインタが上書きされます。上書きされるから、派生クラスAのShow関数が呼び出されるようになっているわけです。

<h2>終わりに</h2>
仮想関数を用意すると、オブジェクトのサイズが大きくなったり、呼び出しオーバーヘッドがかかる理由は、アセンブラによって吐かれたコードを観察すれば分かるという話でした