Chapter 22

부팅·SBI·플랫폼 규격

ISA 매뉴얼만 지킨 칩으로는 아직 컴퓨터가 되지 않는다. 전원이 켜지면 누가 먼저 일어나는지, 운영체제가 타이머를 맞추거나 다른 코어를 깨우려면 누구에게 부탁하는지, 장치의 인터럽트는 어떤 길로 하트에 닿는지까지 약속해야 같은 리눅스 이미지가 여러 칩에서 돈다. 이 장은 ISA 바깥에서 RISC-V 시스템을 묶어 주는 규격들, 곧 SBI·인터럽트 컨트롤러·IOMMU·디버그·부트 규격을 부팅 순서를 따라 살펴본다.

ISA 바깥의 약속: 플랫폼이란

17–21장에서 본 특권 아키텍처는 하트 안쪽의 규칙이다. CSR이 몇 번지에 있고 트랩이 나면 무엇이 바뀌는지를 정한다. 하지만 운영체제는 하트 바깥도 알아야 한다. 타이머 장치는 어디에 있는지, 인터럽트 컨트롤러는 어떤 레지스터를 가졌는지, 메모리는 몇 번지부터 몇 번지까지인지, 다른 하트를 깨우려면 무엇을 써야 하는지. 이런 것들을 칩마다 다르게 만들면 운영체제는 칩마다 따로 고쳐야 한다.

RISC-V는 이 문제를 층을 나누는 방식으로 푼다. 칩마다 달라지는 일은 M 모드에서 도는 펌웨어가 흡수하고, 운영체제(S 모드)는 펌웨어에게 정해진 방식으로 부탁만 한다. 그 부탁의 형식이 SBI(Supervisor Binary Interface)다. 하드웨어 목록은 장치 트리(Device Tree)나 ACPI 표로 전달하고, 인터럽트·IOMMU·디버그처럼 운영체제가 직접 만지는 장치는 아예 표준 규격으로 정한다.

U 모드 · 응용 프로그램 셸 · 서버 · 브라우저 ecall → 시스템 호출 S 모드 · 운영체제 커널 리눅스 · FreeBSD · RTOS SBI · ecall 로 펌웨어 호출 M 모드 · 런타임 펌웨어 OpenSBI · RustSBI (SBI 구현) 하드웨어 · 하트 · 메모리 · 장치 ISA 매뉴얼(1·2권)이 정하는 부분 인터럽트 컨트롤러 ACLINT · PLIC · AIA IOMMU 장치 DMA 주소 변환 플랫폼 규격 Server Platform · SoC 부트 · 하드웨어 기술 UEFI · ACPI · Device Tree 디버그 · 트레이스 외부 디버거 · JTAG
그림 22-1. RISC-V 시스템의 층. 운영체제(S)는 하드웨어 차이를 M 모드 펌웨어에 맡기고 SBI라는 정해진 형식으로 부탁한다(금색 점). 하드웨어 목록은 UEFI·ACPI·장치 트리로 넘겨받고, 인터럽트 컨트롤러·IOMMU처럼 운영체제가 직접 다루는 장치는 별도 규격으로 표준화되어 있다.

이 장에서 다루는 규격은 모두 ISA 매뉴얼 밖에 있는 별도 문서다. 공식 규격 라이브러리의 버전을 정리하면 다음과 같다.

규격비준 버전무엇을 정하나이 장
SBI (Supervisor Binary Interface)v3.0S 모드 ↔ M 모드(또는 하이퍼바이저) 호출 규약과 확장들SBI · HSM · 타이머
PLICv1.0.0선으로 들어오는 외부 인터럽트의 우선순위·전달인터럽트
AIA (Advanced Interrupt Architecture)v1.0APLIC, IMSIC, MSI, 인터럽트 관련 CSR인터럽트
IOMMUv1.0.1 (2026-02-22 개정판)장치 DMA 주소의 변환과 보호IOMMU
Debugv1.0Debug Module, DTM, Sdext·Sdtrig디버그
E-Trace · N-Tracev2.0 · v1.0실행 흐름 기록 형식디버그
Semihostingv1.0대상 프로그램이 디버거 호스트의 입출력을 빌려 쓰는 방법디버그
BRS (Boot and Runtime Services)v1.0UEFI·ACPI·SBI·SMBIOS 요구사항 묶음플랫폼 규격
UEFI Protocolv1.0.0RISCV_EFI_BOOT_PROTOCOL (부트 하트 번호 전달)플랫폼 규격
Server Platform · Server SoCv1.0 · v1.0서버가 갖춰야 할 ISA·하드웨어·펌웨어 조건플랫폼 규격
RPMI · ACPI RIMT · ACPI FFHv1.0 · v1.0 · v1.0.1플랫폼 관리 메시지, IOMMU 기술 표, ACPI 고정 하드웨어 기능플랫폼 규격

전원이 켜진 뒤: 부팅 단계

RISC-V 하트는 리셋되면 M 모드에서 구현이 정한 리셋 벡터 주소부터 실행한다(특권 규격의 리셋 규칙: 특권 모드는 M, mstatus.MIE는 0, pc는 구현이 정한 값). 그 순간에는 DRAM도 초기화되지 않았고 MMU도 꺼져 있다. 여기서부터 리눅스가 뜨기까지는 여러 단계의 프로그램이 바통을 넘기며 차례로 더 큰 세상을 준비한다. 이름과 단계 수는 칩마다 다르지만, 리눅스를 돌리는 RISC-V 보드에서 흔히 보는 순서는 다음과 같다.

