본문으로 건너뛰기

안동민 개발노트

본문 시작

세그먼테이션

코드·데이터·힙·스택을 논리적 세그먼트로 분리하고 세그먼트 테이블의 주소 변환과 보호 검사를 추적합니다.

연속 할당의 단편화 문제를 해결하는 첫 번째 접근법은 프로그램을 논리적 단위로 나누는 것입니다.

프로그램은 코드, 데이터, 스택, 힙 등 서로 다른 성격의 영역으로 구성됩니다.

각 영역을 별도의 세그먼트(Segment)로 관리하면, 프로세스 전체가 연속될 필요가 없어집니다.

세그먼테이션은 프로그래머의 관점에서 메모리를 바라봅니다.

프로그래머는 main 함수의 코드, 전역 변수 영역, 스택을 따로 생각하지, 주소 0x0000부터 0xFFFF까지의 연속 공간으로 생각하지 않습니다.

세그먼테이션은 프로그램을 의미 단위로 나눠 배치한다

코드, 데이터, 힙, 스택은 성격과 보호 규칙이 다르다. 세그먼트는 각 단위에 base와 limit을 따로 주어 필요한 곳에 배치한다.

  1. Text 실행 코드 보통 읽기

    실행만 허용해 코드 변경을 막는다.

  2. Data 전역 데이터 초기값

    있는 전역 변수와 정적 변수를 담는다.

  3. Heap 동적 할당 영역 실행 중 크기

    변하며 조각화 영향을 받기 쉽다.

  4. Stack 호출 기록 함수 호출

    지역 변수, 반환 주소를 관리한다.

관점장점주의점
프로그래머 시각논리 단위가 코드 구조와 잘 맞는다.세그먼트 번호와 offset을 함께 다뤄야 한다.
보호와 공유코드 세그먼트 공유나 권한 분리가 자연스럽다.권한 설정이 잘못되면 보호 경계가 흔들린다.
메모리 배치각 세그먼트를 빈 공간에 따로 둘 수 있다.가변 크기 때문에 외부 단편화가 생긴다.

세그먼트의 개념

세그먼테이션(Segmentation)은 프로세스의 주소 공간을 의미 있는 논리적 단위로 분할하는 기법입니다.

각 세그먼트는 독립적인 메모리 블록으로, 물리 메모리의 서로 다른 위치에 배치될 수 있습니다.

일반적인 C 프로그램의 세그먼트:

  • 텍스트 세그먼트(Text/Code): 컴파일된 기계어 코드. 읽기 전용, 실행 가능. 여러 프로세스가 공유 가능(같은 프로그램의 인스턴스).
  • 데이터 세그먼트(Data): 초기화된 전역/정적 변수. 프로세스별 독립.
  • BSS 세그먼트: 초기화되지 않은 전역/정적 변수. 0으로 초기화됨. 실행 파일에 실제 데이터를 저장하지 않으므로 파일 크기를 줄임.
  • 힙 세그먼트(Heap): malloc/new로 동적 할당되는 영역. 주소가 위로 자람.
  • 스택 세그먼트(Stack): 함수 호출 프레임, 지역 변수. 주소가 아래로 자람.
segments_example.c
#include <stdio.h>
#include <stdlib.h>

int global_init = 42;          /* Data 세그먼트 */
int global_uninit;             /* BSS 세그먼트 */

void function() {              /* Text 세그먼트 */
    int local_var = 10;        /* Stack 세그먼트 */
    int *dynamic = malloc(64); /* Heap 세그먼트 */

    printf("Text:  %p (function 주소)\n", (void *)function);
    printf("Data:  %p (global_init)\n", (void *)&global_init);
    printf("BSS:   %p (global_uninit)\n", (void *)&global_uninit);
    printf("Heap:  %p (dynamic)\n", (void *)dynamic);
    printf("Stack: %p (local_var)\n", (void *)&local_var);

    free(dynamic);
}

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

실행하면 각 세그먼트의 주소가 서로 떨어져 있는 것을 확인할 수 있습니다.

Text와 Data는 낮은 주소, Stack은 높은 주소에, Heap은 그 사이에 위치합니다.

Linux에서 /proc/<pid>/maps로 프로세스의 세그먼트 매핑을 확인할 수 있습니다.

memory_map.py
import subprocess

# 현재 프로세스의 메모리 맵 확인 (Linux)
with open(f"/proc/self/maps") as f:
    for line in f:
        # 주소범위 권한 오프셋 디바이스 inode 경로
        print(line.strip())

