C: 메모리 레이아웃

hyunahn·2025년 12월 1일
post-thumbnail

프로그램(Program)은 파일이지만, 프로세스(Process)는 메모리 위의 생명체다.

여러분이 컴파일한 C 프로그램(실행 파일)이 하드디스크에 저장되어 있을 때는 그저 차가운 바이너리 데이터 덩어리에 불과합니다. 하지만 이를 실행하는 순간, 운영체제(OS)의 로더(Loader)는 이 파일을 메모리(RAM)로 복사하고 CPU가 실행할 수 있는 구조로 재배치합니다.
이때 메모리는 아무렇게나 쓰이는 것이 아니라, 세그먼트(Segment)라는 논리적 구획으로 철저히 나뉘어 관리됩니다. 이는 CPU가 효율적으로 접근하고, OS가 메모리를 보호하기 위한 필수적인 하드웨어적 설계입니다.

이러한 C언어가 가진 특성과 더불어, 컴파일러 및 메모리 세그먼트 등을 공부해보려고 합니다.

하드웨어 투시해보기


[ 4GB (32-bit 기준) / User Space Limits ]
+-----------------------------------+ <--- High Address (0xFFFFFFFF)
|                                   |
|   Stack Segment (스택)            | [지역 변수, 함수 매개변수, 리턴 주소]
|   - 아래로 성장 (Grows Down) ↓    | * LIFO (Last In, First Out) 구조
|                                   |
+-----------------------------------+ <--- Stack Pointer (ESP/RSP)
|                 |                 |
|                 V                 | (Free Space: 스택과 힙 사이의 빈 공간)
|                                   |
|                 ^                 |
|                 |                 |
+-----------------------------------+ <--- Program Break (brk)
|   Heap Segment (힙)               | [malloc/free 동적 할당 메모리]
|   - 위로 성장 (Grows Up) ↑        |
+-----------------------------------+
|   BSS Segment (Uninitialized)     | [초기화되지 않은 전역/static 변수]
|   - 실행 시 0으로 자동 초기화     | * 디스크 용량 차지 안 함
+-----------------------------------+
|   Data Segment (Initialized)      | [초기화된 전역/static 변수]
|   - 예: int g_var = 10;           |
+-----------------------------------+
|   Text (Code) Segment             | [기계어 코드, 상수 문자열]
|   - Read-Only (수정 불가)         | * CPU 명령어가 위치하는 곳
+-----------------------------------+ <--- Program Counter (PC/EIP)
+-----------------------------------+ <--- Low Address (0x00000000)

CPU가 바라보는 가상 메모리 공간(Virtual Address Space)을 엑스레이로 찍어보겠습니다. 주소는 위쪽이 높은 주소(High Address), 아래쪽이 낮은 주소(Low Address)입니다.

세그먼트별 상세 분석 해보기

Text (Code) Segment:

내용: 여러분이 작성한 if, for, 함수들이 기계어로 번역되어 저장됩니다.

특징: Read-Only. 운영체제는 이 영역을 수정하려는 시도가 감지되면 즉시 Segmentation Fault를 발생시켜 프로그램을 강제 종료합니다(해킹 방지). CPU의 Instruction Cache(I-Cache)가 주로 이곳을 읽어들입니다.

Data Segment:

내용: int g_count = 100; 처럼 초기값이 있는 전역 변수나 정적(static) 변수.

특징: 프로그램 시작 시 실행 파일에서 값을 읽어와 메모리에 그대로 복사합니다. Read-Write가 가능합니다.

BSS (Block Started by Symbol) Segment:

내용: int g_buffer[1024]; 처럼 초기값이 없는(혹은 0으로 초기화된) 전역/정적 변수.

하드웨어적 비밀: 왜 Data와 분리했을까요? 만약 1MB짜리 배열을 0으로 초기화한다고 합시다. 이를 Data 영역에 넣으면 실행 파일 크기도 1MB 커집니다. 하지만 BSS에 두면 실행 파일에는 "이만큼의 크기가 필요해"라는 정보만 기록됩니다. 로더(Loader)는 프로그램 실행 직전, 이 영역을 메모리에 잡고 빠르게 0으로 밀어버립니다(Zeroing).

왜 나눴을까?

  1. 실행 파일(디스크) 크기 절감

    Data 세그먼트에 있는 변수는 “초기값 배열”이 통째로 실행 파일 안에 저장됩니다. 예를 들어 int arr[100000] = {1,2,...}; 같은 것은 실제 값 100000개를 바이너리에 써 넣어야 합니다.​

    BSS는 “초기값이 0인 전역/static 변수”만 모아 두고, 실제 데이터는 저장하지 않고 “이 영역 크기는 N바이트다”라는 메타정보만 기록합니다.​

    따라서 1MB짜리 전역 배열을 전부 0으로 쓰고 싶을 때, Data에 두면 실행 파일이 1MB 커지지만, BSS에 두면 실행 파일은 몇 바이트의 크기 정보만 증가합니다.​

  2. 로드/부팅 시간 최적화

    Data는 디스크(또는 Flash)에 있는 초기값들을 그대로 RAM으로 복사해야 하므로, 크기가 클수록 로딩 시간이 늘어납니다.​

    BSS는 “0으로 채우면 된다”는 정보만 있으므로, 로더나 스타트업 코드가 memset(bss_start, 0, bss_size); 같은 식으로 한 번에 0으로 초기화하면 됩니다.​

    대형 임베디드/커널 코드에서 이 차이는 부팅 시간과 I/O 비용에 꽤 큰 영향을 줍니다.​

  3. ROM/Flash 자원 절약 (임베디드에서 특히 중요)

    Data 세그먼트의 초기값은 Flash/ROM 공간을 실제로 차지합니다. 전원이 꺼져도 값이 남아있어야 하고, 부팅 시 RAM에 복사해서 쓰기 때문입니다.​

    BSS 변수는 어차피 “처음엔 0이고, 이후 RAM에서만 쓰는 값”이므로, Flash에 굳이 0을 N번 써둘 필요가 없습니다. 크기 정보만 저장하고, RAM에서 0으로 밀어버리면 됩니다

