Address space & Memory API
이번 포스트는 OS의 주소공간에 대한 개념과 Memory API에 대해 다룬다.
Address Space
Memory Virtualization
Memory Virtualization (메모리 가상화) 이란?
- 운영체제가 실제 메모리를 가상화하여 각 프로세스에게 자신만의 독립된 메모리를 가진 것처럼 보이게 하는 기술이다.
- OS가 물리 메모리를 잘개 쪼개서, 여러 프로세스가 동시에 메모리를 사용하는 것처럼 환상을 제공한다.
왜 필요한가?
초기 메모리 시스템 (Without Virtualization)
예전에는 메모리 내에 한 번에 하나의 프로세스만 로드가 가능하다.
- 메모리 낭비가 심하고
- 멀티프로그램이 불가하고
- 성능이 낮음.
=> 매우 비효율적인 구조
Multiprogramming and Time Sharing
아이디어 : 하나의 메모리에 여러 프로세스를 올려 더 효율적으로 사용
초기 OS : 한 번에 하나의 프로그램만
- CPU가 노는 시간이 너무 많아서 비효율적
Load multiple processes in memory.
- 메모리에 여러 프로세스를 동시에 올려주고 번갈아서 실행을 한다.
- 한 프로세스를 잠시 실행하고 중단한다.
- 정해진 타임 슬라이스마다 다른 프로세스로 교체를 한다.
- Time Sharing
- 프로세스 A가 I/O를 기다릴때, 프로세스 B가 CPU를 사용하면 CPU가 놀지 않기 때문에, 전체 시스템의 효율이 증가한다.
예시 그림)
아래와 같이 구간을 나누어 각 프로세스는 자신에게 배정된 공간 범위만 접근이 가능하다.
[ OS ]
[ Free ]
[ Process C ]
[ Process B ]
[ Free ]
[ Process A ]
[ Free ]
문제 : Protection Issue
- 다른 프로세스의 메모리에 잘 못 접근할 수 있다.
- 예시) 프로세스 A가 프로세스 B의 데이터를 덮어씌우는 상황
Address Space (주소 공간)
개념 : OS가 만드는 가짜의 개인 메모리 공간
- 하나의 실제 물리 메모리를 여러 프로세스가 공유하면서도 각 프로세스는 자기만의 독립된 메모리를 갖는 것처럼 보인다.
- 각 프로세스는 자기만의 독립된 메모리를 전체를 다 쓰는 것처럼 보이게 한다.
- 실제 물리 메모리와 1:1 대응이 아니라 OS가 매핑해서 연결해준다.
C언어에서 주소를 출력하는 코드를 짤 때,
이때 나오는 주소가 가상주소 Address Space 의 값이다.
즉, 각 프로세스는 같은 가상의 주소 0KB를 사용하지만 (예시), 실제로는 다른 물리 주소에 매핑한다. 그리고 이 주소는 OS만 알고 있다.
- 이 공간을 => Address Space (주소 공간) 이라고 함.
0KB Program Code
1KB Heap
(free space)
15KB Stack
16KB
위와 같이 프로그램 실행 시 모든 데이터가 이 구조 안에 배치
Address Space 구성 요소
- Code : 프로그램의 기계어 (instruction)이 저장되는 영역
- Data : 전역 변수, static 변수
- Heap : 동적 메모리 (malloc / new로 증가하고, free / delete로 감소)
- Stack : 함수 호출 스택 (지역 변수, return address 저장)
- (Free) : Heap, Stack 사이의 비어있는 공간으로 시간에 따라 공간을 확장
Stack과 Heap이 서로 반대 방향으로 향하는 이유
- 두 공간은 만나지 않고 서로 마주보며 커지도록 설계되어있다.
- 그 이유는
- 유연하고
- 동적 크기로 증가가 가능하다.
- (중간 Free 영역이 넓게 존재)
왜 Address Space를 사용하는가?
1. 보호 (Protection)
- 여러 프로세스가 동시에 메모리를 쓸 때 한 프로세스가 다른 프로세스 메모리를 맘대로 건드리면 안된다.
- OS는 프로세스마다 독립된 address space를 만들어 서로의 메모리 간섭을 원천 봉쇄한다.
- 아까의 프로세스 A가 프로세스 B의 메모리를 건드리는 것과 같은 문제는 발생 X
2. 편리함 (Convenience)
- 모든 프로세스는 항상 0KB ~ max 사이의 동일한 구조를 가진다.
- 개발자가 물리 주소를 신경 쓸 필요가 없다.
3. 효율성 (Efficiency)
- 실제 물리 메모리는 여러 프로세스가 공유하지만, Address Space는 독립적이므로 물리 메모리의 활용도를 최대로 끌어올릴 수 있다.
Memory API
Memory Allocation Interface
유저 프로그램은 직접 물리 메모리를 만질 수 없다.
메모리 요청의 흐름은 다음과 같다.
Application → libc (malloc/free) → System calls (brk/sbrk) → Kernel
libc란?malloc(), free(), calloc(), realloc()은 라이브러리 함수 (libc)- 실제 메모리 확장은 OS 시스템 콜인
brk()/sbrk()가 실행함. malloc()은 커널에 직접 요청을 하지 않고, libc가 요청을 처리하고 필요할 때만 커널로 시스템 콜을 호출한다.
- Heap의 구조
[ metadata | allocated block ]
- metadata : 할당된 블록의 크기, 상태 정보
- 포인터가 가리키는 것은 metadata 뒤의 실제 데이터 부분
- free 시에 metadata를 참조하여 얼마만큼의 메모리를 회수해야 하는지를 알게 된다.
malloc()
-
Heap 영역에서 size만큼 동적 할당하여 그 시작 주소를 반환하는 함수
-
사용 방법
#include <stdlib.h> void* malloc (size_t size)
size_t : unsigned integer (부호 없는 정수 타입) , byte 단위
반환값 :
- 성공 시에 :
void *포인터 - 실패 시에 : NULL
사용시 주의사항 :
- malloc은 실제 할당된 크기 + metadata를 포함한 블록을 내부적으로 관리한다.
- 포인터가 가리키는 것은 사용자의 데이터 영역이다.
sizeof()
왜 pointer와 array는 값이 다를까?
sizeof()연산자는 컴파일 타입에 타입의 크기를 계산한다.- 실제로 들어있는 값이나 malloc으로 할당된 크기와는 관계가 없다.
- x가 pointer일 때
int *x = malloc(10 * sizeof(int));
printf("%d\n", sizeof(x));
// result = 8 (64bit 기준), 4 (32bit 기준)
이유 : sizeof(x)는 포인터 변수 자체의 크기를 알려주는 것이기 때문
64-bit system : pointer는 8바이트, 32-bit는 4 바이트
sizeof(pointer) 와 malloc으로 할당한 메모리 크기는 다르다.
- x가 array일 때
int x[10];
printf("%d\n", sizeof(x));
// result = 40 (10 * 4bytes)
이유 : 배열은 고정 크기를 가지며, sizeof(array)는 전체 배열의 바이트 수를 계산한다.
free()
free(ptr)은malloc()으로 할당했던 메모리를 반환하는 함수- OS에 바로 반환하는 것이 아니라, heap allocation(libc)가 관리
- 동작 방식
void free(void* ptr);
ptr 이 가리키는 메모리 블록의 앞쪽에 있는 metadata(크기, 상태 등)를 보고 해당 블록을 ‘free list’ 로 돌려놓는다.
malloc()은 할당한 메모리 앞부분에 메타데이터를 숨겨놓는다.
free()는 ptr 바로 앞의 meta 정보를 읽어서 어느 영역을 해제해야 할지를 결정한다.
- 주의할 부분
- malloc으로 할당하지 않은 메모리 free 금지
- 한 번 free한 포인터 다시 free 금지 (double free)
- free 이후 포인터 사용 금지 (Dangling pointer)
Common Errors
1. Forgetting To Allocate Memory
(메모리에 할당하지 않은 포인터에 strcpy 하는 경우)
char *src = "hello"; // 문자열 리터럴 (data segment)
char *dst; // unallocated pointer
strcpy(dst, src); // segfault
dst는 쓰레기 주소(garbage)를 가지고 있다.- 어디를 가리키는지 OS는 모른다.
- 힙도 아니고 스택도 아닐 수 있음.
strcpy(dst, src)는 src의 문자열을 dst가 가리키는 위치에 무조건 복사한다.
=> 무작위 메모리 영역에 write를 발생하여 보호된 영역이라면 segmentation fault가 일어난다.
-> 올바른 코드
char *src = "hello"; // 문자열 리터럴
char *dst = (char*)malloc(strlen(src) + 1); // 널 문자까지 공간 확보!
strcpy(dst, src); // OK
2. Not Allocating Enough Memory
(널 문자를 위한 공간을 확보하지 않아 생기는 문제)
char *src = "hello";
char *dst = (char*)malloc(strlen(src)); // too small (5 bytes)
strcpy(dst, src); // 위험하지만 작동할 수도 있음
strlen(src) = 5- 그러나 문자열은 반드시
hello\0로 위 예시에서는 6바이트가 필요 malloc(5)은 NULL 문자가 들어갈 공간이 없다.- 이는 6번째 바이트를 힙의 다른 영역을 침범해서 기록한다.
- overflow가 생긴다.
- 더 무서운 부분
- 프로그램은 죽지 않고 실행될 수 있다. (silent corruption)
- 하지만 결국 heap corruption, segfault, 예측 불가 동작을 일으킨다.
3. Forgetting to Initalize Allocated Memory
(초기화하지 않은 메모리 읽기)
malloc()은 메모리만 할당하지, 값을 초기화하지 않는다.- 따라서 할당된 메모리 안에는 이전에 그 위치를 사용했던 프로그램의 쓰레기 값이 들어있다.
- 초기화 되지 않은 메모리를 읽으면 undefined behavior로 예측이 불가능한 값을 출력한다.
int *x = (int*)malloc(sizeof(int));
printf("%d\n", *x); // 초기화되지 않은 메모리 접근
위의 예시로는 힙의 해당 위치에 어떤 값을 갖고 있었는지를 알 수 없기에 *x에는 쓰레기 값이 들어있고, 이를 읽는 것은 위험하다.
=> 따라서 메모리를 받은 뒤에는 반드시 다음과 같이 초기화를 해야한다.
*x = 0;
// or
int *x = calloc(1, sizeof(int)); // calloc은 0으로 초기화
4. Memory Leak (Forgetting to Free Memory)
malloc()한 메모리를free()하지 않으면 누수가 일어난다. (Memory Leak)- 누수가 반복되면 힙의 공간이 고갈되고 이는 프로그램의 crash가 일어난다.
- C에서는 GC(Garbage Collector)가 없기 때문에
free()는 반드시 직접 호출해야 한다.
int *a;
while(1) {
a = (int*)malloc(sizeof(int)); // free 없음 → 누수 발생
}
- 반복문 안에서 힙에 계속 메모리를 요청
- 반환을 하지 않기에 힙 영역이 점점 많아진다.
- 이는 메모리 부족 (out of memory) 로 프로그램이 종료된다.
5. Dangling Pointer (Freeing Memory While in Use)
(사용중인 메모리를 free)
- Dangling Pointer : 해제된 (freed) 메모리를 가리키는 pointer
- free한 뒤에도 포인터가 그대로 남아있으면, 접근 시에 segementation fault 또는 예측 불가능한 버그가 발생한다.
- free 이후에는 반드시 pointer를 NULL로 설정해야 한다.
ptr = NULL;
int *b = malloc(4);
free(b); // 메모리 해제
*b = 10; // danger! dangling pointer 사용!
6. Double Free (Freeing Memory Repeatedly)
- Double Free : 이미
free()한 포인터를 또free()하는 것 - 첫 번째 free로 정상적인 메모리를 해제
- 두 번째 free 는 이미 해제된 메모리를 다시 해제하려고 한다.
- undefined behavior
- 프로그램이 바로 죽을 수도 있고, 조용히 망가질 수도 있다.
- (예) heap metadata 손상 -> heap corruption 발생
malloc / free 는 thread-safe 하지 않을 수 있기에
동시성 (Synchronization)문제도 발생할 수 있다.
7. Invalid Free (Calling free() Incorrectly)
malloc()으로 할당된 시작 주소만free()가 가능하다.- 포인터 연산 등으로 다른 주소를
free()하면 heap metadata가 손상된다.
int *x = malloc(4 * sizeof(int)); // allocated
free(x + sizeof(int)); // invalid free
free는 실제로 malloc 블록의 헤더 (metadata)를 기반으로 해제한다.- 잘못된 주소를 free 하면:
- heap metadata가 꼬인다.
- 다음 malloc / free가 이상하게 동작한다.
- 프로그램의 전체가 불안정해지는 결과를 낸다.
calloc(), realloc()
calloc()
calloc(num, size)- 메모리 할당 + 0으로 초기화
int *arr = calloc(10, sizeof(int)); // 10개 int를 모두 0으로 초기화
realloc()
- 기존 메모리 블록을 더 크게 / 더 작게 변경할 때 사용
- 내부에서 새로운 블록을 만들고 복사한 뒤 기존 블록을 free 할 수 있다.
ptr = realloc(ptr, new_size);
-> 바로 덮어쓰면 realloc 실패로 메모리를 잃어버릴 수 있다.
void *tmp = realloc(ptr, new_size);
if (tmp != NULL)
ptr = tmp;
System Calls : brk(), sbrk()
malloc()내부에서 필요한 힙 공간이 부족하면 OS에게 요청하는 시스템 콜:brk(): 힙의 끝 (brk pointer)을 특정 위치로 이동한다.sbrk(): brk pointer를 상대적으로 증가 / 감소한다.
- brk pointer
-
힙의 끝을 나타내는 주소로 Heap은 아래 방향으로 확장되기에 brk가 내려가면 힙이 커진다.
- 동작 흐름
malloc()요청이 들어온다.- 현재 free 리스트에 적절한 블록이 없으면
brk() or sbrk()호출에 힙이 확장된다.- 새 힙 영역을 블록으로 관리하여 반환한다.
힙 관리자는 glibc 내부에서 관리중이기에
직접 brk/sbrk를 쓰면 heap 구조를 망가뜨릴 위험이 존재한다.