M 모드 S 모드 U 모드 OpenSBI는 계속 상주하며 SBI 호출을 받는다 mret · a0=hartid · a1=DTB 주소 ecall → 시스템 호출 ZSBL 온칩 ROM FSBL U-Boot SPL OpenSBI SBI 구현 U-Boot · EDK2 부트로더 Linux 커널 S 모드 OS init (PID 1) 사용자 공간
그림 22-2. 리눅스 RISC-V 보드의 전형적인 부팅 사슬. 앞의 세 단계는 M 모드에서, 부트로더와 커널은 S 모드에서, init은 U 모드에서 돈다. 다른 단계는 일을 마치면 사라지지만 OpenSBI만은 M 모드에 남아 런타임 서비스(SBI)를 계속 제공한다.
단계모드어디서 도나하는 일예
ZSBL (zero-stage)M칩 안의 마스크 ROM부트 매체를 고르고 다음 단계를 SRAM으로 복사칩 제조사 ROM 코드
FSBL (first-stage)M칩 안의 SRAMDRAM 초기화, 펌웨어·부트로더를 DRAM으로 적재U-Boot SPL, coreboot
런타임 펌웨어MDRAM (상주)PMP 설정, 트랩 위임, SBI 서비스 제공OpenSBI, RustSBI
부트로더SDRAM커널·장치 트리/ACPI·initramfs 적재, UEFI 서비스U-Boot 본체, EDK2
운영체제SDRAMMMU 켜기, 보조 하트 깨우기, 장치 구동Linux
ANIMATION

부팅 흐름 따라가기

부트 하트 (hart 0)보조 하트주차됨 (대기·STOPPED)
진행
단계1 / 10
부트 하트 모드M
깨어 있는 하트4
⑤단계에서 보조 하트 세 개가 OpenSBI 안에 주차되는 것, ⑧단계에서 리눅스가 SBI 호출로 그 하트들을 하나씩 깨우는 것을 눈여겨보자. 부트 하트만 처음부터 끝까지 일하고 나머지는 운영체제가 부를 때까지 기다린다.
부트 하트 복권(hart lottery)

리셋 직후에는 모든 하트가 같은 코드를 동시에 실행한다. 누가 초기화를 맡을지 정하려고 OpenSBI와 리눅스 커널은 공유 변수에 원자적 연산(AMO)을 걸어 가장 먼저 성공한 하트를 부트 하트로 뽑는다. 이 방식을 흔히 “복권”이라 부른다. 규격은 hartid가 연속일 필요는 없고 최소 하나의 하트가 0번이어야 한다고만 정하므로, 소프트웨어는 hartid 0이 반드시 부트 하트라고 가정하지 않는다.

리눅스가 받는 인계 상태

리눅스 RISC-V 부트 규약은 커널 진입 시 a0에 현재 하트의 hartid, a1에 메모리에 놓인 장치 트리(DTB)의 물리 주소, satp=0(MMU 꺼짐)을 요구한다. 커널은 이 두 레지스터만으로 자신이 누구이고 하드웨어가 무엇인지 알아낸다.

SBI: 운영체제가 펌웨어에게 부탁하는 법

PC의 BIOS 호출처럼, S 모드 운영체제는 ecall 명령으로 한 단계 높은 실행 환경(M 모드 펌웨어, 또는 하이퍼바이저)에 서비스를 요청한다. 이 요청의 레지스터 사용법을 정한 것이 SBI다. SBI를 제공하는 쪽을 규격은 SEE(Supervisor Execution Environment) 또는 SBI 구현이라 부른다.

규격 원문에서
“The SBI allows supervisor-mode (S-mode or VS-mode) software to be portable across all RISC-V implementations by defining an abstraction for platform (or hypervisor) specific functionality.”

SBI v3.0, Introduction — 같은 커널 이미지가 실제 칩(M 모드 펌웨어 아래)에서도, 가상 머신(하이퍼바이저 아래)에서도 그대로 도는 이유가 여기에 있다.

호출 규약

호출할 때 a0인자 0 a1인자 1 a2인자 2 a3인자 3 a4인자 4 a5인자 5 a6FID a7EID ecall → (M 모드에서 처리) → mret 돌아올 때 a0error a1value a0·a1을 뺀 모든 레지스터는 호출 전후 값이 보존된다 struct sbiret { long error; long value; }
그림 22-3. SBI 호출 규약. a7에 확장 번호(EID), a6에 함수 번호(FID), a0–a5에 인자를 넣고 ecall한다. 돌아오면 a0은 오류 코드, a1은 결과 값이다. 2×XLEN 폭의 인자는 번호가 낮은 레지스터에 하위 절반을 넣는다.

EID와 FID는 부호 있는 32비트 정수로 정의된다. 표준 확장의 EID는 대개 확장 이름의 ASCII 코드를 그대로 숫자로 만든 것이라, 16진수를 문자로 읽으면 이름이 보인다. 타이머 확장의 EID 0x54494D45는 바이트 54 49 4D 45, 곧 “TIME”이다. 지원하지 않는 EID나 FID를 부르면 반드시 SBI_ERR_NOT_SUPPORTED(−2)가 돌아온다.

S 모드 · 커널 M 모드 · OpenSBI 인자 준비 a7=EID · a6=FID · a0–a5 결과 확인 a0=error · a1=value ecall mret 트랩 진입 mcause = 9 mepc ← ecall 주소 확장 찾기 a7 → 확장 표 없으면 −2 함수 실행 a6 → 함수 장치 조작 · 검사 결과 기록 a0, a1 ← 결과 mepc ← mepc + 4
그림 22-4. SBI 호출 한 번의 왕복. S 모드에서 실행한 ecall은 위임되지 않은 예외(원인 9, Environment call from S-mode)로 M 모드에 트랩된다. OpenSBI는 a7로 확장을, a6로 함수를 찾아 실행하고, 복귀 주소를 ecall 다음 명령으로 옮긴 뒤 mret한다.

