C 언어
Cpp 언어
Kotlin
Android App
알고리즘
Git/CI/CD
GDB로 분석하는 Virtual 함수의 구성 원리
C++에서는 함수의 다형성을 위해 Virtual 함수를 도입하였다.
함수의 다형성이란, "동일한 명령에 대해서, 서로 다른 객체가 서로 다르게 수행한다" 로 정리할 수 있으며,
상속 관계에 있는 클래스에서 사용된다.

Virtual 함수가 어떠한 것인지는 알고 있으리라 생각하고,
여기서는 C++ 컴파일러가 Virtual 함수를 어떻게 구현해 내는지를 간단한 코드와 GDB를 통해 알아보도록 한다.
#include <iostream>
#include <stdio.h>

using namespace std;

class base {
    private :
        int data;
    public :
        virtual void func() { }
};

class child : public base {
    private :
        int childdata;
    public :
        void func() { }
};


int main() {
    child c;
    base b,*p;

    p = &b;
    p = &c;

    return 0;
}

위의 코드에는 base 클래스와 child 클래스가 있으며, base가 child의 부모가 되는 상속관계이다.
두 클래스는 func()라는 함수를 가지고 있으며, base에서 virtual을 선언하였으므로 이는 가상함수가 된다.
main 함수 내부를 살펴보면, base 클래스 포인터 p는 당연히 같은 자료형인 객체 b를 가리킬 수 있으며,
자신의 자식 클래스 객체인 c도 함께 가리킬 수 있다.

그런데, 이 두 가지 각각의 경우에, func()함수를 호출하게 되면 어떤 일이 발생할 것인가?
func()함수는 base클래스에서 가상함수로 선언하였으므로,
객체 b를 가리킬 때는 base클래스 내의 func() 를,
자식 클래스 객체인 c를 가리킬 때에는 child클래스 내의 func()를 호출하도록 되어 있다.

하지만, 실제로 이러한 동작이 어떻게 이루어지는지가 중요하다.
이를 위해 gdb 툴을 통해 위의 코드를 분석하여 virtual 함수를 분석해 보도록 하자.

[Step 1]
-g 옵션을 걸어 디버깅 가능하도록 컴파일 후 gdb로 실행파일을 연다.
# g++ -g virtual.cpp
# gdb a.out

GNU gdb (Ubuntu/Linaro 7.4-2012.04-0ubuntu2.1) 7.4-2012.04
Copyright (C) 2012 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later /gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
For bug reporting instructions, please see:
/bugs.launchpad.net/gdb-linaro/>...
Reading symbols from /root/a.out...done.


[Step 2]
main함수에 break를 걸어 놓고 실행을 한다.
그러면 아래와 같이 main함수가 처음 시작되는 지점에서 중단된다.
(gdb) b main
Breakpoint 1 at 0x4006fc: file virtual.cpp, line 25.

(gdb) run
Starting program: ~/a.out
Breakpoint 1, main () at virtual.cpp:25
25 child c;


[Step 3]
코드의 25, 26 라인은 변수 선언문이므로 next를 입력하여 넘어간다.
(gdb) next
26 base b,*p;

(gdb) next
28 p = &b;


[Step 4]
25, 26라인을 실행하면서 객체 base b, child c가 생성되으므로,
각각의 내용을 print <변수명>을 입력하여 확인해보자.
(gdb) print b
$3 = {_vptr.base = 0x400900, data = 4195856}

(gdb) print c
$4 = { = {_vptr.base = 0x4008e0, data = 4196304}, childdata = 0}


b는 _vptr.base와 data 멤버를 가지고 있으며,
c는 _vptr.base와 data, 그리고 childdata를 추가로 가지고 있음을 확인할 수 있다.

base 클래스가 멤버로 func() 함수와 data 변수를 가지고 있고
child 클래스가 멤버로 base클래스의 멤버들과 childdata 변수를 가지고 있는 것을
고려해 보았을 때, 위에서 b와 c가 가지고 있는 _vptr.base는 func() 함수 대신 생긴 심볼이며
b는 0x400900을, c는 0x4008e0 값을 가지고 있는 것을 알 수 있다.

