Microsoft MVP성태의 닷넷 이야기
VC++: 113. C++ 클래스 상속 관계의 vtable 생성 과정 [링크 복사], [링크+제목 복사]
조회: 16702
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
 
(연관된 글이 2개 있습니다.)
(시리즈 글이 3개 있습니다.)
VC++: 112. C++의 가상 함수 테이블 (vtable)은 언제 생성될까요?
; https://www.sysnet.pe.kr/2/0/11167

VC++: 113. C++ 클래스 상속 관계의 vtable 생성 과정
; https://www.sysnet.pe.kr/2/0/11168

VC++: 114. C++ vtable의 가상 함수 호출 가로채기
; https://www.sysnet.pe.kr/2/0/11169




C++ 클래스 상속 관계의 vtable 생성 과정

지난 글에서 클래스 상속 관계의 메모리 구조와,

C++ 클래스의 상속에 따른 메모리 구조
; https://www.sysnet.pe.kr/2/0/11164

vtable이 언제 어느 곳에 생성되는지에 대해 이야기를 했었는데요.

C++의 가상 함수 테이블 (vtable)은 언제 생성될까요?
; https://www.sysnet.pe.kr/2/0/11167

기왕 살펴본 김에, vtable의 내용까지 마저 파헤쳐 보도록 하겠습니다. 실습 예제는 아래의 소스 코드로 시작합니다.

#include "stdafx.h"

class A
{
public:
    int n1 = 1;

    void func1() { n1++; }

    virtual void vfunc1() { }
};

class BonA : A
{
public:
    int n2 = 2;

    void func2() { n2++; }

    virtual void vfunc2() { }
};

class ConBonA : BonA
{
public:
    int n3 = 2;

    void func3() { n3++; }

    virtual void vfunc3() { }
};

int main()
{
    ConBonA *table1 = new ConBonA();

    return 0;
}

new ConBonA 코드로 메모리가 할당되고 그 위치를 ecx 포인터에 this 값으로 전달하면서 생성자를 호출합니다.

    38:     ConBonA *table1 = new ConBonA();
00CE1A29 8B 8D 20 FF FF FF    mov         ecx,dword ptr [table1]  
00CE1A40 E8 CE F6 FF FF       call        ConBonA::ConBonA (0CE1113h)  

ConBonA의 생성자를 가보면, 자신의 클래스에서 해야 하는 일보다 먼저 기반 클래스의 생성자를 호출합니다.

ConBonA::ConBonA:
00CE18E6 E8 12 FA FF FF       call        BonA::BonA (0CE12FDh)  
...[생략: 초기화 코드]...
00CE1914 C3                   ret  

다시 BonA 생성자로 가면 마찬가지로 자신의 클래스를 위한 초기화 코드보다는 기반 클래스의 생성자를 호출합니다.

BonA::BonA:
00CE1876 E8 28 FA FF FF       call        A::A (0CE12A3h)  
...[생략: 초기화 코드]...
00CE18A4 C3                   ret  

가장 상단에 위치한 기반 클래스 A는 메모리가 할당된 위치에 "class A"를 컴파일하면서 구성해놓았던 "vtable"의 주소를 new로 할당된 메모리의 첫 번째 4바이트에 씁니다.

A::A:
00CE1823 8B 45 F8             mov         eax,dword ptr [this]  
00CE1826 C7 00 34 7B CE 00    mov         dword ptr [eax],offset A::`vftable' (0CE7B34h)  
...[생략: n1 = 1과 같은 생성자 함수의 초기화 코드]...
00CE183F C3                   ret  

A::A() 생성자가 반환되면 이후 BonA::BonA() 생성자 이후의 코드가 실행되는데,

BonA::BonA:
00CE1873 8B 4D F8             mov         ecx,dword ptr [this]  
00CE1876 E8 28 FA FF FF       call        A::A (0CE12A3h)  
00CE187B 8B 45 F8             mov         eax,dword ptr [this]  
00CE187E C7 00 40 7B CE 00    mov         dword ptr [eax],offset BonA::`vftable' (0CE7B40h)  
...[생략: n2 = 2와 같은 생성자 함수의 초기화 코드]
00CE18A4 C3                   ret  

보는 바와 같이, 기반 클래스에서 썼던 vtable 주소를 다시 자신의 vtable 위치로 덮어쓰고 있습니다. 이후 다시 ConBonA::ConBonA 생성자의 남은 코드로 넘어가면,

