들어가기에 앞서 malloc에 대해 짧게 설명하자면, malloc은 힙(heap)이라는 메모리 영역에서 동적으로 메모리를 할당받는 C 표준 라이브러리 함수다.
스택 변수와 다르게, 힙 메모리는:
- 프로그램이 런타임에 크기를 결정할 수 있음 (컴파일 타임에 크기가 고정되는 배열과 다름)
- 명시적으로 free()를 호출하기 전까지 계속 살아있음 (함수가 끝나도 사라지지 않음)
- 힙 관리자(heap manager)라는 별도의 소프트웨어가 이 영역을 추적하고 관리함 (OS가 직접 관리하는 게 아니라, CRT/힙 라이브러리가 OS로부터 큰 덩어리를 받아와서 잘게 쪼개 나눠주는 방식)
라느 특징을 가진다.
malloc 내부 구조
malloc(12)를 호출하면 힙 관리자는 단순히 "12바이트를 뚝 떼어서 준다"가 아니라, 그 블록을 관리하기 위한 메타데이터를 함께 만든다. 대략적인 구조는 이렇다 (구현마다 세부는 다름)
[헤더: 블록 크기, 사용중/free 여부, 이전·다음 블록 링크 등 관리정보]
[정렬용 패딩 (필요 시)]
[실제 데이터 영역: 내가 요청한 12바이트] ← p가 가리키는 지점
[정렬용 패딩 (필요 시, 다음 블록 시작 위치 맞추기 위함)]
- 헤더: 힙 관리자가 이 블록의 크기, 상태(사용중/free), free list 연결 정보 등을 기록해두는 영역. free(p)를 호출하면 힙 관리자는 p를 기준으로 앞쪽에 있는 이 헤더를 읽어서 "아, 이 블록은 몇 바이트짜리구나" 판단하고 free list에 돌려놓는다.
- 데이터 영역: 실제로 내가 쓸 수 있는 12바이트. p는 정확히 이 영역의 시작 주소를 가리킨다.
- 패딩: 정렬 요구사항을 맞추기 위해 붙는 여유 공간 (아래에서 자세히).
정렬은 항상 보장될까?
C 표준은 malloc이 반환하는 주소가 어떤 타입으로 캐스팅해도 안전한 정렬을 만족하도록 요구한다. x64에서는 보통 16바이트 정렬이다. 즉, malloc이 반환하는 주소값 자체가 항상 16의 배수가 되도록 힙 관리자가 보장해준다는 뜻이다. 이건 "요청 크기"까지 항상 정렬 단위로 올리는 건 아니다. 힙 관리자 구현 방식은 크게 두 가지로 나뉜다.
방식 A: Size class(bin) 기반 — glibc malloc, Windows 힙 관리자 등 대부분
작은 요청은 미리 정해진 "크기 등급"에 배정됨: 16, 24, 32, 48, 64, 96, 128, ...
malloc(12) → 가장 작은 등급(예: 16 또는 24)에 배정
malloc(20) → 다음 등급(예: 32)에 배정
정렬을 위해 올린다기보다, 메모리 단편화 방지와 관리 효율을 위해 미리 정해둔 "그릇 크기"에 담는 것. malloc(12)와 malloc(15)가 같은 등급에 들어가 _msize 결과가 동일할 수 있음.
방식 B: 헤더 오버헤드가 정렬 패딩 역할
[헤더 8~16바이트][요청한 데이터 영역][정렬 맞추기용 패딩]
블록마다 개별적으로 전체 크기가 정렬 단위(8/16)의 배수가 되도록 패딩을 끼워넣는 방식.
_msize가 알려주는 건 "물리적 크기"가 아니다
_msize는 Windows CRT가 제공하는 비표준 함수로, "이 포인터가 가리키는 힙 블록이 얼마나 크냐"를 알려주는 용도로 흔히 소개된다.
char* p = (char*)malloc(12);
printf("%d", _msize(p));
이걸 실제로 Release 모드에서 돌려보면 12가 그대로 나온다. (Debug 모드에서도 12가 나온다.)
왜 물리적으로는 더 크게 잡았을 수 있는데 _msize는 12를 돌려주는가?
Windows 힙 관리자는 블록마다 다음 두 가지를 별도로 추적한다:
- 실제 물리적으로 확보한 블록 크기 (정렬/size class 때문에 12보다 클 수 있음, 예: 16)
- UnusedBytes — "이 블록에서 실제로 안 쓰이는 뒷부분이 몇 바이트인지"를 정확히 기록
그리고 _msize(내부적으로 HeapSize API 호출)는 다음과 같이 계산해서 반환한다:
usable_size = 물리적_확보_크기 - UnusedBytes
= 16 - 4
= 12 ← 요청한 크기 그대로 나옴
결론
_msize는 물리적 블록 크기가 아니라, 힙 관리자가 별도로 계산해서 알려주는 "사용 가능한(usable) 크기"를 반환한다. 이 값은 대체로 요청한 크기와 동일하게 나오며, 내부 패딩량을 반영하지 않는다. 즉, _msize로는 "실제 물리적으로 몇 바이트가 확보되어 있는지" 알아낼 방법이 없다. 이건 힙 구현이 완전히 캡슐화해서 감춰버린 정보다.
만약 12바이트를 할당받고 13바이트를 쓴다면?
직관적으론 "12바이트는 정리되고 1바이트만 쓰레기값으로 남는다"라고 생각할 수 있겠지만, 앞서 말했듯 malloc(12)를 호출하면 힙 관리자는 단순히 "12바이트를 뚝 떼어서 준다"가 아니라, 그 블록을 관리하기 위한 메타데이터를 함께 만든다.따라서 넘어간 바이트가 어디를 침범했냐에 따라 결과가 달라진다.
| 넘어간 위치 | free(p) 시점 | 나중 결과 |
| 정렬/size-class 여유 공간 내부 (내 블록 소유) | 문제없음 | 우연히 안전 — 재현 안 되는 시한폭탄 |
| 옆 블록 헤더 (메타데이터) | 문제없이 통과 가능 | 옆 블록 malloc/free 시 크래시·힙 손상 에러 |
| 옆 블록 데이터 (사용 중) | 문제없음 | 그 데이터를 쓰는 코드에서 값 오염 발견 — 조용한 논리 버그 |
어느 경우든 오버플로우가 발생한 순간에는 아무 에러도 나지 않는다. 버그를 심는 시점과 증상이 터지는 시점이 완전히 분리되어 디버깅이 매우 어렵다. 앞서 본 것처럼 _msize로도 이 여유 공간의 존재 여부를 미리 알 방법이 없으니, "여유 공간이 있으니 조금 넘쳐도 괜찮겠지"라는 가정 자체가 애초에 성립하지 않는다.
'공부' 카테고리의 다른 글
| [C++] STL vector - size/capacity부터 iterator invalidation까지 (0) | 2026.08.02 |
|---|---|
| const int* vs int* const 차이 (0) | 2026.07.26 |
| strlen, strcpy, strcmp / memcpy, memcmp 구현 (0) | 2026.07.11 |
| 스파이럴(달팽이) 배열 구현하기 - 방향배열(dx, dy) 방식 (0) | 2026.07.09 |
| 스파이럴(달팽이) 배열 구현하기 - 경계값(Boundary) 방식 (0) | 2026.07.08 |