Search
Duplicate
🥕

소멸자에서 가상함수를 호출하면 생기는 에러

간단소개
pure virtual function call error
팔만코딩경 컨트리뷰터
ContributorNotionAccount
주제 / 분류
CS
C++
Scrap
태그
9 more properties

개요

소멸자에서 가상함수를 호출하지 말라는 얘기를 많이 들어봤을 것이다. 당연한거지만 에러가 난다. 그래도 왜 그런지 좀 더 자세히 보고 싶어서 디스어셈블리를 뜯어봤다.

전체 코드

class A { public: A() {} void intermediate_call() { // Bad: virtual function call during object destruction virtual_function(); } virtual void virtual_function() = 0; virtual ~A() { intermediate_call(); } }; class B : public A { public: // override virtual function in A void virtual_function() { printf("B::virtual_function called\n"); } }; int main() { B myObject; // This call behaves like a normal virtual function call. // Print statement shows it invokes B::virtual_function. myObject.virtual_function(); return 0; }
C++
복사

디버깅

실행하면 자식 클래스인 B myObject의 소멸자가 실행되는 main의 종료지점에서 에러가 난다. 좀 더 자세히 뜯어보자.
void intermediate_call() { // Bad: virtual function call during object destruction virtual_function(); }
C++
복사
extern "C" int __cdecl _purecall()
C++
복사
부모 소멸자에서 호출하는 intermediate_call은 가상함수 virtual_function을 호출하는데, 실행해보면 virtual_function이 아닌 purecall() 이라는 이름의 다른 함수로 호출해버린다. 다른 함수로 점프했다. 라는 것은 잘못된 주소를 로드하고 호출했다는 뜻이다. c++ 코드로는 자세히 보기 힘드니 디스어셈블리를 보자.
11: void intermediate_call() { 12: // Bad: virtual function call during object destruction 13: virtual_function(); 004E1ADA mov eax,dword ptr [this] 004E1ADD mov edx,dword ptr [eax] 004E1AE1 mov ecx,dword ptr [this] 004E1AE4 mov eax,dword ptr [edx] 004E1AE6 call eax
C++
복사
가상함수는 첫 인자에 현재 인스턴스의 주소인 this 포인터를 넣어 실행해야 한다. (첫 인자는 ecx 레지스터에 넣어야 함) (https://learn.microsoft.com/ko-kr/cpp/cpp/thiscall?view=msvc-170) 코드를 보니 ecx에 this 포인터를 제대로 넣고 호출한다. ecx는 문제가 없어보인다. 그럼 eax를 보자. call eax로 점프하는 걸 보니 eax가 vftable에서 virtual_function()의 주소를 가져오는 것일 것이다. vftable은 인스턴스 메모리의 맨 앞에 있으므로 [this]에서 곧바로 들고오는 모습이다.

vftable

vftable의 내용을 확인해보자.
vfptr[0] 에 virtual_function()이 아닌 purecall()이 들어있다. 원래 들어있어야 하는 자식 클래스의 vftable이 언제 수정되었는지 찾아보자.
18: virtual ~A() { /* 생략 */ 004E1929 mov eax,dword ptr [this] 004E192C mov dword ptr [eax],offset A::vftable (04E8B34h) 19: intermediate_call(); 004E1932 mov ecx,dword ptr [this] 004E1935 call A::intermediate_call (04E121Ch)
C++
복사
intermediate_call 호출 전 부모 소멸자의 프롤로그에서 this의 자식 클래스 vftable을 부모의 vftable로 교체하는 것을 최종적으로 찾을 수 있다.

정리

부모의 소멸자가 호출되면 vftable을 부모의 vftable로 교체해버린다. 부모의 추상함수는 부모에서 구현되지 않았으므로, purecall()이라는 기본 함수가 vftable에 대신 들어간다.

여담

purecall() 코드를 보니 내부에 purecall_handler를 호출하는 부분이 있다.
extern "C" int __cdecl _purecall() { _purecall_handler const purecall_handler = _get_purecall_handler(); if (purecall_handler) { purecall_handler(); // The user-registered purecall handler should not return, but if it does, // continue with the default termination behavior. } abort(); }
C++
복사
purecall 에러를 잡을 때 사용할 수 있겠다.