프로세스 메모리
이 글은 “프로세스 메모리 구조”를 처음 접하는 사람도 이해할 수 있도록 친절하게 설명합니다.
개요
운영체제에서 실행 중인 프로그램을 “프로세스”라고 부릅니다. 각 프로세스는 실행에 필요한 여러 메모리 구역을 가집니다. 이 문서에서는 가상 주소 공간 기준으로 흔히 보는 주요 영역(코드, 데이터, 힙, 스택 등)을 설명하고, 간단한 예제와 함께 실무에서 주의할 점을 정리합니다.
짧게: 프로세스 메모리는 크게 코드(text), 데이터(data/static), 힙(heap), 스택(stack)으로 나뉩니다.
가상 주소 공간(한눈에)
아래는 전형적인 64비트 프로세스의 메모리 레이아웃(단순화)입니다. 실제 주소는 운영체제에 따라 다를 수 있습니다.
高 주소 -----------------------
| 스택 (stack) | 함수 호출의 지역 변수와 복귀 주소가 저장됩니다.
--------------------------
| (안정적) OS/라이브러리 |
--------------------------
| 힙 (heap) | 동적 할당(malloc/new)이 사용하는 영역, 크기는 실행 중 변경됩니다.
--------------------------
| BSS (초기화되지 않은 전역)| 전역 변수 중 값이 0으로 초기화된 변수들
--------------------------
| 데이터 (초기화된 전역) | 전역/static 변수, 초기값을 가지는 변수
--------------------------
| 코드(.text) | 프로그램의 기계어 코드
低 주소 -----------------------