ConBonA::ConBonA:
00CE18E6 E8 12 FA FF FF       call        BonA::BonA (0CE12FDh)  
00CE18EB 8B 45 F8             mov         eax,dword ptr [this]  
00CE18EE C7 00 50 7B CE 00    mov         dword ptr [eax],offset ConBonA::`vftable' (0CE7B50h)  
...[생략: n3 = 3과 같은 생성자 함수의 초기화 코드]
00CE1914 C3                   ret  

결국, 마지막 클래스의 생성자 실행이 완료된 시점에는 vtable을 가리키는 [this + 0] 항목의 값이 ConBonA의 vtable 주소로 바뀌게 됩니다.




그럼, 컴파일러가 생성한 vtable의 구조를 한번 들여다볼까요?

우선, class A의 정의를 통해 컴파일러는 다음과 같은 vtable 주소를 하나 마련할 수 있습니다.

[오프셋 0: A::vfunc1 가상 함수의 주소]

그다음, class BonA를 컴파일하면서 이것이 A 클래스를 상속받고 있음을 알기 때문에 BonA의 vtable에는 A 클래스의 가상 함수 목록이 병합됩니다.

[오프셋 4: BonA::vfunc2 가상 함수의 주소]
[오프셋 0: A::vfunc1 가상 함수의 주소]

마찬가지로 class ConBonA의 vtable 구조는 다음과 같이 구성되는 것을 예상할 수 있습니다.

[오프셋 8: ConBonA::vfunc3 가상 함수의 주소]
[오프셋 4: BonA::vfunc2 가상 함수의 주소]
[오프셋 0: A::vfunc1 가상 함수의 주소]

이쯤에서 약간 구조를 바꿔보겠습니다. 가령, 상속받은 클래스에서 기반 클래스의 virtual 함수를 재정의했다면 어떻게 될까요?

class ConBonA : BonA
{
public:
    int n3 = 2;

    void func3() { n3++; }

    virtual void vfunc2() { n3++; } // BonA::vfunc2의 가상 함수를 재정의
};

당연히 C++ 컴파일러 입장에서는 이 사실을 인지하게 되고, vtable을 병합하는 과정에서 이 주소를 반영해,

[오프셋 4: ConBonA::vfunc2 가상 함수의 주소]
[오프셋 0: A::vfunc1 가상 함수의 주소]

ConBonA의 vtable을 위와 같이 생성해 줍니다. 컴파일러의 이런 수고로움 덕분에 다음과 같이 OOP의 전형적인 polymorphism이 가능하게 된 것입니다.

ConBonA *table1 = new ConBonA();
BonA *bInst = (BonA *)table1;
bInst->vfunc2();

위에서 table1 포인터는 ConBonA의 생성자가 실행된 이후 다음과 같은 내용의 주소를 가리키게 되고,

ConBonA table1 객체 (16바이트)

    [오프셋12: 필드 ConBonA::n3]
    [오프셋 8: 필드 BonA::n2]  
    [오프셋 4: 필드 A::n1]     
    [오프셋 0: ConBonA::vtable 포인터]

예전 글의 내용에 따라, "BonA *bInst = (BonA *)table1;"처럼 형 변환을 해도 bInst 포인터는 여전히 [오프셋 0] 위치의 값을 가리키는 포인터이므로, ConBonA::vtable의 내용으로는 vfunc2 가상 함수의 주소가 ConBonA 클래스에 정의된 것을 가리키기 때문에 다형성이 C++ 언어에서 구현되는 것입니다.




그렇다면, 하위 클래스에서 새로운 가상 함수를 정의하지도 않고 기반 클래스의 가상 함수를 재정의하지도 않았다면 어떻게 될까요?

class ConBonA : BonA
{
public:
    int n3 = 2;

    void func3() { n3++; }
};

얼핏 생각해 보면, 굳이 ConBonA 클래스가 BonA 클래스와 동일한 vtable을 가질 필요는 없어 보입니다. 또는 BonA의 것을 그대로 재사용하는 것도 가능할 텐데요.

Visual C++의 경우, 이런 상황에서도 무조건 ConBonA의 vtable을 전용으로 생성하고 (물론 BonA의 vtable과 내용은 같습니다.) 심지어 ConBonA 생성자에서 vtable을 초기화하는 코드도 여전히 넣어두도록 처리합니다. (사실, 이런 부분은 구현하는 측에서 임의 재량으로 해결할 문제입니다.)




마지막으로 추상 함수(abstract function)에 대한 처리를 잠깐 짚어보겠습니다.

기반 클래스에서 추상 함수를 가진 클래스를 정의하면,

class AbstractA
{
public:
    int n1 = 1;

    virtual void vfunc1() = 0;
};

class BonAbstractA
{
public:
    int n2 = 2;

    virtual void vfunc1() { };
};

Visual C++ 컴파일러는 AbstractA의 vtable을 다음과 같이 구성합니다.

01331876  mov         dword ptr [eax],offset AbstractA::`vftable' (01337C90h)  

0x01337C90  0b 14 33 01  // 0x0133140b

BonAbstractA 클래스 역시 1개의 항목을 가진 vtable을 구성하는데 당연히 첫 번째 항목의 주소가 BonAbstractA::vfunc1의 시작 코드로 바뀝니다.

