컴퓨터 구조 05 - CPU 권한 수준과 보호
CPU가 왜 권한 수준을 구분하는지, x86 보호 링과 ARM 예외 수준이 어떻게 시스템을 보호하는지
CPU는 왜 권한을 나누는가?
초기의 컴퓨터에는 권한이라는 개념이 없었다. 프로그램이 메모리의 어느 주소든 자유롭게 읽고 쓸 수 있었고, 하드웨어의 어떤 포트에든 직접 접근할 수 있었다. 단일 프로그램만 실행되는 환경에서는 이것이 문제가 되지 않았지만, 여러 프로그램이 동시에 실행되는 다중 프로그래밍 환경이 등장하면서 상황이 달라졌다.
과연 모든 프로그램이 동등한 권한을 가져도 안전한가? 그렇지 않다. 하나의 사용자 프로그램이 다른 프로그램의 메모리를 덮어쓰거나, 디스크의 파일 시스템을 직접 조작하거나, 시스템 전체를 멈추는 명령어를 실행할 수 있다면, 안정적인 운영은 불가능하다. 운영체제가 자원을 관리하고 프로그램 간 격리를 보장하려면, 하드웨어 수준에서 "이 코드는 무엇을 할 수 있고, 무엇을 할 수 없는가"를 강제할 수 있어야 한다.
이것이 CPU 권한 수준이 존재하는 이유이다. 권한 수준은 소프트웨어적인 규약이 아니라 하드웨어가 강제하는 제약이다. 권한이 없는 코드가 금지된 동작을 시도하면 CPU가 예외(exception)를 발생시키고, 해당 동작은 실행되지 않는다.
리얼 모드에서 보호 모드로
x86 프로세서의 역사를 살펴보면 권한의 필요성이 어떻게 인식되었는지 알 수 있다. 1978년에 출시된 8086 프로세서는 리얼 모드(Real Mode)에서만 동작했다. 리얼 모드에서는 모든 코드가 물리 메모리에 직접 접근할 수 있었고, 어떤 명령어든 제한 없이 실행할 수 있었다. 메모리 보호도, 권한 분리도 없었다.
1982년에 등장한 80286 프로세서에서 보호 모드(Protected Mode)가 도입되었다. 보호 모드에서는 메모리 접근이 세그먼트 디스크립터를 통해 제어되며, 각 세그먼트에는 접근 권한이 부여된다. CPU는 매 메모리 접근마다 현재 코드의 권한이 해당 세그먼트에 접근할 자격이 있는지를 검사한다.
현대의 x86 프로세서도 전원이 켜지면 리얼 모드에서 시작한다. 이는 하위 호환성을 유지하기 위한 것이다. 부트로더가 초기 설정을 마친 뒤 보호 모드(혹은 64비트 롱 모드)로 전환하면, 그때부터 하드웨어 수준의 권한 보호가 활성화된다.
x86 보호 링
x86 아키텍처는 4개의 권한 수준을 정의한다. 이를 보호 링(Protection Ring)이라고 부르며, Ring 0에서 Ring 3까지 존재한다.
┌──────────────────────────────────────┐
│ Ring 3 │
│ 사용자 애플리케이션 │
│ ┌──────────────────────────────┐ │
│ │ Ring 2 │ │
│ │ 장치 드라이버 (거의 미사용) │ │
│ │ ┌──────────────────────┐ │ │
│ │ │ Ring 1 │ │ │
│ │ │ OS 서비스 (거의 미사용)│ │ │
│ │ │ ┌──────────────┐ │ │ │
│ │ │ │ Ring 0 │ │ │ │
│ │ │ │ 커널 │ │ │ │
│ │ │ └──────────────┘ │ │ │
│ │ └──────────────────────┘ │ │
│ └──────────────────────────────┘ │
└──────────────────────────────────────┘
Ring 0은 가장 높은 권한을 가진다. 모든 메모리에 접근할 수 있고, 모든 CPU 명령어를 실행할 수 있으며, 하드웨어 설정을 변경할 수 있다. 운영체제의 커널이 Ring 0에서 실행된다. Ring 3은 가장 낮은 권한이며, 일반 사용자 프로그램이 이 수준에서 실행된다. Ring 3의 코드는 자신에게 할당된 메모리에만 접근할 수 있고, 특권 명령어를 실행할 수 없다.
Ring 1과 Ring 2는 원래 운영체제 서비스와 장치 드라이버를 위해 설계되었지만, 현실에서는 거의 사용되지 않는다. 대부분의 운영체제가 Ring 0과 Ring 3만을 사용하는 2단계 모델을 채택했기 때문이다. 다만 일부 하이퍼바이저 환경에서는 이 중간 링이 활용되기도 한다.
현재 권한 수준(CPL, Current Privilege Level)은 코드 세그먼트 셀렉터의 하위 2비트에 저장된다. CPU는 명령어를 실행할 때마다 이 CPL을 확인하여 해당 동작이 허용되는지 검사한다.
특권 명령어
Ring 0에서만 실행 가능한 명령어를 특권 명령어(Privileged Instruction)라고 한다. Ring 3에서 이러한 명령어를 실행하려 하면 CPU는 일반 보호 예외(General Protection Fault, #GP)를 발생시킨다.
| 명령어 | 동작 | 특권 이유 |
|---|---|---|
HLT | CPU 정지 | 시스템 전체에 영향 |
LGDT / LIDT | GDT/IDT 레지스터 로드 | 메모리 보호 체계 변경 |
MOV CR0, ... | 제어 레지스터 수정 | 보호 모드/페이징 설정 변경 |
IN / OUT | I/O 포트 접근 | 하드웨어 직접 제어 |
WRMSR | 모델 고유 레지스터 쓰기 | CPU 동작 방식 변경 |
이러한 구분은 단순한 규약이 아니라 실리콘 수준에서 구현된 것이다. CPU의 디코드 단계에서 현재 CPL을 확인하고, 권한이 부족하면 명령어 실행 전에 예외를 발생시킨다. 소프트웨어가 이 검사를 우회할 방법은 없는 것이다.
권한 수준 전환: 게이트 디스크립터
Ring 3의 사용자 프로그램이 파일을 읽거나 네트워크 패킷을 보내려면 커널의 도움이 필요하다. 그런데 커널 코드는 Ring 0에서 실행되어야 한다. 그렇다면 Ring 3에서 Ring 0으로의 전환은 어떻게 이루어지는가?
아무 코드로나 점프할 수 있게 허용하면 보호의 의미가 없어진다. Ring 3 코드가 커널의 임의 주소로 점프할 수 있다면, 보안 검사를 우회하는 코드 경로로 진입하는 것이 가능해지기 때문이다. 따라서 x86은 게이트 디스크립터(Gate Descriptor)라는 메커니즘을 통해 정확히 허용된 진입점으로만 전환이 가능하도록 제한한다.
게이트 디스크립터에는 세 가지 주요 유형이 있다. 콜 게이트(Call Gate)는 CALL FAR 명령어를 통해 상위 권한 코드로 진입하는 경로를 제공한다. 인터럽트 게이트(Interrupt Gate)는 하드웨어 인터럽트나 INT 명령어가 발생했을 때 인터럽트 처리 루틴으로 전환하며, 전환 시 인터럽트를 자동으로 비활성화한다. 트랩 게이트(Trap Gate)는 인터럽트 게이트와 유사하지만 인터럽트를 비활성화하지 않는다는 차이가 있다.
각 게이트에는 대상 세그먼트, 오프셋, 그리고 해당 게이트를 사용할 수 있는 최소 권한 수준이 기록되어 있다. CPU는 게이트를 통과할 때 스택 전환도 함께 수행한다. Ring 3의 스택을 그대로 사용하면 커널 코드가 사용자 스택에 의존하게 되어 보안 취약점이 발생하기 때문이다. TSS(Task State Segment)에 각 링별 스택 포인터가 저장되어 있으며, 권한 전환 시 CPU가 자동으로 해당 링의 스택으로 교체한다.
시스템 콜: 현대적 링 전환
현대 운영체제에서 가장 빈번한 Ring 3 → Ring 0 전환은 시스템 콜이다. 초기에는 INT 0x80 같은 소프트웨어 인터럽트를 사용했지만, 인터럽트 기반 전환은 IDT 조회, 스택 전환, 파이프라인 플러시 등의 오버헤드가 상당했다.
이 문제를 해결하기 위해 x86에서는 SYSENTER/SYSEXIT(인텔)과 SYSCALL/SYSRET(AMD, 이후 공통 표준) 명령어를 도입했다. 이 명령어들은 게이트 디스크립터를 거치지 않고 MSR(Model Specific Register)에 미리 설정된 커널 진입점으로 직접 전환한다. 세그먼트 셀렉터와 스택을 하드와이어된 규칙에 따라 교체하므로, 메모리 조회가 최소화되어 전환 비용이 크게 줄어드는 것이다.
사용자 프로그램 (Ring 3)
│
│ SYSCALL
▼
┌─────────────────────────────┐
│ MSR에서 커널 진입점 로드 │
│ CPL을 0으로 변경 │
│ 커널 스택으로 전환 │
└──────────────┬──────────────┘
▼
커널 시스템 콜 핸들러 (Ring 0)
│
│ 요청 처리
│
│ SYSRET
▼
사용자 프로그램 (Ring 3)
리눅스에서는 이 전환 비용을 더욱 줄이기 위해 vDSO(virtual Dynamic Shared Object)라는 기법을 사용한다. 시간 조회(gettimeofday)처럼 커널 데이터를 읽기만 하면 되는 시스템 콜은 커널이 사용자 공간에 매핑한 읽기 전용 페이지에서 직접 수행하여, Ring 전환 자체를 생략한다.
세그먼트 보호와 페이지 수준 보호
x86의 보호 메커니즘은 두 계층으로 작동한다. 세그먼트 수준 보호와 페이지 수준 보호이다.
세그먼트 디스크립터에는 해당 세그먼트의 DPL(Descriptor Privilege Level)이 지정된다. CPU는 세그먼트에 접근할 때 현재 CPL과 DPL을 비교하여, CPL의 숫자가 DPL보다 크면(즉, 권한이 낮으면) 접근을 거부한다. 그러나 현대 64비트 운영체제에서는 세그먼트를 평탄(flat) 모델로 설정하여 세그먼트 보호의 역할이 크게 축소되었다. 실질적인 메모리 보호는 페이지 테이블이 담당한다.
페이지 테이블의 각 엔트리에는 User/Supervisor 비트가 존재한다. 이 비트가 Supervisor로 설정된 페이지는 Ring 0에서만 접근 가능하다. 추가로 읽기/쓰기 비트, 실행 불가(NX) 비트 등이 페이지별로 세밀한 접근 제어를 제공한다. 커널 메모리를 사용자 공간에서 읽을 수 없는 것은 바로 이 비트 덕분인 것이다.
최근에는 Meltdown 취약점 이후 KPTI(Kernel Page Table Isolation)가 도입되어, 사용자 모드에서 실행될 때는 커널 페이지 테이블 대부분을 아예 매핑하지 않는 방식으로 보호를 한층 강화했다.
ARM 예외 수준
ARM 아키텍처는 x86의 보호 링과는 다른 방식으로 권한을 구분한다. ARM(AArch64)에서는 예외 수준(Exception Level)이라는 개념을 사용하며, EL0에서 EL3까지 4단계가 있다.
| 수준 | 용도 | x86 대응 |
|---|---|---|
| EL0 | 사용자 애플리케이션 | Ring 3 |
| EL1 | 운영체제 커널 | Ring 0 |
| EL2 | 하이퍼바이저 | - |
| EL3 | 보안 모니터 (TrustZone) | - |
x86과의 가장 큰 차이는 EL2와 EL3이 아키텍처 수준에서 공식적으로 정의되어 있다는 것이다. x86에서 하이퍼바이저는 VT-x 같은 확장을 통해 별도의 루트/비루트 모드로 동작하지만, ARM에서는 예외 수준의 일부로 자연스럽게 통합되어 있다. EL3는 ARM TrustZone의 보안 모니터가 실행되는 수준으로, 보안 세계(Secure World)와 비보안 세계(Non-secure World) 사이의 전환을 관리한다.
ARM에서의 권한 전환은 예외를 통해 이루어진다. 하위 수준에서 상위 수준으로의 전환은 반드시 예외(인터럽트, 시스템 콜 등)를 통해서만 가능하며, 상위 수준에서 하위 수준으로의 복귀는 ERET 명령어를 사용한다. 이 구조는 x86의 게이트 디스크립터와 동일한 목적을 달성하되, 보다 간결한 모델을 제공하는 셈이다.
하드웨어 강제의 의미
권한 수준을 소프트웨어가 아닌 하드웨어가 강제한다는 것은 결정적인 의미를 갖는다. 운영체제 커널 자체에 버그가 있어도, 하드웨어 수준의 보호가 기본적인 격리를 보장한다. 사용자 프로그램이 아무리 악의적이라 해도, Ring 3에서 실행되는 한 직접적으로 다른 프로세스의 메모리를 읽거나 하드웨어를 조작할 수 없는 것이다.
물론 완벽한 보호는 존재하지 않는다. Spectre, Meltdown 같은 마이크로아키텍처 수준의 취약점이 보여주듯이, 하드웨어 보호도 구현의 세부사항에서 허점이 발견될 수 있다. 그러나 이러한 취약점이 큰 뉴스가 되었다는 사실 자체가, 하드웨어 수준 보호가 얼마나 기본적이고 중요한 보안 경계인지를 반증한다.
다음 글에서는 인터럽트와 예외를 다룬다.