MMOS 아키텍처 보충 자료

CPU 권한 모드와
전환 메커니즘

RISC-V · x86-64 · Arm64 비교


권한 모드의 목적과 구성, 예외 진입 및 복귀,

전환 전 반드시 준비해야 할 레지스터·메모리·스택,

그리고 아키텍처별 어셈블리 예제를 정리한다.

『직접 만들며 배우는 RISC-V 운영체제』 독립 해설 문서

작성 기준: 2026년 10월 · 예제 대상: RV64 / x86-64 long mode / AArch64

이 문서의 코드 범위 모든 예제는 특정 CPU 모드와 초기화 조건을 전제로 하는 핵심 명령 시퀀스다. 부트로더, 예외 벡터, 스택, 주소 공간, 인터럽트 제어, 메모리 보호 정책 전체를 대체하는 완성형 부팅 코드는 아니다. 설정이 빠진 채 실제 장치에서 실행하면 예외 또는 재부팅이 발생할 수 있다.

CPU 권한 모드란 무엇인가

CPU 권한 모드(privilege mode)는 실행 중인 코드가 어떤 시스템 자원에 접근하고 어떤 제어 명령을 실행할 수 있는지 결정한다. 사용자 프로그램이 커널의 메모리나 장치 제어 레지스터를 마음대로 변경하지 못하게 하는 가장 기본적인 보호 장치다.

권한 모드가 다르면 일반적으로 다음 능력이 달라진다.

권한 모드 전환과 주소 공간 전환은 다르다. 예를 들어 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)는 게스트 운영체제가 별도 실행 환경에 있는 것처럼 동작하도록 만드는 기능이다. 세 개는 연관되지만 같은 개념이 아니다.

RISC-V: M-mode, S-mode, U-mode

각 모드의 역할

인코딩 모드 주요 역할과 사용 예
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과 트랩 기록

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]

M-mode에서 S-mode로 전환: mret

부팅 중 M-mode 펌웨어가 S-mode 커널로 제어를 넘기는 대표적인 방법이다. 중요한 점은 mret가 단순한 점프가 아니라 mstatus.MPP와 mepc를 사용하는 특권 복귀 명령이라는 것이다.

전제조건

  1. 실제 CPU가 S-mode를 지원하고, 현재 코드가 M-mode에서 실행 중이어야 한다.

  2. S-mode 진입 주소가 실행 가능한 메모리에 있어야 하며, S-mode 스택을 준비해야 한다.

  3. 필요한 PMP 접근 허가와 트랩 위임 정책이 설정되어야 한다. 일부 플랫폼에서는 펌웨어가 이미 준비한다.

  4. 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에서 시작할 수 있다.

S-mode에서 U-mode로 진입: sret

사용자 프로그램 실행 직전에는 sepc에 사용자 진입 PC를 기록하고 sstatus.SPP=0으로 설정한다. 이후 sret를 실행하면 U-mode로 복귀한다.

전제조건

  1. satp가 사용자 코드·데이터·스택을 접근 가능한 물리 페이지로 매핑해야 한다. 사용자 페이지의 PTE_U와 읽기/쓰기/실행 권한이 적절해야 한다.

  2. 커널 코드는 S-mode 전용으로 유지해야 한다. U-mode가 커널 페이지에 접근할 수 있어서는 안 된다.

  3. stvec와 트랩 진입 코드가 준비되고, 유저/커널 스택 전환 규칙이 정해져야 한다.

  4. 사용자 진입 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]

U-mode에서 S-mode로 진입: 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 서비스 요청

S-mode 코드가 M-mode 제어 명령을 직접 실행할 수 있는 것은 아니다. 필요한 경우 ecall로 SBI 서비스 등을 요청하고, 트랩 처리 주체인 M-mode 펌웨어가 요청을 검증·처리한 뒤 복귀한다. M-mode로의 임의 분기 또는 임의의 mret 실행으로 권한을 올릴 수 없다.

