프로세스 메모리

이 글은 “프로세스 메모리 구조”를 처음 접하는 사람도 이해할 수 있도록 친절하게 설명합니다.

개요

운영체제에서 실행 중인 프로그램을 “프로세스”라고 부릅니다. 각 프로세스는 실행에 필요한 여러 메모리 구역을 가집니다. 이 문서에서는 가상 주소 공간 기준으로 흔히 보는 주요 영역(코드, 데이터, 힙, 스택 등)을 설명하고, 간단한 예제와 함께 실무에서 주의할 점을 정리합니다.

짧게: 프로세스 메모리는 크게 코드(text), 데이터(data/static), 힙(heap), 스택(stack)으로 나뉩니다.

가상 주소 공간(한눈에)

아래는 전형적인 64비트 프로세스의 메모리 레이아웃(단순화)입니다. 실제 주소는 운영체제에 따라 다를 수 있습니다.

高 주소  -----------------------
    | 스택 (stack)            |  함수 호출의 지역 변수와 복귀 주소가 저장됩니다.
    --------------------------
    | (안정적) OS/라이브러리   |
    --------------------------
    | 힙 (heap)               |  동적 할당(malloc/new)이 사용하는 영역, 크기는 실행 중 변경됩니다.
    --------------------------
    | BSS (초기화되지 않은 전역)|  전역 변수 중 값이 0으로 초기화된 변수들
    --------------------------
    | 데이터 (초기화된 전역)   |  전역/static 변수, 초기값을 가지는 변수
    --------------------------
    | 코드(.text)              |  프로그램의 기계어 코드
低 주소  -----------------------

image

각 영역의 역할과 특징

  • 코드 영역 (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)을 이해하면 더 안전하고 효율적인 코드를 쓸 수 있습니다. 위의 간단한 예제를 실행해 보고, 스택/힙/데이터의 차이를 직접 확인해 보세요.


results matching ""

    No results matching ""