• Home
  • About
    • 검은색 잉크 블로그 photo

      검은색 잉크 블로그

      github GIST → https://gist.github.com/BlaCkinkGJ입니다.

    • Learn More
    • Email
    • Github
  • Posts
    • All Posts
    • All Tags
  • Projects

[유닉스] 동적 할당과 포인터

11 Apr 2019

Reading time ~9 minutes

동적 할당과 포인터

동적인 할당을 하는 함수들에는 아래와 같이 있습니다.

#include <stdlib.h>
void *malloc(size_t size);
void *calloc(size_t nobj, size_t size);
void *realloc(void *ptr, size_t newsize);
void free(void *ptr);

#include <alloca.h>
void *alloca(size_t size);

ISO C는 메모리를 할당을 하는 데 있어서 3개의 함수를 사용합니다.

  1. malloc은 특정한 바이트 수만큼의 메모리를 할당을 합니다. 메모리의 초기 값으로 아무 값이나 들어가 있을 수 있습니다.
  2. calloc은 특정 크기의 오브젝트들의 수만큼 공간을 할당을 합니다. 이를 사용하여 공간을 할당하면 메모리의 초기 값은 0이 되게 됩니다.
  3. realloc은 이전에 할당한 영역의 크기를 줄이거나 늘릴 수 있습니다. 새롭게 할당되는 영역의 초기 값은 아무 값이나 들어가 있을 수 있습니다.

이 이외 free는 메모리의 해제를 지칭하고 alloca는 일반적인 동적 할당이 힙에 할당하는 것과 달리 스택에 동적으로 할당하는 함수에 해당합니다. 후자는 스택 오버플로를 야기할 수 있기 때문에 권장되지는 않습니다.

아래와 같은 소스 코드를 작성하는 경우를 생각해봅시다.

#include "apue.h"
#include <sys/shm.h>

#define ARRAY_SIZE   40000
#define MALLOC_SIZE  100000
#define SHM_SIZE     100000
#define SHM_MODE     0600

char array[ARRAY_SIZE];

int main(void){
    int   shmid;
    char  *ptr, *shmptr;
    
    printf("array[] from %lx to %lx\n", (unsigned long)&array[0],
           (unsigned long)&array[ARRAY_SIZE]);
    printf("stack around %lx\n", (unsigned long)&shmid);
    
    if ((ptr = malloc(MALLOC_SIZE)) == NULL)
        err_sys("malloc error");
    printf("malloced from %lx to %lx\n", (unsigned long) ptr,
          (unsigned long)ptr+MALLOC_SIZE);

    // 나중에 배울 내용에 해당합니다.
    if((shmid = shmget(IPC_PRIVATE, SHM_SIZE, SHM_MODE)) < 0)
        err_sys("shmget error");
    if((shmptr = shmat(shmid, 0, 0)) == (void *)-1)
        err_sys("shmat error");
    printf("shared memory attached from %lx to %lx\n",
          (unsigned long)shmptr, (unsigned long)shmptr+SHM_SIZE);
    
    if (shmctl (shmid, IPC_RMID, 0) < 0)
        err_sys("shmctl error");
    exit(0);
}

여기서 char array[ARRAY_SIZE];는 전역 변수로 메모리의 데이터 세그먼트Data Segment에 들어가게 됩니다. 그러한 데이터 세그먼트 중에서도 초기화되지 않은 데이터uninitialized data 영역인 bss 영역에 들어갑니다.

다음으로 ptr은 동적 할당되는 내용으로 malloc을 통한 동적 할당은 heap 영역에 들어가게 됩니다.

그 이외에 shmid를 통해 shmat으로 하는 부분이 있습니다. 이 부분은 뒤에서 배우시겠지만 이 부분은 공유 메모리shared memory에 할당되는 부분으로 IPCInter-Process Communication와 관련되어 있습니다.

메모리 계층을 보자면 다음과 같이 될 수 있습니다.

memory hierarchy

hierarchy 2

그리고 위의 코드에서도 알 수 있듯이 malloc은 void *를 반환을 합니다. 이 말은 malloc 입장에서는 이것이 어떤 포인터인지 알지 못하기에 일단 매개 변수로 주어진 크기만큼 힙 영역에서 할당을 해주고, 시작 주소를 반환해주는 것을 의미합니다. 따라서 이를 변환해 줄 필요가 있고, 일반적으로 malloc으로 할당을 할 때에 아래와 같이 진행을 하도록 합니다.

int *ptr = (int *)malloc(sizeof(int)*20); //=> int ptr[20];과 사실상 동일한 의미

물론 (int *)이 없어도 문제없이 할당이 되기는 합니다. 왜냐하면 컴파일러에 의해서 묵시적으로 변환이 되기 때문입니다. (int *)을 붙여주는 이유는 순전히 명시적으로 변환됨을 표현하기 위해서 입니다.

