디버깅 이야기: malloc, free, new[], delete[]
일반적으로 c에서 자주 쓰이는 동적 메모리 할당의 간단한 예와...
char * p = (char *) malloc (128); free(p);
c++ 에서 쓰이는 array type의 new 연산자의 예를 보면...
int * iArray = new int[10]; delete[] iArray;
둘의 공통점이 메모리 해제시에 할당할때 받은 크기를 인자로 넘기지 않는 것에 의아해 해보신적이 있을 것입니다.
어딘가에 저장을 해놓을 것이고, pointer가 넘어 왔을때, pointer:size 쌍의 값에서 size를 찾아낸후 메모리를 제거한다고 막연하게 생각할 수있죠.
물론, 표준은 상세한 구현에 대한 것은 언급하고 있지 않습니다.
어떻게 구현하든 할당시에는 크기를 주고 pointer를 돌려주며, 해제시에는 그 pointer 만을 요구하는 것으로 interface가 이루어져 있으니까요.
저 또한 궁금하다고만 생각하고, 실제 구현 내용을 코드상에서 확인해보기 전까지는 막연했었습니다.
간단히 테스트 하나 해 보죠.
http://bbs.kldp.org/viewtopic.php?t=1133
에서 소개했던 hexdump를 가지고, 할당 받은 메모리보다 앞에서부터 덤프해 보겠습니다.
int main() { char * p; char * q; char * r; p = (char *) malloc(16); q = (char *) malloc(32); r = (char *) malloc(48); strcpy(p, "123456789" ); strcpy(q, "abcdefghijklmnopqrstuvwxyz" ); strcpy(r, "ABCDEFGHIJKLMNOPQRSTUVWXYZ" ); hexdump( "Malloc", p-32, 192 ); return 0; }
실행 결과입니다.
(6244 ) Malloc --------------------------------------------------------- [00000] 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ [00010] 00 00 00 00 00 00 00 00-00 00 00 00 19 00 00 00 ................ [00020] 31 32 33 34 35 36 37 38-39 00 00 00 00 00 00 00 123456789....... [00030] 00 00 00 00 29 00 00 00-61 62 63 64 65 66 67 68 ....)...abcdefgh [00040] 69 6A 6B 6C 6D 6E 6F 70-71 72 73 74 75 76 77 78 ijklmnopqrstuvwx [00050] 79 7A 00 00 00 00 00 00-00 00 00 00 39 00 00 00 yz..........9... [00060] 41 42 43 44 45 46 47 48-49 4A 4B 4C 4D 4E 4F 50 ABCDEFGHIJKLMNOP [00070] 51 52 53 54 55 56 57 58-59 5A 00 00 00 00 00 00 QRSTUVWXYZ...... [00080] 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ [00090] 00 00 00 00 41 05 00 00-00 00 00 00 00 00 00 00 ....A........... [000a0] 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ [000b0] 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
잘 살펴보실까요?
진행하기 전에 본 예제는 glibc 에 구현되어 있는 heap 관리자의 malloc을 나타냅니다.
Solaris, HPUX의 c library는 약간 다릅니다.
실제 p,q,r이 가리키는 주소에는 내용이 시작하는 부분이므로, dump 상에서 어딘지 알 수 있을 것입니다.
p 는 [00020] 번에서 시작하고, q는 [00038] r은 [00060] 부터 시작하는 군요.
그런데 각 시작 위치의 앞부분 4 byte를 잘 보시면,
19 00 00 00
29 00 00 00
39 00 00 00
임을 알 수 있습니다. 이게 웬일입니까? 우리가 할당한 크기는 16, 32, 48 인데, 각각의 hex 값은
0x10, 0x20, 0x30
이죠.. 9씩 증가되어 있는 모습을 볼 수 있군요..!
사실 대부분의 구현은 그 내용이 조금씩 다르지만 다음과 같은 방식으로 구현합니다.
malloc(10);
이라는 요청에 대해 (할당정보 크기) + (memory 할당 block 중 최소 값) -- 8의 배수라고 생각한다면, 16정도 되겠군요.-- 을 Heap 관리자에 요청합니다.
위 코드의 예에서는 앞에 두개의 long 데이터를 정보로 가지고 있습니다.
그중 처음 하나는 늘 0 처럼 보입니다만, 사실 이전 block이 해제된 block이라면, 그 해제된 크기를 가지고 있습니다.
+----------------+------------------+---------------------+ |이전해제블록사이즈 | 현재 블록사이즈 | 사용자 데이터.... | +----------------+------------------+---------------------+
즉, heap에는 사용자가 요청한 것보다 좀 크게 요청하고 일단 정보를 기록한 뒤, 사용자 데이터 부분의 번지를 돌려주는 것이지요.
현재 블록사이즈값중 마지막 1bit는 이전 블록이 사용중인지를 나타냅니다. 따라서 짝수값이면 "이전해제블록사이즈"가 유효한 값이 됩니다.
예에서 9 가 증가 했지만 마지막 bit는 "사용중 여부" 이므로 8이 증가한 것이며, 이 크기는 사실 "이전해제블록사이즈", "현재블록사이즈" 가 저장되는 크기 8 byte를 더한 값이 됩니다.
앞에서도 말했지만, heap 구현체마다 다릅니다. 위 예는 glibc의 malloc을 나타냅니다.
c++의 new[] delete[]도 마찬가지 입니다.
class C { public: int a; C() {}; ~C() { printf("Destroy..\n"); }; }; int main() { C * pc = new C [13]; pc[0].a = 0x1234; hexdump( "Int array", (char *)pc - 32, 128 ); delete[] pc; return 0; }
13 이라는 값의 hex 값은 0D 입니다. 이것을 실행시킨 결과는
(6867 ) [0x804ad30] Int array ------------------------------------------ [00000] 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ [00010] 00 00 00 00 41 00 00 00-0D 00 00 00 00 00 00 00 ....A........... [00020] 34 12 00 00 00 00 00 00-00 00 00 00 00 00 00 00 4............... [00030] 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ [00040] 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ [00050] 00 00 00 00 81 02 00 00-00 00 00 00 00 00 00 00 ................ [00060] 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ [00070] 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ Destroy.. Destroy.. Destroy.. Destroy.. Destroy.. Destroy.. Destroy.. Destroy.. Destroy.. Destroy.. Destroy.. Destroy.. Destroy..
와 같이 되어 잘 살펴보면, 처음 값이 저장되는 위치 이전 메모리에 배열의 크기(0x0D)와 전체 배열의 크기(0x41)가 저장되어 있음을 볼 수 있습니다.
즉, malloc에 의해 생기는 정보와 new[] 에 의해 생기는 정보 두가지가 저장된 것이지요.
이 값을 가지고, delete[] 시에 각 요소들의 소멸자를 부르게 됩니다.
그리고 그 소멸자가 불리면, 또한 전체 메모리가 free 되겠지요.
재미로 배열의 값을 바꾸어 볼까요?
class C { public: int a; C() {}; ~C() { printf("Destroy..\n"); }; }; int main() { C * pc = new C [13]; pc[0].a = 0x1234; hexdump( "Int array", (char *)pc - 32, 128 ); long * jaemi = (long *)pc; jaemi -= 2; *jaemi = 5; delete[] pc; return 0; } (6876 ) [0x804ad50] Int array ------------------------------------------ [00000] 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ [00010] 00 00 00 00 41 00 00 00-0D 00 00 00 00 00 00 00 ....A........... [00020] 34 12 00 00 00 00 00 00-00 00 00 00 00 00 00 00 4............... [00030] 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ [00040] 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ [00050] 00 00 00 00 61 02 00 00-00 00 00 00 00 00 00 00 ....a........... [00060] 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ [00070] 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ Destroy.. Destroy.. Destroy.. Destroy.. Destroy..
이게 왠일입니까... Destroy가 다섯번만 불리는 군요....
-------------------------------------------
이상 살펴본바는, malloc, new[] 에서 돌려주는 pointer는 빈 메모리를 할당받고, 약간의 정보를 앞에 둔 다음의 주소였습니다.
따라서 heap상에 할당된 메모리 공간을 넘어 기록하는 일이 생길때,
다음 블럭의 사이즈 정보등을 망치므로, free가 잘못 되거나, malloc할 때 남은양 계산이 잘못되는 경우가 발생합니다.
strace, truss로 debug 하실때,
brk
시스템 콜이 들어가는 부분은 malloc, new[] 연산자가 사용될 때, OS로부터 메모리를 더 받기 위해 불리는 것입니다. heap으로 사용되는 영역이 늘어날 때 사용되지요.
man brk
해보시고, 즐거운 debugging이 되시길 바랍니다.
http://korea.gnu.org/manual/bmtp/onlined
http://korea.gnu.org/manual/bmtp/onlinedocs/gxxint_1.html#SEC17
위의 g++ internal 에도 같은 내용이 있군요.
---
http://coolengineer.com
좋은강좌 감사드립니다.
계속 좋은 강좌 부탁드립니다. 감사~ :D :D
--
Life is short. damn short...
뒤늦게... 제가 쓴 글에 오류가 있었음을 알고 고칩니다.사용중
뒤늦게... 제가 쓴 글에 오류가 있었음을 알고 고칩니다.
사용중 bit에 대한 것을 잘못이해했더군요...
아래는 glibc 소스의 malloc.c 에 들어 있는 내용입니다.
---
http://coolengineer.com
더 추가하자면
malloc()은 linked list 형태로 구현한게 대부분이죠..
linked list의 헤더에는 윗 분들이 말씀하신대로 앞블럭과의 링크
정보와 블럭의 크기 정보가 들어갑니다.
그리고 이걸 호출한 사용자에게는 블럭의 크기 정보라든가 등등의
위치를 절대 안알려주죠. 단지 사용자가 쓸 수 있는 영역만 알려줄 뿐이죠..
그리고 유닉스 계열에서 OS로부터의 실제 메모리 할당은 brk()와
mmap() 두가지를 씁니다. 해제시는 반대로 munmap()을 쓰지요.
(brk()로 할당된 메모리는 brk()의 크기를 줄임으로서 메모리를 해제시킵니다)
mmap()는 본래 메모리 특정 영역이 파일의 특정 영역과 대응되도록
매핑하는 함수인데 보통 메모리 할당할때는
/dev/zero에다 대고 걸어버리죠. 그리고 리눅스에서는 /dev/zero에다
거는 대신에 MAP_ANONYMOUS를 사용하고요. (MAP_ANONYMOUS는
리눅스 전용입니다. 다른 유닉스에는 없습니다)
brk()는 프로그램이 사용하는 메모리 영역의 한계값을 수정해서 프로그램이
더 많은 메모리 영역을 쓸 수 있도록 합니다.
malloc()을 직접 구현해 보고 싶으신 분은 해보시기 바랍니다.
단 brk()의 경우 glibc에서 sbrk()라는 함수로 커버하고 있으니 직접
사용할때는 어셈블리를 이용해서 별도의 함수로 돌려 쓰시기 바랍니다.
sbrk()로 해도 상관은 물론 없습니다만, brk() 쓸때는 맨 먼저 brk()의
위치를 알아내는 작업이 필요하기 때문입니다. 불행히 glibc에서는
brk()위치를 알아내는 방법을 제공하지 않더군요.
보통 brk()는 소량의 메모리를 할당해야 하는 경우에 사용하고 mmap()은
대량의 메모리를 할당할 때 사용합니다.
Written By the Black Knight of Destruction
좋은 내용 감사합니다!
"따라서 heap상에 할당된 메모리 공간을 넘어 기록하는 일이 생길때,
다음 블럭의 사이즈 정보등을 망치므로, free가 잘못 되거나, malloc할 때 남은양 계산이 잘못되는 수가 발생하게됩니다."
이래서 제 코드에서 free 할때 오류가 발생했던 것이네요 ㅎㅎ;;
^^
막연히 그러겠지 했던것을 이렇게 메모리로 보게 되니,
막연히 그러겠지 했던것을 이렇게 메모리로 보게 되니, 무릎을 탁 치게 됩니다!
댓글 달기