표준 확장 목록

SBI는 작은 Base 확장과 선택적 확장들로 이루어진다. Base 확장(EID 0x10)은 모든 구현이 반드시 제공하며, 버전 조회와 sbi_probe_extension()으로 다른 확장이 있는지 묻는 기능만 들어 있다. 운영체제는 부팅 초기에 필요한 확장을 하나씩 탐색한 뒤 쓴다.

확장EIDASCII도입하는 일
Base0x10—0.2SBI 버전, 구현 ID, 확장 탐색, mvendorid 등 조회
Legacy0x00–0x0F—0.1옛 호출 방식. TIME·IPI·RFENCE·SRST·DBCN으로 대체되어 폐기 권고
TIME0x54494D45TIME0.2다음 타이머 인터럽트 시각 설정
IPI0x735049sPI0.2다른 하트에 S 모드 소프트웨어 인터럽트 보내기
RFENCE0x52464E43RFNC0.2원격 FENCE.I, SFENCE.VMA, HFENCE.GVMA/VVMA
HSM0x48534DHSM0.2하트 시작·정지·상태 조회·일시 중지(suspend는 0.3)
SRST0x53525354SRST0.3시스템 종료, 콜드·웜 재시작
PMU0x504D55PMU0.3하드웨어·펌웨어 성능 카운터 설정
DBCN0x4442434EDBCN2.0디버그 콘솔 읽기·쓰기 (부팅 초기 출력)
SUSP0x53555350SUSP2.0시스템 전체를 절전 상태로
CPPC0x43505043CPPC2.0ACPI CPPC 성능 제어 레지스터 접근
NACL0x4E41434CNACL2.0중첩 가상화 가속(공유 메모리로 CSR 동기화)
STA0x535441STA2.0가상 CPU가 빼앗긴 시간(steal time) 알림
SSE0x535345SSE3.0모든 트랩보다 앞서는 S 모드 소프트웨어 이벤트(RAS 등)
FWFT0x46574654FWFT3.0펌웨어 기능 켜고 끄기(예: 정렬 안 된 접근 예외 위임)
DBTR0x44425452DBTR3.0S 모드에서 디버그 트리거 설치·관리
MPXY0x4D505859MPXY3.0펌웨어를 거쳐 RPMI 같은 메시지 프로토콜 주고받기

오류 코드는 v3.0 기준으로 다음 열다섯 가지가 정의되어 있다. 성공이 아니면 a1의 값은 함수가 따로 정하지 않는 한 정의되지 않는다.

이름값뜻이름값뜻
SBI_SUCCESS0성공SBI_ERR_ALREADY_STOPPED−8이미 정지됨
SBI_ERR_FAILED−1실패SBI_ERR_NO_SHMEM−9공유 메모리 없음
SBI_ERR_NOT_SUPPORTED−2지원 안 함SBI_ERR_INVALID_STATE−10잘못된 상태
SBI_ERR_INVALID_PARAM−3잘못된 인자SBI_ERR_BAD_RANGE−11잘못된 범위
SBI_ERR_DENIED−4거부됨SBI_ERR_TIMEOUT−12시간 초과
SBI_ERR_INVALID_ADDRESS−5잘못된 주소SBI_ERR_IO−13입출력 오류
SBI_ERR_ALREADY_AVAILABLE−6이미 사용 가능SBI_ERR_DENIED_LOCKED−14잠금 때문에 거부
SBI_ERR_ALREADY_STARTED−7이미 시작됨
SIMULATOR

SBI 호출 탐색기

ecall 직전 레지스터

어셈블리

경우
실행
a7 · EID—
a6 · FID—
복귀 a0 (error)—
복귀 a1 (value)—
함수를 바꿔 가며 a7의 16진수가 어떤 ASCII 이름이 되는지 보자. 오류 경우로 바꾸면 이미 켜진 하트를 다시 시작하거나 없는 하트에 IPI를 보내는 등 흔한 실수가 어떤 오류 코드로 돌아오는지 볼 수 있다. sbi_system_reset은 성공하면 돌아오지 않는다.

HSM: 하트를 켜고 끄는 약속

보조 하트를 깨우는 방법이 칩마다 다르면 운영체제는 곤란하다. 어떤 칩은 전원 도메인을 켜야 하고, 어떤 칩은 특정 레지스터에 시작 주소를 써야 한다. SBI의 HSM(Hart State Management) 확장은 이 차이를 펌웨어 안에 숨기고, 운영체제에는 하트마다 일곱 가지 상태와 네 가지 함수만 보여 준다.

STOPPED 상태 1 · 펌웨어 소유 START_PENDING 상태 2 STARTED 상태 0 · S 모드 실행 STOP_PENDING 상태 3 SUSPEND_PENDING 상태 5 SUSPENDED 상태 4 · 저전력 RESUME_PENDING 상태 6 hart_start 시작 완료 hart_stop 정지 완료 hart_suspend 대기 인터럽트 재개
그림 22-5. HSM 확장의 하트 상태 기계(EID 0x48534D). 왼쪽 고리는 시작·정지, 오른쪽 줄은 일시 중지·재개다. _PENDING 상태는 펌웨어가 요청을 처리하는 중간 단계다. 금색 점은 하트 하나가 켜졌다 꺼지는 한 바퀴를 나타낸다.
함수 (FID)설명
sbi_hart_start (0)인자 hartid, start_addr, opaque. STOPPED 하트를 S 모드 start_addr(물리 주소)에서 시작시킨다. 깨어난 하트는 satp=0, sstatus.SIE=0, a0=hartid, a1=opaque. 비동기 호출이다.
sbi_hart_stop (1)인자 없음. 호출한 하트 자신을 정지시켜 펌웨어에 돌려준다. 성공하면 돌아오지 않는다.
sbi_hart_get_status (2)인자 hartid. 하트의 HSM 상태 번호(0–6)를 a1로 돌려준다.
sbi_hart_suspend (3)인자 suspend_type, resume_addr, opaque. 호출한 하트를 저전력 상태로 보낸다. 레지스터를 보존하는(retentive) 방식과 보존하지 않는(non-retentive) 방식이 있다.
start_addr이 XLEN 하나로 충분한 이유