세그먼트 테이블

논리 주소는 (세그먼트 번호 s, 오프셋 d) 쌍으로 구성됩니다.

세그먼트 번호로 세그먼트 테이블(Segment Table)을 조회하면 해당 세그먼트의 물리적 정보를 얻습니다.

세그먼트 테이블 엔트리

각 엔트리에는 다음 정보가 포함됩니다.

  • 베이스(Base): 세그먼트의 시작 물리 주소
  • 한계(Limit): 세그먼트의 크기
  • 보호 비트(Protection Bits): 읽기(R), 쓰기(W), 실행(X) 권한

주소 변환 과정

세그먼테이션에서는 주소 변환, 테이블 갱신, 메모리/디스크 비용을 확인합니다.

세그먼트 주소는 limit 검사 뒤 base를 더한다

논리 주소의 segment 번호로 descriptor를 찾고 offset이 limit 안에 있을 때만 base + offset이 물리 주소가 된다.

  1. segment:offset

    logical segment:offset 프로그램이 만든 주소

  2. base, limit

    descriptor base, limit 세그먼트 표 조회

  3. offset < limit

    protection offset < limit 권한과 범위 검사

  4. base + offset

    physical base + offset 실제 메모리 위치

  1. 논리 주소에서 세그먼트 번호 s와 오프셋 d를 추출합니다.
  2. 세그먼트 테이블에서 엔트리 s를 조회합니다.
  3. 오프셋 d가 한계(Limit)보다 크면 → 세그먼테이션 폴트 발생.
  4. 보호 비트를 확인합니다. 쓰기 시도인데 W 비트가 없으면 → 보호 위반.
  5. 물리 주소 = 베이스(Base) + 오프셋(d).
segment_translation.c
/* 개념적 의사 코드 */
typedef struct {
    unsigned int base;
    unsigned int limit;
    unsigned char read;
    unsigned char write;
    unsigned char execute;
} SegmentEntry;

SegmentEntry segment_table[MAX_SEGMENTS];

unsigned int translate(unsigned int seg_num, unsigned int offset,
                       int is_write) {
    SegmentEntry *entry = &segment_table[seg_num];

    /* 범위 검사 */
    if (offset >= entry->limit) {
        raise_trap(SEGFAULT);
        return (unsigned int)-1;
    }

    /* 권한 검사 */
    if (is_write && !entry->write) {
        raise_trap(PROTECTION_FAULT);
        return (unsigned int)-1;
    }

    return entry->base + offset;
}

세그먼트별 보호의 장점

세그먼트별로 권한을 다르게 설정할 수 있는 것은 큰 보안 이점입니다.

  • 코드 세그먼트: R+X (읽기+실행). 쓰기 불가. 악성 코드가 프로그램 자체를 수정하는 것을 방지합니다.
  • 데이터 세그먼트: R+W (읽기+쓰기). 실행 불가. 데이터 영역에 쉘코드를 삽입해도 실행할 수 없습니다.
  • 스택: R+W. NX(No eXecute) 비트로 실행을 금지합니다. 스택 기반 버퍼 오버플로우 공격을 방어합니다.

이 보호 비트가 바로 현대 CPU의 DEP(Data Execution Prevention) / NX bit의 기원입니다.


세그먼테이션 폴트

개발자라면 한 번쯤 만나봤을 Segmentation Fault입니다.

이 오류는 프로세스가 자신에게 할당되지 않은 메모리 영역에 접근하거나, 권한이 없는 접근을 시도할 때 발생합니다.

세그먼테이션 폴트는 주소가 틀렸다는 하나의 메시지처럼 보이지만, 실제로는 범위 검사와 권한 검사 중 어디서 실패했는지 나누어 볼 수 있습니다.

세그먼테이션 폴트 판정 흐름

