C언어의 매력은 매모리를 직접 관리할 수 있다는 점이다.
하지만 자유도가 높은 만큼 메모리 누수, 이중 해제, 댕글링 포인터, 힙 오버플로우 같은 문제들도 동적 메모리 관리 실수에서 시작된다. 이번 글에서는 필자가 직접 C언어 개발을 하며 터득한 메모리 관리 방법을 설명하도록 하겠다.
malloc은 사이즈만큼의 연속된 메모리 블록을 요청한다.
성공시 할당된 메모리의 시작 주소를 반환하고, 실패시 Null값은 반환한다.
실제 시스텀에서는 malloc() 호출시 내부적으로 힙 영역을 사용하며, 필요하면 커널에 brk() 또는 mmap() 시스템콜을 요청한다.
malloc 또는 realloc으로 받은 메모리를 반환한다. 이후 해당 포인터는 사용이 불가능하다.
이때 해제를 잘 하지 않으면 메모리 누수가 발생하게 된다. 또한 같은 포인터를 두번 해제하거나 이미 해제된 포인터를 사용하면 에러가 발생하게 되니 이 점을 주의해야한다.
realloc은 이미 할당된 메모리 블록의 크기를 늘리거나 줄인다. 내부적으로 새 블록을 할당하고, 데이터를 복사 후 기존 블록을 해제할 수도 있다. 만약 realloc이 실패하면 Null을 반환하지만 기존 블록은 그대로 남아있다.
calloc은 malloc과 같지만 할당된 값을 0으로 초기화 시켜 보안적으로 안전한 초기값을 보장한다는 특징이 있다.
malloc 함수는 크게 다음과 같은 흐름으로 동작한다.
이 내용에 대해서는 다른 글로 더 자세하게 설명하도록 하겠다.
외부 단편화는 여러 개의 작은 해제 블록이 흩어져 있어 총 공간은 충분해도 큰 연속 블록을 만들 수 없는 상황이다.
요청한 크기보다 더 큰 블록을 할당받은 경우에 남는 공간이 낭비되는 상황이다.
메모리의 단편화가 생기면 메모리를 낭비하게 되기 때문에 이러한 단편화를 줄여야 한다.
단편화를 줄이는 대표적인 기법은 메모리 풀, 슬랩 할당자, 버디 시스템 등이 있다.
메모리 풀은 실제로 고성능 서버, 게임엔진 등에서 필수적으로 사용된다.
동적할당 대신 미리 만들어둔 메모리 덩어리에서 빠르게 꺼내 쓰고 반납하는 것이다.
다음은 간단한 메모리 풀 예시이다.
#define POLL_SIZE 1024
char polll[POLL_SIZE];
size_t offset = 0;
void* pool_alloc (size_t size){
if (offset + size > POLL_SIZE) return NULL;
void* ptr = &pool[offset];
offset += size;
return ptr;
}
컴파일할때 -fsanitize=address
옵션을 추가하면 런타임 메모리 오류를 즉시 탐지한다.
gcc -fsanitize=address -g myprogram.c -o myprogram
./myprogram
해당 옵션으로 컴파일을 하면 use after free
, heap buffer overflow
, stack buffer overflow
, Memory Leak
같은 버그들을 컴파일러가 잡아준다.
valgrind --leak-check+full ./myprogram
발그라인드를 사용하면 런타임 메모리 오류와 누수를 감지한다.
하지만 실행 속도가 느리다.
그럼 필자는 다음 글로 돌아오겠다.