성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] How can I tell whether two programs...
[정성태] The case of the fail-fast crashes c...
[정성태] Creating Docker multi-arch images f...
[정성태] BinaryFormatter removed from .NET 9...
[정성태] Extending the Windows Shell Progres...
[우광현] 와..... 범위를 잡았으니 클라이언트가 해당 범위를 확인해본다...
[정성태] 딱히, 그것 이상으로 더 설명할 내용이 없습니다. 동적 포...
[정성태] If Windows 3.11 required a 32-bit p...
[정성태] What is a hard error, and what make...
[괴물신인] 질문작성자인데 이 글을 이제봤네요 ㄷㄷ 이 글처럼 타입별로 인...
글쓰기
제목
이름
암호
전자우편
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;' > #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(); ConBonA *table2 = new ConBonA(); return 0; } </pre> <br /> 이제 디버그 모드로 실행하면서 생성자 호출로 인한 코드를 한번 살펴보겠습니다.<br /> <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 call ConBonA::ConBonA (0CE1113h) </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;' > ConBonA::ConBonA: 00CE18E6 E8 12 FA FF FF call BonA::BonA (0CE12FDh) ...[생략]... 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;' > BonA::BonA: 00CE1876 E8 28 FA FF FF call A::A (0CE12A3h) ...[생략]... 00CE18A4 C3 ret </pre> <br /> 마찬가지로 자신의 클래스를 위한 초기 코드보다는 기반 클래스의 생성자를 호출합니다. 이제 가장 상단에 위치하는 기반 클래스 A의 생성자는,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 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 </pre> <br /> "ConBonA *table1 = new ConBonA();" 코드로 메모리가 할당된 table1 주소에 "class A"를 컴파일하면서 구성해놓았던 "vtable"의 주소를 첫 번째 4바이트에 씁니다. 그래서 위의 코드까지만 실행된 상태에서는 this 포인터 주소의 내용이 다음과 같이 구성됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > [오프셋12: 필드 ConBonA::n3] - 4바이트 [오프셋 8: 필드 BonA::n2] - 4바이트 [오프셋 4: 필드 A::n1] - 4바이트 <span style='color: blue; font-weight: bold'>[오프셋 0: A 클래스의 vtable 포인터]</span> - 4바이트 </pre> <br /> 그리고 "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 /> 그러니까... 적어도 main 함수 실행 중에는 vtable의 내용이 이미 구성된 상태인 것입니다. 게다가 vtable의 값은 instance 별로 생성되는 것이 아닌, 클래스 별로 생성된다는 것을 알 수 있습니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 조금 더 깊게 파헤쳐 볼까요? ^^<br /> <br /> 결론부터 말하면, vtable은 컴파일러가 해당 소스 코드 파일을 컴파일하면서 알아낸 정보를 바탕으로 PE 파일의 Section에 미리 기록해 둡니다. 재미 삼아서 계산까지 한번 해보겠습니다. ^^<br /> <br /> Visual Studio에서 디버깅 모드로 예제 EXE 프로그램을 실행시킨 후, "Ctrl + Alt + U" 키를 눌러 "Modules" 창을 띄우면 EXE 파일의 이미지 로딩 주소를 알아낼 수 있습니다. (코드로는 <a target='tab' href='https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-getmodulehandlea'>GetModuleHandle</a> Win32 API로 반환받은 HMODULE 값이 됩니다.) EXE 파일의 경우 IMAGE_OPTIONAL_HEADER에 지정된 "Image Base (보통 0x400000)"로 로딩이 되지만 Vista 운영체제부터는 <a target='tab' href='http://www.sysnet.pe.kr/2/0/545'>ASLR</a> 덕분에 주소가 가변으로 바뀝니다. 제 경우에 테스트용 ConsoleApplication1.exe의 이미지 매핑이 "00CD0000-00CEF000"로 되어 있었습니다.<br /> <br /> 그다음, "mov dword ptr [eax],offset A::`vftable' (0CE7B34h)"로 나왔던 0x00ce7b34 주소에서 0x00cd0000 기반 주소를 빼면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 0CE7B34h - 0x00cd0000 = <span style='color: blue; font-weight: bold'>0x17b34</span> </pre> <br /> 0x17b34 변위가 나오고 이를 ConsoleApplication1.exe 파일에 기록된 IMAGE_SECTION_HEADER들의 정보를 통해 어느 Section에 있는지 찾아낼 수 있습니다. 대개의 경우, Visual C++라면 다음과 같이 ".rdata" 섹션에서 찾을 수 있습니다.<br /> <br /> <img onclick='toggle_img(this)' class='imgView' alt='about_vtable_1.png' src='/SysWebRes/bbs/about_vtable_1.png' /><br /> <br /> .rdata 섹션의 RVA 값이 0x00017000이고 크기가 0x2564이기 때문에,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 0x17000 <= <span style='color: blue; font-weight: bold'>0x17b34</span> <= 0x19564 </pre> <br /> 0x17b34 변위가 정확히 .rdata 섹션의 범위안에 드는 것을 볼 수 있습니다. 그럼, Visual C++가 .rdata 섹션에 기록한 vtable 내용을 찾아볼까요?<br /> <br /> .rdata 섹션의 시작이 0x17000 위치이니, vtable이 위치한 주소의 섹션 내 변위는 "0x17b34 - 0x17000 = 0xb34"가 됩니다. ConsoleApplication1.exe의 PE 정보에서 .rdata 섹션의 파일 위치 정보를 가리키는 "Pointer to Raw Data" 값이 0x5c00라고 했으니 "0x5c00 + 0xb34 = 0x6734"가 됩니다. 이 위치를 찾아 보면,<br /> <br /> <img onclick='toggle_img(this)' class='imgView' alt='about_vtable_2.png' src='/SysWebRes/bbs/about_vtable_2.png' /><br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 오프셋 0: 0x00411361 (A::vfunc1 가상 함수를 가리키는 주소) </pre> <br /> 위와 같이 값이 담겨 있습니다. 메모리에서와 마찬가지로 A 클래스의 가상 함수가 1개이기 때문에 PE 파일의 Section 내에도 1개가 보입니다. 물론, 이 값들이 그대로 가상 함수의 주소로 사용되지는 않습니다. 왜냐하면 ASLR로 인해 컴파일러가 가정한 Image Base 주소가 0x400000에서 0x00CD0000으로 변경되었기 때문에 이미지 로딩 시에 Relocation 계산이 이뤄져서 그 값이 반영된 주소로 메모리에서는 덮어써집니다. 역시 직접 계산을 해볼까요? ^^<br /> <br /> 우선 조정된 Image Base 주소의 차이를 계산하고,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 0x00CD0000 - 0x400000 = 0x8d0000 </pre> <br /> 그 값에 PE 파일에서 찾아낸 vtable의 함수 주소인 0x00411361을 더하면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 0x8d0000 + 0x411361 = 0xce1361 </pre> <br /> 정확히 0x00ce1361이 나옵니다. 이 값을 디버깅 중인 A 클래스의 생성자에 있던,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > mov dword ptr [eax],offset A::`vftable' (0CE7B34h) </pre> <br /> 0CE7B34h 주소를 메모리 창으로 확인하면 동일한 값이 나오는 것을 볼 수 있습니다.<br /> <br /> <img alt='about_vtable_3.png' src='/SysWebRes/bbs/about_vtable_3.png' /><br /> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
1343
(왼쪽의 숫자를 입력해야 합니다.)