사용자 공간과 커널 공간: 가상 주소가 분리되는 실제 방식

반응형

사용자 공간과 커널 공간은 RAM을 물리적으로 반으로 나눈 이름이 아니다. 운영체제가 가상 주소와 CPU 권한을 함께 사용해 접근 가능한 범위를 나눈 것이다.

이 구분 덕분에 일반 프로세스는 커널 코드나 다른 프로세스의 메모리를 마음대로 읽고 쓸 수 없다. 다만 “아래쪽 주소는 사용자, 위쪽 주소는 커널”이라는 그림만 외우면 아키텍처와 보안 완화 기법에 따라 달라지는 실제 동작을 놓치기 쉽다.

먼저 구분할 세 가지

구분 의미 서로 같은가?
가상 주소 프로세스와 CPU가 명령에서 사용하는 주소 물리 주소와 다르다
물리 메모리 실제 RAM의 page frame 같은 page가 여러 가상 주소에 매핑될 수도 있다
실행 권한 user mode와 kernel mode에서 허용되는 명령·page 접근 범위 주소만으로 결정되지 않는다

사용자 공간과 커널 공간을 이해할 때는 이 세 층을 섞지 않는 것이 출발점이다.

프로세스마다 가상 주소 공간이 있다

각 프로세스는 자신만의 가상 주소 공간을 본다. 두 프로세스가 같은 숫자의 가상 주소를 사용하더라도 page table을 거쳐 서로 다른 물리 page에 연결될 수 있다.

Process A virtual address ─┐
                          ├─ page table ─> physical page
Process B virtual address ─┘                (mapping can differ)

일반적인 사용자 공간에는 다음 영역이 나타난다.

  • 실행 파일의 code와 read-only data
  • 전역·정적 data
  • heap
  • thread별 stack
  • shared library와 memory-mapped file
  • anonymous mapping

Linux에서는 다음처럼 현재 shell 프로세스의 매핑을 읽어 볼 수 있다.

cat /proc/$$/maps

이 출력은 가상 주소 구간과 permission, 연결된 file을 보여 준다. RAM을 그 크기만큼 이미 점유했다는 뜻은 아니다. resident page가 얼마나 되는지까지 보려면 /proc/<pid>/smapssmaps_rollup 같은 별도 자료가 필요하다.

커널 공간은 단순한 별도 RAM 구역이 아니다

커널은 kernel code, page cache, device mapping, allocator 영역과 여러 kernel object를 자신의 가상 주소로 접근한다. 중요한 점은 가상 주소 배치와 물리 page의 소유를 같은 것으로 보면 안 된다는 것이다.

전통적인 설명에서는 하나의 가상 주소 공간 아래쪽을 사용자 영역, 위쪽을 커널 영역으로 그린다. x86-64 Linux의 대표적인 layout도 이 직관에 가깝다. 하지만 정확한 경계와 크기는 다음 조건에 따라 달라진다.

  • CPU architecture와 지원하는 virtual address bit 수
  • 4-level 또는 5-level paging
  • kernel build configuration
  • KASLR 같은 address randomization
  • 운영체제와 보안 완화 기법

따라서 특정 숫자를 모든 Linux 시스템의 고정 경계처럼 외우기보다, “page table permission과 현재 privilege level이 접근을 통제한다”는 원리를 잡는 편이 낫다.

user mode와 kernel mode가 접근을 가른다

일반 application code는 user mode에서 실행된다. system call, interrupt, exception으로 kernel에 진입하면 CPU는 kernel mode에서 운영체제 코드를 실행한다.

user code
  │ system call / interrupt / exception
  ▼
kernel entry ─> validate request ─> privileged work
  │ return
  ▼
user code

page table entry에는 user mode 접근 여부와 read, write, execute 같은 permission이 담긴다. 사용자 프로세스가 kernel-only page를 직접 읽으려 하면 CPU가 접근을 막고 page fault를 발생시킨다.

kernel mode라고 해서 사용자 pointer를 평범한 pointer처럼 신뢰해도 되는 것은 아니다. 사용자 프로세스는 system call 직전이나 검사 도중 값을 바꿀 수 있고, 잘못된 주소를 넘길 수도 있다. Linux kernel code는 copy_from_user(), copy_to_user(), get_user() 같은 전용 API로 접근하고 실패를 처리한다.

즉 system call은 “커널 주소를 사용자에게 열어 주는 문”이 아니다. 커널이 요청과 사용자 buffer를 검증한 뒤 제한된 일을 대신 수행하는 경계다.

PTI가 켜지면 page table도 더 분리된다