깨어난 하트는 MMU가 꺼진 S 모드에서 시작한다. 그래서 start_addr은 가상 주소가 아닌 물리 주소이고, 커널은 보조 하트용 진입 코드에서 스스로 페이지 테이블을 켠다. 이미 STARTED인 하트에 다시 sbi_hart_start를 부르면 SBI_ERR_ALREADY_AVAILABLE(−6)이 돌아온다.

타이머·IPI·원격 펜스: 펌웨어 경유와 하드웨어 직행

특권 규격에서 타이머 비교 레지스터 mtimecmp와 소프트웨어 인터럽트 비트는 M 모드만 만질 수 있는 메모리 매핑 장치였다. 그래서 초기 RISC-V 리눅스는 타이머를 맞출 때마다, 다른 하트를 깨울 때마다 SBI로 펌웨어를 불렀다. 문제는 비용이다. 타이머 이벤트 하나에 M 모드 트랩이 두 번(설정 ecall, 만료 인터럽트) 끼어든다.

Sstc 없음 · SBI 경유 Sstc 있음 · 직접 설정 S: sbi_set_timer(t) → ecall M: OpenSBI가 mtimecmp ← t 하드웨어: mtime ≥ mtimecmp → M 타이머 인터럽트 M: OpenSBI가 mip.STIP ← 1 S: 타이머 인터럽트 처리 M 모드 진입 2번 S: csrw stimecmp, t (CSR 0x14D) 하드웨어: time ≥ stimecmp → STIP S: 타이머 인터럽트 처리 M 모드 진입 0번
그림 22-6. 타이머 한 번을 맞추는 두 경로. 왼쪽은 SBI TIME 확장을 쓰는 경로로 펌웨어가 M 모드 타이머를 대신 다루고 S 모드에 인터럽트를 넘겨준다. 오른쪽 Sstc 확장은 S 모드 전용 비교 레지스터 stimecmp를 두어 펌웨어를 거치지 않는다. 가상 머신에서는 vstimecmp(0x24D)가 같은 일을 한다.

같은 흐름이 IPI와 원격 TLB 무효화에도 있다. 리눅스가 다른 하트의 TLB를 비우려면 그 하트에게 “sfence.vma를 실행하라”고 알려야 하는데, 소프트웨어 인터럽트를 거는 장치가 M 모드 전용이면 SBI의 IPI·RFENCE 확장을 쓸 수밖에 없다. AIA의 IMSIC이 있으면 S 모드가 다른 하트의 인터럽트 파일에 직접 MSI를 써서 IPI를 보낼 수 있다. BRS 규격은 바로 이 관계를 규칙으로 적어 두었다.

BRS 규칙내용
SBI_010SBI 구현은 SBI v2.0 이상을 따라야 한다.
SBI_020HSM 확장은 반드시 구현한다.
SBI_030Sstc가 없으면 TIME 확장을 구현해야 한다.
SBI_040 · SBI_050IMSIC이 없으면 IPI(sPI)와 RFENCE(RFNC) 확장을 구현해야 한다.
SBI_060카운터 위임 확장(Sscsrind·Ssccfg)이 없으면 PMU 확장을 구현해야 한다.
SBI_070ACPI SPCR 표가 인터페이스 유형 0x15를 가리키면 DBCN 확장을 구현해야 한다.
추세: 펌웨어는 점점 얇아진다

Sstc(타이머), AIA IMSIC(IPI·외부 인터럽트), Svinval·Sscofpmf 같은 확장이 하나씩 비준되면서 “자주 일어나는 일은 S 모드가 하드웨어로 직접, 드물고 플랫폼마다 다른 일은 SBI로”라는 분업이 굳어지고 있다. RVA23 프로파일이 Sstc를 필수로 넣은 것도 같은 이유다(23장).

인터럽트 구조의 진화: CLINT에서 AIA까지

특권 규격은 하트 안의 인터럽트 비트(mip·mie의 소프트웨어·타이머·외부 인터럽트)만 정하고, 그 비트를 바깥에서 누가 올리는지는 정하지 않았다. 그 바깥 장치가 세 세대에 걸쳐 바뀌어 왔다.

  1. CLINT / ACLINT — 하트마다 타이머(mtime·mtimecmp)와 소프트웨어 인터럽트 비트(msip)를 메모리 주소에 둔 단순한 장치. CLINT는 SiFive 구현에서 시작한 사실상의 표준이고, 이를 정리한 ACLINT는 공개 규격이지만 공식 비준 목록에는 없다.
  2. PLIC — 장치의 인터럽트 선을 모아 우선순위를 매기고 하트의 외부 인터럽트 선(EIP)으로 전달한다. 원래 특권 규격 초안에 있다가 분리되어 PLIC v1.0.0으로 비준되었다.
  3. AIA — 인터럽트를 선 대신 메모리 쓰기(MSI)로 전달하는 IMSIC과, 선 인터럽트를 받아 MSI로 바꿔 주는 APLIC을 정의한다. 가상화(게스트 인터럽트 파일)와 수천 개의 인터럽트를 염두에 둔 설계다.
