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

비밀번호

댓글 작성자
 




... 16  17  18  19  20  [21]  22  23  24  25  26  27  28  29  30  ...
NoWriterDateCnt.TitleFile(s)
13107정성태7/25/20226658Linux: 52. Debian/Ubuntu 계열의 docker container에서 자주 설치하게 되는 명령어
13106정성태7/24/20226367오류 유형: 819. 닷넷 6 프로젝트의 "Conditional compilation symbols" 기본값 오류
13105정성태7/23/20227661.NET Framework: 2034. .NET Core/5+ 환경에서 (프로젝트가 아닌) C# 코드 파일을 입력으로 컴파일하는 방법 - 두 번째 이야기 [1]
13104정성태7/23/202210738Linux: 51. WSL - init에서 systemd로 전환하는 방법
13103정성태7/22/20227284오류 유형: 818. WSL - systemd-genie와 관련한 2가지(systemd-remount-fs.service, multipathd.socket) 에러
13102정성태7/19/20226684.NET Framework: 2033. .NET Core/5+에서는 구할 수 없는 HttpRuntime.AppDomainAppId
13101정성태7/15/202215566도서: 시작하세요! C# 10 프로그래밍
13100정성태7/15/20228030.NET Framework: 2032. C# 11 - shift 연산자 재정의에 대한 제약 완화 (Relaxing Shift Operator)
13099정성태7/14/20227905.NET Framework: 2031. C# 11 - 사용자 정의 checked 연산자파일 다운로드1
13098정성태7/13/20226170개발 환경 구성: 647. Azure - scale-out 상태의 App Service에서 특정 인스턴스에 요청을 보내는 방법 [1]
13097정성태7/12/20225553오류 유형: 817. Golang - binary.Read: invalid type int32
13096정성태7/8/20228383.NET Framework: 2030. C# 11 - UTF-8 문자열 리터럴
13095정성태7/7/20226447Windows: 208. AD 도메인에 참여하지 않은 컴퓨터에서 Kerberos 인증을 사용하는 방법
13094정성태7/6/20226187오류 유형: 816. Golang - "short write" 오류 원인
13093정성태7/5/20227108.NET Framework: 2029. C# - HttpWebRequest로 localhost 접속 시 2초 이상 지연
13092정성태7/3/20228100.NET Framework: 2028. C# - HttpWebRequest의 POST 동작 방식파일 다운로드1
13091정성태7/3/20226880.NET Framework: 2027. C# - IPv4, IPv6를 모두 지원하는 서버 소켓 생성 방법
13090정성태6/29/20226026오류 유형: 815. PyPI에 업로드한 패키지가 반영이 안 되는 경우
13089정성태6/28/20226490개발 환경 구성: 646. HOSTS 파일 변경 시 Edge 브라우저에 반영하는 방법
13088정성태6/27/20225579개발 환경 구성: 645. "Developer Command Prompt for VS 2022" 명령행 환경의 폰트를 바꾸는 방법
13087정성태6/23/20228584스크립트: 41. 파이썬 - FastAPI / uvicorn 호스팅 환경에서 asyncio 사용하는 방법 [1]
13086정성태6/22/20228007.NET Framework: 2026. C# 11 - 문자열 보간 개선 2가지파일 다운로드1
13085정성태6/22/20228098.NET Framework: 2025. C# 11 - 원시 문자열 리터럴(raw string literals)파일 다운로드1
13084정성태6/21/20226669개발 환경 구성: 644. Windows - 파이썬 2.7을 msi 설치 없이 구성하는 방법
13083정성태6/20/20227300.NET Framework: 2024. .NET 7에 도입된 GC의 메모리 해제에 대한 segment와 region의 차이점 [2]
13082정성태6/19/20226320.NET Framework: 2023. C# - Process의 I/O 사용량을 보여주는 GetProcessIoCounters Win32 API파일 다운로드1
... 16  17  18  19  20  [21]  22  23  24  25  26  27  28  29  30  ...