프로그램이 실행될 때 운영체제는 메모리를 어떻게 관리할까요? 이번 포스팅에서는 프로그램 실행 시 메모리가 어떻게 구성되고 동작하는지 깊이 있게 알아보겠습니다.
메모리 구조의 4가지 영역
프로그램이 실행되면 운영체제는 메모리를 크게 4개의 영역으로 나누어 관리합니다.
1. Text 영역 (Code 영역)
Text 영역은 실행 가능한 코드가 저장되는 공간입니다.
1.1. 특징
- 프로그램의 실제 명령어(기계어)가 저장됩니다.
- 함수, 제어문(if, for, while 등), 상수 리터럴이 포함됩니다.
읽기 전용(Read-Only)영역으로 설정되어 코드 변조를 방지합니다.- 프로그램 실행 중 크기가 변하지 않는 정적 영역입니다.
- 여러 프로세스가 같은 프로그램을 실행할 경우 Text 영역을 공유할 수 있습니다.
2. Data 영역
Data 영역은 프로그램 전체에서 접근 가능한 전역 변수와 정적 변수가 저장되는 공간입니다.
2.1. 특징
- 프로그램 시작 시 할당되고 종료 시 해제됩니다.
- 프로그램 실행 동안 크기가 고정됩니다.
- 프로세스가 종료될 때까지 메모리에 유지됩니다.
2.2. 세부 구분
Initialized Data Segment (초기화된 데이터 영역)
- 명시적으로 초기화된 전역 변수와 static 변수가 저장됩니다.
- 예:
int global_var = 10;,static int count = 0;
Uninitialized Data Segment (BSS, Block Started by Symbol)
- 초기화되지 않았거나 0으로 초기화된 전역/정적 변수가 저장됩니다.
- 예:
int global_var;,static int count; - 실제 프로그램 파일 크기를 줄이기 위해 별도로 관리됩니다.
3. Heap 영역
Heap은 프로그래머가 동적으로 메모리를 할당하고 해제할 수 있는 영역입니다.
3.1. 특징
- 런타임에 크기가 결정되는 데이터를 저장합니다.
- C에서는
malloc(),calloc()등으로 할당하고free()로 해제합니다. - C++에서는
new연산자로 할당하고delete로 해제합니다. - 낮은 주소에서 높은 주소 방향으로 메모리가 확장됩니다.
- 메모리 누수(Memory Leak)의 주범: 할당 후 해제하지 않으면 메모리가 계속 쌓입니다.
동적 할당이 필요한 이유?
컴파일 타임에 크기를 알 수 없는 데이터(예: 사용자 입력에 따른 배열, 가변 크기 자료구조)를 다루기 위해서입니다.
4. Stack 영역
Stack은 함수 호출과 관련된 지역 변수와 매개변수가 저장되는 영역입니다.
4.1. 특징
- 높은 주소에서 낮은 주소 방향으로 메모리가 확장됩니다. (Heap과 반대)
- LIFO(Last In First Out) 구조로 동작합니다.
- 함수가 호출되면 스택 프레임(Stack Frame)이 생성됩니다.
- 함수가 종료되면 해당 스택 프레임이 자동으로 정리됩니다.
- 지역변수, 매개변수, 반환주소, 이전 함수의 베이스 포인터 등이 포함됩니다.
스택 오버플로우(Stack Overflow)?
재귀 함수가 너무 깊게 호출되거나 큰 지역 배열을 선언하면 스택 영역을 초과하여 프로그램이 crash될 수 있습니다.
Heap과 Stack의 충돌
Heap은 낮은 주소에서 높은 주소로, Stack은 높은 주소에서 낮은 주소로 자라나기 때문에 서로 반대 방향으로 확장됩니다. 이 두 영역이 만나면 메모리 부족 오류가 발생합니다.
낮은 주소
↓
[ Text ] ← 코드 영역
[ Data ] ← 전역/정적 변수
[ Heap ] ← 동적 할당 (↓ 아래로 확장)
|
| (사용 가능한 메모리)
|
[ Stack ] ← 지역 변수 (↑ 위로 확장)
↑
높은 주소
메모리 주소 체계
32bit vs 64bit 시스템
32bit 시스템
- 메모리 주소를 32비트(4바이트)로 표현합니다.
- 최대 2³² = 4GB의 메모리 공간을 사용할 수 있습니다.
- 포인터 변수의 크기: 4바이트
64bit 시스템
- 메모리 주소를 64비트(8바이트)로 표현합니다.
- 이론상 2⁶⁴ = 16EB(엑사바이트)의 메모리 공간을 사용할 수 있습니다.
- 포인터 변수의 크기: 8바이트
메모리의 기본 단위
- 메모리 한 칸의 크기는 1바이트(8비트)입니다.
- 각 칸은 고유한 주소를 가집니다.
- 64bit 시스템에서는 이 주소를 8바이트로 표현합니다.
포인터 크기의 의미?
포인터는 메모리 주소를 저장하는 변수입니다. 따라서 포인터의 크기는 시스템이 메모리 주소를 표현하는 데 필요한 바이트 수와 같습니다.
실습으로 확인하는 메모리 구조
이제 실제 코드를 통해 각 영역이 어떻게 동작하는지 확인해보겠습니다.
예제 1. 메모리 영역별 변수 위치 확인
#include <stdio.h>
#include <stdlib.h>
// Data 영역 - 초기화된 전역 변수
int global_init = 100;
// Data 영역 - 초기화되지 않은 전역 변수 (BSS)
int global_uninit;
// Data 영역 - 정적 변수
static int static_var = 200;
void function() {
// Stack 영역 - 지역 변수
int local_var = 10;
// Stack 영역 - 정적 지역 변수는 Data 영역에 저장됨
static int static_local = 20;
// Heap 영역 - 동적 할당
int* heap_var = (int*)malloc(sizeof(int));
*heap_var = 30;
printf("=== 메모리 주소 확인 ===\n");
printf("Text 영역 (함수): %p\n", (void*)function);
printf("Data 영역 (전역-초기화): %p\n", (void*)&global_init);
printf("Data 영역 (전역-미초기화): %p\n", (void*)&global_uninit);
printf("Data 영역 (정적): %p\n", (void*)&static_var);
printf("Data 영역 (정적지역): %p\n", (void*)&static_local);
printf("Heap 영역: %p\n", (void*)heap_var);
printf("Stack 영역 (지역): %p\n", (void*)&local_var);
free(heap_var);
}
int main() {
function();
return 0;
}
실행결과
- 메모리 주소는 실행 환경에 따라 다르게 나타날 수 있습니다.
- 주소값으로 알 수 있는 점은 Text 가 가장 낮은 주소에 위치해 있으며 Stack 영역이 가장 높은 주소에 위치한걸 알 수 있습니다. (
Text < Data < Heap < Stack)
=== 메모리 주소 확인 ===
Text 영역 (함수): 0x105b89940
Data 영역 (전역-초기화): 0x105b8b000
Data 영역 (전역-미초기화): 0x105b8b00c
Data 영역 (정적): 0x105b8b008
Data 영역 (정적지역): 0x105b8b004
Heap 영역: 0x600002b60030
Stack 영역 (지역): 0x7ff7ba3760bc
예제 2. 포인터 크기 확인
#include <stdio.h>
int main() {
int var = 10;
int* ptr = &var;
printf("int 변수의 크기: %zu bytes\n", sizeof(var));
printf("포인터의 크기: %zu bytes\n", sizeof(ptr));
printf("포인터가 가리키는 주소: %p\n", (void*)ptr);
printf("포인터 자신의 주소: %p\n", (void*)&ptr);
return 0;
}
실행결과
- 64bit 시스템에서의 출력 결과값입니다.
int 변수의 크기: 4 bytes
포인터의 크기: 8 bytes
포인터가 가리키는 주소: 0x7ff7bee950d8
포인터 자신의 주소: 0x7ff7bee950d0
예제 3. Stack 과 Heap 의 확장 방향 확인
#include <stdio.h>
#include <stdlib.h>
void stack_growth() {
int var1, var2, var3;
printf("=== Stack 확장 방향 (높은 주소 → 낮은 주소) ===\n");
printf("var1 주소: %p\n", (void*)&var1);
printf("var2 주소: %p\n", (void*)&var2);
printf("var3 주소: %p\n", (void*)&var3);
}
void heap_growth() {
int* ptr1 = (int*)malloc(sizeof(int));
int* ptr2 = (int*)malloc(sizeof(int));
int* ptr3 = (int*)malloc(sizeof(int));
printf("\n=== Heap 확장 방향 (낮은 주소 → 높은 주소) ===\n");
printf("ptr1 주소: %p\n", (void*)ptr1);
printf("ptr2 주소: %p\n", (void*)ptr2);
printf("ptr3 주소: %p\n", (void*)ptr3);
free(ptr1);
free(ptr2);
free(ptr3);
}
int main() {
stack_growth();
heap_growth();
return 0;
}
실행결과
Stack에서는 주소가 감소하고,Heap에서는 주소가 증가하는 것을 확인할 수 있습니다.
=== Stack 확장 방향 (높은 주소 → 낮은 주소) ===
var1 주소: 0x7ff7baf440bc
var2 주소: 0x7ff7baf440b8
var3 주소: 0x7ff7baf440b4
=== Heap 확장 방향 (낮은 주소 → 높은 주소) ===
ptr1 주소: 0x60000053c030
ptr2 주소: 0x60000053c040
ptr3 주소: 0x60000053c050
예제 4. 메모리 누수(Memory Leak) 확인
#include <stdio.h>
#include <stdlib.h>
void memory_leak_example() {
for (int i = 0; i < 5; i++) {
int* leak = (int*)malloc(sizeof(int) * 1000);
printf("할당 %d: 주소 %p\n", i+1, (void*)leak);
// free(leak)를 호출하지 않음 -> 메모리 누수 발생!
}
}
void proper_memory_management() {
for (int i = 0; i < 5; i++) {
int* ptr = (int*)malloc(sizeof(int) * 1000);
printf("할당 %d: 주소 %p\n", i+1, (void*)ptr);
free(ptr); // 올바른 메모리 해제
}
}
int main() {
printf("=== 메모리 누수 예제 ===\n");
memory_leak_example();
printf("\n=== 올바른 메모리 관리 ===\n");
proper_memory_management();
return 0;
}
실행결과
- 메모리 누수 예제 할당 값을 살펴보면 할당된 주소값이 각각 다르다는걸 알 수 있다. (메모리가 부족해지는 현상이 발생할 수 있음)
free()함수로 사용한 메모리를 해제 해주어 메모리 누수가 발생하지 않은걸 확인할 수 있다.
=== 메모리 누수 예제 ===
할당 1: 주소 0x7fe2a8809200
할당 2: 주소 0x7fe2a880a200
할당 3: 주소 0x7fe2a880b200
할당 4: 주소 0x7fe2a880c200
할당 5: 주소 0x7fe2a880d200
=== 올바른 메모리 관리 ===
할당 1: 주소 0x7fe2a880e200
할당 2: 주소 0x7fe2a880e200
할당 3: 주소 0x7fe2a880e200
할당 4: 주소 0x7fe2a880e200
할당 5: 주소 0x7fe2a880e200
마치며
프로그램의 메모리 구조를 이해하는 것은 단순히 이론적 지식을 넘어 실제 프로그래밍에서 매우 중요한 역할을 합니다. 왜 크래시가 났는지 응답이 들쭉날쭉한지, 왜 GC가 폭주하는지의 근본 원인은 대부분 어느 영역에서 무엇이 할당, 해제, 이동 되는지 이해하면 도움이 된다고 생각합니다.
C/C++에선 포인터와 수명관리가, Java/Node 같은 런타임에선 GC 특성과 객체/클로저의 생존 경로가 핵심입니다. "스택은 빠르고 힙은 느리다" 같은 표면적인 말보다, 내 코드가 지금 메모리 어디를 어떻게 쓰는지를 계측하고 확인하는 습관을 만드려고 합니다.
'CS > 운영체제' 카테고리의 다른 글
| 디스크 스케줄링 알고리즘 (0) | 2025.06.05 |
|---|