과거의 단순 그림은 사용자 code가 실행될 때도 kernel mapping이 같은 page table에 존재하되 user mode permission으로 접근을 막는 모습을 자주 보여 준다. 현대 x86 Linux에서는 Page Table Isolation, PTI가 이 설명을 한 단계 더 바꾼다.

PTI가 활성화되면 사용자 mode용 page table에는 kernel 진입과 복귀에 필요한 최소 영역만 남기고, kernel에 진입할 때 full kernel page table로 전환한다. 이는 Meltdown 계열의 shared user/kernel address-space side channel을 줄이기 위한 완화다.

따라서 다음 두 문장은 구분해야 한다.

  • 논리적 원리: user mode에서는 kernel page에 접근할 수 없다.
  • 실제 구현: kernel mapping을 같은 page table에 둘지, 별도 page table로 더 강하게 분리할지는 architecture와 설정에 따라 달라진다.

kernel은 user memory를 언제나 자유롭게 읽는가

kernel은 system 전체를 관리하므로 user mapping을 다룰 수 있다. 그러나 실무적인 답은 “필요한 순간에 검증된 경로로 접근한다”에 가깝다.

CPU에는 accidental supervisor access를 줄이는 SMAP, PAN 계열 보호가 있고, Linux의 user-access helper는 fault와 permission 경계를 처리한다. kernel이 잘못된 user pointer를 단순 역참조하면 보안 취약점이나 kernel fault가 될 수 있다.

여기서 얻을 수 있는 기준은 명확하다.

  1. user pointer는 신뢰하지 않는다.
  2. 길이와 범위를 함께 검증한다.
  3. copy 결과와 -EFAULT 같은 실패를 처리한다.
  4. 검사한 값과 실제 사용한 값이 달라질 수 있는 경쟁 조건을 고려한다.

주소 공간 분리가 막아 주는 것과 못 막는 것

막아 주는 것

  • 한 프로세스의 임의 pointer가 다른 프로세스의 일반 memory를 바로 덮어쓰는 일
  • user mode code가 kernel-only page를 직접 실행하거나 수정하는 일
  • 잘못된 user-space memory access가 곧바로 kernel 전체 손상으로 이어지는 일

이것만으로 막지 못하는 것

  • kernel bug나 취약한 driver가 만드는 system-wide fault
  • 권한 있는 system call을 잘못 설계해 생기는 privilege escalation
  • side-channel attack
  • 프로세스의 CPU·memory·I/O 고갈
  • 무한 loop가 CPU 시간을 소비하는 문제

마지막 두 문제는 scheduler, cgroup, resource limit와 timeout 같은 다른 통제가 필요하다. 주소 격리는 fault containment의 중요한 한 층이지 resource isolation 전체가 아니다.

segmentation의 연장으로 보면 왜 헷갈릴까

code, data, stack segment와 user/kernel 분리를 한 줄로 이어 설명하기도 한다. 역사적으로 segmentation이 protection에 쓰인 시스템은 있지만, 현대 x86-64 Linux의 핵심 경계는 paging과 privilege level이다.

x86-64에서는 CS·SS·DS·ES segment base가 사실상 flat하게 동작하고, FS·GS가 thread-local storage 같은 제한된 용도로 남는다. 따라서 user/kernel 공간을 “text·data·stack segment를 더 크게 확장한 것”으로 이해하면 현재 구현을 잘못 그리게 된다.

정리

사용자 공간과 커널 공간을 한 문장으로 정리하면 다음과 같다.

프로세스별 가상 주소 공간과 page table permission, CPU privilege mode를 결합해 일반 application과 운영체제 핵심 영역의 접근을 분리한 경계다.

주소 숫자보다 다음 질문을 기억하면 다른 architecture에서도 설명이 흔들리지 않는다.

  • 지금 보는 주소는 virtual인가 physical인가?
  • 이 mapping은 어느 프로세스의 page table에 있는가?
  • 현재 CPU는 user mode인가 kernel mode인가?
  • page permission과 PTI 같은 추가 보호는 어떻게 적용됐는가?
  • kernel이 user data를 검증하고 복사하는 경로는 무엇인가?

CPU·메모리·스레드를 물리적으로 이해하기에서 hardware와 operating system의 층을 먼저 잡을 수 있다. 메모리·프로세스 용어 정리는 가상 메모리 용어를 연결하고, malloc이 메모리를 얻는 과정/proc/smaps 분석은 사용자 공간 mapping을 실제 관측으로 이어 간다.

참고 자료

반응형
KEEP READING
카테고리 전체 보기 →

댓글