개요
소멸자에서 가상함수를 호출하지 말라는 얘기를 많이 들어봤을 것이다.
당연한거지만 에러가 난다.
그래도 왜 그런지 좀 더 자세히 보고 싶어서 디스어셈블리를 뜯어봤다.
전체 코드
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++
복사
찾아보니 _set_pure_call_handler()로 핸들러를 지정할 수 있다.
https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/get-purecall-handler-set-purecall-handler?view=msvc-170
purecall 에러를 잡을 때 사용할 수 있겠다.