CPU와 OS는 주소 변환 중 세그먼트 범위와 권한을 확인하고, 위반이 있으면 트랩을 통해 프로세스를 중단합니다.

  1. 1
    실패 유형

    fault 범위 초과 세그먼트 한계보다 큰 오프셋을 접근하면 유효하지 않은 주소입니다. 권한 위반 읽기 전용 코드나 실행 불가 스택에 쓰기/실행을 시도하면 차단됩니다.

  2. 2
    검사 순서

    translation 논리 주소 세그먼트 번호와 오프셋을 분리합니다. 테이블 조회 Base, Limit, R/W/X 비트를 읽습니다. 범위·권한 검사 한계와 접근 종류를 비교해 위반을 찾습니다. 물리 주소 통과하면 Base + Offset으로 실제 주소를 만듭니다.

  3. 3
    디버깅 단서

    debug NULL 역참조 주소 0 근처는 보통 매핑하지 않아 즉시 SIGSEGV가 납니다. Use-After-Free 이미 반환한 힙 블록을 다시 읽으면 운에 따라 늦게 터질 수 있습니다. 버퍼 오버런 배열 밖 쓰기는 인접 세그먼트나 보호 페이지에서 드러납니다.

segfault_examples.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* 원인 1: NULL 포인터 역참조 */
void null_deref() {
    int *ptr = NULL;
    *ptr = 42;  /* 주소 0번지 접근 → SIGSEGV */
}

/* 원인 2: 해제된 메모리 접근 (Use-After-Free) */
void use_after_free() {
    int *ptr = malloc(sizeof(int));
    *ptr = 100;
    free(ptr);
    printf("%d\n", *ptr);  /* 운이 좋으면 동작, 나쁘면 SIGSEGV */
}

/* 원인 3: 배열 범위 초과 */
void buffer_overflow() {
    int arr[10];
    arr[10000] = 1;  /* 스택 세그먼트 범위 초과 → SIGSEGV */
}

/* 원인 4: 읽기 전용 메모리 쓰기 */
void readonly_write() {
    char *str = "Hello";  /* 문자열 리터럴은 Text 세그먼트(읽기 전용) */
    str[0] = 'h';  /* 쓰기 시도 → SIGSEGV */
}

NULL 포인터를 역참조하면 주소 0번지에 접근합니다.

대부분의 OS에서 주소 공간의 가장 앞 부분(보통 첫 4KB)은 의도적으로 매핑하지 않습니다.

NULL 포인터 역참조를 확실히 잡기 위해서입니다.

MMU가 이 접근을 감지하면 트랩을 발생시키고, OS가 프로세스에 SIGSEGV 시그널을 보냅니다.

프로세스가 이 시그널을 처리하지 않으면 종료됩니다.


세그먼테이션의 장단점

장점

  • 프로그래머 관점 일치: 코드, 데이터, 스택이라는 논리적 단위와 일치합니다.
  • 세그먼트별 보호: 코드는 읽기 전용, 스택은 실행 불가 등 세밀한 권한 설정이 가능합니다.
  • 세그먼트 공유: 두 프로세스가 같은 텍스트 세그먼트를 공유하면 메모리를 절약합니다. 공유 라이브러리의 기반입니다.
  • 세그먼트별 독립 증가: 힙과 스택이 독립적으로 크기가 변할 수 있습니다.

단점

  • 외부 단편화: 세그먼트 크기가 가변적이므로 여전히 외부 단편화가 발생합니다. 큰 세그먼트를 위한 연속 공간을 찾기 어려울 수 있습니다.
  • 크기 제한: 세그먼트는 연속된 물리 메모리여야 하므로, 물리 메모리보다 큰 세그먼트는 불가능합니다.

세그먼테이션 + 페이징

x86 아키텍처는 세그먼테이션과 페이징을 함께 사용합니다.

논리 주소가 세그먼테이션을 거쳐 선형 주소(Linear Address)가 되고, 선형 주소가 페이징을 거쳐 물리 주소가 됩니다.

세그먼테이션과 페이징을 함께 쓰면 보호와 배치 문제를 서로 다른 단계에서 해결합니다.

논리 단위 보호와 고정 크기 배치 분리

세그먼테이션은 먼저 의미와 권한을 확인하고, 페이징은 선형 주소를 작은 프레임으로 나누어 외부 단편화를 제거합니다.

  1. 논리 주소

    (segment, offset) 프로그래머가 보는 코드, 데이터, 스택 같은 의미 단위입니다.

  2. 세그먼트 검사

    base + limit 범위와 R/W/X 권한을 확인해 보호 위반을 잡습니다.

  3. 선형 주소

    linear address 세그먼트 기준 주소와 오프셋을 더한 중간 주소입니다.

  4. 페이지 변환

    page -> frame 고정 크기 페이지를 임의의 물리 프레임에 매핑합니다.

  5. 권한은 세그먼트에서 먼저 거른다

    보호 코드는 실행 전용, 데이터는 쓰기 가능처럼 논리 영역별 정책을 붙이기 쉽습니다.

  6. 프레임은 연속될 필요가 없다

    배치 페이지 단위로 흩어 배치하므로 큰 연속 공간을 찾는 부담이 사라집니다.

  7. 평탄 모델은 세그먼트를 낮춘다

    현대 x86-64 베이스 0, 큰 한계로 맞춘 뒤 실질적인 격리와 변환은 페이징이 담당합니다.

