Overview
프로그램을 작성하고 실행할 때, 내부적으로 메모리가 어떻게 배치되고 관리되는지를 이해하는 것은
성능 최적화는 물론, 버그 예방과 디버깅 능력 향상에도 직결된다.
특히 C/C++, Rust, 또는 저수준 시스템 프로그래밍을 다루는 개발자라면,
포인터 오류, 세그멘테이션 폴트, 메모리 누수와 같은 문제를 정확히 파악하고 원인을 추적할 수 있어야 한다.
이 글에서는 시스템의 관점에서 프로세스 메모리 구조를 살펴보고,
Stack과 Heap 메모리의 차이점, 그리고 실제로 자주 마주치는 버그 사례와 메모리 디버깅 도구들을 정리해본다.

📅 관련 글
2023.01.13 - [CS 지식] - [CS 지식1.] 웹 브라우저의 동작원리
2023.02.23 - [CS 지식] - [CS 지식2.] DNS의 동작원리(Domain Name System)
2023.03.06 - [CS 지식] - [CS 지식3.] HTTP / HTTPS 란?
2023.03.07 - [CS 지식] - [CS 지식4.] OSI 7계층 & TCP/IP 4계층이란?
2023.03.17 - [CS 지식] - [CS 지식5.] 가상화란?
2023.05.24 - [CS 지식] - [CS 지식6.] HTTP 메서드(Method)란? / HTTP Status Code
2023.12.05 - [CS 지식] - [CS 지식7.] Kubernetes 구성요소와 Pod 생성 방식이란?
2023.12.19 - [CS 지식] - [CS 지식8.] 프로세스(Process)와 스레드(Thread)란?
2023.12.30 - [CS 지식] - [CS 지식9.] 클라우드 컴퓨팅이란?(Public & Private Cloud / IaaS SaaS PaaS / Multitenancy)
2024.01.05 - [CS 지식] - [CS 지식10.] 웹1.0(Web1.0) vs 웹2.0(Web2.0) vs 웹3.0(Web3.0)
2024.02.02 - [CS 지식] - [CS 지식11.] NAT(Network Address Translation)란?
2024.05.22 - [CS 지식] - [CS 지식13.] 동기 및 비동기 처리란?
2024.05.23 - [CS 지식] - [CS 지식14.] 3tier 아키텍처란?
2024.08.28 - [CS 지식] - [CS 지식15.] SSR vs CSR vs ISR vs SSG
2024.11.09 - [CS 지식] - [CS 지식16.] stdin(표준입력) vs stdout(표준출력) vs stderr(표준에러)
2024.11.11 - [CS 지식] - [CS 지식17.] IPsec vs SSL/TLS
2024.11.22 - [CS 지식] - [CS 지식18.] Quantum Computing(양자 컴퓨팅)
2025.04.01 - [CS 지식] - [CS 지식19.] C/C++ 개발자도 다시 보는 메모리 구조
메모리 구조 (Memory Layout)
C/C++ 프로그램이 실행되면 운영체제는 프로세스마다 메모리를 다음과 같은 영역으로 나누어 할당한다.
- Text 영역 (코드 영역)
- 실행할 프로그램 코드가 저장됨
- 보통 읽기 전용이며, 실행 권한이 있음
- Data 영역
- 초기화된 전역 변수, static 변수 저장
- BSS 영역
- 초기화되지 않은 전역 변수, static 변수 저장
- Heap 영역
- malloc, new 등 동적 메모리 할당 시 사용됨
- 런타임 중 크기 조절 가능
- Stack 영역
- 함수 호출 시 지역 변수, 매개변수, 복귀 주소 등을 저장
- 후입선출(LIFO) 구조
Stack vs Heap
| 구분 | Stack | Heap |
| 메모리 위치 | 낮은 주소에서 높은 주소로 자람 | 높은 주소에서 낮은 주소로 자람 |
| 할당 시점 | 컴파일 타임 또는 함수 호출 시 | 런타임 (실행 중) |
| 할당 방식 | 자동 (함수 호출 시 자동 할당/해제) | 수동 (개발자가 직접 해제 필요) |
| 속도 | 빠름 | 느림 |
| 크기 제한 | 상대적으로 작음 | 상대적으로 큼 |
| 누수 가능성 | 거의 없음 | 메모리 누수 가능 |
실무에서의 사례
Stack Overflow
재귀 함수의 종료 조건을 잘못 설계하거나, 너무 큰 지역 배열을 선언할 때 발생
void recurse() {
int arr[1000000]; // 스택 공간 부족
recurse();
}
Use After Free
이미 `free()` 된 메모리를 사용하는 실수
int* p = malloc(sizeof(int));
free(p);
*p = 10; // 정의되지 않은 동작
Memory Leak
`malloc` 등으로 할당 후 `free` 하지 않아 계속 메모리를 잡아먹는 상황
char* str = malloc(100);
// str 사용 후 free 생략 → 누수 발생
Dangling Pointer
해제된 메모리를 참조하는 포인터를 잘못 사용하는 경우
메모리 디버깅 팁
- `valgrind` : 런타임 메모리 오류, 누수 탐지
- `gdb` : 스택 프레임, 포인터 주소 확인
- `ASAN (AddressSanitizer)` : 클랭/LLVM 기반 메모리 오류 감지 도구
동적 메모리 재할당 (realloc)
`realloc()` 은 이미 할당된 메모리 블록의 크기를 동적으로 변경할 때 사용된다.
하지만 `realloc()` 호출 후 반환된 포인터는 원래 포인터와 다를 수 있으므로, 원본 포인터를 무조건 대체해야 한다:
char *data = malloc(10);
data = realloc(data, 100); // 반드시 반환값으로 덮어쓰기
함수 내 Stack 메모리의 주의점
로컬 배열의 주소를 반환하면 Dangling Pointer 문제가 발생할 수 있다.
char* dangerous() {
char temp[100];
return temp; // 함수 종료와 함께 temp는 소멸
}
Heap Fragmentation (조각화)
많은 `malloc()/free()` 호출이 반복되면 힙 메모리에 빈 공간이 쪼개져 남아, 할당 가능한 총 공간은 충분해도 큰 블록은 할당하지 못하는 현상이 발생할 수 있다.
해결법: 메모리 풀, 가비지 컬렉션, slab allocator 사용
Modern C++의 RAII 패턴
`new/delete` 기반 메모리 관리를 직접 하지 않고, 스마트 포인터(`std::unique_ptr, std::shared_ptr`)를 사용하는 것이 현대 C++ 스타일이다.
std::unique_ptr<int> ptr = std::make_unique<int>(10); // 자동 해제
메모리 누수 방지 패턴
// RAII 스타일의 C 구현
typedef struct {
char* data;
size_t size;
} Buffer;
Buffer* buffer_create(size_t size) {
Buffer* buf = malloc(sizeof(Buffer));
if (!buf) return NULL;
buf->data = malloc(size);
if (!buf->data) {
free(buf);
return NULL;
}
buf->size = size;
return buf;
}
void buffer_destroy(Buffer** buf) {
if (buf && *buf) {
free((*buf)->data);
free(*buf);
*buf = NULL; // 댕글링 포인터 방지
}
}
큰 데이터 처리 시 고려사항
// 스트리밍 방식으로 큰 파일 처리
FILE* fp = fopen("large_file.txt", "r");
char buffer[4096]; // 작은 버퍼로 청크 단위 처리
while (fgets(buffer, sizeof(buffer), fp)) {
// 청크별 처리
}
fclose(fp);
메모리 정렬(Memory Alignment)과 패딩
구조체 패딩의 영향
struct BadExample {
char a; // 1바이트
int b; // 4바이트 (3바이트 패딩 발생)
char c; // 1바이트 (3바이트 패딩 발생)
}; // 총 12바이트
struct GoodExample {
int b; // 4바이트
char a; // 1바이트
char c; // 1바이트 (2바이트 패딩)
}; // 총 8바이트
최적화 팁
- 큰 타입부터 작은 타입 순으로 배치
- `__attribute__((packed))` 사용 시 성능 트레이드오프 고려
- 캐시 라인 크기(보통 64바이트)를 의식한 구조체 설계
가상 메모리와 페이지 시스템
페이지 폴트 처리
// 큰 배열 선언 시 실제 메모리는 접근할 때 할당됨
char huge_array[1000000]; // 가상 메모리만 예약
huge_array[0] = 'A'; // 이 시점에서 실제 페이지 할당
메모리 매핑 활용
#include <sys/mman.h>
void* addr = mmap(NULL, size, PROT_READ|PROT_WRITE,
MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
// 큰 파일 처리 시 read/write보다 효율적
멀티스레딩 환경에서의 메모리 관리
스레드별 스택 공간
// 각 스레드마다 독립적인 스택 공간
void* thread_func(void* arg) {
char local_buffer[1024]; // 각 스레드별로 별도 할당
// ...
}
주의사항
- 스레드 간 스택 영역 공유 불가
- 힙 영역은 모든 스레드가 공유
- 동기화 없이 힙 메모리 접근 시 레이스 컨디션 발생 가능
메모리 접근 패턴과 성능
캐시 친화적 코드
// 나쁜 예: 열 우선 접근
for (int j = 0; j < COLS; j++) {
for (int i = 0; i < ROWS; i++) {
matrix[i][j] = 0; // 캐시 미스 빈발
}
}
// 좋은 예: 행 우선 접근
for (int i = 0; i < ROWS; i++) {
for (int j = 0; j < COLS; j++) {
matrix[i][j] = 0; // 캐시 히트율 높음
}
}
메모리 지역성 원칙
- 시간적 지역성: 최근 접근한 데이터를 다시 접근할 가능성
- 공간적 지역성: 인접한 메모리 위치를 연속으로 접근
보안 관련 메모리 보호
스택 카나리(Stack Canary)
# 컴파일 시 스택 보호 활성화
gcc -fstack-protector-all program.c
버퍼 오버플로우 방지
// 위험한 함수 대신 안전한 함수 사용
char buffer[100];
// strcpy(buffer, user_input); // 위험
strncpy(buffer, user_input, sizeof(buffer) - 1); // 안전
buffer[sizeof(buffer) - 1] = '\0';
메모리 풀링 패턴
고정 크기 메모리 풀
ctypedef struct {
char data[BLOCK_SIZE];
int in_use;
} MemoryBlock;
MemoryBlock pool[POOL_SIZE];
void* pool_alloc() {
for (int i = 0; i < POOL_SIZE; i++) {
if (!pool[i].in_use) {
pool[i].in_use = 1;
return pool[i].data;
}
}
return NULL; // 풀이 가득 참
}
장점
- 할당/해제 시간 예측 가능
- 메모리 단편화 방지
- 실시간 시스템에 적합
추가 디버깅 도구와 기법
메모리 사용량 모니터링
# 실행 중 메모리 사용량 확인
ps aux | grep process_name
top -p process_id
# 메모리 맵 확인
cat /proc/[pid]/maps
정적 분석 도구
# 정적 분석으로 메모리 이슈 미리 발견
cppcheck --enable=all program.c
scan-build gcc program.c
성능 측정과 프로파일링
메모리 할당 성능 측정
#include <time.h>
clock_t start = clock();
for (int i = 0; i < 1000000; i++) {
void* ptr = malloc(100);
free(ptr);
}
clock_t end = clock();
double time_spent = ((double)(end - start)) / CLOCKS_PER_SEC;
printf("Time spent: %f seconds\n", time_spent);
메모리 사용량 프로파일링
# 힙 사용량 상세 분석
valgrind --tool=massif ./program
ms_print massif.out.xxx
마무리
Stack과 Heap은 단순한 저장 위치의 차이를 넘어서, 프로그램의 실행 구조와 자원 관리 전략에 깊이 연결된 개념이다.
특히 C/C++과 같이 메모리 수동 관리 언어를 다루는 개발자라면,
Stack Overflow, 메모리 누수, Use After Free와 같은 메모리 이슈를 방지하고 디버깅하는 능력이 필수적이다.
운영체제와 컴파일러는 점점 더 많은 보호 장치를 제공하고 있지만, 여전히 많은 버그와 보안 취약점은 메모리에서 발생한다.
따라서 아래와 같은 태도를 갖는 것이 중요하다.
- 항상 free()나 delete를 빠뜨리지 않도록 주의하고,
- 메모리 사용 전 초기화하고, 사용 후 해제하며,
- 디버깅 도구(valgrind, gdb, ASAN 등)를 적극적으로 활용하자.
안정적인 시스템은, 보이지 않는 메모리 위에 세워진다.
Reference
- https://en.wikipedia.org/wiki/Memory_management
- https://learn.microsoft.com/en-us/cpp/c-runtime-library/memory-allocation
- https://valgrind.org/
- https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_Management (타 언어 비교용)
- https://en.cppreference.com/w/cpp/memory/unique_ptr
- https://en.cppreference.com/w/cpp/memory/shared_ptr
'CS 지식' 카테고리의 다른 글
| [CS 지식21.] top에 나오는 load average, 진짜 뜻은? (2) | 2025.08.27 |
|---|---|
| [CS 지식20.] OS 캐시와 디스크 I/O: MySQL, Redis 퍼포먼스 분석 (3) | 2025.08.13 |
| [CS 지식18.] Quantum Computing(양자 컴퓨팅) (2) | 2024.11.22 |
| [CS 지식17.] IPsec vs SSL/TLS (0) | 2024.11.11 |
| [CS 지식16.] stdin(표준입력) vs stdout(표준출력) vs stderr(표준에러) (0) | 2024.11.09 |