void 포인터를 사용하면 다음과 같이 사용하는 것이 가능해지게 됩니다.

#include <stdio.h>

int main(void) {
    int  x     = 4;
    void *ptrA = &x;
    
    int *ptrB = ptrA;                // 묵시적인 형  변환이 일어납니다.
    int *ptrC = (char *)ptrB;        // Warning을 발생 시킵니다.
    printf("%d %d\n", *ptrB, *ptrC); // 4 4가 나오게 됩니다.
    return 0;
}

정확히 void라는 것은 nothing. 즉, 형type이 없음을 나타냅니다.

이런 포인터를 사용할 때 유의사항으로는 만약에 누군가가 Read-only 위치를 쓰게 된다면 segfault가 발생하게 됩니다. 예를 들어, char *s= "Hello World"의 경우에는 메모리상의 텍스트 세그먼트에 해당하게 됩니다. 이런 상황에서 *s = 'h'로 하게 되면 segfault가 발생하게 됩니다.

또 다른 유의사항으로는 SIGBUS가 발생하는 경우가 있습니다. 이는 지정된 int 형의 포인터에 대해서 char 형 5 바이트를 할당을 하는 경우에 4 바이트씩 읽는 int 형 포인터에는 읽는 데 문제를 발생을 시키게 됩니다. 예를 들어 아래와 같이 작성을 하도록 하겠습니다.

/************
 * 만약에 integer pointer를 1 byte 단위로 이동을 하고 싶다면
 * char를 사용해야 합니다.
 ************/
#include <stdio.h>
#include <stdlib.h>

int main(void){
    
#if defined(__GNUC__) 
# if defined(__i386__) 
    /* Enable Alignment Checking on x86 */
    __asm__("pushf\norl $0x40000,(%esp)\npopf"); 
# elif defined(__x86_64__)  
     /* Enable Alignment Checking on x86_64 */
    __asm__("pushf\norl $0x40000,(%rsp)\npopf"); 
# endif 
#endif 
    int *iptr; // integer pointer
    char *cptr; // character pointer

    // malloc() always provides aligned memory << This is very important!!
    // Increment the pointer by one, making it misaligned
    cptr = malloc(sizeof(int) + 1);

    iptr = (int*)++cptr;
    // dereference it as an int pointer, causing an unaligned access
    *iptr = 42;
    printf("%d\n", *iptr);
    return 0;
}

Alignment를 체크를 하는 이 소스를 실행을 하게 되면 SIGBUS가 발생을 하는 것을 알 수 있습니다. 이런 문제가 발생하는 이유는 메모리에서 레지스터로 값을 가져올 때에 32 비트의 컴퓨터에서는 4 바이트 단위로 가져옵니다. 위 코드에서 cptr은 malloc을 통해 0 ~ 4번지를 할당을 받습니다. 이 경우에 malloc은 0 ~ 3번지와 4 ~ 7번지를 읽어서 0 ~ 4 번지를 할당을 해주도록 합니다.

이렇게 할당된 cptr은 0번지를 먼저 가지고 있습니다. 이 경우 ++cptr을 해주변 1번지를 가지고 iptr에 의해서 4 바이트가 읽어지면서 1번지 ~ 4번지를 가져오도록 합니다. 이런 경우에 alignment가 맞지 않게 되고, SIGBUS가 발생하게 됩니다. 이런 문제도 조심을 해야 합니다.

malloc의 사용

그렇다면 본격적으로 malloc을 사용해보도록 하겠습니다. 아래와 같은 소스 코드를 작성해주시길 바랍니다.

#include <stdio.h>
#include <stdlib.h>

int main(void){
    int *student_id; // saved in garbage data
    student_id = (int *)malloc(sizeof(int) * 3); // 12 bytes가 할당됩니다.
    // *(student_id + 1) == student_id[1]

    if (student_id == NULL) { // 메모리 할당을 실패한 경우
        fprintf(stderr, "Out of memory\n");
    }
    
    student_id[0] = 1;
    student_id[1] = 2;
    student_id[2] = 3;

    printf("%d\n", student_id[0]);
    printf("%d\n", student_id[1]);
    printf("%d\n", student_id[2]);
    
    free(student_id);
    return 0;
}

위의 코드는 정수형 배열을 동적으로 할당하는 코드에 해당합니다. student_id는 정수형 포인터로 정수형 데이터에 대한 주소를 받을 수 있습니다.

이런 포인터에 대해서 정수형 3개짜리 배열을 만든다고 하면 malloc의 매개 변수의 크기로 먼저 정수 크기인 sizeof(int)를 써서 4 바이트임을 알리고, 3을 곱해주어서 총 12 바이트를 할당을 받게 만들어줍니다.