UART소스 1 디스크소스 2 네트워크소스 3 게이트웨이 게이트웨이 게이트웨이 PLIC 코어 우선순위 · 소스마다 대기 비트 IP · 소스마다 허용 비트 IE · 컨텍스트마다 문턱값 · 컨텍스트마다 claim / complete 컨텍스트 0 · 하트 0 M 컨텍스트 1 · 하트 0 S 컨텍스트 2 · 하트 1 M 컨텍스트 3 · 하트 1 S EIP 선 · 번호 배정은 구현마다 다르다 claim = 컨텍스트의 claim/complete 레지스터를 MMIO로 읽기 → 가장 우선인 소스 ID complete = 같은 레지스터에 그 ID를 MMIO로 쓰기 → 게이트웨이가 다음 요청을 받아 줌
그림 22-7. PLIC의 구조. 컨텍스트는 “어느 하트의 어느 특권 모드”라는 인터럽트 목적지다. 우선순위가 문턱값보다 크고 허용된 대기 인터럽트가 있으면 그 컨텍스트의 EIP 선이 올라간다. PLIC은 소스 최대 1023개(0번은 없음), 컨텍스트 최대 15872개를 지원한다.
PLIC 레지스터 (base 기준 오프셋)내용
0x000004–0x000FFC소스 1–1023의 우선순위 (4바이트씩, 0x000000은 예약)
0x001000–0x00107C대기 비트 (소스 0–1023, 32개씩 한 워드)
0x002000 + 0x80 × 컨텍스트그 컨텍스트의 허용 비트
0x200000 + 0x1000 × 컨텍스트그 컨텍스트의 우선순위 문턱값
0x200004 + 0x1000 × 컨텍스트claim (읽기) / complete (쓰기)

AIA: 인터럽트를 메모리 쓰기로

PCI Express 장치는 원래 인터럽트를 메모리 쓰기로 알린다(MSI). 정해진 주소에 정해진 값을 쓰면 그것이 곧 인터럽트다. AIA의 IMSIC(Incoming MSI Controller)은 하트마다 하나씩 붙어 이 쓰기를 받는다. IMSIC 안에는 특권 수준마다 인터럽트 파일이 있고(M 파일, S 파일, H 확장이 있으면 게스트 파일 GEILEN개), 파일마다 4 KiB 페이지 하나를 차지한다. 그 페이지 오프셋 0x000의 seteipnum_le에 식별자 번호를 쓰면 해당 대기 비트가 켜진다. 파일 하나가 지원하는 식별자 수는 64의 배수보다 하나 적은 값으로 최소 63, 최대 2047이다.

UART (선) GPIO (선) PCIe 장치선 없음 · MSI만 APLIC 루트 도메인 (M) └ 자식 도메인 (S) domaincfg.DM = 1 MSI 전달 모드 IOMMU · 인터커넥트 MSI 주소 검사·변환 MSI 쓰기 MSI 쓰기 하트 0 의 IMSIC M 파일4 KiB S 파일4 KiB 게스트× GEILEN +0x000 seteipnum_le ← 식별자 하트는 stopei CSR로 받는다 하트 1 의 IMSIC M 파일4 KiB S 파일4 KiB 게스트× GEILEN +0x000 seteipnum_le ← 식별자 IPI도 이 페이지에 쓰면 된다
그림 22-8. AIA의 구성. 선으로 인터럽트를 내는 장치는 APLIC이 받아 MSI로 바꾸고(MSI 전달 모드), PCIe 장치는 처음부터 MSI를 쓴다. MSI는 결국 목적지 하트 IMSIC의 특정 인터럽트 파일 페이지에 대한 4바이트 쓰기다. APLIC의 도메인은 트리 구조이며 루트 도메인은 언제나 M 수준이다.

하트 쪽에서는 새 CSR로 인터럽트를 받는다. stopei(0x15C)를 읽으면 비트 26:16에 가장 우선인(번호가 가장 작은) 대기 식별자가 나오고, csrrw a0, stopei, zero처럼 읽으면서 쓰면 그 대기 비트가 지워진다. PLIC의 claim처럼 버스를 건너는 MMIO 읽기가 필요 없다. M 수준은 mtopei(0x35C), 게스트는 vstopei를 쓴다.

ANIMATION

PLIC과 AIA의 인터럽트 전달 비교

전달 방식
실행
단계0 / 5
하트의 MMIO 접근0
MSI 메모리 쓰기0
CSR 접근0
세 방식을 차례로 실행하고 마지막 줄의 숫자를 비교하자. PLIC은 처리기가 claim과 complete로 MMIO를 두 번 왕복해야 하지만, IMSIC은 CSR 한 번으로 끝난다. MMIO 접근은 수백 사이클이 걸릴 수 있어 인터럽트가 잦은 고속 네트워크·저장 장치에서 차이가 커진다.
CLINT · ACLINTPLICAIA (APLIC + IMSIC)
다루는 것하트별 타이머·소프트웨어 인터럽트장치의 선 인터럽트선 인터럽트 + MSI, IPI도 MSI로
규격 상태사실상 표준·공개 규격(비준 목록 밖)PLIC v1.0.0 비준AIA v1.0 비준
규모하트 수만큼소스 1023 · 컨텍스트 15872파일당 식별자 63–2047, 하트마다 IMSIC
처리기 확인—claim MMIO 읽기 + complete MMIO 쓰기*topei CSR 읽기·쓰기
가상화—하이퍼바이저가 흉내 내야 함게스트 인터럽트 파일로 직접 전달

