Microsoft MVP성태의 닷넷 이야기
VC++: 113. C++ 클래스 상속 관계의 vtable 생성 과정 [링크 복사], [링크+제목 복사],
조회: 17144
글쓴 사람
정성태 (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

비밀번호

댓글 작성자
 




... 61  62  [63]  64  65  66  67  68  69  70  71  72  73  74  75  ...
NoWriterDateCnt.TitleFile(s)
12060정성태11/20/201912227디버깅 기술: 132. windbg/Visual Studio - HeapFree x64의 동작 분석
12059정성태11/20/201911821디버깅 기술: 131. windbg/Visual Studio - HeapFree x86의 동작 분석
12058정성태11/19/201912625디버깅 기술: 130. windbg - CoTaskMemFree/FreeCoTaskMem에서 발생한 덤프 분석 사례
12057정성태11/18/20199756오류 유형: 579. Visual Studio - Memory 창에서 유효한 주소 영역임에도 "Unable to evaluate the expression." 오류 출력
12056정성태11/18/201913575개발 환경 구성: 464. "Microsoft Visual Studio Installer Projects" 프로젝트로 EXE 서명 및 MSI 파일 서명 방법파일 다운로드1
12055정성태11/17/20199317개발 환경 구성: 463. Visual Studio의 Ctrl + Alt + M, 1 (Memory 1) 등의 단축키가 동작하지 않는 경우
12054정성태11/15/201910640.NET Framework: 869. C# - 일부러 GC Heap을 깨뜨려 GC 수행 시 비정상 종료시키는 예제
12053정성태11/15/201912307Windows: 166. 윈도우 10 - 명령행 창(cmd.exe) 속성에 (DotumChe, GulimChe, GungsuhChe 등의) 한글 폰트가 없는 경우
12052정성태11/15/201911402오류 유형: 578. Azure - 일정(schedule)에 등록한 runbook이 1년 후 실행이 안 되는 문제(Reason - The key used is expired.)
12051정성태11/14/201913870개발 환경 구성: 462. 시작하자마자 비정상 종료하는 프로세스의 메모리 덤프 - procdump [1]
12050정성태11/14/201911561Windows: 165. AcLayers의 API 후킹과 FaultTolerantHeap
12049정성태11/13/201911651.NET Framework: 868. (닷넷 프로세스를 대상으로) 디버거 방식이 아닌 CLR Profiler를 이용해 procdump.exe 기능 구현
12048정성태11/12/201912461Windows: 164. GUID 이름의 볼륨에 해당하는 파티션을 찾는 방법
12047정성태11/12/201914301Windows: 163. 안전하게 eject시킨 USB 장치를 물리적인 재연결 없이 다시 인식시키는 방법
12046정성태10/29/201910264오류 유형: 577. windbg - The call to LoadLibrary(...\sos.dll) failed, Win32 error 0n193
12045정성태10/27/20199601오류 유형: 576. mstest.exe 실행 시 "Visual Studio Enterprise is required to execute the test." 오류 - 두 번째 이야기
12044정성태10/27/20199843오류 유형: 575. mstest.exe - System.Resources.MissingSatelliteAssemblyException: The satellite assembly named "Microsoft.VisualStudio.ProductKeyDialog.resources.dll, ..."
12043정성태10/27/201910616오류 유형: 574. Windows 10 설치 시 오류 - 0xC1900101 - 0x4001E
12042정성태10/26/201911000오류 유형: 573. OneDrive 하위에 위치한 Documents, Desktop 폴더에 대한 권한 변경 시 "Unable to display current owner"
12041정성태10/23/201911015오류 유형: 572. mstest.exe - The load test results database could not be opened.
12040정성태10/23/201911274오류 유형: 571. Unhandled Exception: System.Net.Mail.SmtpException: Transaction failed. The server response was: 5.2.0 STOREDRV.Submission.Exception:SendAsDeniedException.MapiExceptionSendAsDenied
12039정성태10/22/20199718스크립트: 16. cmd.exe의 for 문에서는 ERRORLEVEL이 설정되지 않는 문제
12038정성태10/17/20199284오류 유형: 570. SQL Server 2019 RC1 - SQL Client Connectivity SDK 설치 오류
12037정성태10/15/201914917.NET Framework: 867. C# - Encoding.Default 값을 바꿀 수 있을까요?파일 다운로드1
12036정성태10/14/201916222.NET Framework: 866. C# - 고성능이 필요한 환경에서 GC가 발생하지 않는 네이티브 힙 사용파일 다운로드1
12035정성태10/13/201912361개발 환경 구성: 461. C# 8.0의 #nulable 관련 특성을 .NET Framework 프로젝트에서 사용하는 방법 [2]파일 다운로드1
... 61  62  [63]  64  65  66  67  68  69  70  71  72  73  74  75  ...