다음에 할당된 메모리는 void * 로 반환이 되므로 int * 형식으로 명시적으로 변환을 표기해주도록 합니다.

그리고 이렇게 만들어진 동적 배열에 대해서 접근은 student_id[1] 또는 *(student_id + 1)과 같이 해주도록 합니다. 둘의 의미는 동치equivalent이기에 어느 것을 사용해도 괜찮습니다.

다음으로 이런 배열 할당뿐만 아니라 구조체에서도 이를 사용할 수 있습니다.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct student {
    int student_id;
    char name[20];
};

int main(void){
    struct student *a_student;
    a_student = (struct student*)malloc(sizeof(struct student));

    a_student->student_id = 123456789;
    strcpy(a_student->name, "BlaCkinkGJ");

    free(a_student);
    return 0;
}

거의 배열과 동일한 방식으로 할당을 할 수 있습니다. 특이사항으로는 일반적인 정적 구조체와 달리 동적 구조체의 경우에는 ->를 사용을 해서 멤버에 접근을 해야 한다는 것입니다. 그리고 이러한 구조체를 복사를 하는 경우에는 매우 주의해야 할 것이 몇 가지가 있습니다.

  1. 구조체 내 포인터의 내용은 복사할 수가 없다.
  2. 정적인 내용을 가지는 구조체의 복사는 되도록이면 memcpy();를 사용해서 해주시길 바랍니다.

1의 경우

먼저 1의 경우를 보도록 하겠습니다.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct student {
    int  id;
    char *name;
};

int main(void) {
    struct student *src, *dst;

    src = (struct student *)malloc(sizeof(struct student));
    dst = (struct student *)malloc(sizeof(struct student));

    src->name = (char *)malloc(sizeof(char)*20);

    src->id = 123456789;
    strcpy(src->name, "BlaCkinkGJ");

    printf("struct location: %p\n"
           "i  d: %d\n"
           "name: %s (location:%p)\n\n",
            src, src->id, src->name, src->name);

    memcpy(dst, src, sizeof(struct student));


    printf("struct location: %p\n"
           "i  d: %d\n"
           "name: %s (location:%p)\n\n",
            dst, dst->id, dst->name, dst->name);

    free(src->name);
    free(src);
    free(dst->name);
    free(dst);
    return 0;
}

이 코드를 실행한 결과는 아래와 같이 나오게 됩니다.

struct location: 0x55c1d22ac260
i  d: 123456789
name: BlaCkinkGJ (location:0x55c1d22ac2a0)

struct location: 0x55c1d22ac280
i  d: 123456789
name: BlaCkinkGJ (location:0x55c1d22ac2a0)

여기서 구조체의 위치는 다르지만 name 포인터가 가리키는 위치는 동일한 것을 알 수 있습니다. 이것은 꽤나 치명적인 문제가 되고, 이렇게 내용 전체가 복사되지 않는 것을 얕은 복사shallow copy라고 합니다. 그렇다면 만약에 src에 대한 free를 dst의 결과를 출력하기 전에 수행을 하면 어떤 일이 벌어지는 지 확인해보겠습니다.

memcpy(dst, src, sizeof(struct student));

free(src->name);
free(src);

printf("struct location: %p\n"
       "i  d: %d\n"
       "name: %s (location:%p)\n\n",
       dst, dst->id, dst->name, dst->name);

위 코드와 같이 먼저 변경을 해줍니다. 그러면 결과는 아래와 같습니다.

struct location: 0x55b23e55d260
i  d: 123456789
name: BlaCkinkGJ (location:0x55b23e55d2a0)

struct location: 0x55b23e55d280
i  d: 123456789
name:  (location:0x55b23e55d2a0)

해당 내용이 free가 되어 아무 내용도 출력하지 않는 것을 알 수 있습니다. 이것이 지금은 간단한 문자열 출력이니 큰 문제가 없지만 좀만 복잡한 프로그램이면 충분히 큰 문제를 야기할 것입니다. 단적인 예입니다. 위의 코드를 부분적으로 수정했습니다.

printf("struct location: %p\n"
       "i  d: %d\n"
       "name: %s (location:%p)\n\n",
       dst, dst->id, dst->name, dst->name);

if(!strcmp(dst->name, "BlaCkinkGJ"))
    fputs("I found!\n", stdout);
else
    fputs("I can't find...\n", stdout);

free(src->name);
free(src);

이 경우 결과는 “I found!”가 나옵니다. 하지만 만약에 free의 위치를 아래와 같이 변경을 한 경우를 생각해보겠습니다.

printf("struct location: %p\n"
       "i  d: %d\n"
       "name: %s (location:%p)\n\n",
       dst, dst->id, dst->name, dst->name);

free(src->name);
free(src);