S-mode에서 M-mode 서비스를 요청하는 어셈블리

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를 선택하는 것은 아니다.

RISC-V에서 기억할 제한

x86-64: 보호 링과 CPL

Ring 0–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]

Long mode, GDT, IDT, TSS

64비트 커널의 권한 전환에서는 다음 구성이 중요하다.

Ring 0에서 Ring 3으로 전환: iretq

iretq는 스택에 미리 준비한 복귀 프레임에서 RIP, CS, RFLAGS, 사용자 RSP, SS를 읽어 낮은 권한으로 복귀할 수 있다.

전제조건

  1. CPU가 x86-64 long mode의 64비트 코드에서 CPL 0으로 실행 중이어야 한다.

  2. 사용자 코드·데이터 선택자와 GDT 엔트리가 올바르고, CS는 CPL 3 코드 세그먼트, SS는 데이터 세그먼트여야 한다.

  3. 사용자 RIP/RSP가 canonical 주소이고, 코드·스택에 사용자 접근이 가능한 페이지 매핑이 있어야 한다.

  4. 사용자 코드가 다시 예외·시스템콜을 발생시켰을 때 진입할 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]

Ring 3에서 Ring 0으로: syscall

64비트 시스템콜에서는 대개 syscall을 사용한다. CPU는 IA32_LSTAR로 커널 진입 주소를 선택하고, IA32_STAR와 IA32_FMASK를 사용해 세그먼트·플래그 상태를 조정한다. 사용자 복귀 주소는 RCX, 이전 플래그는 R11에 저장된다.

전제조건

  1. CPUID로 syscall/sysret 지원을 확인하고 IA32_EFER.SCE를 활성화해야 한다.

  2. 커널이 IA32_LSTAR, IA32_STAR, IA32_FMASK를 미리 설정해야 한다.

  3. 진입 직후 사용할 커널 스택 전환 규칙이 반드시 있어야 한다. syscall은 자동으로 RSP를 커널 스택으로 바꾸지 않는다.

  4. 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 또는 iretq

sysretq는 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 스택 전환 방식을 그대로 사용하지 않는다.

.intel_syntax noprefix
    int 0x80                # only if IDT gate is configured

여기서 int 0x80은 해당 벡터를 호출할 권한과 핸들러가 설정된 경우에만 동작한다. 모든 x86-64 커널에서 이 명령을 보편적인 시스템콜 ABI로 사용한다는 의미는 아니다.

Ring 1과 Ring 2는 어떻게 사용하는가

Ring 1과 Ring 2도 x86 보호 모델에서 정의된 CPL이다. 이론적으로 커널이 해당 권한의 코드 세그먼트와 페이지 접근 정책을 구성하고 iretq 같은 복귀 경로를 준비하면 해당 CPL의 코드를 실행할 수 있다. 높은 권한으로의 진입에는 권한 검사를 통과하는 인터럽트·예외 또는 적절한 게이트 구성이 필요하다. 그러나 64비트 운영체제를 이해할 때는 보통 CPL 0과 CPL 3의 경계를 먼저 살펴보는 것이 실용적이다.

가상화와 CPU 모드

Intel VMX의 root/non-root 모드 또는 AMD SVM의 호스트/게스트 실행 상태는 CPL 0–3과 별개다. 게스트 OS가 CPL 0에서 실행되더라도 하이퍼바이저의 VM-exit 정책과 2단계 주소 변환 등에 의해 제약될 수 있다. [4]

Arm64(AArch64): EL0–EL3

Exception Level과 일반적인 역할

모드 일반적인 주체 의미
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로 복귀할 주소와 상태.

현재 EL 확인

mrs x0, CurrentEL
    lsr x0, x0, #2         // x0 = 0, 1, 2, or 3

CurrentEL의 EL 필드는 비트 [3:2]에 있으므로 오른쪽으로 2비트 이동하여 번호를 얻는다.

