디버깅 이야기: malloc, free, new[], delete[]

pynoos의 이미지

일반적으로 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이 되시길 바랍니다.

Forums: 
pynoos의 이미지

http://korea.gnu.org/manual/bmtp/onlinedocs/gxxint_1.html#SEC17

위의 g++ internal 에도 같은 내용이 있군요.

jj의 이미지

계속 좋은 강좌 부탁드립니다. 감사~ :D :D

--
Life is short. damn short...

pynoos의 이미지

뒤늦게... 제가 쓴 글에 오류가 있었음을 알고 고칩니다.

사용중 bit에 대한 것을 잘못이해했더군요...

아래는 glibc 소스의 malloc.c 에 들어 있는 내용입니다.

/*

   malloc_chunk details:

    (The following includes lightly edited explanations by Colin Plumb.)

    Chunks of memory are maintained using a `boundary tag' method as
    described in e.g., Knuth or Standish.  (See the paper by Paul
    Wilson ftp://ftp.cs.utexas.edu/pub/garbage/allocsrv.ps for a
    survey of such techniques.)  Sizes of free chunks are stored both
    in the front of each chunk and at the end.  This makes
    consolidating fragmented chunks into bigger chunks very fast.  The
    size fields also hold bits representing whether chunks are free or
    in use.

    An allocated chunk looks like this:  


    chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             Size of previous chunk, if allocated            | |
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             Size of chunk, in bytes                         |P|
      mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             User data starts here...                          .
            .                                                               .
            .             (malloc_usable_space() bytes)                     .
            .                                                               |
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             Size of chunk                                     |
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+


    Where "chunk" is the front of the chunk for the purpose of most of
    the malloc code, but "mem" is the pointer that is returned to the
    user.  "Nextchunk" is the beginning of the next contiguous chunk.

    Chunks always begin on even word boundries, so the mem portion
    (which is returned to the user) is also on an even word boundary, and
    thus double-word aligned.

    Free chunks are stored in circular doubly-linked lists, and look like this:

    chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             Size of previous chunk                            |
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    `head:' |             Size of chunk, in bytes                         |P|
      mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             Forward pointer to next chunk in list             |
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             Back pointer to previous chunk in list            |
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             Unused space (may be 0 bytes long)                .
            .                                                               .
            .                                                               |
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    `foot:' |             Size of chunk, in bytes                           |
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

    The P (PREV_INUSE) bit, stored in the unused low-order bit of the
    chunk size (which is always a multiple of two words), is an in-use
    bit for the *previous* chunk.  If that bit is *clear*, then the
    word before the current chunk size contains the previous chunk
    size, and can be used to find the front of the previous chunk.
    (The very first chunk allocated always has this bit set,
    preventing access to non-existent (or non-owned) memory.)

    Note that the `foot' of the current chunk is actually represented
    as the prev_size of the NEXT chunk. (This makes it easier to
    deal with alignments etc).

    The two exceptions to all this are 

     1. The special chunk `top', which doesn't bother using the 
        trailing size field since there is no
        next contiguous chunk that would have to index off it. (After
        initialization, `top' is forced to always exist.  If it would
        become less than MINSIZE bytes long, it is replenished via
        malloc_extend_top.)

     2. Chunks allocated via mmap, which have the second-lowest-order
        bit (IS_MMAPPED) set in their size fields.  Because they are
        never merged or traversed from any other chunk, they have no
        foot size or inuse information.

    Available chunks are kept in any of several places (all declared below):

    * `av': An array of chunks serving as bin headers for consolidated
       chunks. Each bin is doubly linked.  The bins are approximately
       proportionally (log) spaced.  There are a lot of these bins
       (128). This may look excessive, but works very well in
       practice.  All procedures maintain the invariant that no
       consolidated chunk physically borders another one. Chunks in
       bins are kept in size order, with ties going to the
       approximately least recently used chunk.

       The chunks in each bin are maintained in decreasing sorted order by
       size.  This is irrelevant for the small bins, which all contain
       the same-sized chunks, but facilitates best-fit allocation for
       larger chunks. (These lists are just sequential. Keeping them in
       order almost never requires enough traversal to warrant using
       fancier ordered data structures.)  Chunks of the same size are
       linked with the most recently freed at the front, and allocations
       are taken from the back.  This results in LRU or FIFO allocation
       order, which tends to give each chunk an equal opportunity to be
       consolidated with adjacent freed chunks, resulting in larger free
       chunks and less fragmentation. 

    * `top': The top-most available chunk (i.e., the one bordering the
       end of available memory) is treated specially. It is never
       included in any bin, is used only if no other chunk is
       available, and is released back to the system if it is very
       large (see M_TRIM_THRESHOLD).

    * `last_remainder': A bin holding only the remainder of the
       most recently split (non-top) chunk. This bin is checked
       before other non-fitting chunks, so as to provide better
       locality for runs of sequentially allocated chunks. 

    *  Implicitly, through the host system's memory mapping tables.
       If supported, requests greater than a threshold are usually 
       serviced via calls to mmap, and then later released via munmap.

*/
Necromancer의 이미지

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

bluebriz의 이미지

"따라서 heap상에 할당된 메모리 공간을 넘어 기록하는 일이 생길때,
다음 블럭의 사이즈 정보등을 망치므로, free가 잘못 되거나, malloc할 때 남은양 계산이 잘못되는 수가 발생하게됩니다."

이래서 제 코드에서 free 할때 오류가 발생했던 것이네요 ㅎㅎ;;

^^

최익필의 이미지

막연히 그러겠지 했던것을 이렇게 메모리로 보게 되니, 무릎을 탁 치게 됩니다!

댓글 달기

Filtered HTML

  • 텍스트에 BBCode 태그를 사용할 수 있습니다. URL은 자동으로 링크 됩니다.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>
  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.

BBCode

  • 텍스트에 BBCode 태그를 사용할 수 있습니다. URL은 자동으로 링크 됩니다.
  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param>
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.

Textile

  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • You can use Textile markup to format text.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>

Markdown

  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • Quick Tips:
    • Two or more spaces at a line's end = Line break
    • Double returns = Paragraph
    • *Single asterisks* or _single underscores_ = Emphasis
    • **Double** or __double__ = Strong
    • This is [a link](http://the.link.example.com "The optional title text")
    For complete details on the Markdown syntax, see the Markdown documentation and Markdown Extra documentation for tables, footnotes, and more.
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>

Plain text

  • HTML 태그를 사용할 수 없습니다.
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.
  • 줄과 단락은 자동으로 분리됩니다.
댓글 첨부 파일
이 댓글에 이미지나 파일을 업로드 합니다.
파일 크기는 8 MB보다 작아야 합니다.
허용할 파일 형식: txt pdf doc xls gif jpg jpeg mp3 png rar zip.
CAPTCHA
이것은 자동으로 스팸을 올리는 것을 막기 위해서 제공됩니다.