IOMMU: 장치의 DMA에도 울타리를

CPU의 메모리 접근은 MMU와 PMP가 지키지만, 네트워크 카드나 저장 장치는 DMA로 메모리를 직접 읽고 쓴다. 장치 드라이버의 버그나 악의적인 장치가 아무 물리 주소에나 쓸 수 있다면 운영체제의 격리는 무너진다. 가상 머신에 장치를 통째로 넘겨줄 때는 더 심각하다. 게스트는 자기 “물리 주소”를 장치에 알려 주지만, 그것은 사실 게스트 물리 주소(GPA)일 뿐이다.

RISC-V IOMMU 규격은 장치가 낸 주소(IOVA)를 CPU와 같은 페이지 테이블 형식으로 변환한다. 장치는 device_id(최대 24비트, PCIe라면 버스·장치·함수 번호)로 구별되고, 공유 가상 주소를 쓰는 장치는 process_id(최대 20비트, PCIe의 PASID)도 함께 낸다.

장치 (DMA) device_id process_id IOVA IOMMU ① DDT 검색: device_id → 장치 문맥(DC) ② PDT 검색: process_id → 프로세스 문맥 (선택) ③ 1단계: VA → GPA (Sv39 · Sv48 · Sv57) ④ 2단계: GPA → SPA (Sv39x4 · Sv48x4 · Sv57x4) 결과는 IOTLB에 캐시 · 어느 단계든 Bare로 끌 수 있다 메모리 데이터 버퍼 DDT · PDT 페이지 테이블 명령 큐 폴트 큐
그림 22-9. IOMMU의 변환 과정. 장치 번호로 장치 문맥을 찾고, 그 문맥이 정한 방식으로 2단계 변환(21장의 하이퍼바이저와 같은 VS 단계·G 단계 구조)을 한다. 소프트웨어는 메모리에 놓인 명령 큐로 IOMMU에 무효화 같은 명령을 보내고, IOMMU는 폴트 큐로 오류를 알린다.
1단계2단계장치가 내는 주소(IOVA)의 뜻쓰임
BareBare그대로 물리 주소(SPA)변환·보호 없음
Bare켜짐게스트 물리 주소(GPA)장치를 가상 머신에 직접 넘김
켜짐Bare가상 주소(VA)호스트 OS의 DMA 격리, 공유 가상 주소
켜짐켜짐게스트 가상 주소(VA)게스트 안에서도 DMA 격리
MSI도 IOMMU를 지난다

장치의 MSI 역시 메모리 쓰기이므로 IOMMU를 통과한다. IOMMU 규격은 장치 문맥마다 MSI 전용 페이지 테이블을 두어, 게스트에 넘긴 장치의 MSI를 그 게스트의 IMSIC 게스트 인터럽트 파일로 바로 보낼 수 있게 한다. 21장의 하이퍼바이저가 장치 인터럽트를 흉내 내지 않아도 되는 이유다.

디버그·트레이스·세미호스팅: 칩을 처음 깨울 때

새 칩에 처음 전원을 넣었을 때는 콘솔도 운영체제도 없다. 이때 쓰는 것이 외부 디버거다. RISC-V Debug 규격은 JTAG 같은 전송 수단으로 들어온 디버거가 하트를 멈추고, 레지스터와 메모리를 읽고 쓰고, 다시 실행시키는 방법을 정한다.

디버그 호스트 (예: 노트북) 디버거gdb 디버그 변환기OpenOCD 전송 하드웨어USB-JTAG 어댑터 JTAG (TCK · TMS · TDI · TDO) 칩 (하드웨어 플랫폼) DTM JTAG TAP IR 0x10 dtmcs IR 0x11 dmi DMI Debug Module (DM) dmcontrol · dmstatus 추상 명령 · 프로그램 버퍼 시스템 버스 접근 (선택) 하트 0+ 트리거 모듈 하트 1+ 트리거 모듈
그림 22-10. RISC-V 디버그 시스템. 디버그 전송 모듈(DTM)은 JTAG 같은 외부 신호를 Debug Module Interface(DMI) 버스 접근으로 바꾼다. DM은 자신이 맡은 하트들을 멈추고(halt) 재개(resume)시키며, 추상 명령으로 레지스터에 접근한다. 하트마다 하나의 DM이 그 하트를 맡는다.
규격 원문에서
“Each hart in the hardware platform is controlled by exactly one DM. … Abstract commands provide access to GPRs. Additional registers are accessible through abstract commands or by writing programs to the optional Program Buffer.”

The RISC-V Debug Specification v1.0, System Overview

Debug 모드와 추상 명령

하트가 멈추면 M 모드보다도 높은 Debug 모드(Debug Mode)에 들어간다(Sdext 확장). 이때 하트는 멈춘 주소를 dpc(0x7B1)에, 멈춘 이유와 이전 특권 수준을 dcsr(0x7B0)에 기록한다. dcsr.cause(비트 8:6)는 1=ebreak, 2=트리거, 3=haltreq, 4=한 단계 실행(step), 5=resethaltreq, 6=halt group이고, dcsr.prv(비트 1:0)는 멈추기 전 특권 수준이다. 디버그 전용 CSR 0x7B0–0x7BF는 Debug 모드에서만 보이며, 하드웨어 중단점을 만드는 트리거 CSR(tselect 0x7A0, tdata1–tdata3 0x7A1–0x7A3, Sdtrig 확장)은 M 모드에서도 접근할 수 있다.

