#include <string>
#include <iostream>
class WrongAnimal
{
protected:
std::string type_;
public:
~WrongAnimal() { std::cout << "WrongAnimal" << std::endl; };
};
class WrongCat : public WrongAnimal
{
public:
virtual ~WrongCat() { std::cout << "WrongCat" << std::endl; };
};
int main(void)
{
WrongAnimal* wa = new WrongCat();
delete wa;
return 0;
}
위의 코드를 실행하면 다음과 같은 출력문이 뜬다
WrongAnimal
malloc: *** error for object 주소값: pointer being freed was not allocated
malloc: *** set a breakpoint in malloc_error_break to debug
도대체 왜?
WrongCat의 소멸자가 호출이 된 것도 아닌데, WrongAnimal의 소멸자가 호출이 되고 잘못된 주소값에 대한 free()가 발생하는지 의문이었다
디버깅을 해보았지만 정확한 답은 나오지 않았기에 어쩔 수 없이 어셈블리를 열어보기로 했다
Compiler Explorer
godbolt.org
에서 해당 코드를 입력하고 어셈블리어를 얻었다
어셈블리어
mov edi, 40
call operator new(unsigned long) ; 40바이트 크기의 메모리 할당
mov rdi, rax ; rbx에 메모리 시작 주소 저장
mov qword ptr [rbp - 32], rdi ; [rbp - 32]에 메모리 시작 주소 저장
.
.
.
mov rcx, qword ptr [rbp - 32] ; rcx로 할당된 메모리 시작 주소 저장
xor eax, eax
cmp rcx, 0
mov qword ptr [rbp - 24], rax
je .LBB1_2
mov rax, qword ptr [rbp - 32]
add rax, 8
mov qword ptr [rbp - 24], rax
.
.
.
mov rdx, QWORD PTR [rbp-40] ; rdx에 객체 포인터 (rbx+8) 저장
mov rsi, rdx
mov rdi, rax ; 반환값 저장할 공간의 주소
call WrongAnimal::getType[abi:cxx11]() const
.
.
.
mov rbx, QWORD PTR [rbp-40] ; rbx에 rbx+8 저장
test rbx, rbx
je .L26
mov rdi, rbx ; rbx+8 주소로 소멸자 호출
call WrongAnimal::~WrongAnimal()
mov esi, 32
mov rdi, rbx ; rbx+8 주소로 operator delete 호출
call operator delete(void*, unsigned long)
위의 어셈블리를 통해 delete 연산자에 WrongAnimal의 시작 주소가 아닌 WrongCat의 시작주소로 추정되는 WrongAnimal의 부분 객체의 주소를 넘기는 것을 알 수 있다
그렇기에 원래 delete 연산자에 할당받은 메모리의 시작 주소 rbx가 아닌 그보다 8바이트 뒤의 주소 rbx + 8을 전달하기에 메모리의 중간값을 받아 free invalid pointer 라는 에러가 나는 것이다
그렇다면 왜 자원을 해제할 때 메모리의 시작주소를 넘겨야 할까?
그 이유는 메모리의 헤더 정보 때문인데, 기본적으로 각 할당된 메모리의 앞부분에 있는 헤더 정보는 OS의 메모리 관리자가 할당 크기나 블록의 상태를 추적하는 데 사용하며 해제할 때에도 이 정보를 이용하기에 시작 부분을 받아야 제대로 된 해제가 이루어질 수 있기 때문이다!
그렇기에 여러 테스트를 진행해 보았다!
#include <string>
#include <iostream>
class WrongAnimal
{
protected:
// 멤버 변수는 각 객체마다 고유하게 메모리에 할당되기에 객체의 크기에 영향을 끼친다
// base 객체에 멤버 변수가 할당이 된다면 dervied 객체와의 시작 주소가 달라진다
std::string type_;
public:
~WrongAnimal() { std::cout << "WrongAnimal" << std::endl; };
// 멤버 함수는 객체 내부가 아닌 text segment에 저장되기 때문에
// 객체의 크기에 영향을 끼치지 않는다
void makeSound() const {};
};
class WrongCat : public WrongAnimal
{
public:
// 영향을 받아 부모 객체의 크기가 커진 지금 자식 객체의 소멸자가 virtual로 호출이 되면
// base 객체에서 멤버 변수의 크기를 더한 derived 객체의 시작 주소값을 전달하게되어
// 잘못된 위치의 메모리 해제가 이루어지게 된다
virtual ~WrongCat() { std::cout << "WrongCat" << std::endl; };
void makeSound() const {};
};
int main(void)
{
WrongAnimal* wa = new WrongCat();
WrongCat *wc = new WrongCat();
// 두 객체의 size를 재본 결과 크기는 1임을 알 수 있다 (virtual table크기는 계산 안함)
// 이를 통해 base 객체가 비어있으면 dervied 객체와 시작 주소가 동일하게 설정됨을 알 수 있다
std::cout << "WrongCat size: " << sizeof(*wa) << std::endl; // WrongAnimal size: 1
std::cout << "WrongCat size: " << sizeof(*wc) << std::endl; // WrongCat size: 8 (vtable size)
delete wa;
delete wc;
return 0;
}
위와 같이 base 객체의 크기에 따라 동작방식이 달라짐을 알 수 있다
하지만 항상 이렇게 free invalid pointer에러가 발생하는 것은 아니다
어떤 컴파일러는 컴파일 단계에서 해당 에러를 잡아 내기도 하였기 때문이다
정확히는 Undefined Behavior라고 볼 수 있겠다
이는 컴파일러와 환경마다 다르게 작동할 수 있으니 말이다