Programmer

purecall virtual function error

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

전체 코드

https://iamskylover.tistory.com/90

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;
}

디버깅

실행하면 자식 클래스인 B myObject의 소멸자가 실행되는 main의 종료지점에서 에러가 난다.
좀 더 자세히 뜯어보자.

void intermediate_call() {
    // Bad: virtual function call during object destruction
    virtual_function();
}
    extern "C" int __cdecl _purecall()

부모 소멸자에서 호출하는 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

가상함수는 첫 인자에 현재 인스턴스의 주소인 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의 내용을 확인해보자.

image
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)  
    20:     }

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();
}

찾아보니 _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 에러를 잡을 때 사용할 수 있겠다.