EL3에서 EL2로 진입: eret

EL3 펌웨어가 EL2 하이퍼바이저로 실행 제어를 넘기려면, 보안 정책과 EL2 지원 여부를 확인하고 SCR_EL3, SPSR_EL3, ELR_EL3 등을 준비해야 한다.

전제조건

  1. 현재 EL3이고 하드웨어에 EL2가 구현되어 있어야 한다.

  2. SCR_EL3.RW를 설정해 목표 EL2 코드가 AArch64로 실행되도록 한다. 목표 보안 상태는 SCR_EL3.NS 등으로 구성한다.

  3. 목표 EL2에서 사용할 스택, 예외 벡터, 메모리 상태를 준비해야 한다.

  4. 보안 펌웨어의 기존 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으로 진입

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 전에 별도로 끝나 있어야 한다.

EL1에서 EL0로 진입: eret

EL1 커널이 사용자 프로그램을 처음 실행할 때는 SP_EL0에 사용자 스택, ELR_EL1에 사용자 진입 주소, SPSR_EL1에 EL0 복귀 상태를 설정한다.

전제조건

  1. 커널이 EL1에서 실행 중이며 EL0 사용자 코드와 스택을 접근 가능한 주소로 매핑해야 한다.

  2. VBAR_EL1과 EL1 커널 예외 처리 스택이 준비되어 있어야 한다.

  3. 페이지 접근 권한, 메모리 속성, 명령 캐시 동기화, 인터럽트 상태를 일관되게 설정해야 한다.

  4. 아래 예제처럼 사용자 예외를 처음에는 마스킹한다면, 나중에 안전한 시점에 해제할 방법이 있어야 한다.

// 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 -> EL0

SPSR_EL1의 낮은 모드 필드가 EL0t이고, ELR_EL1은 실행을 시작할 PC가 된다. 이 예제는 IRQ·FIQ 등 비동기 예외를 마스킹한 초기 상태를 사용한다. 실제 사용자 실행 환경에서는 커널이 복귀 상태를 적절하게 구성해 필요한 인터럽트가 처리되도록 해야 한다.

EL0에서 EL1으로 진입: svc

사용자 프로그램이 svc를 실행하면 동기 예외가 발생한다. 일반적인 운영체제 구성에서는 EL1의 벡터 테이블에 있는 시스템콜 처리기로 진입한다. EL2의 트랩 설정 등으로 실제 라우팅이 달라질 수 있다.

mov x8, #64             // illustrative syscall number
    svc #0                  // request a supervisor service
// 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]

EL1에서 EL2로 요청하기: hvc

EL2가 구현되어 있고 하이퍼바이저 호출이 허용된 환경에서는 EL1 코드가 hvc로 EL2 서비스를 요청할 수 있다. hvc를 실행할 수 있도록 상위 수준의 설정(예: SCR_EL3.HCE)과 하이퍼바이저의 호출 규약이 준비되어야 한다.

// x0..x7: hypervisor-defined call arguments
    hvc #0                   // invoke EL2 service
    // return values depend on the hypervisor ABI

EL1에서 EL3 서비스로: smc

보안 모니터가 제공하는 서비스를 호출할 때 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

공통 전환 절차

  1. 실행 조건 확인: 현재 권한 모드와 CPU 지원 기능을 확인한다.

  2. 메모리 준비: 목표 코드·데이터·스택을 필요한 권한으로 매핑하고 초기화한다.

  3. 예외 처리 준비: 목표 모드에서 예외가 발생해도 돌아올 처리기와 커널 스택을 준비한다.

  4. 복귀 상태 구성: 목표 PC, 스택 포인터, 복귀 권한·상태 레지스터를 지정한다.

  5. 전환 명령 실행: 아키텍처에 맞는 복귀 명령을 실행한다.

  6. 실제 실행 검증: 낮은 권한에서 코드가 실행되는지, 시스템콜을 통해 다시 커널에 진입하는지 확인한다.

