성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] systemd for Developers I ; https:/...
[정성태] 엄밀히 object 타입의 인스턴스가 다른 타입으로 형변환 가능...
[정성태] 아래의 글에서 나오는 "Windows Application Pa...
[정성태] The history of calling conventions,...
[정성태] Secure and Deploy .NET Windows Form...
[정성태] Get Started with Milvus Vector DB i...
[정성태] cyberark/PipeViewer - A tool that...
[정성태] WinForms in a 64-Bit world – our st...
[정성태] 예제에서 SELECT_SQL도 내부적으로는 SqlCommand/...
[victor] SELECT_LINQ SELECT_SQL 같은 쿼리인...
글쓰기
제목
이름
암호
전자우편
HTML
홈페이지
유형
제니퍼 .NET
닷넷
COM 개체 관련
스크립트
VC++
VS.NET IDE
Windows
Team Foundation Server
디버깅 기술
오류 유형
개발 환경 구성
웹
기타
Linux
Java
DDK
Math
Phone
Graphics
사물인터넷
부모글 보이기/감추기
내용
<div style='display: inline'> <h1 style='font-family: Malgun Gothic, Consolas; font-size: 20pt; color: #006699; text-align: center; font-weight: bold'>C++ 클래스 상속 관계의 vtable 생성 과정</h1> <p> 지난 글에서 클래스 상속 관계의 메모리 구조와,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > C++ 클래스의 상속에 따른 메모리 구조 ; <a target='tab' href='http://www.sysnet.pe.kr/2/0/11164'>http://www.sysnet.pe.kr/2/0/11164</a> </pre> <br /> vtable이 언제 어느 곳에 생성되는지에 대해 이야기를 했었는데요.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > C++의 가상 함수 테이블 (vtable)은 언제 생성될까요? ; <a target='tab' href='http://www.sysnet.pe.kr/2/0/11167'>http://www.sysnet.pe.kr/2/0/11167</a> </pre> <br /> 기왕 살펴본 김에, vtable의 내용까지 마저 파헤쳐 보도록 하겠습니다. 실습 예제는 아래의 소스 코드로 시작합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > #include "stdafx.h" <span style='color: blue; font-weight: bold'>class A</span> { public: int n1 = 1; void func1() { n1++; } virtual void vfunc1() { } }; <span style='color: blue; font-weight: bold'>class BonA : A</span> { public: int n2 = 2; void func2() { n2++; } virtual void vfunc2() { } }; <span style='color: blue; font-weight: bold'>class ConBonA : BonA</span> { public: int n3 = 2; void func3() { n3++; } virtual void vfunc3() { } }; int main() { <span style='color: blue; font-weight: bold'>ConBonA *table1 = new ConBonA();</span> return 0; } </pre> <br /> new ConBonA 코드로 메모리가 할당되고 그 위치를 ecx 포인터에 this 값으로 전달하면서 생성자를 호출합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 38: ConBonA *table1 = new ConBonA(); 00CE1A29 8B 8D 20 FF FF FF mov ecx,dword ptr [table1] 00CE1A40 E8 CE F6 FF FF <span style='color: blue; font-weight: bold'>call ConBonA::ConBonA (0CE1113h)</span> </pre> <br /> ConBonA의 생성자를 가보면, 자신의 클래스에서 해야 하는 일보다 먼저 기반 클래스의 생성자를 호출합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > <span style='color: blue; font-weight: bold'>ConBonA::ConBonA:</span> 00CE18E6 E8 12 FA FF FF <span style='color: blue; font-weight: bold'>call BonA::BonA (0CE12FDh)</span> ...[생략: 초기화 코드]... 00CE1914 C3 ret </pre> <br /> 다시 BonA 생성자로 가면 마찬가지로 자신의 클래스를 위한 초기화 코드보다는 기반 클래스의 생성자를 호출합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > <span style='color: blue; font-weight: bold'>BonA::BonA:</span> 00CE1876 E8 28 FA FF FF <span style='color: blue; font-weight: bold'>call A::A (0CE12A3h)</span> ...[생략: 초기화 코드]... 00CE18A4 C3 ret </pre> <br /> 가장 상단에 위치한 기반 클래스 A는 메모리가 할당된 위치에 "class A"를 컴파일하면서 구성해놓았던 "vtable"의 주소를 new로 할당된 메모리의 첫 번째 4바이트에 씁니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > <span style='color: blue; font-weight: bold'>A::A:</span> 00CE1823 8B 45 F8 mov eax,dword ptr [this] <span style='color: blue; font-weight: bold'>00CE1826 C7 00 34 7B CE 00 mov dword ptr [eax],offset A::`vftable' (0CE7B34h) </span> ...[생략: n1 = 1과 같은 생성자 함수의 초기화 코드]... 00CE183F C3 ret </pre> <br /> A::A() 생성자가 반환되면 이후 BonA::BonA() 생성자 이후의 코드가 실행되는데,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > <span style='color: blue; font-weight: bold'>BonA::BonA:</span> 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] <span style='color: blue; font-weight: bold'>00CE187E C7 00 40 7B CE 00 mov dword ptr [eax],offset BonA::`vftable' (0CE7B40h) </span> ...[생략: n2 = 2와 같은 생성자 함수의 초기화 코드] 00CE18A4 C3 ret </pre> <br /> 보는 바와 같이, 기반 클래스에서 썼던 vtable 주소를 다시 자신의 vtable 위치로 덮어쓰고 있습니다. 이후 다시 ConBonA::ConBonA 생성자의 남은 코드로 넘어가면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > <span style='color: blue; font-weight: bold'>ConBonA::ConBonA:</span> 00CE18E6 E8 12 FA FF FF call BonA::BonA (0CE12FDh) 00CE18EB 8B 45 F8 mov eax,dword ptr [this] <span style='color: blue; font-weight: bold'>00CE18EE C7 00 50 7B CE 00 mov dword ptr [eax],offset ConBonA::`vftable' (0CE7B50h) </span> ...[생략: n3 = 3과 같은 생성자 함수의 초기화 코드] 00CE1914 C3 ret </pre> <br /> 결국, 마지막 클래스의 생성자 실행이 완료된 시점에는 vtable을 가리키는 [this + 0] 항목의 값이 ConBonA의 vtable 주소로 바뀌게 됩니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 그럼, 컴파일러가 생성한 vtable의 구조를 한번 들여다볼까요? <br /> <br /> 우선, class A의 정의를 통해 컴파일러는 다음과 같은 vtable 주소를 하나 마련할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > [오프셋 0: A::vfunc1 가상 함수의 주소] </pre> <br /> 그다음, class BonA를 컴파일하면서 이것이 A 클래스를 상속받고 있음을 알기 때문에 BonA의 vtable에는 A 클래스의 가상 함수 목록이 병합됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > <span style='color: blue; font-weight: bold'>[오프셋 4: BonA::vfunc2 가상 함수의 주소]</span> [오프셋 0: A::vfunc1 가상 함수의 주소] </pre> <br /> 마찬가지로 class ConBonA의 vtable 구조는 다음과 같이 구성되는 것을 예상할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > <span style='color: blue; font-weight: bold'>[오프셋 8: ConBonA::vfunc3 가상 함수의 주소]</span> [오프셋 4: BonA::vfunc2 가상 함수의 주소] [오프셋 0: A::vfunc1 가상 함수의 주소] </pre> <br /> 이쯤에서 약간 구조를 바꿔보겠습니다. 가령, 상속받은 클래스에서 기반 클래스의 virtual 함수를 재정의했다면 어떻게 될까요? <br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > class ConBonA : BonA { public: int n3 = 2; void func3() { n3++; } <span style='color: blue; font-weight: bold'>virtual void vfunc2() { n3++; } // BonA::vfunc2의 가상 함수를 재정의</span> }; </pre> <br /> 당연히 C++ 컴파일러 입장에서는 이 사실을 인지하게 되고, vtable을 병합하는 과정에서 이 주소를 반영해,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > <span style='color: blue; font-weight: bold'>[오프셋 4: ConBonA::vfunc2 가상 함수의 주소]</span> [오프셋 0: A::vfunc1 가상 함수의 주소] </pre> <br /> ConBonA의 vtable을 위와 같이 생성해 줍니다. 컴파일러의 이런 수고로움 덕분에 다음과 같이 OOP의 전형적인 polymorphism이 가능하게 된 것입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > ConBonA *table1 = new ConBonA(); BonA *bInst = (BonA *)table1; bInst->vfunc2(); </pre> <br /> 위에서 table1 포인터는 ConBonA의 생성자가 실행된 이후 다음과 같은 내용의 주소를 가리키게 되고,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > ConBonA table1 객체 (16바이트) [오프셋12: 필드 ConBonA::n3] [오프셋 8: 필드 BonA::n2] [오프셋 4: 필드 A::n1] [오프셋 0: ConBonA::vtable 포인터] </pre> <br /> <a target='tab' href='http://www.sysnet.pe.kr/2/0/11164'>예전 글의 내용</a>에 따라, "BonA *bInst = (BonA *)table1;"처럼 형 변환을 해도 bInst 포인터는 여전히 [오프셋 0] 위치의 값을 가리키는 포인터이므로, ConBonA::vtable의 내용으로는 vfunc2 가상 함수의 주소가 ConBonA 클래스에 정의된 것을 가리키기 때문에 다형성이 C++ 언어에서 구현되는 것입니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 그렇다면, 하위 클래스에서 새로운 가상 함수를 정의하지도 않고 기반 클래스의 가상 함수를 재정의하지도 않았다면 어떻게 될까요? <br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > class ConBonA : BonA { public: int n3 = 2; void func3() { n3++; } }; </pre> <br /> 얼핏 생각해 보면, 굳이 ConBonA 클래스가 BonA 클래스와 동일한 vtable을 가질 필요는 없어 보입니다. 또는 BonA의 것을 그대로 재사용하는 것도 가능할 텐데요.<br /> <br /> Visual C++의 경우, 이런 상황에서도 무조건 ConBonA의 vtable을 전용으로 생성하고 (물론 BonA의 vtable과 내용은 같습니다.) 심지어 ConBonA 생성자에서 vtable을 초기화하는 코드도 여전히 넣어두도록 처리합니다. (사실, 이런 부분은 구현하는 측에서 임의 재량으로 해결할 문제입니다.)<br /> <br /> <hr style='width: 50%' /><br /> <br /> 마지막으로 추상 함수(abstract function)에 대한 처리를 잠깐 짚어보겠습니다.<br /> <br /> 기반 클래스에서 추상 함수를 가진 클래스를 정의하면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > class AbstractA { public: int n1 = 1; <span style='color: blue; font-weight: bold'>virtual void vfunc1() = 0;</span> }; class BonAbstractA { public: int n2 = 2; <span style='color: blue; font-weight: bold'>virtual void vfunc1() { };</span> }; </pre> <br /> Visual C++ 컴파일러는 AbstractA의 vtable을 다음과 같이 구성합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 01331876 mov dword ptr [eax],offset AbstractA::`vftable' (<span style='color: blue; font-weight: bold'>01337C90h</span>) <span style='color: blue; font-weight: bold'>0x01337C90</span> 0b 14 33 01 // 0x0133140b </pre> <br /> BonAbstractA 클래스 역시 1개의 항목을 가진 vtable을 구성하는데 당연히 첫 번째 항목의 주소가 BonAbstractA::vfunc1의 시작 코드로 바뀝니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 0133192E mov dword ptr [eax],offset BonAbstractA::`vftable' (<span style='color: blue; font-weight: bold'>01337C80h</span>) <span style='color: blue; font-weight: bold'>0x01337C80</span> 06 14 33 01 // 0x01331406 == BonAbstractA::vfunc1 </pre> <br /> 그런데, 재미있는 것은 AbstractA의 vtable에 있는 첫 번째 항목의 주소 값입니다. 이에 대해서는 다음의 글에 자세하게 나오는데요.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > __purecall이 무엇일까? ; <a target='tab' href='http://www.jiniya.net/tt/597'>http://www.jiniya.net/tt/597</a> </pre> <br /> 실제로 Visual C++로 컴파일된 경우 abstract 함수의 주소로 채워진 주소(위의 예에서는 0x0133140b)를 가보면, (Visual C++ 런타임이 만들어 넣어준) __purecall 함수임을 확인할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > __purecall: 01331840 jmp dword ptr [__imp___purecall (0133B0BCh)] </pre> <br /> 이 정도면 대충 vtable 정리는 끝난 것 같습니다. 다음 글에서는, <a target='tab' href="http://www.sysnet.pe.kr/2/0/11169">가상 함수 호출을 가로채는 이야기</a>를 해보겠습니다. ^^<br /> </p><br /> <br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
1644
(왼쪽의 숫자를 입력해야 합니다.)