vtable 이란? (GPT4’s answer)
C++에서의 vtable (가상 함수 테이블)은 객체 지향 프로그래밍에서 다형성을 지원하기 위해 사용되는 메커니즘 중 하나입니다. 클래스에 가상 함수가 하나 이상 포함되어 있을 때, 컴파일러는 각 클래스 타입에 대해 vtable을 생성합니다. 이 테이블은 해당 클래스의 모든 가상 함수에 대한 주소를 저장합니다.
vtable의 역할과 작동 방식
•
가상 함수의 주소 저장: vtable은 해당 클래스의 모든 가상 함수에 대한 포인터(함수 주소)를 저장합니다. 이를 통해 런타임에 어떤 함수를 호출할지 결정할 수 있습니다.
•
다형성 지원: C++에서 다형성은 하위 클래스에서 상위 클래스의 메서드를 오버라이딩할 수 있게 해줍니다. vtable을 사용하면 객체의 실제 타입에 따라 적절한 오버라이딩된 메서드가 호출됩니다.
•
객체당 vptr (vtable 포인터): 각 객체에는 해당 객체의 클래스 타입에 대한 vtable을 가리키는 포인터(vptr)가 있습니다. 객체가 생성될 때 vptr은 객체의 실제 클래스 타입에 대한 vtable을 가리키도록 설정됩니다.
혹시 vtable에 대해서 처음 접하는 사람이라면, GPT의 답변이 조금 혼란스러울 수 있는데, 아래 예제와 이를 분해해 보면서 이게 어떤 의미를 가지는지 알아보겠습니다.
vtable의 구조 파악
구조를 예제를 통해서 파악하기 전에, 글로 구조를 조금 설명해보겠습니다.
vtable pointer는 동적 바인딩을 위해 사용되는 포인터로 vitrual 함수를 멤버로 가진 클래스가 생성되는 객체의 처음 주소에 생성됩니다. (정적 바인딩의 경우 생성되지 않습니다.)
vtable pointer를 통해, 런타임에 호출된 동적 바인딩된 객체의 멤버함수들은 해당 vtable pointer를 역참조하여 적절한 vtable에 저장된 함수를 찾아서 실행하게 됩니다. 적절한 함수를 찾는 것은 컴파일 단계에서 vtable에서의 함수 위치를 index로 미리 담는 것으로 알 수 있습니다.
이때 주의할 점은, vtable은 컴파일 타임에 결정된다는 것 입니다. 런타임에 실행되는 것은 vtable pointer에 어떤 vtable을 담느냐 입니다.
저장되는 메모리 섹션은 각각 vtable pointer는 객체를 할당한 섹션(힙, 스택)에 저장되고, vtable은 정적 데이터 영역, vtable이 가리키는 각각의 함수는 텍스트 영역에 저장됩니다.
이제 예제를 통해서 위에서 설명한 내용을 직접 확인해보겠습니다.
#include <iostream>
class Base {
public:
virtual void func() { std::cout << "Base\n"; }
};
class Derived : public Base {
public:
void func() override { std::cout << "Derived\n"; }
};
class John {
public:
void func() { std::cout << "John\n"; }
};
int main() {
Base* b = new Derived();
John* c = new John();
b->func();
c->func();
delete b;
delete c;
return 0;
}
C++
복사
vtable 구조 예제
먼저 동적, 정적 바인딩된 객체의 처음 주소를 확인해 봄으로써 vtable을 확인해보겠습니다.
b, c의 주소를 lldb로 직접 주소의 정보를 확인해보겠습니다.
각각 예제 코드에서 b, c의 주소가 그림과 같을 때, Derived클래스의 첫 주소를 lldb로 확인해봄으로써 vtable을 직접 메모리에서 확인해보겠습니다. lldb에 익숙하지 않으신 분은 해당 포스팅을 참고해주세요. [lldb 기본 사용방법]
이제 한 줄씩, 코드를 분석해 보겠습니다.
p b
b에 담긴 값인 Derived의 주소를 확인했습니다. 예제에서 b는 0x0000600001430000의 주소를 가집니다.
x/1wg 0x0000600001430000
이전에 얻은 주소값의 첫 8byte를 확인합니다. 예제에서는 0x0000000100c180c8의 주소값을 얻을 수 있었습니다.
dis -s 0x0000000100c180c8 -e 0x0000000100c180d0
이제 위에서 얻은 주소를 가지고 disassemble 명령을 실행해보면 vtable을 직접 확인해볼 수 있습니다. 이때 범위의 제한은 vtable의 값만 확인하고자 넣어줬습니다.
x/1wg 0x0000000100c180c8
이제 vtable이 있음을 확인했으니 해당 테이블의 첫 주소에 접근해서 함수의 목록을 직접 확인했습니다. 예제에서는 하나의 함수만을 넣어줬으므로 8byte를 확인하여 func의 주소만 확인하고자 했습니다. 예제에서는 0x0000000100c17144의 값을 얻었습니다.
dis -s 0x0000000100c17144
이제 마지막으로 해당 주소를 disassemble 해보면 해당 함수의 어셈블리 언어를 직접 확인해 볼 수 있습니다.
일련의 과정을 통해서 간단하게 vtable을 직접 확인해 볼 수 있습니다.
하지만 정적 바인딩된 c의 경우는 vtable이 없음을 확인해볼 수 있습니다.
글이 길어질듯 하여, 마무리하고 다음 포스팅에서는 위에서 설명한 부분의 나머지 부분을 알아보겠습니다.