디버깅할 때 구분해야 할 실패 유형

증상 먼저 확인할 사항
전환 직후 예외 목표 PC의 정렬·매핑·실행 권한, 복귀 상태의 유효성, 현재 EL/CPL/모드.
스택 접근 실패 사용자 스택 매핑, 커널 트랩 스택의 보존, 모드 전환 중 SP 변경 방법.
트랩 처리기 진입 실패 벡터 레지스터·테이블의 주소와 정렬, 위임/라우팅 설정, 대상 코드의 실행 권한.
시스템콜 뒤 복귀 실패 복귀 PC, 이전 레지스터 저장/복원, 반환값 레지스터, 사용자 플래그·권한.
커널 메모리 노출 사용자 페이지 권한, 커널 매핑의 보호 비트, 사용자 포인터 검사 정책.
주소 공간 변경 후 이상 페이지 테이블 엔트리, TLB 동기화, 커널 항등/고주소 매핑 유지 여부.

MMOS 구현과 연결하기

이 문서의 RISC-V 설명은 『직접 만들며 배우는 RISC-V 운영체제』의 M/S/U 개요를 확장한 것이다. 책에서 사용하는 satp, sscratch, sepc, sret는 각각 주소 변환, 트랩 진입 보조 상태, 복귀할 PC, 권한 복귀 실행이라는 독립된 역할을 갖는다.

OpenSBI와 커널의 책임 분리

QEMU virt와 OpenSBI를 사용하는 구성에서는 보통 M-mode 펌웨어가 초기 플랫폼 준비를 맡고 MMOS 커널은 S-mode에서 실행된다. 따라서 MMOS의 사용자 모드 진입을 설명하는 예제에서 mret를 직접 사용해야 하는 것은 아니다. 커널은 stvec, sscratch, sstatus, sepc, satp를 준비해 sret로 U-mode에 진입하는 흐름을 중심으로 구현한다.

권한 모드와 주소 공간을 따로 검증한다

  1. 권한: U-mode에서 커널 전용 페이지 접근이 차단되는가.

  2. 주소 변환: 사용자 진입 VA와 스택 VA가 실제 할당 프레임을 가리키는가.

  3. 코드 배치: ELF 데이터가 물리 프레임에 기록되고 실행 권한이 올바른가.

  4. 트랩 왕복: ecall 후 S-mode에 들어갔다가 sret로 지정한 사용자 PC에 복귀하는가.

  5. 문맥 전환: 다음 프로세스의 satp를 사용하면서 커널 실행에 필요한 매핑이 유지되는가.

세 아키텍처에서 공통으로 기억할 한 문장 권한 모드를 바꾸는 명령은 새 모드에서 실행할 코드와 스택을 만들어 주지 않는다. 커널은 전환 전에 메모리·권한·예외 처리 환경을 준비해야 하며, 복귀 명령은 준비된 상태로 실행을 옮기는 역할만 한다.

참고 문헌과 적용 범위

아래 자료는 아키텍처 동작의 근거로 사용한 공식 문서다. 각 아키텍처의 확장 기능, 보안 옵션, 최신 기능에 따라 세부 동작이 달라질 수 있으므로 제품 코드에는 해당 CPU의 구현 설명서와 펌웨어 ABI도 확인해야 한다.

[1]

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

[2]

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

[3]

RISC-V International, “H Extension for Hypervisor Support, Version 1.0.”
https://docs.riscv.org/reference/isa/priv/hypervisor

[4]

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

[5]
[6]

이 문서는 학습과 구현 검토를 위한 보충 해설이다. 어셈블리 예제는 플랫폼 초기화 및 예외 처리 전체를 포함하지 않으며, 실제 전환 전에 해당 아키텍처와 보드의 상태를 확인해야 한다.