0133192E  mov         dword ptr [eax],offset BonAbstractA::`vftable' (01337C80h)  

0x01337C80  06 14 33 01  // 0x01331406 == BonAbstractA::vfunc1

그런데, 재미있는 것은 AbstractA의 vtable에 있는 첫 번째 항목의 주소 값입니다. 이에 대해서는 다음의 글에 자세하게 나오는데요.

__purecall이 무엇일까?
; http://www.jiniya.net/tt/597

실제로 Visual C++로 컴파일된 경우 abstract 함수의 주소로 채워진 주소(위의 예에서는 0x0133140b)를 가보면, (Visual C++ 런타임이 만들어 넣어준) __purecall 함수임을 확인할 수 있습니다.

__purecall:
01331840  jmp         dword ptr [__imp___purecall (0133B0BCh)]  

이 정도면 대충 vtable 정리는 끝난 것 같습니다. 다음 글에서는, 가상 함수 호출을 가로채는 이야기를 해보겠습니다. ^^





[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 4/28/2017]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




1  2  3  4  5  [6]  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13473정성태12/5/20232307닷넷: 2179. C# - 값 형식(Blittable)을 메모리 복사를 이용해 바이트 배열로 직렬화/역직렬화파일 다운로드1
13472정성태12/4/20232134C/C++: 164. Visual C++ - InterlockedCompareExchange128 사용 방법
13471정성태12/4/20232163Copilot - To enable GitHub Copilot, authorize this extension using GitHub's device flow
13470정성태12/2/20232475닷넷: 2178. C# - .NET 8부터 COM Interop에 대한 자동 소스 코드 생성 도입파일 다운로드1
13469정성태12/1/20232186닷넷: 2177. C# - (Interop DLL 없이) CoClass를 이용한 COM 개체 생성 방법파일 다운로드1
13468정성태12/1/20232172닷넷: 2176. C# - .NET Core/5+부터 달라진 RCW(Runtime Callable Wrapper) 대응 방식파일 다운로드1
13467정성태11/30/20232166오류 유형: 882. C# - Unhandled exception. System.Runtime.InteropServices.COMException (0x800080A5)파일 다운로드1
13466정성태11/29/20232376닷넷: 2175. C# - DllImport 메서드의 AOT 지원을 위한 LibraryImport 옵션
13465정성태11/28/20232108개발 환경 구성: 689. MSBuild - CopyToOutputDirectory가 "dotnet publish" 시에는 적용되지 않는 문제파일 다운로드1
13464정성태11/28/20232212닷넷: 2174. C# - .NET 7부터 UnmanagedCallersOnly 함수 export 기능을 AOT 빌드에 통합파일 다운로드1
13463정성태11/27/20232115오류 유형: 881. Visual Studio - NU1605: Warning As Error: Detected package downgrade
13462정성태11/27/20232188오류 유형: 880. Visual Studio - error CS0246: The type or namespace name '...' could not be found
13461정성태11/26/20232246닷넷: 2173. .NET Core 3/5+ 기반의 COM Server를 registry 등록 없이 사용하는 방법파일 다운로드1
13460정성태11/26/20232208닷넷: 2172. .NET 6+ 기반의 COM Server 내에 Type Library를 내장하는 방법파일 다운로드1
13459정성태11/26/20232192닷넷: 2171. .NET Core 3/5+ 기반의 COM Server를 기존의 regasm처럼 등록하는 방법파일 다운로드1
13458정성태11/26/20232197닷넷: 2170. .NET Core/5+ 기반의 COM Server를 tlb 파일을 생성하는 방법(tlbexp)
13457정성태11/25/20232149VS.NET IDE: 187. Visual Studio - 16.9 버전부터 추가된 "Display inline type hints" 옵션
13456정성태11/25/20232442닷넷: 2169. C# - OpenAI를 사용해 PDF 데이터를 대상으로 OpenAI 챗봇 작성 [1]파일 다운로드1
13455정성태11/25/20232329닷넷: 2168. C# - Azure.AI.OpenAI 패키지로 OpenAI 사용파일 다운로드1
13454정성태11/23/20232675닷넷: 2167. C# - Qdrant Vector DB를 이용한 Embedding 벡터 값 보관/조회 (Azure OpenAI) [1]파일 다운로드1
13453정성태11/23/20232211오류 유형: 879. docker desktop 설치 시 "Invalid JSON string. (Exception from HRESULT: 0x83750007)"
13452정성태11/22/20232294닷넷: 2166. C# - Azure OpenAI API를 이용해 사용자가 제공하는 정보를 대상으로 검색하는 방법파일 다운로드1
13451정성태11/21/20232425닷넷: 2165. C# - Azure OpenAI API를 이용해 ChatGPT처럼 동작하는 콘솔 응용 프로그램 제작파일 다운로드1
13450정성태11/21/20232251닷넷: 2164. C# - Octokit을 이용한 GitHub Issue 검색파일 다운로드1
13449정성태11/21/20232347개발 환경 구성: 688. Azure OpenAI 서비스 신청 방법
13448정성태11/20/20232620닷넷: 2163. .NET 8 - Dynamic PGO를 결합한 성능 향상파일 다운로드1
1  2  3  4  5  [6]  7  8  9  10  11  12  13  14  15  ...