각 영역의 역할과 특징
- 코드 영역 (text)
- 실행할 기계어(함수들의 명령어)가 저장됩니다.
- 일반적으로 읽기 전용이고 실행 가능(Execute) 속성을 가집니다.
- 데이터 영역
- 초기화된 전역 변수와 static 변수가 위치합니다.
- 프로그램 시작 시 이미 크기가 정해져 있습니다.
- BSS 영역
- 초기화되지 않은 전역 변수(또는 0으로 초기화된 것)가 위치합니다.
- 파일에는 값이 저장되지 않지만 실행 시 메모리에서 0으로 초기화됩니다.
- 힙 (heap)
- malloc/new 같은 동적 메모리 할당이 사용하는 영역입니다.
- 크기가 동적으로 늘어나거나 줄어듭니다(프로그램 실행 중 관리).
- 힙은 프로그래머 책임(할당, 해제)을 잘 관리해야 합니다.
- 스택 (stack)
- 함수 호출 시 생성되는 스택 프레임(지역 변수, 복귀 주소, 호출자 레지스터 저장 공간 등)이 쌓입니다.
- LIFO(후입선출) 방식이며, 스택 오버플로우는 재귀 깊거나 큰 지역변수 사용 시 발생합니다.
함수포인터
함수포인터는 함수의 주소를 저장하는 포인터입니다. 즉, 함수 자체를 가리키는 값(주소)을 변수에 넣어 호출할 수 있습니다. 함수 포인터를 이해하면 콜백(callback), 플러그인 구조, 상태 머신 등 유연한 설계가 쉬워집니다.
주요 포인트
- 함수 코드는 코드(.text) 영역에 저장됩니다. 함수포인터 변수는 스택/데이터/힙 어디에든 위치할 수 있습니다(선언 위치에 따라 다름).
- 함수포인터의 크기는 포인터 크기(예: 64비트 시스템에서는 8바이트)와 동일합니다.
- 함수 호출은 결국 RIP(명령 포인터)를 그 함수의 시작 주소로 바꾸는 동작입니다. 함수 포인터로 호출하면 RIP가 포인터가 가리키는 주소로 설정됩니다.
기본 문법(간단한 예)
// 선언: 반환형(*이름)(매개변수목록)
void (*fp)(void); // 매개변수 없고 반환 void인 함수 포인터
void hello(void) { puts("hello"); }
fp = hello; // 함수 이름은 주소로 사용 가능
fp(); // 호출: hello()
typedef로 가독성 높이기
typedef void (*func_t)(void);
func_t fp2 = hello;
fp2();
인자를 받는 함수 포인터와 반환값
int add(int a, int b) { return a + b; }
int (*adder)(int, int) = add;
int s = adder(2, 3); // s == 5
함수 포인터의 활용 예
- 콜백: 라이브러리 함수에 동작을 넘겨주어 호출 시 실행하게 함 (예: qsort의 비교 함수).
- 상태 기계나 테이블 기반 디스패치: 인덱스나 이벤트에 따라 다른 함수로 분기.
- 테스트/모킹: 런타임에 동작을 바꿔 실행 흐름 제어.
배열과 함수 포인터
int (*ops[])(int,int) = { add, subtract, multiply };
int r = ops[0](4,5); // add 호출
안전과 주의사항
- 함수 포인터를 올바른 시그니처로만 호출하세요. 잘못된 시그니처로 호출하면 정의되지 않은 동작이 발생합니다.
- 함수 포인터를 NULL로 초기화하고 사용 전에 NULL 체크를 하는 습관을 권장합니다.
- 플랫폼과 호출 규약(ABI)에 따라 호출 규칙이 다를 수 있으므로, 특히 크로스-컴파일/외부 라이브러리에서 주의가 필요합니다.
요약: 함수 포인터는 코드 영역의 주소를 가리키는 일반 포인터처럼 동작하지만, 타입(시그니처)에 맞춰 안전하게 선언하고 사용해야 합니다. 실습 예제에서 void (*fp)(void) = hello; fp(); 같은 패턴을 직접 실행해 보세요.
안전하고 명확한 C 예제
아래 예제는 스택/힙/데이터/코드가 어떻게 사용되는지 보여주는 간단한 C 코드입니다.
#include <stdio.h>
#include <stdlib.h>
static int static_var = 42; // 데이터 영역
void hello(void) { // 코드 영역
puts("Hello from hello()\n");
}
int main(void) {
int local = 10; // 스택(지역 변수)
int *p /*스택(지역 변수)*/ = malloc(sizeof *p); // 힙
if (!p) return 1;
*p = 99;
printf("static_var=%d, local=%d, *p=%d\n", static_var, local, *p);
void (*fp)(void) /*스택(지역 변수)*/ = hello; // 함수 주소 (코드 영역의 값)
fp(); // 함수 포인터로 호출
free(p); // 힙 해제
return 0;
}
설명:
static_var는 데이터 영역에 저장됩니다.local은main의 스택 프레임에 저장됩니다.malloc으로 얻은 메모리는 힙에 배치되고,free로 반환해야 합니다.- 함수 포인터
fp는 코드 영역에 있는hello함수의 주소를 저장하고, 이를 통해 호출할 수 있습니다.
(여담)흔한 실수와 보안 관련 짚기
- 포인터를 해제한 뒤 사용(use-after-free)을 조심하세요. 해제 후 포인터를 NULL로 초기화하면 안전합니다.
- 메모리 누수(leak): 할당한 메모리를 반드시 적절한 시점에 해제해야 합니다.
- 스택 오버플로우: 지나치게 깊은 재귀나 매우 큰 지역 배열을 사용하지 마세요.
- 버퍼 오버플로우: 배열 경계 밖에 쓰면 복귀 주소 같은 중요한 데이터가 덮어씌워질 수 있습니다.
요약
프로세스 메모리는 서로 다른 역할을 가진 여러 영역으로 나뉩니다. 각 영역의 특성과 수명, 그리고 관리 책임(프로그래머 vs 런타임/OS)을 이해하면 더 안전하고 효율적인 코드를 쓸 수 있습니다. 위의 간단한 예제를 실행해 보고, 스택/힙/데이터의 차이를 직접 확인해 보세요.