if(!strcmp(dst->name, "BlaCkinkGJ"))
    fputs("I found!\n", stdout);
else
    fputs("I can't find...\n", stdout);

이 경우 결과로 “I can’t find…“가 나옵니다. 결과적으로, 우리는 “I found!”를 기대했지만 실질적으로 다른 것이 나오는 논리적 오류를 나타나게 됩니다. 따라서 이런 copy를 수행을 할 때에는 깊은 복사deep copy를 수행해서 위와 같은 문제를 해결하도록 해야 합니다. 즉, 다음과 같이 코드를 작성하도록 해야 합니다.

printf("struct location: %p\n"
       "i  d: %d\n"
       "name: %s (location:%p)\n\n",
       src, src->id, src->name, src->name);

dst->id = src->id;
dst->name = (char *)malloc(sizeof(char)*20);
strcpy(dst->name, src->name);

printf("struct location: %p\n"
       "i  d: %d\n"
       "name: %s (location:%p)\n\n",
       dst, dst->id, dst->name, dst->name);

조금 기존보다는 더러워졌지만 이렇게 한 후에 결과를 위와 같이 free(src); free(src->name)을 수행하고 결과를 돌리면 아래와 같이 나옵니다.

struct location: 0x557f16f3c260
i  d: 123456789
name: BlaCkinkGJ (location:0x557f16f3c2a0)

struct location: 0x557f16f3c280
i  d: 123456789
name: BlaCkinkGJ (location:0x557f16f3c6d0)

I found!

결과적으로, 논리적 문제를 해결할 수 있습니다.

2의 경우

이는 구조체 내부에 정적인 내용만 있는 경우에 해당합니다. 즉, 아래와 같은 구조체인 경우에 해당합니다.

struct student {
    int  id;
    char name[20];
};

이런 경우에는 memcpy를 쓰는 게 =보다 안전합니다. 예를 들어, 아래와 같은 경우를 보도록 하겠습니다.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct student {
    int  id;
    char name[20];
};

int main(void) {
    struct student src = {
        .id   = 123456789,
        .name = "Hello World!" 
    };
    struct student dst;
    dst = src;
    printf("struct location: %p\n"
           "i  d: %d\n"
           "name: %s (location:%p)\n\n",
            &src, src.id, src.name, &src.name[0]);

    printf("struct location: %p\n"
           "i  d: %d\n"
           "name: %s (location:%p)\n\n",
            &dst, dst.id, dst.name, &dst.name[0]);
    return 0;
}

이 경우에는 문제없습니다. 결과는 아래와 같이 나옵니다.

struct location: 0x7ffc44095e50
i  d: 123456789
name: Hello World! (location:0x7ffc44095e54)

struct location: 0x7ffc44095e70
i  d: 123456789
name: Hello World! (location:0x7ffc44095e74)

여기까지는 좋습니다. 하지만 만약에 이런 경우에는 어떻게 될까요?

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct student {
    int  id;
    char name[20];
};

int main(void) {
    struct student *src = (struct student*)malloc(sizeof(struct student));
    struct student *dst = (struct student*)malloc(sizeof(struct student));
    
    src->id = 123456789;
    strcpy(src->name, "Hello World!");
    dst = src;
    printf("struct location: %p\n"
           "i  d: %d\n"
           "name: %s (location:%p)\n\n",
            src, src->id, src->name, src->name);

    printf("struct location: %p\n"
           "i  d: %d\n"
           "name: %s (location:%p)\n\n",
            dst, dst->id, dst->name, dst->name);
    free(src);
    free(dst);
    return 0;
}

우리는 아무 생각 없이 src를 dst에 대입했지만 결과는 처참하게 나옵니다.

struct location: 0x55cc7fa04260
i  d: 123456789
name: Hello World! (location:0x55cc7fa04264)

struct location: 0x55cc7fa04260
i  d: 123456789
name: Hello World! (location:0x55cc7fa04264)

동일한 구조체를 가리키게 됩니다. 이런 경우라면 우리는 dst를 고침에도 불구하고 src의 내용까지 변경되는 문제가 발생할 수 있습니다. 따라서 이를 해결하려면 *dst = *src라고 하면 됩니다. 하지만 이들에서 알 수 있지만 이런 방식은 너무 많은 것을 감추게 되기에 사용자로 하여금 혼란을 야기합니다.

따라서 정적인 구조체에 대해서는 memcpy(&dst, &src, sizeof(struct student));라고 하여 할당하고, 동적인 구조체에 대해서는 memcpy(dst, src, sizeof(struct student));로 할당해주도록 합니다. 이러면 좀 더 명시적으로 어떤 복사가 일어나고 그 크기는 어느 정도인지를 알 수 있게 됩니다.



UNIX Share Tweet +1