하지만 현대 x86-64에서는 세그먼테이션이 사실상 비활성화되어 있습니다.

모든 세그먼트의 베이스를 0, 한계를 최대로 설정하여 평탄 모델(Flat Model)을 사용합니다.

실질적으로 페이징만으로 주소 변환을 수행합니다.

세그먼테이션의 가치는 논리적 보호에 있고, 한계는 가변 크기 배치에서 생기는 단편화 비용에 있습니다.

세그먼트는 의미와 권한을 주고, 페이징은 배치를 푼다

세그먼테이션은 프로그래머가 보는 코드, 데이터, 스택 단위를 잘 표현하지만 세그먼트 크기가 가변적이라 배치 문제를 완전히 없애지는 못합니다.

  1. 가치
    논리 단위 보호

    치 코드는 실행 가능, 데이터는 쓰기 가능처럼 영역별 권한을 자연스럽게 붙입니다.

  2. 세그먼트 한계
    가변 크기 배치

    세그먼트 한계 각 세그먼트는 연속 공간을 요구하므로 큰 구멍을 찾는 외부 단편화가 남습니다.

  3. 주소 분해

    논리 주소를 세그먼트 번호와 오프셋으로 나눕니다.

  4. 테이블 조회

    베이스, 한계, R/W/X 권한을 한 번에 확인합니다.

  5. 오류 처리

    범위 초과나 권한 위반은 세그먼테이션 폴트로 이어집니다.

  6. 페이징 보완

    고정 크기 페이지가 물리 배치 부담을 줄입니다.

외부 단편화를 근본적으로 해결하는 것이 바로 페이징입니다.

페이징은 메모리를 고정 크기로 나누어 외부 단편화를 완전히 제거합니다.

다음 절에서 스와핑과 동적 메모리를 다루고, 9장에서 페이징을 본격적으로 살펴보겠습니다.

세그먼테이션 주소 변환 기준

세그먼테이션은 코드, 데이터, 스택처럼 의미 단위로 메모리를 나누고 base-limit 보호를 적용합니다.

  1. 01

    segment 번호

  2. 02

    offset

  3. 03

    limit 검사

  4. 04

    권한 검사

  5. 05

    base 더하기

  6. 논리 주소

    (segment 2, offset 320)

  7. table[2]

    base=4096 · limit=600 · R/W 320 < 600 ✓

  8. 물리 주소

    4096 + 320 = 4416 limit 초과면 주소 대신 fault

  9. Segment Table 각 엔트리

    base, limit, protection bit를 갖고 논리 단위의 경계를 표현합니다.

  10. Protection 코드

    실행, 데이터는 읽기/쓰기처럼 세그먼트별 권한을 다르게 둘 수 있습니다.

  11. Segmentation Fault offset

    limit를 넘거나 권한이 맞지 않으면 보호 예외가 발생합니다.

  12. Paging 결합 세그먼트 내부

    페이지로 나누면 논리 보호와 물리 배치 유연성을 함께 얻습니다.

세그먼테이션 주소 검사

세그먼테이션은 논리 주소를 segment selector와 offset으로 나눕니다. base/limit 검사와 권한 비트가 통과해야 실제 물리 주소가 계산됩니다.

  1. 01

    selector

  2. 02

    base + limit

  3. 03

    protection

  4. 04

    physical addr

  5. 입력

    (selector 3, offset 480) write 접근

  6. descriptor[3]

    base=8192 · limit=640 · R/W 480 < 640 · write ✓

  7. 통과

    8192 + 480 = 8672 limit 또는 권한 실패면 #GP fault

  8. selector 코드, 데이터, 스택처럼 의미

    단위별 descriptor를 고르는 인덱스다.

  9. base + limit offset

    limit을 넘으면 보호 예외가 발생하고, 통과하면 base를 더한다.

  10. protection

    read/write/execute 권한과 privilege level이 접근 가능성을 결정한다.

  11. physical addr 현대 시스템에서

    세그먼트 결과가 다시 페이징 입력이 되는 경우가 많다.