Microsoft MVP성태의 닷넷 이야기
VC++: 113. C++ 클래스 상속 관계의 vtable 생성 과정 [링크 복사], [링크+제목 복사]
조회: 6202
글쓴 사람
홈페이지
첨부 파일
 

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@outlook.com

비밀번호

댓글 쓴 사람
 




[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
12069정성태12/2/201994디버깅 기술: 139. windbg - x64 덤프 분석 시 메서드의 인자 또는 로컬 변수의 값을 확인하는 방법
12068정성태11/28/2019132디버깅 기술: 138. windbg와 Win32 API로 알아보는 Windows Heap 정보 분석 [1]파일 다운로드2
12067정성태11/27/201992디버깅 기술: 137. 실제 사례를 통해 Debug Diagnostics 도구가 생성한 닷넷 웹 응용 프로그램의 성능 장애 보고서 설명파일 다운로드1
12066정성태11/27/201946디버깅 기술: 136. windbg - C# PInvoke 호출 시 마샬링을 담당하는 함수 분석 - OracleCommand.ExecuteReader에서 OpsSql.Prepare2 PInvoke 호출 분석
12065정성태11/25/201970디버깅 기술: 135. windbg - C# PInvoke 호출 시 마샬링을 담당하는 함수 분석파일 다운로드1
12064정성태11/25/201939오류 유형: 580. HTTP Error 500.0/500.33 - ANCM In-Process Handler Load Failure
12063정성태11/21/201978디버깅 기술: 134. windbg - RtlReportCriticalFailure로부터 parameters 정보 찾는 방법
12062정성태11/21/201974디버깅 기술: 133. windbg - CoTaskMemFree/FreeCoTaskMem에서 발생한 덤프 분석 사례 - 두 번째 이야기
12061정성태11/20/201971Windows: 165. CoTaskMemAlloc/CoTaskMemFree과 윈도우 Heap의 관계
12060정성태11/21/2019100디버깅 기술: 132. windbg/Visual Studio - HeapFree x64의 동작 분석
12059정성태11/20/2019101디버깅 기술: 131. windbg/Visual Studio - HeapFree x86의 동작 분석
12058정성태11/19/2019100디버깅 기술: 130. windbg - CoTaskMemFree/FreeCoTaskMem에서 발생한 덤프 분석 사례
12057정성태11/18/201955오류 유형: 579. Visual Studio - Memory 창에서 유효한 주소 영역임에도 "Unable to evaluate the expression." 오류 출력
12056정성태11/18/2019111개발 환경 구성: 464. "Microsoft Visual Studio Installer Projects" 프로젝트로 EXE 서명 및 MSI 파일 서명 방법파일 다운로드1
12055정성태11/17/201981개발 환경 구성: 463. Visual Studio의 Ctrl + Alt + M, 1 (Memory 1) 등의 단축키가 동작하지 않는 경우
12054정성태11/15/2019228.NET Framework: 869. C# - 일부러 GC Heap을 깨뜨려 GC 수행 시 비정상 종료시키는 예제
12053정성태11/15/201971Windows: 164. 윈도우 10 - 명령행 창(cmd.exe) 속성에 (DotumChe, GulimChe, GungsuhChe 등의) 한글 폰트가 없는 경우
12052정성태11/15/201957오류 유형: 578. Azure - 일정(schedule)에 등록한 runbook이 1년 후 실행이 안 되는 문제(Reason - The key used is expired.)
12051정성태12/2/2019117개발 환경 구성: 462. 시작하자마자 비정상 종료하는 프로세스의 메모리 덤프 - procdump
12050정성태11/14/201991Windows: 163. AcLayers의 API 후킹과 FaultTolerantHeap
12049정성태11/13/201974.NET Framework: 868. (닷넷 프로세스를 대상으로) 디버거 방식이 아닌 CLR Profiler를 이용해 procdump.exe 기능 구현
12048정성태11/12/201996Windows: 163. GUID 이름의 볼륨에 해당하는 파티션을 찾는 방법
12047정성태11/12/2019224Windows: 163. 안전하게 eject시킨 USB 장치를 물리적인 재연결 없이 다시 인식시키는 방법
12046정성태11/9/2019153오류 유형: 577. windbg - The call to LoadLibrary(...\sos.dll) failed, Win32 error 0n193
12045정성태10/27/2019130오류 유형: 576. mstest.exe 실행 시 "Visual Studio Enterprise is required to execute the test." 오류 - 두 번째 이야기
[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...