DMI 주소레지스터쓰임
0x04–0x0fdata0–data11추상 명령의 인자·결과 (64비트 값은 data0=하위, data1=상위)
0x10dmcontrolhaltreq(31), resumereq(30), hartsel, ndmreset(1), dmactive(0)
0x11dmstatusallhalted(9), anyhalted(8), allrunning(11), allresumeack(17), version(3:0)=3이면 1.0
0x16abstractcsprogbufsize(28:24), busy(12), cmderr(10:8, 1을 써서 지움), datacount(3:0)
0x17command쓰는 순간 추상 명령 실행
0x20–progbuf0…프로그램 버퍼: 하트가 Debug 모드에서 실행할 짧은 명령들
3124 23 2220 1918 1716 150 0 0 3 0 0 1 0 0x100a cmdtype (0 = Access Register) aarsize regno (0x1000 + GPR 번호) aarpostincrement postexec transfer write = 0x0032100a : a0(x10)을 64비트로 읽어 data0·data1에 담아라
그림 22-11. Access Register 추상 명령(cmdtype 0)의 비트 배치. aarsize는 2=32비트, 3=64비트, 4=128비트. regno는 0x0000–0x0FFF가 CSR, 0x1000–0x101F가 정수 레지스터, 0x1020–0x103F가 부동소수점 레지스터다. 멈춘 하트의 정수 레지스터 읽기·쓰기는 모든 DM이 반드시 지원해야 한다.
SIMULATOR

Debug Module 조작해 보기

디버거 gdb + OpenOCD JTAG DTM dmi (0x11) DMI Debug Module dmcontrol 0x00000001 dmstatus 0x00000c83 abstractcs 0x02000002 command — data0 0x00000000 data1 0x00000000 DMI 레지스터 (32비트) 하트 0 실행 중 pc 0x80200100 a0 = 0 dpc — dcsr.cause —
하트 제어
읽을 레지스터
추상 명령
하트 상태실행 중
dmstatus.allhalted0
abstractcs.cmderr0
DMI 트랜잭션2
먼저 하트가 실행 중일 때 “레지스터 읽기”를 눌러 보자. 이 DM은 실행 중인 하트의 레지스터 접근을 지원하지 않으므로 cmderr=4(halt/resume)가 되고, 디버거는 1을 써서 지워야 한다. 그다음 정지 → 읽기 → a0 쓰기 → 재개 순서로 해 보고, 재개 뒤 a0가 42부터 다시 늘어나는지 확인하자.

트레이스와 세미호스팅

디버거로 멈추면 실시간 동작이 깨진다. 멈추지 않고 실행 흐름을 기록하려면 트레이스를 쓴다. E-Trace(Efficient Trace, v2.0)는 모든 명령을 기록하지 않고 분기 결과와 불연속 지점만 압축해 내보내며, 디코더가 프로그램 바이너리와 맞춰 보며 실행 경로를 복원한다. N-Trace(v1.0)는 Nexus 표준 메시지 형식을 쓰는 방식이다. 트레이스 제어 인터페이스, 커넥터, 캡슐화 형식도 각각 별도 규격으로 비준되어 있다.

세미호스팅(Semihosting v1.0)은 반대 방향이다. 운영체제가 없는 대상 프로그램이 디버거 호스트의 파일·콘솔을 빌려 쓴다. RISC-V에는 ebreak가 하나뿐이라, 디버거가 넣은 중단점과 구별하려고 특별한 세 명령 묶음을 쓴다. 연산 번호는 a0, 인자는 a1, 결과는 a0로 돌아온다.

slli x0, x0, 0x1f # 0x01f01013 시작 표시용 NOP ebreak # 0x00100073 디버거로 넘어간다 srai x0, x0, 7 # 0x40705013 끝 표시용 NOP

플랫폼 규격: BRS·UEFI·ACPI·서버

지금까지의 규격은 부품이다. “이 부품들을 어떤 조합으로 갖추면 상용 리눅스 배포판을 그대로 설치할 수 있다”는 약속은 플랫폼 규격이 한다. x86 PC가 UEFI와 ACPI 덕분에 어느 회사 메인보드에서나 같은 설치 USB로 부팅되는 것처럼, RISC-V도 같은 길을 택했다.

규격핵심 내용
BRS v1.0두 가지 조합(recipe)을 정의한다. BRS-I(Interoperable)는 UEFI + ACPI로 범용 OS가 그대로 돌게 하는 조합, BRS-B(Bespoke)는 특정 용도에 맞춘 조합이다. SBI v2.0 이상과 HSM을 요구한다.
UEFI Protocol v1.0.0RISCV_EFI_BOOT_PROTOCOL의 GetBootHartId()로 부트 하트 번호를 전달한다. UEFI 응용의 호출 규약 때문에 a0로는 hartid를 넘길 수 없고, 장치 트리의 /chosen 방식은 ACPI 시스템에서 쓸 수 없기 때문이다.
ACPI 표RHCT(RISC-V Hart Capabilities Table: ISA 문자열·캐시·MMU 정보), MADT의 하트별 RINTC 구조와 PLIC/APLIC 구조, IOMMU가 있으면 RIMT(RISC-V IO Mapping Table). ACPI FFH 규격은 저전력 상태·CPPC 같은 기능을 SBI 호출로 연결한다.
Server Platform v1.0응용 프로세서 하트는 RVA23S64 프로파일에 더해 Sv48, Sdtrig, Sdext, Zkr, Ssccfg, Ssstrict, Ssaia를 갖춰야 하고, BRS-I를 따르며, IOMMU는 RIMT로 기술하고, UEFI 보안 부팅을 구현해야 한다.
RPMI v1.0응용 프로세서와 전원·클록을 관리하는 플랫폼 마이크로컨트롤러 사이의 메시지 형식. S 모드는 SBI MPXY 확장을 거쳐 이 메시지를 주고받을 수 있다.
임베디드 · 보드 중심 (BRS-B 등) 범용 서버 · PC (BRS-I) OpenSBI (M) · SBI U-Boot (S) 커널a0=hartid · a1=장치 트리(DTB) OpenSBI (M) · SBI v2.0 이상 · HSM EDK2 등 UEFI 펌웨어 (S)GetBootHartId · ACPI 표 · SMBIOS 커널RHCT · MADT(RINTC) · RIMT로 하드웨어 파악
그림 22-12. 하드웨어를 운영체제에 알려 주는 두 길. 보드 중심 시스템은 장치 트리를 a1로 넘기고, 범용 시스템은 UEFI 서비스와 ACPI 표로 알려 준다. 어느 쪽이든 그 아래에서 SBI 구현이 M 모드 서비스를 제공한다는 점은 같다.
PC가 걸어온 길을 따라서

