세그먼테이션
코드·데이터·힙·스택을 논리적 세그먼트로 분리하고 세그먼트 테이블의 주소 변환과 보호 검사를 추적합니다.
프로세스 전체를 연속 배치해야 하는 제약을 줄이는 접근법은 프로그램을 논리적 단위로 나누는 것입니다.
프로그램은 코드, 데이터, 스택, 힙 등 서로 다른 성격의 영역으로 구성됩니다.
각 영역을 별도의 세그먼트(Segment)로 관리하면, 프로세스 전체가 연속될 필요가 없어집니다.
세그먼테이션은 프로그래머의 관점에서 메모리를 바라봅니다.
프로그래머는 main 함수의 코드, 전역 변수 영역, 스택을 따로 생각하지, 주소 0x0000부터 0xFFFF까지의 연속 공간으로 생각하지 않습니다.
세그먼트의 개념
세그먼테이션(Segmentation)은 프로세스의 주소 공간을 의미 있는 논리적 단위로 분할하는 기법입니다.
각 세그먼트는 독립적인 메모리 블록으로, 물리 메모리의 서로 다른 위치에 배치될 수 있습니다.
일반적인 C 실행 환경에서 구분하는 영역은 다음과 같습니다. 이는 코드·데이터의 용도 구분이며, 현대 OS가 각 영역을 하드웨어 세그먼트 하나로 구현한다는 뜻은 아닙니다.
- 텍스트 세그먼트(Text/Code): 컴파일된 기계어 코드. 읽기 전용, 실행 가능. 여러 프로세스가 공유 가능(같은 프로그램의 인스턴스).
- 데이터 세그먼트(Data): 초기화된 전역/정적 변수. 프로세스별 독립.
- BSS 세그먼트: 초기화되지 않은 전역/정적 변수. 0으로 초기화됨. 실행 파일에 실제 데이터를 저장하지 않으므로 파일 크기를 줄임.
- 힙 세그먼트(Heap):
malloc/new로 동적 할당되는 영역. 전통적인 배치 예에서는 높은 주소 쪽으로 증가하지만mmap할당 등은 별도 매핑을 사용할 수 있음. - 스택 세그먼트(Stack): 함수 호출 프레임, 지역 변수. 많은 환경에서 낮은 주소 쪽으로 증가하지만 방향은 ABI에 따름.
#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;
}이 예제는 객체·함수의 주소를 출력합니다. 배치 순서와 간격은 컴파일·링크·ASLR·할당 설정에 따라 달라지며, 실제 실행값을 고정하지 않습니다. 함수 포인터를 void *로 바꾸는 출력 방식도 모든 ISO C 구현에 이식 가능한 표현은 아닙니다.
Linux의 /proc/<pid>/maps는 가상 메모리 매핑을 보여 줍니다. 아래 내용은 하드웨어 세그먼트 테이블을 덤프하는 코드가 아닙니다.
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은 길이이므로 유효한 오프셋은 0 ≤ offset < limit입니다. 테이블을 읽기 전에 세그먼트 번호도 검사합니다.
/* 개념적 의사 코드 */
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) {
if (seg_num >= MAX_SEGMENTS) {
raise_trap(SEGFAULT);
return (unsigned int)-1;
}
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;
}주소 변환 조각이 검사하는 조건
테이블을 읽기 전에 번호를 확인한다. base와 offset을 합한다. 각 단계의 값과 전환 이유를 비교합니다.
주소 변환 · 검사 범위
번호·offset 범위·쓰기 권한을 통과하면 base + offset을 반환합니다
본문의 개념적 의사 코드 순서입니다. limit은 길이이므로 유효한 offset은 0 이상·limit 미만이며, C 객체의 배열 경계와 수명을 모두 아는 검사가 아닙니다.
| 도달한 검사 | 해당 조건 | 의사 코드 결과 |
|---|---|---|
| 테이블 조회 전 | seg_num ≥ MAX_SEGMENTS | 범위 오류 처리 |
| 유효한 세그먼트 | offset ≥ limit | 범위 오류 처리 |
| 범위 안의 쓰기 | is_write이고 write 권한이 없음 | 보호 오류 처리 |
| 앞 검사를 모두 통과 | base + offset을 계산 | 변환 주소 반환 |
- 테이블 조회 전
- 해당 조건: seg_num ≥ MAX_SEGMENTS의사 코드 결과: 범위 오류 처리
- 유효한 세그먼트
- 해당 조건: offset ≥ limit의사 코드 결과: 범위 오류 처리
- 범위 안의 쓰기
- 해당 조건: is_write이고 write 권한이 없음의사 코드 결과: 보호 오류 처리
- 앞 검사를 모두 통과
- 해당 조건: base + offset을 계산의사 코드 결과: 변환 주소 반환
이 조각은 read·execute 비트를 선언하지만 검사하지 않습니다. 유효한 테이블 값과 주소 폭 안의 덧셈을 전제하며 전체 MMU 구현은 아닙니다.
이 예의 조건
- 본문과 전체 도표의 예를 따라 번호 검사부터 주소 반환까지 진행합니다.
관계도의 보충 설명
| 구분 | 전체 도표의 내용 |
|---|---|
| 관계 설명 | 번호>=MAX이면 조회 전 SEGFAULT. 아니면 entry 조회 뒤 offset>=limit이면 SEGFAULT. 아니면 쓰기이고 write 권한이 없을 때 PROTECTIONFAULT. 세 조건이 모두 거짓이면 base+offset을 반환한다. 모바일 왼쪽 외곽선은 각 아니요 분기이며 오류 노드를 거쳐 다음 검사로 진행하지 않는다. read와 execute는 원 코드가 검사하지 않는다. |
| 노드·선 표기 | 아니요 |
| 노드·선 표기 | 아니요: 다음 검사 |
| 표의 값 | 테이블 조회 전. 해당 조건: seg_num ≥ MAX_SEGMENTS. 의사 코드 결과: 범위 오류 처리. 유효한 세그먼트. 해당 조건: offset ≥ limit. 의사 코드 결과: 범위 오류 처리. 범위 안의 쓰기. 해당 조건: is_write이고 write 권한이 없음. 의사 코드 결과: 보호 오류 처리. 앞 검사를 모두 통과. 해당 조건: base + offset을 계산. 의사 코드 결과: 변환 주소 반환. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 번호 검사 테이블을 읽기 전에 번호를 확인한다. |
|
|
| 범위 검사 limit은 길이이며 같은 값은 범위 밖이다. |
|
|
| 쓰기 조건 현재 요청은 읽기다. |
|
|
| 주소 반환 base와 offset을 합한다. |
|
|
주소 변환 조각이 검사하는 조건
테이블을 읽기 전에 번호를 확인한다. base와 offset을 합한다. 각 단계의 값과 전환 이유를 비교합니다.
주소 변환 · 검사 범위
번호·offset 범위·쓰기 권한을 통과하면 base + offset을 반환합니다
본문의 개념적 의사 코드 순서입니다. limit은 길이이므로 유효한 offset은 0 이상·limit 미만이며, C 객체의 배열 경계와 수명을 모두 아는 검사가 아닙니다.
| 도달한 검사 | 해당 조건 | 의사 코드 결과 |
|---|---|---|
| 테이블 조회 전 | seg_num ≥ MAX_SEGMENTS | 범위 오류 처리 |
| 유효한 세그먼트 | offset ≥ limit | 범위 오류 처리 |
| 범위 안의 쓰기 | is_write이고 write 권한이 없음 | 보호 오류 처리 |
| 앞 검사를 모두 통과 | base + offset을 계산 | 변환 주소 반환 |
- 테이블 조회 전
- 해당 조건: seg_num ≥ MAX_SEGMENTS의사 코드 결과: 범위 오류 처리
- 유효한 세그먼트
- 해당 조건: offset ≥ limit의사 코드 결과: 범위 오류 처리
- 범위 안의 쓰기
- 해당 조건: is_write이고 write 권한이 없음의사 코드 결과: 보호 오류 처리
- 앞 검사를 모두 통과
- 해당 조건: base + offset을 계산의사 코드 결과: 변환 주소 반환
이 조각은 read·execute 비트를 선언하지만 검사하지 않습니다. 유효한 테이블 값과 주소 폭 안의 덧셈을 전제하며 전체 MMU 구현은 아닙니다.
이 예의 조건
- 본문과 전체 도표의 예를 따라 번호 검사부터 주소 반환까지 진행합니다.
관계도의 보충 설명
| 구분 | 전체 도표의 내용 |
|---|---|
| 관계 설명 | 번호>=MAX이면 조회 전 SEGFAULT. 아니면 entry 조회 뒤 offset>=limit이면 SEGFAULT. 아니면 쓰기이고 write 권한이 없을 때 PROTECTIONFAULT. 세 조건이 모두 거짓이면 base+offset을 반환한다. 모바일 왼쪽 외곽선은 각 아니요 분기이며 오류 노드를 거쳐 다음 검사로 진행하지 않는다. read와 execute는 원 코드가 검사하지 않는다. |
| 노드·선 표기 | 아니요 |
| 노드·선 표기 | 아니요: 다음 검사 |
| 표의 값 | 테이블 조회 전. 해당 조건: seg_num ≥ MAX_SEGMENTS. 의사 코드 결과: 범위 오류 처리. 유효한 세그먼트. 해당 조건: offset ≥ limit. 의사 코드 결과: 범위 오류 처리. 범위 안의 쓰기. 해당 조건: is_write이고 write 권한이 없음. 의사 코드 결과: 보호 오류 처리. 앞 검사를 모두 통과. 해당 조건: base + offset을 계산. 의사 코드 결과: 변환 주소 반환. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 번호 검사 테이블을 읽기 전에 번호를 확인한다. |
|
|
| 범위 검사 limit은 길이이며 같은 값은 범위 밖이다. |
|
|
| 쓰기 조건 현재 요청은 읽기다. |
|
|
| 주소 반환 base와 offset을 합한다. |
|
|
MAX_SEGMENTS=4이고 entry[1]의 base=0x20000, limit=0x1000, write=0인 입력을 별도로 정해 추적해 봅시다. (1,0x0fff)의 읽기 요청은 번호와 범위를 통과하고 is_write=false이므로 0x20fff를 반환합니다. 같은 번호의 offset=0x1000은 limit과 같아서 범위 오류이고, offset=0x0010의 쓰기 요청은 범위 안이어도 write=0이라 보호 오류입니다. 번호 4는 entry[4]를 읽기 전에 거절합니다.
원 조각에 read·execute 필드가 있다는 것과 실제 검사한다는 것은 다릅니다. 이 함수의 세 번째 조건은 쓰기 요청만 읽습니다. 따라서 read=0인 입력에서 읽기가 성공할 수 있으며, 실행 권한을 요청하는 인자나 검사도 없습니다. 앞의 주소·권한 예는 이 조각이 수행한 검사 범위 안에서 해석해야 합니다.
독립 확인: entry[2]의 base=0x30000, limit=0x80, write=1이며 MAX_SEGMENTS는 4입니다. (2,0x7f)의 쓰기, (2,0x80)의 쓰기, (4,0)의 읽기를 원 코드 순서대로 판정하세요. read=0으로 바꾸면 첫 결과가 달라지나요?
풀이 확인
첫 요청은 0x7f<0x80이고 write=1이므로 0x3007f를 반환합니다. 두 번째는 범위 검사에서 SEGFAULT로 끝나며 쓰기 권한 검사까지 가지 않습니다. 세 번째는 번호 검사에서 SEGFAULT로 끝나 entry[4]를 읽지 않습니다. read는 검사하지 않아 0으로 바꿔도 첫 결과는 같습니다. 유효한 테이블 값과 주소 폭 안의 덧셈을 전제한 계산이며, 실제 CPU의 모든 접근 보호나 C 객체 수명을 검증한 것은 아닙니다.
세그먼트별 보호의 장점
세그먼트별로 권한을 다르게 설정할 수 있는 것은 큰 보안 이점입니다.
- 코드 세그먼트: R+X (읽기+실행). 쓰기 불가. 그 매핑을 통한 코드 쓰기를 막습니다.
- 데이터 세그먼트: R+W (읽기+쓰기). 실행 불가. 그 매핑의 데이터를 명령으로 실행하지 못하게 합니다.
- 스택: R+W. NX(No eXecute) 비트로 실행을 금지합니다. 스택에 삽입한 코드의 실행을 막지만 버퍼 범위를 넘는 쓰기 자체를 막는 것은 아닙니다.
현대의 페이지 기반 보호도 읽기·쓰기·실행 권한을 구분합니다. DEP(Data Execution Prevention)와 NX는 실행 금지 보호와 관련됩니다.
세그먼테이션 폴트
개발자라면 한 번쯤 만나 봤을 Segmentation Fault입니다.
이 오류는 프로세스가 자신에게 할당되지 않은 메모리 영역에 접근하거나, 권한이 없는 접근을 시도할 때 발생합니다.
다음 함수들은 잘못된 C 메모리 접근의 예입니다. 호출하면 모두 정의되지 않은 동작이며, 특정 출력이나 SIGSEGV 발생을 보장하는 테스트가 아닙니다.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* 원인 1: NULL 포인터 역참조 */
void null_deref() {
int *ptr = NULL;
*ptr = 42; /* NULL 역참조: 정의되지 않은 동작 */
}
/* 원인 2: 해제된 메모리 접근 (Use-After-Free) */
void use_after_free() {
int *ptr = malloc(sizeof(int));
*ptr = 100;
free(ptr);
printf("%d\n", *ptr); /* 해제 뒤 접근: 정의되지 않은 동작 */
}
/* 원인 3: 배열 범위 초과 */
void buffer_overflow() {
int arr[10];
arr[10000] = 1; /* 배열 범위 초과: 정의되지 않은 동작 */
}
/* 원인 4: 읽기 전용 메모리 쓰기 */
void readonly_write() {
char *str = "Hello"; /* 문자열 리터럴: 수정하면 안 됨 */
str[0] = 'h'; /* 문자열 리터럴 수정: 정의되지 않은 동작 */
}Linux 등에서는 낮은 주소 매핑을 제한해 흔한 NULL 역참조를 발견하도록 돕습니다. 제한 범위는 설정에 따라 달라집니다. 접근이 MMU의 매핑·보호 검사를 위반하면 OS가 SIGSEGV로 처리할 수 있지만, C 배열 경계나 해제된 객체의 수명까지 MMU가 모두 아는 것은 아닙니다.
세그먼테이션의 장단점
장점
- 프로그래머 관점과 일치: 코드, 데이터, 스택이라는 논리적 단위와 일치합니다.
- 세그먼트별 보호: 코드는 읽기 전용, 스택은 실행 불가 등 세밀한 권한 설정이 가능합니다.
- 세그먼트 공유: 두 프로세스가 같은 텍스트 세그먼트를 공유하면 메모리를 절약합니다. 공유 라이브러리의 기반입니다.
- 세그먼트별 독립 증가: 힙과 스택의 크기가 독립적으로 변할 수 있습니다.
단점
- 외부 단편화: 세그먼트 크기가 가변적이므로 여전히 외부 단편화가 발생합니다. 큰 세그먼트를 위한 연속 공간을 찾기 어려울 수 있습니다.
- 크기 제한: 세그먼트는 연속된 물리 메모리 영역에 배치되어야 하므로, 물리 메모리보다 큰 세그먼트는 불가능합니다.
세그먼테이션 + 페이징
x86 아키텍처는 세그먼테이션과 페이징을 함께 사용합니다.
논리 주소가 세그먼테이션을 거쳐 선형 주소(Linear Address)가 되고, 선형 주소가 페이징을 거쳐 물리 주소가 됩니다.
x86-64의 64비트 모드에서는 CS·DS·ES·SS의 주소 베이스 효과가 0으로 취급됩니다. 다만 FS와 GS는 계속 사용되며 TLS 같은 용도로 별도 베이스를 가질 수 있습니다.
페이징은 프로세스 전체나 가변 크기 세그먼트를 하나의 연속 물리 블록에 담는 제약을 줄입니다. 큰 연속 물리 블록을 따로 요구하는 경우의 단편화까지 없애는 것은 아닙니다.
다음 절에서 스와핑과 동적 메모리를 다루고, 9장에서 페이징을 본격적으로 살펴보겠습니다.