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

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)
11906정성태5/21/2019601.NET Framework: 835. .NET Core/C# - 리눅스 syslog에 로그 남기는 방법
11905정성태5/21/2019753.NET Framework: 834. C# - 폴더 경로 문자열에서 "..", "." 표기를 고려한 최종 문자열을 얻는 방법 - 두 번째 이야기
11904정성태5/21/20191371.NET Framework: 833. C# - Open Hardware Monitor를 이용한 CPU 온도 정보파일 다운로드1
11903정성태5/21/2019889오류 유형: 540. .NET Core - System.PlatformNotSupportedException: The named version of this synchronization primitive is not supported on this platform.
11902정성태5/21/2019581오류 유형: 539. mstest 실행 시 "The directory name is invalid." 오류 발생
11901정성태5/21/2019975오류 유형: 538. msbuild 오류 - Could not find a part of the path '%LOCALAPPDATA%\Temp\2\.NETFramework,Version=v4.0.AssemblyAttributes.cs'
11900정성태5/18/2019895오류 유형: 537. "sfc /scannow" 실행 중 시스템이 부팅되는 현상
11899정성태5/17/20191096Linux: 9. Linux에서 윈도우의 OutputDebugString 대신 사용할 수 있는 syslog [1]
11898정성태5/20/2019999VC++: 130. C++ string의 c_str과 data 함수의 차이점 [3]
11897정성태5/16/20191746오류 유형: 536. Visual Studio - "Developer Pack"을 설치했는데도 "대상 프레임워크" 목록에 나오지 않는 경우 [1]
11896정성태5/15/20191221개발 환경 구성: 440. C#, C++ - double의 Infinity, NaN 표현 방식파일 다운로드1
11895정성태5/12/20191065.NET Framework: 832. ML.NET Model Builder - 회귀(Regression), 다중 분류(Multi-class classification) 예제파일 다운로드1
11894정성태5/12/20191421VS.NET IDE: 135. Visual Studio - ML.NET Model Builder 소개 [1]
11893정성태5/10/20191053오류 유형: 535. C# 6.0 이상의 문법을 컴파일 시 오류가 발생한다면?
11892정성태5/10/2019783웹: 38. HTTP Cookie의 expires 시간 형식(RFC7231)
11891정성태5/9/2019985.NET Framework: 831. (번역글) .NET Internals Cookbook Part 12 - Memory structure, attributes, handles
11890정성태5/8/2019779개발 환경 구성: 439. "Visual Studio Enterprise is required to execute the test." 메시지와 관련된 코드 기록
11889정성태5/8/2019830개발 환경 구성: 438. mstest, QTAgent의 로그 파일 설정 방법
11888정성태5/8/20191207.NET Framework: 830. C# - 비동기 호출을 취소하는 CancellationToken의 간단한 예제 코드파일 다운로드1
11887정성태5/8/20191104.NET Framework: 829. C# - yield 문을 사용할 수 있는 메서드의 조건
11886정성태5/8/20191112오류 유형: 534. mstest.exe 실행 시 "Visual Studio Enterprise is required to execute the test." 오류 [2]
11885정성태5/7/2019656오류 유형: 533. mstest.exe 실행 시 "File extension specified '.loadtest' is not a valid test extension." 오류 발생
11884정성태5/5/20191194.NET Framework: 828. C# DLL에서 Win32 C/C++처럼 dllexport 함수를 제공하는 방법 - 두 번째 이야기
11883정성태5/3/20191315.NET Framework: 827. C# - 인터넷 시간 서버로부터 받은 시간을 윈도우에 적용하는 방법파일 다운로드1
11882정성태5/9/2019844.NET Framework: 826. (번역글) .NET Internals Cookbook Part 11 - Various C# riddles파일 다운로드1
11881정성태4/28/20191185오류 유형: 532. .NET Core 프로젝트로 마이그레이션 시 "CS0579 Duplicate 'System.Reflection.AssemblyCompanyAttribute' attribute" 오류 발생
1  2  3  4  5  6  7  8  9  [10]  11  12  13  14  15  ...