초기 RISC-V 보드는 장치 트리와 보드별 부트로더로 충분했다. 하지만 서버 시장에서는 “배포판 설치 이미지 하나로 모든 서버에 설치”가 기본 요구다. BRS와 Server Platform 규격은 그 요구를 RVA23 프로파일(23장), UEFI, ACPI, 그리고 이 장의 SBI·AIA·IOMMU를 묶어 하나의 체크리스트로 만든 것이다.

핵심 정리

  1. 하트는 리셋되면 M 모드로 시작하고, ZSBL → FSBL → 런타임 펌웨어(OpenSBI) → 부트로더(S) → 커널(S) → init(U) 순으로 바통을 넘긴다. 런타임 펌웨어만 M 모드에 상주한다.
  2. SBI는 S 모드가 ecall로 상위 실행 환경에 부탁하는 규약이다. a7=EID, a6=FID, a0–a5=인자, 돌아오면 a0=오류, a1=값. EID는 대개 이름의 ASCII 코드다(TIME = 0x54494D45).
  3. HSM 확장의 sbi_hart_start(hartid, start_addr, opaque)로 보조 하트를 깨운다. 깨어난 하트는 MMU가 꺼진 S 모드에서 a0=hartid, a1=opaque로 시작한다.
  4. 타이머·IPI·원격 펜스는 SBI로 펌웨어를 거칠 수도, Sstc(stimecmp)와 AIA IMSIC으로 하드웨어에서 직접 처리할 수도 있다. 추세는 후자다.
  5. 인터럽트 구조는 CLINT/ACLINT(타이머·소프트웨어) → PLIC(선, claim/complete MMIO) → AIA(APLIC + IMSIC, MSI 쓰기와 *topei CSR)로 발전했다.
  6. IOMMU는 CPU와 같은 페이지 테이블 형식으로 장치 DMA를 2단계 변환해 격리하며, MSI도 게스트 인터럽트 파일로 보낼 수 있다.
  7. Debug 규격은 DTM → DMI → Debug Module 구조로 하트를 멈추고 추상 명령으로 레지스터에 접근한다. BRS·UEFI·ACPI·Server Platform 규격이 이 모든 부품을 소프트웨어 호환성 약속으로 묶는다.

확인 퀴즈

SBI 호출에서 확장 번호(EID)를 담는 레지스터는?

a7=EID, a6=FID, a0–a5=인자다. 돌아올 때 a0는 오류 코드, a1은 값이다.

EID 0x48534D를 ASCII로 읽으면 어떤 확장인가?

0x48 'H', 0x53 'S', 0x4D 'M'. 하트를 시작·정지·일시 중지하는 HSM 확장이다.

이미 STARTED 상태인 하트에 sbi_hart_start를 다시 부르면 돌아오는 오류 코드는?

HSM 규격의 hart start 오류 표는 “이미 시작된 하트”에 SBI_ERR_ALREADY_AVAILABLE을 쓰도록 정한다. 이름이 비슷한 ALREADY_STARTED와 헷갈리기 쉽다.

Sstc 확장이 있을 때 타이머 이벤트 하나를 처리하는 데 필요한 M 모드 진입 횟수는?

S 모드가 stimecmp를 직접 쓰고, 만료되면 하드웨어가 STIP를 올린다. SBI 경로는 설정 ecall과 M 타이머 인터럽트로 두 번 M 모드에 들어간다.

PLIC에서 처리기가 “어느 장치의 인터럽트인가”를 알아내는 방법은?

claim 읽기는 가장 우선인 대기 인터럽트 ID를 돌려주며 그 대기 비트를 지운다. 처리 후 같은 레지스터에 ID를 써서 complete한다. stopei는 AIA IMSIC의 방법이다.

AIA에서 장치가 하트 1의 S 모드에 인터럽트 식별자 42를 보내려면?

MSI는 그냥 메모리 쓰기다. 인터럽트 파일 페이지의 오프셋 0x000 seteipnum_le에 식별자를 쓰면 그 대기 비트가 켜진다.

Debug Module의 Access Register 명령으로 정수 레지스터 x10(a0)을 지정하는 regno 값은?

regno 0x1000–0x101F가 정수 레지스터이므로 x10은 0x100a다. 0x0000–0x0FFF는 CSR(0x7b1은 dpc), 0x1020부터는 부동소수점 레지스터다.

UEFI로 부팅하는 RISC-V 시스템에서 OS 로더가 부트 하트 번호를 얻는 표준 방법은?

UEFI 호출 규약 때문에 a0로 hartid를 받을 수 없고, mhartid는 M 모드 CSR이라 S 모드에서 읽을 수 없다. RIMT는 IOMMU를 기술하는 표다.

더 읽을거리