즉, 같은 func()라도 가지고 있는 값이 다르다는 것을 알 수 있는데, 여기서
Virtual 함수가 구성되는 원리의 첫 번째 단서를 얻을 수 있는 것이다.

[Step 5]
우선 base와 child 클래스가 서로 func()에 대해 다른 값을 가진다는 것을 알았으니,
이번에는 base 포인터로 각각을 가리켰을 때에도 func()가 다른 값을 가지는지 확인해 보도록 하자.
(gdb) next
29 p = &c;

(gdb) print *p
$8 = {_vptr.base = 0x400900, data = 4195856}

(gdb) next
31 return 0;

(gdb) print *p
$13 = {_vptr.base = 0x4008e0, data = 4196304}


p가 b를 가리킬 경우에는 _vptr.base가 0x400900을,
p가 c를 가리킬 경우에는 _vptr.base가 0x4008e0을 가진다는 것을 확인할 수 있었다.

즉, base포인터가 base와 child 클래스를 가리키는 경우, func()가 달라진다는 것을 알 수 있는 것이다. 즉, 여기서부터 함수의 다형성이 시작된다.

[Step 6]
그렇다면, _vptr.base가 가리키는 0x400900과 0x4008e0은 무슨 값을 의미하는 것일까?
궁금하므로 gdb에서 출력해보도록 한다.
(gdb) x/x 0x4008e0
0x4008e0 <_ZTV5child+16>: 0x0040078a

(gdb) x/x 0x400900
0x400900 <_ZTV4base+16>: 0x00400780


x/x 명령어는 특정 주소의 값을 hex로 읽겠다는 뜻이다.
즉, a.out 프로세스 내의 0x4008e0과 0x400900 주소의 값이 각각 0x0040078a, 0x00400780이라는 의미이다.
그렇다면 저 값은 무엇을 의미할까? 언뜻 보면 메모리 주소처럼 보인다.

[Step 7]
메모리 주소처럼 보이는 저 주소를 다시 한번 출력해 보도록 하자.
(gdb) x/x 0x40078a
0x40078a : 0xe5894855

(gdb) x/x 0x400780
0x400780 : 0xe5894855


자세히 한번 보자. 각각의 주소에 들어있는 값이 무엇을 의미하는지는 모르겠지만,
0x40078a 에는 child::func() 심볼이 매칭되어 있고,
0x400780 에는 base::func() 심볼이 매칭되어 있다!

그렇다면 이 주소들이 함수의 시작주소라는 의미이다.

[Step 8]
그러면 각각의 함수들을 disas 명령어를 통해 살펴보도록 하자.
disas <함수주소> 명령어는 해당 함수를 disassemble한다.

(gdb) disas 0x40078a

Dump of assembler code for function child::func():
0x000000000040078a <+0>: push %rbp
0x000000000040078b <+1>: mov %rsp,%rbp
0x000000000040078e <+4>: mov %rdi,-0x8(%rbp)
0x0000000000400792 <+8>: pop %rbp
0x0000000000400793 <+9>: retq
End of assembler dump.

(gdb) disas 0x400780
Dump of assembler code for function base::func():
0x0000000000400780 <+0>: push %rbp
0x0000000000400781 <+1>: mov %rsp,%rbp
0x0000000000400784 <+4>: mov %rdi,-0x8(%rbp)
0x0000000000400788 <+8>: pop %rbp
0x0000000000400789 <+9>: retq
End of assembler dump.


결국, 각각의 주소는 child 클래스의 func() 함수와 base 클래스의 func() 함수를 의미하는 것이다.


위의 gdb 분석 결과를 그림으로 그려보면 아래와 같다.


즉, Virtual 함수는 C++에서 vtable을 생성하여 각 엔트리들이 가상함수들에 대한 포인터를 가지게 하며,
객체들은 가상함수를 사용할 때 vtable의 엔트리 주소를 포함하도록 하여 사용하고 있는 것이다.