Heap Segment:

내용: malloc(), calloc()으로 런타임에 사용자가 요청한 메모리.

특징: 메모리 주소 낮은 곳에서 높은 곳으로 자라납니다. 개발자가 직접 관리해야 하며, 관리 비용(Overhead)이 가장 큽니다.

Stack Segment:

내용: 함수 내의 지역 변수, 매개 변수.

특징: 함수 호출 시 생성되고 리턴 시 즉시 사라집니다. CPU의 레지스터(ESP, EBP)가 직접 관리하므로 가장 속도가 빠릅니다. 캐시 적중률(Cache Hit Ratio)이 매우 높습니다.

근본적인 질문 왜 이렇게 나누는 걸까요?

바로 "효율성"과 "안전" 때문입니다.

여기서 예를 한번 들어보죠.

Factory(Process)

Text (작업 지시서): 벽에 붙은 작업 매뉴얼입니다. 작업자가 맘대로 수정하면 차가 엉망이 되겠죠? 그래서 코팅해서 못 고치게(Read-Only) 해둡니다.

Data (기본 자재): 공장 가동 전에 미리 배치해둔 부품들(타이어, 핸들 등)입니다. 항상 그 자리에 있어야 합니다.

BSS (빈 상자): "완성품 보관함"이라고 라벨만 붙어있는 빈 공간입니다. 공장 지을 때 미리 상자를 다 채워두면 공간 낭비니까, 라벨만 붙여두고 필요할 때 씁니다.

Stack (작업대): 작업자가 그때그때 쓰는 개인 작업 공간입니다. 작업을 마치면 다음 사람을 위해 바로바로 치워야(Pop) 합니다. 가장 손이 많이 가고 빠릅니다.

Heap (창고): 갑자기 주문 폭주로 추가 자재가 필요할 때 지게차를 불러 가져오는 공간입니다. 가져오는 데 시간이 걸리고(Alloc), 다 쓰면 반납해야(Free) 합니다.

코드를 통해 공부해봅시다

실제 코드가 메모리에 어떻게 매핑되는지 확인해 봅시다.

#include <stdio.h>
#include <stdlib.h>

/* [Text Segment] (Read-Only) */
const int ro_global = 999; 

/* [Data Segment] (Read-Write, 초기값 있음) */
int global_init = 10;

/* [BSS Segment] (Read-Write, 초기값 없음 -> 0으로 자동 초기화) */
int global_uninit; 

void function(int param) { /* param은 [Stack] */
    
    /* [Stack] 함수 호출 시 생성, 리턴 시 소멸 */
    int local_var = 20; 
    
    /* [Heap] 런타임에 동적 할당 */
    int *heap_var = (int*)malloc(sizeof(int)); 
    *heap_var = 30;

    printf("Stack Address: %p\n", (void*)&local_var);
    printf("Heap Address : %p\n", (void*)heap_var);
    
    free(heap_var);
}

int main() {
    function(5);
    return 0;
}

Embedded/kernel적으로 사고하기

  1. BSS를 사랑하자 (Embedded System)

임베디드 장비는 저장 공간(Flash Memory)이 비쌉니다. int big_array[1000] = {0}; 대신 int big_array[1000];로 선언하십시오. 전자는 실행 파일 크기를 키우지만, 후자는 키우지 않습니다. (BSS의 마법)

  1. Stack Overflow 주의:

스택 크기는 제한적입니다 (보통 Linux에서 8MB, 임베디드에서는 수 KB). int huge_array[1000000]; 같은 큰 배열을 지역 변수로 선언하면 스택이 터집니다. 큰 데이터는 반드시 malloc을 통해 Heap을 사용하거나, static을 붙여 BSS/Data 영역으로 빼야 합니다.

  1. 캐시 지역성(Cache Locality):

스택 변수는 서로 인접해 있어 CPU 캐시 효율이 매우 좋습니다. 반면, 힙은 메모리가 파편화(Fragmentation)될 수 있어 속도가 느릴 수 있습니다. 성능이 중요하다면 힙 할당을 최소화하십시오.

구분저장되는 데이터 예시저장 위치 (임베디드)생명 주기
Textmain(),printf()등의 코드Flash (ROM)영구적
Dataint a = 10;(전역)Flash에 저장 → 부팅 시 RAM 복사프로그램 전체
BSSint b;(전역, 초기화 X)RAM (부팅 시 0으로 초기화)프로그램 전체
Heapmalloc(100)RAMfree()할 때까지
Stackint i;(함수 내 지역변수)RAM함수 실행 중일 때만

0개의 댓글