주소 바인딩
논리 주소와 물리 주소를 구분하고 컴파일·적재·실행 시점의 바인딩과 MMU의 동적 주소 변환을 이해합니다.
malloc(100)을 호출하면 메모리 주소가 반환됩니다.
그런데 이 주소는 실제 물리 메모리의 주소일까요?
아닙니다.
프로그램이 사용하는 주소와 실제 RAM의 주소는 다릅니다.
운영체제가 이 둘을 연결해주는 과정이 주소 바인딩(Address Binding)입니다.
주소 바인딩을 이해하지 못하면 왜 두 프로세스가 같은 주소를 사용해도 충돌하지 않는지, 왜 프로그램을 다시 실행하면 포인터 값이 달라지는지, 세그먼테이션 폴트가 왜 발생하는지를 설명할 수 없습니다.
malloc(100)을 호출하면 메모리 주소가 반환됩니다. 그런데 이 주소는 실제 물리 메모리의 주소일까요?
- CPU
프로세스 기준 논리 주소 생성 0x0120
- MMU
base 또는 page table 조회 + 0x7000
- 보호 검사
범위·R/W/X 권한 확인 valid
- RAM
변환된 물리 위치 접근 0x7120
- 논리 주소와 물리 주소
논리 주소는 CPU가 만드는 가상 주소입니다.
- 바인딩 시점
논리 주소가 물리 주소로 변환되는 시점에 따라 세 가지 방식이 있습니다.
- MMU와 주소 변환
MMU는 논리 주소를 물리 주소로 바꾸는 하드웨어입니다.
- 동적 링킹과 적재
정적 링킹 vs 동적 링킹 정적 링킹(Static Linking): 라이브러리 코드가 실행 파일에 포함됩니다.
- 주소 바인딩 기준
주소 생성 운영체제가 이 둘을 연결해주는 과정이 주소 바인딩(Address Binding)입니다. 변환 흐름 논리 주소는 CPU가 명령 실행 중 생성하는 주소이며, MMU가 물리 주소로 변환합니다. 매핑 확인 바인딩 시점 논리 주소가 물리 주소로 변환되는 시점에 따라 세 가지 방식이 있습니다. address binding 한계 컴파일 시간, 적재 시간, 실행 시간 바인딩의 차이를 재배치 가능성과 사용 시점으로 비교합니다.
- 주소 바인딩(Address Binding)
주소 생성 운영체제가 이 둘을 연결해주는 과정이 주소 바인딩(Address Binding)입니다.
- 논리 주소와 물리 주소
변환 흐름 논리 주소는 CPU가 명령 실행 중 생성하는 주소이며, MMU가 물리 주소로 변환합니다.
- 바인딩 시점
매핑 확인 바인딩 시점 논리 주소가 물리 주소로 변환되는 시점에 따라 세 가지 방식이 있습니다.
- MMU와 주소 변환
address binding 한계 컴파일 시간, 적재 시간, 실행 시간 바인딩의 차이를 재배치 가능성과 사용 시점으로 비교합니다.
논리 주소와 물리 주소
논리 주소 (Virtual Address)
논리 주소(Logical Address)는 CPU가 생성하는 주소이며, 가상 주소(Virtual Address)라고도 합니다.
프로그램의 관점에서 보는 주소입니다.
핵심 특성: 프로세스마다 독립적인 주소 공간을 가집니다.
프로세스 A의 주소 0x1000과 프로세스 B의 주소 0x1000은 전혀 다른 물리 위치를 가리킵니다.
이것이 프로세스 격리의 기반입니다.
32비트 시스템에서 논리 주소 공간은 GB 입니다.
64비트 시스템에서는 이론적으로 이지만, 실제로는 48비트(256TB, x86-64) 또는 57비트(128PB, 5-level paging)만 사용합니다.
물리 주소 (Physical Address)
물리 주소(Physical Address)는 실제 RAM 칩에서의 위치입니다.
메모리 버스에 실리는 주소이며, DRAM 컨트롤러가 이 주소로 데이터를 읽고 씁니다.
#include <stdio.h>
#include <stdlib.h>
int global_var = 42;
int main() {
int stack_var = 10;
int *heap_var = malloc(sizeof(int));
printf("global_var 논리 주소: %p\n", (void *)&global_var);
printf("stack_var 논리 주소: %p\n", (void *)&stack_var);
printf("heap_var 논리 주소: %p\n", (void *)heap_var);
/* 이 주소들은 논리 주소입니다.
물리 주소는 프로그램에서 알 수 없습니다.
같은 프로그램을 두 번 실행하면 주소가 다릅니다 (ASLR). */
free(heap_var);
return 0;
}같은 프로그램을 두 번 실행하면 출력되는 주소가 달라집니다.
이는 ASLR(Address Space Layout Randomization) 때문인데, 보안을 위해 프로세스의 메모리 배치를 매번 랜덤하게 합니다.
공격자가 특정 함수나 버퍼의 주소를 예측하기 어렵게 만듭니다.
왜 분리해야 하는가
두 주소가 분리되어 있지 않다면(모든 프로세스가 물리 주소를 직접 사용한다면):
- 프로세스 A의 버그가 프로세스 B의 메모리를 덮어쓸 수 있습니다.
- 프로그램을 특정 물리 주소용으로 컴파일해야 합니다. 그 주소가 이미 사용 중이면 실행할 수 없습니다.
- 프로세스를 메모리의 다른 위치로 옮길 수 없습니다.
- 물리 메모리보다 큰 프로그램은 실행 불가능합니다.
바인딩 시점
논리 주소가 물리 주소로 변환되는 시점에 따라 세 가지 방식이 있습니다.
컴파일 시 바인딩 (Compile-time Binding)
컴파일러가 절대 주소(Absolute Address)를 생성합니다.
프로그램이 항상 같은 물리 주소에 적재되어야 합니다.
MS-DOS 시절의 COM 프로그램이 이 방식이었습니다.
프로그램은 항상 주소 0100h에 적재된다고 가정하고 컴파일했습니다.
현대 시스템에서는 동시에 여러 프로그램이 실행되므로 모든 프로그램이 같은 주소를 사용할 수 없어 거의 사용하지 않습니다.
적재 시 바인딩 (Load-time Binding)
컴파일러가 재배치 가능 코드(Relocatable Code)를 생성합니다.
프로그램을 메모리에 적재할 때 시작 주소를 결정하고, 코드 내의 모든 주소를 그에 맞게 조정합니다.
단점: 적재 후에는 프로세스를 다른 위치로 옮길 수 없습니다.
모든 주소가 이미 고정되었기 때문입니다.
메모리 압축(compaction)이 불가능합니다.
실행 시 바인딩 (Execution-time Binding)
주소 변환을 실행 시점으로 미룹니다.
CPU가 명령어를 실행할 때마다 논리 주소를 물리 주소로 변환합니다.
이 변환은 하드웨어(MMU)가 수행하므로 성능 저하가 거의 없습니다.
장점: 프로세스를 실행 중에 다른 물리 위치로 옮길 수 있습니다.
가상 메모리, 페이징, 스와핑 등 모든 현대적 메모리 관리 기법의 전제 조건입니다.
현대 운영체제는 모두 실행 시 바인딩을 사용합니다.
| 방식 | 주소 결정 시점 | 재배치 가능 | 사용 시기 |
|---|---|---|---|
| 컴파일 시 | 컴파일 | 불가 | MS-DOS COM |
| 적재 시 | 로더 실행 시 | 불가 | 초기 멀티프로그래밍 |
| 실행 시 | 매 명령어 실행 시 | 가능 | 현대 OS |
바인딩 시점은 결국 주소 결정을 어느 단계까지 미룰 수 있는지의 차이입니다.
아래 다이어그램은 세 방식의 책임 위치와 재배치 가능성을 한눈에 묶어 보여줍니다.
바인딩이 늦어질수록 실행 중 이동과 격리가 쉬워지고, 하드웨어 MMU의 역할이 커집니다.
- 컴파일러가 절대 주소 생성
Compile-time 프로그램은 정해진 물리 위치에만 올라갈 수 있어 동시에 여러 작업을 돌리기 어렵습니다. 이동성 실행 중 재배치 불가
- 로더가 시작 주소를 반영
Load-time 적재 순간에는 위치를 고를 수 있지만, 실행이 시작되면 주소가 고정됩니다. 이동성 적재 전까지만 가능
- MMU가 매 접근을 변환
Execution-time 프로세스를 옮겨도 논리 주소는 그대로라 페이징, 스와핑, 보호가 자연스럽게 이어집니다. 이동성 실행 중에도 가능
MMU와 주소 변환
MMU(Memory Management Unit)는 CPU 내부(또는 CPU와 메모리 사이)에 위치한 하드웨어로, 논리 주소를 물리 주소로 변환합니다.
소프트웨어로는 매 메모리 접근마다 변환을 수행할 수 없습니다 — 너무 느립니다.
따라서 하드웨어가 필수입니다.
베이스-한계 레지스터 방식
가장 단순한 형태의 MMU입니다.
두 개의 레지스터를 사용합니다.
베이스 레지스터(Base Register, Relocation Register): 프로세스의 시작 물리 주소를 저장합니다.
논리 주소에 베이스 값을 더하면 물리 주소가 됩니다.
한계 레지스터(Limit Register): 프로세스의 메모리 크기를 저장합니다.
논리 주소가 한계 값보다 크면 잘못된 접근이므로 트랩을 발생시킵니다.
/* 개념적 의사 코드 — 실제로는 하드웨어가 매 클럭마다 수행 */
typedef struct {
unsigned int base; /* 프로세스 시작 물리 주소 */
unsigned int limit; /* 프로세스 크기 */
} MMU;
unsigned int translate(MMU *mmu, unsigned int logical_addr) {
if (logical_addr >= mmu->limit) {
/* 범위 초과 → 트랩(인터럽트) → OS가 SIGSEGV 전달 */
raise_trap(SEGFAULT);
return (unsigned int)-1;
}
return mmu->base + logical_addr;
}
/*
* 프로세스 A: base = 0x10000, limit = 0x5000
* 논리 주소 0x0000 → 물리 주소 0x10000
* 논리 주소 0x3000 → 물리 주소 0x13000
* 논리 주소 0x6000 → 트랩! (0x6000 >= 0x5000)
*
* 프로세스 B: base = 0x20000, limit = 0x3000
* 논리 주소 0x0000 → 물리 주소 0x20000
* 같은 논리 주소 0x0000이지만 A와 다른 물리 주소!
*/컨텍스트 스위칭 시 OS는 베이스와 한계 레지스터를 새 프로세스의 값으로 교체합니다.
이것이 프로세스마다 독립된 주소 공간이 보장되는 메커니즘입니다.
CPU가 만든 offset이 limit보다 작으면 base를 더해 실제 메모리 주소를 만들고, 범위를 넘으면 trap으로 보호한다.
- 논리 주소 offset 추출 프로세스
자신이 0번지부터 시작한다고 생각한다.
- 범위 검사 offset
limit 비교 작으면 정상 접근, 같거나 크면 보호 위반이다.
- 주소 생성 base에 offset
더함 물리 메모리의 실제 위치로 변환된다.
- 보호 범위 밖이면 trap 다른 프로세스의 메모리
침범하지 못하게 막는다.
| 예시 | 판정 | 결과 |
|---|---|---|
| base 4000, limit 1000, offset 240 | offset이 limit보다 작음 | 물리 주소 4240으로 접근한다. |
| base 4000, limit 1000, offset 1200 | offset이 limit 이상 | trap으로 접근을 차단한다. |
| 문맥 교환 | 프로세스마다 레지스터 값 교체 | 같은 논리 주소도 다른 물리 위치를 가리킨다. |
한계와 발전
베이스-한계 방식은 프로세스의 메모리가 연속적이어야 합니다.
이는 외부 단편화 문제로 이어집니다.
이 한계를 극복하기 위해 세그먼테이션과 페이징이 등장합니다.
현대 CPU의 MMU는 페이지 테이블을 사용하여 4KB 단위로 주소를 변환합니다.
동적 링킹과 적재
정적 링킹 vs 동적 링킹
정적 링킹(Static Linking): 라이브러리 코드가 실행 파일에 포함됩니다.
실행 파일이 크지만 독립적입니다.
동적 링킹(Dynamic Linking): 라이브러리 코드가 별도 파일(.so, .dll)로 존재하며, 실행 시점에 연결됩니다.
여러 프로세스가 같은 라이브러리를 메모리에서 공유할 수 있어 메모리를 절약합니다.
libc.so는 거의 모든 C 프로그램이 사용합니다.
100개 프로세스가 각각 libc를 포함하면 수백 MB가 낭비되지만, 동적 링킹으로 물리 메모리에 한 복사본만 두면 됩니다.
import subprocess
# Linux에서 동적 링킹된 라이브러리 확인
result = subprocess.run(["ldd", "/bin/ls"], capture_output=True, text=True)
print(result.stdout)
# libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f...)
# ld-linux-x86-64.so.2 (0x00007f...)동적 적재
동적 적재(Dynamic Loading): 프로그램의 모든 코드를 한꺼번에 메모리에 올리지 않고, 호출될 때 필요한 루틴만 적재합니다.
에러 처리 루틴처럼 드물게 실행되는 코드는 호출되기 전까지 메모리를 차지하지 않습니다.
OS의 지원 없이도 프로그래머가 직접 구현할 수 있지만(dlopen/dlsym 사용), 현대 OS의 요구 페이징이 이를 자동으로 처리합니다.
아래 다이어그램은 논리 주소가 실행 중에 물리 주소로 바뀌는 흐름과 그 결과로 얻는 격리 효과를 정리한 것입니다.
현대 OS는 실행 시점 바인딩으로 프로세스마다 독립 주소 공간을 유지하고, 실제 RAM 배치는 숨깁니다. 주소 변환은 단순한 덧셈이 아니라 권한 검사, 페이지 테이블 조회, 예외 처리까지 포함한 보호 장치입니다.
- 논리 주소
0x1000 논리 주소 CPU와 프로그램이 보는 프로세스 내부 주소이며, 다른 프로세스의 같은 값과 충돌하지 않습니다.
- 하드웨어 변환
MMU 하드웨어 변환 TLB, 페이지 테이블, 권한 비트로 물리 위치와 접근 가능 여부를 함께 판단합니다.
- 물리 주소
RAM 물리 주소 실제 메모리 버스에 실리는 위치로 바뀌며, 페이지 프레임 단위로 관리됩니다.
- 보호 실패
Trap 보호 실패 범위나 권한을 넘으면 page fault 또는 protection fault로 OS가 제어권을 받습니다.
주소 바인딩을 정리하면, CPU가 만든 논리 주소를 MMU가 보호와 재배치 규칙에 맞춰 물리 위치로 보내는 과정입니다.
프로세스는 자기만의 주소를 쓰고, MMU는 실행 시점에 그 주소가 허용된 범위인지 확인한 뒤 실제 RAM 위치로 보냅니다.
- CPU논리 주소 생성
CPU 명령어가 포인터나 배열 인덱스를 사용해 프로세스 기준 주소를 만듭니다.
- MMU범위와 권한 확인
MMU 베이스, 한계, 페이지 테이블 같은 하드웨어 상태로 접근 가능성을 검사합니다.
- OS컨텍스트마다 기준 교체
OS 프로세스가 바뀔 때 주소 변환 기준도 바뀌어 같은 값이 다른 위치를 가리킵니다.
- RAM물리 주소 접근
RAM 검사를 통과한 요청만 실제 메모리 버스의 주소로 전달됩니다.
다음 절에서는 여러 프로세스의 메모리를 연속으로 할당하는 방식과 그 한계인 단편화 문제를 살펴봅니다.
소스의 변수 이름은 컴파일, 적재, 실행을 거치며 다른 주소 표현으로 바뀝니다. 언제 확정되는지가 유연성과 비용을 결정합니다.
- 01
Symbol
- 02
Compile
- 05
Physical
- 컴파일 시점 시작 위치
고정되어 있으면 빠르지만 재배치 유연성이 거의 없습니다.
- 적재 시점 프로그램
메모리에 올릴 때 기준 주소를 더해 실제 위치를 맞춥니다.
- 실행 시점 MMU
논리 주소를 물리 주소로 매번 변환해 이동과 보호를 가능하게 합니다.
- 오류 단서 세그멘테이션 오류
보호 예외는 변환 또는 접근 권한 경계에서 발생합니다.
컴파일, 로드, 실행 시점 중 언제 주소를 확정하느냐에 따라 재배치 가능성, MMU 필요성, 프로세스 이동 비용이 달라진다.
- 소스/심볼
코드의 변수와 함수 이름은 아직 논리적 이름일 뿐 실제 메모리 위치가 아니다. symbol
- 컴파일 바인딩
적재 위치를 미리 알면 컴파일러가 절대주소를 박을 수 있지만 이동성이 낮다. fixed address
- 로드 바인딩
더가 실제 적재 시작 주소를 보고 재배치 정보를 반영한다. relocation
- 실행 바인딩
프로세스 실행 중에도 MMU가 논리주소를 물리주소로 변환한다. paging/segmentation
- Compile time
count = 0x7120 절대주소를 코드에 고정한다.
- Load time
base + 0x120 적재할 때 한 번 재배치한다.
- Execution time
page 1 → frame 7 접근할 때마다 MMU가 매핑한다.