MMOS 아키텍처 보충 자료
CPU 권한 모드와
전환 메커니즘
RISC-V · x86-64 · Arm64 비교
권한 모드의 목적과 구성, 예외 진입 및 복귀,
전환 전 반드시 준비해야 할 레지스터·메모리·스택,
그리고 아키텍처별 어셈블리 예제를 정리한다.
『직접 만들며 배우는 RISC-V 운영체제』 독립 해설 문서
작성 기준: 2026년 10월 · 예제 대상: RV64 / x86-64 long mode / AArch64
이 문서의 코드 범위 모든 예제는 특정 CPU 모드와 초기화 조건을 전제로 하는 핵심 명령 시퀀스다. 부트로더, 예외 벡터, 스택, 주소 공간, 인터럽트 제어, 메모리 보호 정책 전체를 대체하는 완성형 부팅 코드는 아니다. 설정이 빠진 채 실제 장치에서 실행하면 예외 또는 재부팅이 발생할 수 있다.
CPU 권한 모드(privilege mode)는 실행 중인 코드가 어떤 시스템 자원에 접근하고 어떤 제어 명령을 실행할 수 있는지 결정한다. 사용자 프로그램이 커널의 메모리나 장치 제어 레지스터를 마음대로 변경하지 못하게 하는 가장 기본적인 보호 장치다.
권한 모드가 다르면 일반적으로 다음 능력이 달라진다.
페이지 테이블, 주소 변환, 메모리 보호 등 시스템 제어 상태를 설정할 수 있는가.
인터럽트와 예외를 처리할 진입 주소 및 라우팅을 설정할 수 있는가.
타이머, 장치, CPU 제어 레지스터 등 특권 자원에 접근할 수 있는가.
낮은 권한에서 높은 권한의 메모리와 명령을 사용할 수 있는가.
권한 모드 전환과 주소 공간 전환은 다르다. 예를 들어 RISC-V의 S-mode에서 사용자 U-mode로 내려갈 때도 같은 satp 아래에서 실행할 수 있고, 반대로 S-mode를 유지하면서 satp만 바꿀 수도 있다. x86-64의 CPL과 CR3, Arm64의 EL과 TTBR0_EL1/TTBR1_EL1도 구분해야 한다.
모드 전환의 두 방향 높은 권한 → 낮은 권한: 커널이나 펌웨어가 복귀할 모드와 PC를 준비한 뒤 특별한 복귀 명령으로 이동한다.
낮은 권한 → 높은 권한: 사용자 코드가 임의의 분기 명령으로 권한을 올릴 수 없다. 시스템콜, 예외 또는 인터럽트가 하드웨어의 검사를 거쳐 높은 권한의 처리기로 진입하도록 한다.
| 아키텍처 | 모드 | 일반적인 실행 주체 |
|---|---|---|
| RISC-V | M / S / U | 기계 모드 펌웨어 / 운영체제 커널 / 사용자 프로그램 |
| H 확장: HS / VS / VU | 하이퍼바이저 / 가상 머신 커널 / 가상 머신 사용자 프로그램 | |
| x86-64 | Ring 0(CPL 0) / Ring 1·2 / Ring 3(CPL 3) | 커널 / 아키텍처상 존재하지만 통상 미사용 / 사용자 프로그램 |
| VMX root / non-root 또는 AMD SVM | 가상화 실행 상태: Ring 번호와 별개 | |
| Arm64 | EL3 / EL2 / EL1 / EL0 | 보안 모니터 펌웨어 / 하이퍼바이저 / 운영체제 커널 / 사용자 프로그램 |
| 보안 상태(Secure/Non-secure 등) | EL 번호와 별도로 구분되는 보안 영역 |
세 아키텍처의 숫자를 직접 대응시켜서는 안 된다. RISC-V의 S-mode, x86-64의 CPL 0, Arm64의 EL1은 보통 운영체제가 실행되는 위치라는 역할이 비슷하지만, 하드웨어 권한 모델과 전환 명령은 서로 다르다.
RISC-V는 M-mode가 기본적으로 필요하며, 구현에 따라 S-mode와 U-mode 지원 여부가 달라진다. S-mode를 갖춘 Unix 계열 운영체제에서는 보통 M/S/U의 세 수준을 사용한다. x86-64는 보호 모드의 Ring 0–3 구분을 이어받지만 현대의 범용 64비트 운영체제는 주로 Ring 0과 Ring 3을 사용한다. Arm64에서 EL2와 EL3는 시스템 구성에 따라 존재하거나 소프트웨어에 노출되는 방식이 달라진다.
자주 혼동하는 개념 권한(privilege)은 누가 무엇을 제어할 수 있는가를 뜻한다. 주소 변환(translation)은 어느 가상주소가 어느 물리주소를 가리키는가를 뜻한다. 가상화(virtualization)는 게스트 운영체제가 별도 실행 환경에 있는 것처럼 동작하도록 만드는 기능이다. 세 개는 연관되지만 같은 개념이 아니다.
| 인코딩 | 모드 | 주요 역할과 사용 예 |
|---|---|---|
| 0 | U-mode | 사용자 프로그램을 실행한다. 특권 CSR과 커널 전용 페이지에 직접 접근하지 않는다. 시스템콜에는 보통 ecall을 사용한다. |
| 1 | S-mode | 커널, 스케줄러, 가상 메모리 관리, 사용자 예외 처리 등을 담당한다. satp, sstatus, stvec 등을 사용한다. |
| 2 | Reserved | 기본 권한 모드 인코딩에서 예약되어 있다. 독립된 표준 권한 단계로 취급하지 않는다. |
| 3 | M-mode | 기계 수준에서 트랩, 보호 기능, 플랫폼 자원을 제어한다. 보통 부트 펌웨어와 SBI 제공 코드가 실행된다. |
단순 MCU는 M-mode만, 분리된 애플리케이션을 보호하는 시스템은 M/U, 일반적인 Unix 계열 커널은 M/S/U 구성을 사용할 수 있다. 실제 구현에서 지원하는 모드와 확장 기능은 하드웨어 사양을 확인해야 한다. [1, 2]
| CSR | 의미 |
|---|---|
mstatus.MPP |
M-mode의 mret 이후 복귀할 권한 모드를 지정한다. |
mepc / mtvec |
M-mode 복귀 PC / 트랩 처리기 진입 주소. |
medeleg, mideleg |
낮은 모드에서 발생한 예외·인터럽트의 처리 책임을 S-mode로 위임할지 설정한다. M-mode에서 발생한 예외까지 S-mode로 보내는 설정은 아니다. |
sstatus.SPP |
sret 이후 복귀할 권한 모드(U 또는 S)를 지정한다. |
sepc / stvec |
S-mode 복귀 PC / 트랩 처리기 진입 주소. |
scause, stval |
예외·인터럽트 원인 및 추가 정보. |
sscratch |
트랩 진입 어셈블리가 임시로 활용할 수 있는 CSR. |
satp |
페이지 테이블의 루트 PPN, 주소 공간 식별자, 변환 모드(Bare/Sv39 등)를 지정한다. |
RISC-V 하드웨어가 트랩에 진입할 때 모든 범용 레지스터를 자동으로 저장하지는 않는다. 처리기 초기 코드가 필요한 레지스터와 스택 상태를 명시적으로 보존해야 한다. [2]
mret부팅 중 M-mode 펌웨어가 S-mode 커널로 제어를 넘기는 대표적인 방법이다. 중요한 점은 mret가 단순한 점프가 아니라 mstatus.MPP와 mepc를 사용하는 특권 복귀 명령이라는 것이다.
전제조건
실제 CPU가 S-mode를 지원하고, 현재 코드가 M-mode에서 실행 중이어야 한다.
S-mode 진입 주소가 실행 가능한 메모리에 있어야 하며, S-mode 스택을 준비해야 한다.
필요한 PMP 접근 허가와 트랩 위임 정책이 설정되어야 한다. 일부 플랫폼에서는 펌웨어가 이미 준비한다.
S-mode 커널이 사용할 트랩 벡터와 주소 변환 정책을 진입 이후에 올바르게 설정해야 한다.
# MPP bits = mstatus[12:11]
li t0, (3 << 11)
csrc mstatus, t0 # clear MPP
li t0, (1 << 11)
csrs mstatus, t0 # MPP = 01 (S)
la t0, s_mode_entry
csrw mepc, t0 # next PC
mret # M -> S
s_mode_entry:
# Set a valid S-mode stack before using it.
# Initialize stvec, satp, and kernel runtime.이 예제는 PMP, 인터럽트 위임, 초기 스택 및 메모리 맵 설정을 생략한 핵심 시퀀스다. OpenSBI를 사용하는 MMOS 구성에서는 이러한 M-mode 준비 작업을 대부분 펌웨어가 수행하고 커널은 S-mode에서 시작할 수 있다.
sret사용자 프로그램 실행 직전에는 sepc에 사용자 진입 PC를 기록하고 sstatus.SPP=0으로 설정한다. 이후 sret를 실행하면 U-mode로 복귀한다.
전제조건
satp가 사용자 코드·데이터·스택을 접근 가능한 물리 페이지로 매핑해야 한다. 사용자 페이지의 PTE_U와 읽기/쓰기/실행 권한이 적절해야 한다.
커널 코드는 S-mode 전용으로 유지해야 한다. U-mode가 커널 페이지에 접근할 수 있어서는 안 된다.
stvec와 트랩 진입 코드가 준비되고, 유저/커널 스택 전환 규칙이 정해져야 한다.
사용자 진입 PC와 스택 포인터가 실제로 유효해야 하며, 인터럽트 활성화 상태를 의도대로 설정해야 한다.
# Preconditions: stvec, satp, user pages valid.
# Assume a trap entry convention using sscratch.
la t0, user_entry
csrw sepc, t0
li t0, (1 << 8) # SPP bit
csrc sstatus, t0 # SPP = 0 (U)
la t0, kernel_trap_frame_top
csrw sscratch, t0
la sp, user_stack_top # valid user VA
sret # S -> U여기서 sscratch에 무엇을 보관할지는 커널의 트랩 진입 설계에 따라 달라진다. 위 예제는 트랩 프레임 주소를 저장하는 방식의 한 예일 뿐이다. sstatus.SPIE와 SIE의 상태도 별도로 관리해야 한다. [2]
ecall과 트랩사용자 프로그램은 ecall로 환경에 서비스를 요청한다. 처리 대상 권한 모드는 트랩 위임 설정에 따라 달라진다. 일반적인 S-mode 운영체제에서는 U-mode의 ecall 예외가 S-mode로 위임된다.
# U-mode user program:
li a7, 64 # example syscall number
li a0, 1 # example argument
ecall # trap; hardware enters S
# S-mode trap handler (after saving registers):
csrr t0, scause # inspect trap reason
csrr t1, sepc # PC of ecall
addi t1, t1, 4 # skip 32-bit ECALL
csrw sepc, t1
# Dispatch syscall and set return value in a0.
# Restore all preserved registers / trap frame.
sret # S -> U트랩 진입 시 하드웨어가 sepc, scause, sstatus.SPP 등을 갱신한다. 다만 사용자 레지스터 저장, 시스템콜 디스패치, 커널 스택 전환은 커널 코드의 책임이다. ecall은 32비트 명령이므로 다음 명령으로 넘어갈 경우 sepc를 4 증가시키는 방식이 일반적이다. 블로킹 시스템콜을 재실행해야 한다면 sepc를 증가시키지 않는 등 별도 규칙이 필요하다.
S-mode 코드가 M-mode 제어 명령을 직접 실행할 수 있는 것은 아니다. 필요한 경우 ecall로 SBI 서비스 등을 요청하고, 트랩 처리 주체인 M-mode 펌웨어가 요청을 검증·처리한 뒤 복귀한다. M-mode로의 임의 분기 또는 임의의 mret 실행으로 권한을 올릴 수 없다.
SBI를 제공하는 플랫폼에서는 S-mode의 ecall에 서비스 번호를 지정해 펌웨어에 요청한다. 다음은 SBI 기본 확장(Base Extension)의 사양 버전을 조회하는 예다. 일반적으로 a7에는 확장 ID, a6에는 기능 ID를 지정하며 a0/a1로 결과를 받는다.
li a7, 0x10 # SBI Base Extension EID
li a6, 0 # get_sbi_spec_version FID
ecall # S-mode -> firmware trap
# a0: SBI error code; a1: specification version이 코드는 SBI 지원 및 적절한 펌웨어가 준비된 경우를 전제로 한다. 하드웨어의 M-mode 진입은 트랩 처리 규칙에 따라 이루어지며, 요청자가 직접 M-mode를 선택하는 것은 아니다.
satp에 페이지 테이블 루트를 기록한다고 해서 페이지 테이블의 내용까지 올바른 것은 아니다. 코드·스택 매핑과 접근 권한을 확인해야 한다.
satp를 쓴 사실만으로 TLB 관련 동기화가 완료되는 것은 아니다. 페이지 테이블 변경과 ASID 정책에 따라 sfence.vma가 필요하다.
sstatus.SUM은 S-mode에서 U 페이지의 데이터 읽기·쓰기를 허용하는 조건이다. 사용자 페이지에 S-mode가 명령을 실행할 수 있게 하지는 않는다.
H 확장을 사용하면 HS/VS/VU 및 가상화 상태가 추가된다. 이는 기본 M/S/U 세 모드만 설명할 때의 확장 주제다. [2, 3]
| 권한 | 주된 용도 | 설명 |
|---|---|---|
| Ring 0 / CPL 0 | 커널 | 페이지 테이블, GDT/IDT, 인터럽트 제어, 특권 명령을 다룰 수 있다. |
| Ring 1 / CPL 1 | 일반적으로 미사용 | 아키텍처상 구분되지만 현대의 주류 x86-64 OS가 일상적인 커널/사용자 경계로 활용하지는 않는다. |
| Ring 2 / CPL 2 | 일반적으로 미사용 | Ring 1과 마찬가지로 구조적으로 존재하지만 일반적인 애플리케이션 계층은 아니다. |
| Ring 3 / CPL 3 | 사용자 프로그램 | 시스템콜 또는 예외·인터럽트를 통해 커널 서비스를 요청한다. |
현재 특권 수준은 CPL(Current Privilege Level)이라고 부르며, 일반적으로 CS 선택자의 하위 비트와 연관된다. Ring 번호는 RISC-V의 모드 숫자와 반대 방향이다. x86에서는 0이 가장 높은 권한이며 3이 사용자 수준이다. [4]
64비트 커널의 권한 전환에서는 다음 구성이 중요하다.
Long mode: CR0, CR4, EFER, CR3 및 페이지 테이블을 사용해 64비트 실행 환경을 준비한다.
GDT(Global Descriptor Table): 권한이 다른 코드 세그먼트의 선택자와 특성을 정의한다. 64비트 모드에서는 대다수 세그먼트 기반 주소 계산이 비활성화되지만 권한 검사는 여전히 중요하다.
IDT(Interrupt Descriptor Table): 예외와 인터럽트의 벡터를 어떤 커널 진입 주소로 보낼지 설정한다.
TSS(Task-State Segment): CPL 3에서 CPL 0으로 인터럽트·예외가 진입할 때 사용할 커널 스택(RSP0)과 IST 스택 등을 제공한다.
사용자 페이지: 사용자 코드·스택은 사용자 권한으로 매핑하고 커널 페이지는 보호해야 한다. CR3 변경과 CPL 변경은 서로 독립적인 동작이다.
iretqiretq는 스택에 미리 준비한 복귀 프레임에서 RIP, CS, RFLAGS, 사용자 RSP, SS를 읽어 낮은 권한으로 복귀할 수 있다.
전제조건
CPU가 x86-64 long mode의 64비트 코드에서 CPL 0으로 실행 중이어야 한다.
사용자 코드·데이터 선택자와 GDT 엔트리가 올바르고, CS는 CPL 3 코드 세그먼트, SS는 데이터 세그먼트여야 한다.
사용자 RIP/RSP가 canonical 주소이고, 코드·스택에 사용자 접근이 가능한 페이지 매핑이 있어야 한다.
사용자 코드가 다시 예외·시스템콜을 발생시켰을 때 진입할 IDT, TSS, 커널 스택, 저장 규약이 준비되어야 한다.
.intel_syntax noprefix
# Example GDT layout: user CS index 3, SS index 4.
.equ USER_CS_SEL, 0x1b # index 3, RPL=3
.equ USER_SS_SEL, 0x23 # index 4, RPL=3
# Preconditions: valid GDT/IDT/TSS, CR3, user pages.
mov rax, USER_SS_SEL
push rax # future SS
lea rax, [rip + user_stack_top]
push rax # future RSP (user VA)
mov rax, 0x202
push rax # RFLAGS, IF=1 (if safe)
mov rax, USER_CS_SEL
push rax # future CS
lea rax, [rip + user_entry]
push rax # future RIP
iretq # CPL 0 -> CPL 3위 코드는 선택자 번호가 예제로 제시한 GDT 배치와 일치할 때만 유효하다. RIP/RSP를 lea로 계산해도 해당 주소가 사용자 가상주소로 매핑되어 있다는 보장은 없다. 예제의 진입점은 실제로 접근 가능한 사용자 페이지에 놓아야 한다. RFLAGS.IF=1은 인터럽트 처리 준비가 끝났을 때에만 설정해야 한다. [4]
syscall64비트 시스템콜에서는 대개 syscall을 사용한다. CPU는 IA32_LSTAR로 커널 진입 주소를 선택하고, IA32_STAR와 IA32_FMASK를 사용해 세그먼트·플래그 상태를 조정한다. 사용자 복귀 주소는 RCX, 이전 플래그는 R11에 저장된다.
전제조건
CPUID로 syscall/sysret 지원을 확인하고 IA32_EFER.SCE를 활성화해야 한다.
커널이 IA32_LSTAR, IA32_STAR, IA32_FMASK를 미리 설정해야 한다.
진입 직후 사용할 커널 스택 전환 규칙이 반드시 있어야 한다. syscall은 자동으로 RSP를 커널 스택으로 바꾸지 않는다.
GS 기반 CPU별 데이터, 시스템콜 인자 보존, 예외 처리와 반환 주소 검증을 구성해야 한다.
.intel_syntax noprefix
# CPL 3: registers/ABI depend on the chosen OS.
mov rax, 1 # illustrative syscall number
syscall # enter CPL 0 at IA32_LSTAR
# CPL 0 at configured IA32_LSTAR:
syscall_entry:
swapgs # only with valid GS policy
mov qword ptr gs:[0x00], rsp # save user RSP
mov rsp, qword ptr gs:[0x08] # load kernel RSP
# Save RCX/R11 and other user register state.
# Validate the syscall number and arguments.
# Return via a fully validated SYSRETQ or IRETQ path.중요: syscall은 스택을 자동으로 바꾸지 않는다 사용자 RSP가 유지된 채 CPL 0의 커널 명령이 시작된다. 따라서 운영체제는 CPU별 저장소 등을 사용해 사용자 RSP를 보관하고 커널 RSP로 전환해야 한다. 위의 GS 오프셋은 예시일 뿐이며, swapgs 사용 여부와 실제 구조체 배치는 운영체제 구현에 따라 다르다.
sysretq 또는 iretqsysretq는 RCX에서 사용자 RIP를, R11에서 RFLAGS를 복원한다. CS/SS 선택자도 IA32_STAR 설정에 영향을 받는다. 그러나 RSP를 복원하지 않으므로 커널은 사용자 스택으로 되돌리는 작업을 직접 수행해야 한다. sysretq 직전에는 복귀 주소의 canonical 여부, 권한 전환 안전성, 플래그, GS/커널 스택 전환 중 예외 가능성을 확인해야 한다. 조건이 맞지 않으면 iretq 복귀 경로를 사용하기도 한다. [4]
# Concept only: full register restoration is omitted.
# Preconditions for SYSRETQ are strict:
# RCX = validated user RIP; R11 = safe user RFLAGS
# RSP = restored user RSP; GS = restored user GS
# IA32_STAR and CPU state are consistent.
sysretq
# Alternative: IRETQ uses a prepared 5-qword
# privilege-return frame on the kernel stack.
# iretq페이지 폴트, 외부 인터럽트, int n 등의 진입은 IDT 게이트를 사용한다. CPL 3에서 CPL 0으로 권한이 바뀌면 하드웨어는 TSS에 있는 RSP0 또는 IST 설정에 따라 스택을 바꾸고 복귀 상태를 저장한다. 반면 syscall은 이 IDT/TSS 스택 전환 방식을 그대로 사용하지 않는다.
여기서 int 0x80은 해당 벡터를 호출할 권한과 핸들러가 설정된 경우에만 동작한다. 모든 x86-64 커널에서 이 명령을 보편적인 시스템콜 ABI로 사용한다는 의미는 아니다.
Ring 1과 Ring 2도 x86 보호 모델에서 정의된 CPL이다. 이론적으로 커널이 해당 권한의 코드 세그먼트와 페이지 접근 정책을 구성하고 iretq 같은 복귀 경로를 준비하면 해당 CPL의 코드를 실행할 수 있다. 높은 권한으로의 진입에는 권한 검사를 통과하는 인터럽트·예외 또는 적절한 게이트 구성이 필요하다. 그러나 64비트 운영체제를 이해할 때는 보통 CPL 0과 CPL 3의 경계를 먼저 살펴보는 것이 실용적이다.
Intel VMX의 root/non-root 모드 또는 AMD SVM의 호스트/게스트 실행 상태는 CPL 0–3과 별개다. 게스트 OS가 CPL 0에서 실행되더라도 하이퍼바이저의 VM-exit 정책과 2단계 주소 변환 등에 의해 제약될 수 있다. [4]
| 모드 | 일반적인 주체 | 의미 |
|---|---|---|
| EL0 | 사용자 프로그램 | 운영체제가 정한 가상주소와 시스템콜 규칙 아래에서 실행된다. |
| EL1 | 운영체제 커널 | 일반적인 예외 처리, 사용자 주소 공간 관리, 시스템콜 처리를 수행한다. |
| EL2 | 하이퍼바이저 | 가상 머신 실행, 트랩 제어, 2단계 주소 변환을 수행할 수 있다. 구현과 구성에 따라 사용 여부가 달라진다. |
| EL3 | 보안 모니터/펌웨어 | 보안 상태 전환 및 높은 수준의 시스템 초기화에 사용된다. 구현과 부트 정책에 따라 존재 여부와 접근성이 달라진다. |
AArch64는 권한 수준을 EL(Exception Level)로 표시한다. EL3가 가장 높은 예외 수준이고 EL0가 가장 낮다. Secure/Non-secure 같은 보안 상태는 EL 숫자와 동일한 개념이 아니다. 또한 모든 Arm64 시스템이 EL2나 EL3를 운영체제에 노출하는 것은 아니다. [5, 6]
| 레지스터 | 역할 |
|---|---|
CurrentEL |
현재 실행 중인 예외 수준을 읽는 레지스터. |
VBAR_EL1 |
EL1에서 처리할 예외 벡터 테이블의 기준 주소. |
ELR_EL1 / SPSR_EL1 |
EL1에서 eret할 때 복귀할 주소 / 복귀할 실행 상태. |
SP_EL0 / SP_EL1 |
EL0 및 EL1에서 사용할 스택 포인터 저장소. |
TTBR0_EL1, TTBR1_EL1 |
EL1의 주소 변환에 사용되는 페이지 테이블 루트 관련 레지스터. |
TCR_EL1, MAIR_EL1 |
주소 변환 범위·페이지 크기 및 메모리 속성 등의 설정. |
SCTLR_EL1 |
EL1의 MMU·캐시 등 시스템 제어 설정. |
HCR_EL2 |
EL2가 하위 실행 수준의 실행 상태·트랩·가상화를 제어한다. |
SCR_EL3 |
EL3에서 보안 상태, 하위 실행 상태 등을 제어한다. |
ELR_EL2/SPSR_EL2 |
EL2에서 eret로 복귀할 주소와 상태. |
ELR_EL3/SPSR_EL3 |
EL3에서 eret로 복귀할 주소와 상태. |
CurrentEL의 EL 필드는 비트 [3:2]에 있으므로 오른쪽으로 2비트 이동하여 번호를 얻는다.
eretEL3 펌웨어가 EL2 하이퍼바이저로 실행 제어를 넘기려면, 보안 정책과 EL2 지원 여부를 확인하고 SCR_EL3, SPSR_EL3, ELR_EL3 등을 준비해야 한다.
전제조건
현재 EL3이고 하드웨어에 EL2가 구현되어 있어야 한다.
SCR_EL3.RW를 설정해 목표 EL2 코드가 AArch64로 실행되도록 한다. 목표 보안 상태는 SCR_EL3.NS 등으로 구성한다.
목표 EL2에서 사용할 스택, 예외 벡터, 메모리 상태를 준비해야 한다.
보안 펌웨어의 기존 SCR_EL3 설정을 무분별하게 덮어쓰지 않아야 한다.
// EL3 only; EL2 must exist.
mrs x0, scr_el3
orr x0, x0, #(1 << 10) // RW=1, lower EL AArch64
orr x0, x0, #1 // NS=1, Non-secure
msr scr_el3, x0
mov x1, #0x3c9 // DAIF masked, EL2h
msr spsr_el3, x1
adr x1, el2_entry
msr elr_el3, x1
isb
eret // EL3 -> EL2실제 보안 펌웨어는 SCR_EL3의 다른 필드, EL2 스택 및 예외 벡터를 더 구성해야 한다. 위 예제는 전체 초기화의 대체물이 아니다. [5, 6]
EL2가 EL1 커널로 제어를 넘길 때는 EL1의 AArch64 실행 상태를 설정하고 복귀 주소·상태를 지정한다. SPSR_EL2=0x3c5는 예시적으로 DAIF를 마스킹한 EL1h 상태를 나타낸다.
// EL2 only. Other HCR_EL2 traps must be configured.
mrs x0, hcr_el2
orr x0, x0, #(1 << 31) // RW=1: EL1 is AArch64
msr hcr_el2, x0
adr x1, el1_entry
msr elr_el2, x1
mov x1, #0x3c5 // DAIF masked, EL1h
msr spsr_el2, x1
isb
eret // EL2 -> EL1왜 EL1h인가 AArch64에서는 EL1에서 SP_EL1을 사용하는 실행 상태를 EL1h, SP_EL0를 사용하는 상태를 EL1t라고 부른다. 일반적인 EL1 커널은 자체 커널 스택을 사용하므로 EL1h를 선택한다. 해당 스택의 초기화는 eret 전에 별도로 끝나 있어야 한다.
eretEL1 커널이 사용자 프로그램을 처음 실행할 때는 SP_EL0에 사용자 스택, ELR_EL1에 사용자 진입 주소, SPSR_EL1에 EL0 복귀 상태를 설정한다.
전제조건
커널이 EL1에서 실행 중이며 EL0 사용자 코드와 스택을 접근 가능한 주소로 매핑해야 한다.
VBAR_EL1과 EL1 커널 예외 처리 스택이 준비되어 있어야 한다.
페이지 접근 권한, 메모리 속성, 명령 캐시 동기화, 인터럽트 상태를 일관되게 설정해야 한다.
아래 예제처럼 사용자 예외를 처음에는 마스킹한다면, 나중에 안전한 시점에 해제할 방법이 있어야 한다.
// x0 = validated user entry virtual address
// x1 = validated user stack top virtual address
msr sp_el0, x1
msr elr_el1, x0
mov x2, #0x3c0 // DAIF masked, EL0t
msr spsr_el1, x2
isb
eret // EL1 -> EL0SPSR_EL1의 낮은 모드 필드가 EL0t이고, ELR_EL1은 실행을 시작할 PC가 된다. 이 예제는 IRQ·FIQ 등 비동기 예외를 마스킹한 초기 상태를 사용한다. 실제 사용자 실행 환경에서는 커널이 복귀 상태를 적절하게 구성해 필요한 인터럽트가 처리되도록 해야 한다.
svc사용자 프로그램이 svc를 실행하면 동기 예외가 발생한다. 일반적인 운영체제 구성에서는 EL1의 벡터 테이블에 있는 시스템콜 처리기로 진입한다. EL2의 트랩 설정 등으로 실제 라우팅이 달라질 수 있다.
// Entered via a valid VBAR_EL1 exception vector.
mrs x9, esr_el1 // syndrome identifies SVC
mrs x10, elr_el1 // return PC
// Save user registers and inspect the SVC number.
// Process the syscall; restore user context.
eret // EL1 -> EL0실제 예외 벡터 테이블은 지정된 정렬·슬롯 크기 요구사항을 만족해야 한다. 위 코드는 벡터 전체 구현이 아니라 처리기 내부 명령의 예시다. svc와 eret만으로 커널 진입부 전체가 완성되지는 않는다. [5]
hvcEL2가 구현되어 있고 하이퍼바이저 호출이 허용된 환경에서는 EL1 코드가 hvc로 EL2 서비스를 요청할 수 있다. hvc를 실행할 수 있도록 상위 수준의 설정(예: SCR_EL3.HCE)과 하이퍼바이저의 호출 규약이 준비되어야 한다.
// x0..x7: hypervisor-defined call arguments
hvc #0 // invoke EL2 service
// return values depend on the hypervisor ABIsmc보안 모니터가 제공하는 서비스를 호출할 때 smc를 사용할 수 있다. 다만 EL3 지원 및 모니터 호출 허용 상태, 펌웨어 ABI(예: SMCCC), 인자 규약이 필요하다. smc가 항상 허용되거나 모든 시스템에서 동일한 기능을 제공하는 것은 아니다.
// x0 = firmware-defined function identifier
// x1..x7 = firmware-defined arguments
smc #0
// return values follow the firmware ABI| 점검 항목 | RISC-V | x86-64 | Arm64 |
|---|---|---|---|
| 복귀 주소 | mepc 또는 sepc |
iretq 프레임의 RIP / RCX(SYSRET) |
ELR_ELx |
| 복귀 권한·상태 | mstatus.MPP, sstatus.SPP |
GDT 선택자, CS/RFLAGS 등 | SPSR_ELx |
| 예외 진입점 | mtvec, stvec |
IDT, LSTAR(SYSCALL) | VBAR_ELx |
| 커널 스택 | 커널의 트랩 진입 코드가 준비 | TSS/IST 또는 syscall 진입 코드 | SP_EL1 등 |
| 주소 변환 | satp, Sv39 PTE |
CR3, 페이지 테이블 |
TTBR, TCR, SCTLR |
| 사용자 접근권한 | PTE_U 및 R/W/X |
User/Supervisor 페이지 권한 | EL0 접근 권한(AP), 실행 제한(PXN/UXN 등) |
| 트랩·예외 라우팅 | medeleg, mideleg |
IDT 게이트·DPL·MSR | HCR_EL2, SCR_EL3 등 |
| 핵심 복귀 명령 | mret, sret |
iretq, sysretq |
eret |
| 사용자 요청 명령 | ecall |
syscall, 설정된 int n |
svc |
실행 조건 확인: 현재 권한 모드와 CPU 지원 기능을 확인한다.
메모리 준비: 목표 코드·데이터·스택을 필요한 권한으로 매핑하고 초기화한다.
예외 처리 준비: 목표 모드에서 예외가 발생해도 돌아올 처리기와 커널 스택을 준비한다.
복귀 상태 구성: 목표 PC, 스택 포인터, 복귀 권한·상태 레지스터를 지정한다.
전환 명령 실행: 아키텍처에 맞는 복귀 명령을 실행한다.
실제 실행 검증: 낮은 권한에서 코드가 실행되는지, 시스템콜을 통해 다시 커널에 진입하는지 확인한다.
| 증상 | 먼저 확인할 사항 |
|---|---|
| 전환 직후 예외 | 목표 PC의 정렬·매핑·실행 권한, 복귀 상태의 유효성, 현재 EL/CPL/모드. |
| 스택 접근 실패 | 사용자 스택 매핑, 커널 트랩 스택의 보존, 모드 전환 중 SP 변경 방법. |
| 트랩 처리기 진입 실패 | 벡터 레지스터·테이블의 주소와 정렬, 위임/라우팅 설정, 대상 코드의 실행 권한. |
| 시스템콜 뒤 복귀 실패 | 복귀 PC, 이전 레지스터 저장/복원, 반환값 레지스터, 사용자 플래그·권한. |
| 커널 메모리 노출 | 사용자 페이지 권한, 커널 매핑의 보호 비트, 사용자 포인터 검사 정책. |
| 주소 공간 변경 후 이상 | 페이지 테이블 엔트리, TLB 동기화, 커널 항등/고주소 매핑 유지 여부. |
이 문서의 RISC-V 설명은 『직접 만들며 배우는 RISC-V 운영체제』의 M/S/U 개요를 확장한 것이다. 책에서 사용하는 satp, sscratch, sepc, sret는 각각 주소 변환, 트랩 진입 보조 상태, 복귀할 PC, 권한 복귀 실행이라는 독립된 역할을 갖는다.
QEMU virt와 OpenSBI를 사용하는 구성에서는 보통 M-mode 펌웨어가 초기 플랫폼 준비를 맡고 MMOS 커널은 S-mode에서 실행된다. 따라서 MMOS의 사용자 모드 진입을 설명하는 예제에서 mret를 직접 사용해야 하는 것은 아니다. 커널은 stvec, sscratch, sstatus, sepc, satp를 준비해 sret로 U-mode에 진입하는 흐름을 중심으로 구현한다.
권한: U-mode에서 커널 전용 페이지 접근이 차단되는가.
주소 변환: 사용자 진입 VA와 스택 VA가 실제 할당 프레임을 가리키는가.
코드 배치: ELF 데이터가 물리 프레임에 기록되고 실행 권한이 올바른가.
트랩 왕복: ecall 후 S-mode에 들어갔다가 sret로 지정한 사용자 PC에 복귀하는가.
문맥 전환: 다음 프로세스의 satp를 사용하면서 커널 실행에 필요한 매핑이 유지되는가.
세 아키텍처에서 공통으로 기억할 한 문장 권한 모드를 바꾸는 명령은 새 모드에서 실행할 코드와 스택을 만들어 주지 않는다. 커널은 전환 전에 메모리·권한·예외 처리 환경을 준비해야 하며, 복귀 명령은 준비된 상태로 실행을 옮기는 역할만 한다.
아래 자료는 아키텍처 동작의 근거로 사용한 공식 문서다. 각 아키텍처의 확장 기능, 보안 옵션, 최신 기능에 따라 세부 동작이 달라질 수 있으므로 제품 코드에는 해당 CPU의 구현 설명서와 펌웨어 ABI도 확인해야 한다.
RISC-V International, The RISC-V Instruction Set Manual, Volume II: Privileged Architecture, “Machine-Level ISA, Version 1.13.”
https://docs.riscv.org/reference/isa/priv/machine.html
RISC-V International, The RISC-V Instruction Set Manual, Volume II: Privileged Architecture, “Supervisor-Level ISA, Version 1.13.”
https://docs.riscv.org/reference/isa/priv/supervisor.html
RISC-V International, “H Extension for Hypervisor Support, Version 1.0.”
https://docs.riscv.org/reference/isa/priv/hypervisor
Intel, Intel 64 and IA-32 Architectures Software Developer’s Manual, 특히 System Programming Guide의 Protection, Interrupt/Exception Handling, SYSCALL/SYSRET 절.
https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html
Arm, Learn the Architecture: AArch64 Exception Model.
https://developer.arm.com/-/media/Arm%20Developer%20Community/PDF/Learn%20the%20Architecture/Exception%20model.pdf
Arm, Arm Learning Paths: Switching Exception Levels.
https://learn.arm.com/learning-paths/embedded-and-microcontrollers/bare-metal/exception-levels/
이 문서는 학습과 구현 검토를 위한 보충 해설이다. 어셈블리 예제는 플랫폼 초기화 및 예외 처리 전체를 포함하지 않으며, 실제 전환 전에 해당 아키텍처와 보드의 상태를 확인해야 한다.