malloc 메타데이터 탐방기

Yoon Sunkue·2022년 3월 28일
1

뻘 짓

목록 보기
3/3

new 와 delete 에 대해 잘 모르는 사실,
delete 함수는 어떤 타입의 포인터를 인자로 받든지, 항상 할당된 크기의 메모리를 반환해 준다.

그래서

	while (true)
	{
		volatile auto a = new size_t;
		delete reinterpret_cast<void*>(a);
	}

이런 코드를 돌려도 메모리가 넘치지 않는다.
표준은 UD이다. MSVC Windows 기준은 그렇다.

delete 함수의 실제 선언부 이다.

void __CRTDECL operator delete(void* _Block) noexcept;

void* 를 인자로 받는다. 타입에 상관없이 같은 동작을 한다는 뜻이다.
어떻게 포인터값만 가지고 할당받은 크기를 알아낼 수 있는 것일까?

windows os 는 메모리를 할당 해 줄 때, 리턴해 줄 메모리 근처에 할당받은 메모리의 크기를 저장한다. new를 실행한 후 메모리의 값을 그림으로 나타내면 이렇다. (후첨::그런 줄 알았는데 결론을 보면 이리 단순하지는 않은 듯 하다. 일단 실험의 첫 가정은 이랬다.)

[ A:B에게 할당된 크기 ][ B0:반환된 주소값 ][B1][B2][BE][]

우리는 new 실행시 B0의 값을 리턴받아 사용하게되며, A는 의도하지 않으면 접근 할 수 없다.
delete B0를 실행하면 delete 함수에서는 A의 값을 읽어내어 메모리 반환에 사용한다.

그래서 이러한 코드는 delete 의 인자로 오는 포인터의 타입에 상관없이 동일한 메모리를 반환해주는 것이다.

	TYPE* var = new TYPE; 
	delete reinterpret_cast<int*>(var);

위의 메모리 그림에서, 의도하지 않으면 A에 접근 할 수 없지만, 의도하면 포인터를 조작하여 이 메모리에 접근 할 수 있다.

그래서 접근해보고, 실험해보려 한다.
접근 할 수 있지만 간단하지는 않다.
일단 첫 째로 문제는 A 의 메모리 크기가 몇인지 모른다는 것.
디버깅 모드로 메모리의 값을 뜯어내가며 눈으로 확인하는 방법이 있고,
몇번의 노가다로 유추하는 방법이 있다.
나는 노가다를 선택했다.

아래 코드에서 t의 타입을 바꾸어가며 결과를 확인 할 때, 항상 같은 값을 출력하는 타입의 사이즈가 A 의 사이즈이다.

	using t = int;
	while (true)
	{
		t* x = new t;
	 	cout << *(x - 1) << endl;
		delete x;
	}

나는 unsigned short의 경우부터 36352가 일정하게 출력된다.
여기서 36352 라는 값에 집중하면 안된다.
sizeof(unsigned short). 2byte 만큼 A가 할당된다는 의미로 받아들여야 한다.
메모리에 담긴 비트값은 1000 1110 0000 0000 이다.
이제 이걸 어케 sizeof(t) 라는 값으로 변환시키는 지를 알아야 하는데..

	using t1 = size_t;
	using t2 = unsigned short;
	while (true)
	{
	 	t1* x = new t1;
	 	t2* xx = reinterpret_cast<t2*>(x);
	 	cout << *(xx - 1) << endl;
		delete x;
	}

이렇게 돌리면 34816 가 나온다. 비트값은 1000 1000 0000 0000.
t1을 바꾸어가며 실험한다.

// 1001 0001 0000 0000 => 15byte
// 1001 0110 0000 0000 => 10byte
// 1001 0111 0000 0000 => 9byte
// 1000 1000 0000 0000 => 34816 => 8byte
// 1000 1100 0000 0000 => 35840 => 4byte
// 1000 1110 0000 0000 => 36352 => 2byte
// 1000 1111 0000 0000 => 36608 => 1byte

이제 패턴이 좀 보인다. 사건 해결!, 오늘의 뻘짓은 여기서 끝난다.

...라고 생각했는데

// 1001 0000 0000 0000 => 36864 => 64byte
// 0001 0000 0000 0000 => 4096 => 64byte
// 1000 1000 0000 0000 => 34816 => 56byte 
// 1001 0000 0000 0000 => 36864 => 48byte
// 1000 1000 0000 0000 => 34816 => 40byte
// 1001 0000 0000 0000 => 36864 => 32byte 
// 1000 1000 0000 0000 => 34816 => 24byte 
// 1001 0000 0000 0000 => 36864 => 16byte 
// 1000 1000 0000 0000 => 34816 => 8byte

16바이트를 넘어가자 일관성이 없어지기 시작한다.
delete 를 지우면 하위 1바이트에 노이즈가 끼기 시작한다.
뭘까? 인내심이 모자르다, 답지를 보고싶어졌다.
https://stackoverflow.com/questions/1518711/how-does-free-know-how-much-to-free

:: 결론 ::

메모리 할당시의 정보를 저장하는 부분(메타데이터 라고 부르고 싶다)은 내 생각보다 큼직하다. 또, 단순하지 않아 뵌다. 할당자의 구현이 궁금한데, 메타데이터는 리턴된 포인터의 바로 옆이 아닌 전혀 다른 위치에 따로 저장될 수도 있겠다는 생각이다. 단순 할당된 메모리의 총량 외에, 다양한 정보가 들어 있을 수 있겠다.

여기서 얻어낸 재미난 함수 하나 _msize(void*)
delete free 를 위해 메모리량을 저장한 메타데이터를 해석해주는 함수다. 할당자와 연결되어 작동하려나?

#pragma pack (push, 1)
	using t1 = class A{ size_t v8; int v12; char v13,v14,v15; };
#pragma pack(pop)  

	using t2 = unsigned short;
	while (true)
	{
		t1* x = new t1{};
		t2* xx = reinterpret_cast<t2*>(x);
		cout << _msize(xx) << endl; // 헉!
		cout << (int)*(--xx) << endl;
        // cout << _msize(xx) << endl; 에러가 난다.
	}
    

0개의 댓글