부팅·SBI·플랫폼 규격
ISA 매뉴얼만 지킨 칩으로는 아직 컴퓨터가 되지 않는다. 전원이 켜지면 누가 먼저 일어나는지, 운영체제가 타이머를 맞추거나 다른 코어를 깨우려면 누구에게 부탁하는지, 장치의 인터럽트는 어떤 길로 하트에 닿는지까지 약속해야 같은 리눅스 이미지가 여러 칩에서 돈다. 이 장은 ISA 바깥에서 RISC-V 시스템을 묶어 주는 규격들, 곧 SBI·인터럽트 컨트롤러·IOMMU·디버그·부트 규격을 부팅 순서를 따라 살펴본다.
- ZSBL → FSBL → OpenSBI → 부트로더 → 리눅스로 이어지는 부팅 단계와 각 단계의 특권 모드를 안다.
- SBI 호출 규약(a7=EID, a6=FID, a0–a5 인자, a0=오류·a1=값)을 읽고 직접 레지스터를 채울 수 있다.
- HSM 확장으로 보조 하트가 깨어나는 과정과 하트 상태 기계를 설명한다.
- SBI 타이머와 Sstc, SBI IPI와 IMSIC처럼 펌웨어 경유 경로와 하드웨어 직접 경로를 비교한다.
- CLINT·ACLINT → PLIC → AIA(APLIC·IMSIC)로 이어지는 인터럽트 구조의 변화와 MSI의 장점을 안다.
- IOMMU, 외부 디버그(Debug Module·추상 명령), 트레이스, 세미호스팅의 역할을 구별한다.
- BRS·UEFI·ACPI·Server Platform 규격이 소프트웨어 호환성을 어떻게 보장하는지 안다.
ISA 바깥의 약속: 플랫폼이란
17–21장에서 본 특권 아키텍처는 하트 안쪽의 규칙이다. CSR이 몇 번지에 있고 트랩이 나면 무엇이 바뀌는지를 정한다. 하지만 운영체제는 하트 바깥도 알아야 한다. 타이머 장치는 어디에 있는지, 인터럽트 컨트롤러는 어떤 레지스터를 가졌는지, 메모리는 몇 번지부터 몇 번지까지인지, 다른 하트를 깨우려면 무엇을 써야 하는지. 이런 것들을 칩마다 다르게 만들면 운영체제는 칩마다 따로 고쳐야 한다.
RISC-V는 이 문제를 층을 나누는 방식으로 푼다. 칩마다 달라지는 일은 M 모드에서 도는 펌웨어가 흡수하고, 운영체제(S 모드)는 펌웨어에게 정해진 방식으로 부탁만 한다. 그 부탁의 형식이 SBI(Supervisor Binary Interface)다. 하드웨어 목록은 장치 트리(Device Tree)나 ACPI 표로 전달하고, 인터럽트·IOMMU·디버그처럼 운영체제가 직접 만지는 장치는 아예 표준 규격으로 정한다.
이 장에서 다루는 규격은 모두 ISA 매뉴얼 밖에 있는 별도 문서다. 공식 규격 라이브러리의 버전을 정리하면 다음과 같다.
| 규격 | 비준 버전 | 무엇을 정하나 | 이 장 |
|---|---|---|---|
| SBI (Supervisor Binary Interface) | v3.0 | S 모드 ↔ M 모드(또는 하이퍼바이저) 호출 규약과 확장들 | SBI · HSM · 타이머 |
| PLIC | v1.0.0 | 선으로 들어오는 외부 인터럽트의 우선순위·전달 | 인터럽트 |
| AIA (Advanced Interrupt Architecture) | v1.0 | APLIC, IMSIC, MSI, 인터럽트 관련 CSR | 인터럽트 |
| IOMMU | v1.0.1 (2026-02-22 개정판) | 장치 DMA 주소의 변환과 보호 | IOMMU |
| Debug | v1.0 | Debug Module, DTM, Sdext·Sdtrig | 디버그 |
| E-Trace · N-Trace | v2.0 · v1.0 | 실행 흐름 기록 형식 | 디버그 |
| Semihosting | v1.0 | 대상 프로그램이 디버거 호스트의 입출력을 빌려 쓰는 방법 | 디버그 |
| BRS (Boot and Runtime Services) | v1.0 | UEFI·ACPI·SBI·SMBIOS 요구사항 묶음 | 플랫폼 규격 |
| UEFI Protocol | v1.0.0 | RISCV_EFI_BOOT_PROTOCOL (부트 하트 번호 전달) | 플랫폼 규격 |
| Server Platform · Server SoC | v1.0 · v1.0 | 서버가 갖춰야 할 ISA·하드웨어·펌웨어 조건 | 플랫폼 규격 |
| RPMI · ACPI RIMT · ACPI FFH | v1.0 · v1.0 · v1.0.1 | 플랫폼 관리 메시지, IOMMU 기술 표, ACPI 고정 하드웨어 기능 | 플랫폼 규격 |
전원이 켜진 뒤: 부팅 단계
RISC-V 하트는 리셋되면 M 모드에서 구현이 정한 리셋 벡터 주소부터 실행한다(특권 규격의 리셋 규칙: 특권 모드는 M, mstatus.MIE는 0, pc는 구현이 정한 값). 그 순간에는 DRAM도 초기화되지 않았고 MMU도 꺼져 있다. 여기서부터 리눅스가 뜨기까지는 여러 단계의 프로그램이 바통을 넘기며 차례로 더 큰 세상을 준비한다. 이름과 단계 수는 칩마다 다르지만, 리눅스를 돌리는 RISC-V 보드에서 흔히 보는 순서는 다음과 같다.
| 단계 | 모드 | 어디서 도나 | 하는 일 | 예 |
|---|---|---|---|---|
| ZSBL (zero-stage) | M | 칩 안의 마스크 ROM | 부트 매체를 고르고 다음 단계를 SRAM으로 복사 | 칩 제조사 ROM 코드 |
| FSBL (first-stage) | M | 칩 안의 SRAM | DRAM 초기화, 펌웨어·부트로더를 DRAM으로 적재 | U-Boot SPL, coreboot |
| 런타임 펌웨어 | M | DRAM (상주) | PMP 설정, 트랩 위임, SBI 서비스 제공 | OpenSBI, RustSBI |
| 부트로더 | S | DRAM | 커널·장치 트리/ACPI·initramfs 적재, UEFI 서비스 | U-Boot 본체, EDK2 |
| 운영체제 | S | DRAM | MMU 켜기, 보조 하트 깨우기, 장치 구동 | Linux |
부팅 흐름 따라가기
리셋 직후에는 모든 하트가 같은 코드를 동시에 실행한다. 누가 초기화를 맡을지 정하려고 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 모드 펌웨어 아래)에서도, 가상 머신(하이퍼바이저 아래)에서도 그대로 도는 이유가 여기에 있다.
호출 규약
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)가 돌아온다.
ecall은 위임되지 않은 예외(원인 9, Environment call from S-mode)로 M 모드에 트랩된다. OpenSBI는 a7로 확장을, a6로 함수를 찾아 실행하고, 복귀 주소를 ecall 다음 명령으로 옮긴 뒤 mret한다.표준 확장 목록
SBI는 작은 Base 확장과 선택적 확장들로 이루어진다. Base 확장(EID 0x10)은 모든 구현이 반드시 제공하며, 버전 조회와 sbi_probe_extension()으로 다른 확장이 있는지 묻는 기능만 들어 있다. 운영체제는 부팅 초기에 필요한 확장을 하나씩 탐색한 뒤 쓴다.
| 확장 | EID | ASCII | 도입 | 하는 일 |
|---|---|---|---|---|
| Base | 0x10 | — | 0.2 | SBI 버전, 구현 ID, 확장 탐색, mvendorid 등 조회 |
| Legacy | 0x00–0x0F | — | 0.1 | 옛 호출 방식. TIME·IPI·RFENCE·SRST·DBCN으로 대체되어 폐기 권고 |
| TIME | 0x54494D45 | TIME | 0.2 | 다음 타이머 인터럽트 시각 설정 |
| IPI | 0x735049 | sPI | 0.2 | 다른 하트에 S 모드 소프트웨어 인터럽트 보내기 |
| RFENCE | 0x52464E43 | RFNC | 0.2 | 원격 FENCE.I, SFENCE.VMA, HFENCE.GVMA/VVMA |
| HSM | 0x48534D | HSM | 0.2 | 하트 시작·정지·상태 조회·일시 중지(suspend는 0.3) |
| SRST | 0x53525354 | SRST | 0.3 | 시스템 종료, 콜드·웜 재시작 |
| PMU | 0x504D55 | PMU | 0.3 | 하드웨어·펌웨어 성능 카운터 설정 |
| DBCN | 0x4442434E | DBCN | 2.0 | 디버그 콘솔 읽기·쓰기 (부팅 초기 출력) |
| SUSP | 0x53555350 | SUSP | 2.0 | 시스템 전체를 절전 상태로 |
| CPPC | 0x43505043 | CPPC | 2.0 | ACPI CPPC 성능 제어 레지스터 접근 |
| NACL | 0x4E41434C | NACL | 2.0 | 중첩 가상화 가속(공유 메모리로 CSR 동기화) |
| STA | 0x535441 | STA | 2.0 | 가상 CPU가 빼앗긴 시간(steal time) 알림 |
| SSE | 0x535345 | SSE | 3.0 | 모든 트랩보다 앞서는 S 모드 소프트웨어 이벤트(RAS 등) |
| FWFT | 0x46574654 | FWFT | 3.0 | 펌웨어 기능 켜고 끄기(예: 정렬 안 된 접근 예외 위임) |
| DBTR | 0x44425452 | DBTR | 3.0 | S 모드에서 디버그 트리거 설치·관리 |
| MPXY | 0x4D505859 | MPXY | 3.0 | 펌웨어를 거쳐 RPMI 같은 메시지 프로토콜 주고받기 |
오류 코드는 v3.0 기준으로 다음 열다섯 가지가 정의되어 있다. 성공이 아니면 a1의 값은 함수가 따로 정하지 않는 한 정의되지 않는다.
| 이름 | 값 | 뜻 | 이름 | 값 | 뜻 |
|---|---|---|---|---|---|
SBI_SUCCESS | 0 | 성공 | 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 | 이미 시작됨 |
SBI 호출 탐색기
ecall 직전 레지스터
어셈블리
a7의 16진수가 어떤 ASCII 이름이 되는지 보자. 오류 경우로 바꾸면 이미 켜진 하트를 다시 시작하거나 없는 하트에 IPI를 보내는 등 흔한 실수가 어떤 오류 코드로 돌아오는지 볼 수 있다. sbi_system_reset은 성공하면 돌아오지 않는다.HSM: 하트를 켜고 끄는 약속
보조 하트를 깨우는 방법이 칩마다 다르면 운영체제는 곤란하다. 어떤 칩은 전원 도메인을 켜야 하고, 어떤 칩은 특정 레지스터에 시작 주소를 써야 한다. SBI의 HSM(Hart State Management) 확장은 이 차이를 펌웨어 안에 숨기고, 운영체제에는 하트마다 일곱 가지 상태와 네 가지 함수만 보여 준다.
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) 방식이 있다. |
깨어난 하트는 MMU가 꺼진 S 모드에서 시작한다. 그래서 start_addr은 가상 주소가 아닌 물리 주소이고, 커널은 보조 하트용 진입 코드에서 스스로 페이지 테이블을 켠다. 이미 STARTED인 하트에 다시 sbi_hart_start를 부르면 SBI_ERR_ALREADY_AVAILABLE(−6)이 돌아온다.
타이머·IPI·원격 펜스: 펌웨어 경유와 하드웨어 직행
특권 규격에서 타이머 비교 레지스터 mtimecmp와 소프트웨어 인터럽트 비트는 M 모드만 만질 수 있는 메모리 매핑 장치였다. 그래서 초기 RISC-V 리눅스는 타이머를 맞출 때마다, 다른 하트를 깨울 때마다 SBI로 펌웨어를 불렀다. 문제는 비용이다. 타이머 이벤트 하나에 M 모드 트랩이 두 번(설정 ecall, 만료 인터럽트) 끼어든다.
stimecmp를 두어 펌웨어를 거치지 않는다. 가상 머신에서는 vstimecmp(0x24D)가 같은 일을 한다.같은 흐름이 IPI와 원격 TLB 무효화에도 있다. 리눅스가 다른 하트의 TLB를 비우려면 그 하트에게 “sfence.vma를 실행하라”고 알려야 하는데, 소프트웨어 인터럽트를 거는 장치가 M 모드 전용이면 SBI의 IPI·RFENCE 확장을 쓸 수밖에 없다. AIA의 IMSIC이 있으면 S 모드가 다른 하트의 인터럽트 파일에 직접 MSI를 써서 IPI를 보낼 수 있다. BRS 규격은 바로 이 관계를 규칙으로 적어 두었다.
| BRS 규칙 | 내용 |
|---|---|
SBI_010 | SBI 구현은 SBI v2.0 이상을 따라야 한다. |
SBI_020 | HSM 확장은 반드시 구현한다. |
SBI_030 | Sstc가 없으면 TIME 확장을 구현해야 한다. |
SBI_040 · SBI_050 | IMSIC이 없으면 IPI(sPI)와 RFENCE(RFNC) 확장을 구현해야 한다. |
SBI_060 | 카운터 위임 확장(Sscsrind·Ssccfg)이 없으면 PMU 확장을 구현해야 한다. |
SBI_070 | ACPI SPCR 표가 인터페이스 유형 0x15를 가리키면 DBCN 확장을 구현해야 한다. |
Sstc(타이머), AIA IMSIC(IPI·외부 인터럽트), Svinval·Sscofpmf 같은 확장이 하나씩 비준되면서 “자주 일어나는 일은 S 모드가 하드웨어로 직접, 드물고 플랫폼마다 다른 일은 SBI로”라는 분업이 굳어지고 있다. RVA23 프로파일이 Sstc를 필수로 넣은 것도 같은 이유다(23장).
인터럽트 구조의 진화: CLINT에서 AIA까지
특권 규격은 하트 안의 인터럽트 비트(mip·mie의 소프트웨어·타이머·외부 인터럽트)만 정하고, 그 비트를 바깥에서 누가 올리는지는 정하지 않았다. 그 바깥 장치가 세 세대에 걸쳐 바뀌어 왔다.
- CLINT / ACLINT — 하트마다 타이머(
mtime·mtimecmp)와 소프트웨어 인터럽트 비트(msip)를 메모리 주소에 둔 단순한 장치. CLINT는 SiFive 구현에서 시작한 사실상의 표준이고, 이를 정리한 ACLINT는 공개 규격이지만 공식 비준 목록에는 없다. - PLIC — 장치의 인터럽트 선을 모아 우선순위를 매기고 하트의 외부 인터럽트 선(EIP)으로 전달한다. 원래 특권 규격 초안에 있다가 분리되어 PLIC v1.0.0으로 비준되었다.
- AIA — 인터럽트를 선 대신 메모리 쓰기(MSI)로 전달하는 IMSIC과, 선 인터럽트를 받아 MSI로 바꿔 주는 APLIC을 정의한다. 가상화(게스트 인터럽트 파일)와 수천 개의 인터럽트를 염두에 둔 설계다.
| 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이다.
하트 쪽에서는 새 CSR로 인터럽트를 받는다. stopei(0x15C)를 읽으면 비트 26:16에 가장 우선인(번호가 가장 작은) 대기 식별자가 나오고, csrrw a0, stopei, zero처럼 읽으면서 쓰면 그 대기 비트가 지워진다. PLIC의 claim처럼 버스를 건너는 MMIO 읽기가 필요 없다. M 수준은 mtopei(0x35C), 게스트는 vstopei를 쓴다.
PLIC과 AIA의 인터럽트 전달 비교
| CLINT · ACLINT | PLIC | AIA (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)도 함께 낸다.
| 1단계 | 2단계 | 장치가 내는 주소(IOVA)의 뜻 | 쓰임 |
|---|---|---|---|
| Bare | Bare | 그대로 물리 주소(SPA) | 변환·보호 없음 |
| Bare | 켜짐 | 게스트 물리 주소(GPA) | 장치를 가상 머신에 직접 넘김 |
| 켜짐 | Bare | 가상 주소(VA) | 호스트 OS의 DMA 격리, 공유 가상 주소 |
| 켜짐 | 켜짐 | 게스트 가상 주소(VA) | 게스트 안에서도 DMA 격리 |
장치의 MSI 역시 메모리 쓰기이므로 IOMMU를 통과한다. IOMMU 규격은 장치 문맥마다 MSI 전용 페이지 테이블을 두어, 게스트에 넘긴 장치의 MSI를 그 게스트의 IMSIC 게스트 인터럽트 파일로 바로 보낼 수 있게 한다. 21장의 하이퍼바이저가 장치 인터럽트를 흉내 내지 않아도 되는 이유다.
디버그·트레이스·세미호스팅: 칩을 처음 깨울 때
새 칩에 처음 전원을 넣었을 때는 콘솔도 운영체제도 없다. 이때 쓰는 것이 외부 디버거다. RISC-V Debug 규격은 JTAG 같은 전송 수단으로 들어온 디버거가 하트를 멈추고, 레지스터와 메모리를 읽고 쓰고, 다시 실행시키는 방법을 정한다.
“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.”
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–0x0f | data0–data11 | 추상 명령의 인자·결과 (64비트 값은 data0=하위, data1=상위) |
0x10 | dmcontrol | haltreq(31), resumereq(30), hartsel, ndmreset(1), dmactive(0) |
0x11 | dmstatus | allhalted(9), anyhalted(8), allrunning(11), allresumeack(17), version(3:0)=3이면 1.0 |
0x16 | abstractcs | progbufsize(28:24), busy(12), cmderr(10:8, 1을 써서 지움), datacount(3:0) |
0x17 | command | 쓰는 순간 추상 명령 실행 |
0x20– | progbuf0… | 프로그램 버퍼: 하트가 Debug 모드에서 실행할 짧은 명령들 |
0x0000–0x0FFF가 CSR, 0x1000–0x101F가 정수 레지스터, 0x1020–0x103F가 부동소수점 레지스터다. 멈춘 하트의 정수 레지스터 읽기·쓰기는 모든 DM이 반드시 지원해야 한다.Debug Module 조작해 보기
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로 돌아온다.
플랫폼 규격: 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.0 | RISCV_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 확장을 거쳐 이 메시지를 주고받을 수 있다. |
초기 RISC-V 보드는 장치 트리와 보드별 부트로더로 충분했다. 하지만 서버 시장에서는 “배포판 설치 이미지 하나로 모든 서버에 설치”가 기본 요구다. BRS와 Server Platform 규격은 그 요구를 RVA23 프로파일(23장), UEFI, ACPI, 그리고 이 장의 SBI·AIA·IOMMU를 묶어 하나의 체크리스트로 만든 것이다.
핵심 정리
- 하트는 리셋되면 M 모드로 시작하고, ZSBL → FSBL → 런타임 펌웨어(OpenSBI) → 부트로더(S) → 커널(S) → init(U) 순으로 바통을 넘긴다. 런타임 펌웨어만 M 모드에 상주한다.
- SBI는 S 모드가 ecall로 상위 실행 환경에 부탁하는 규약이다.
a7=EID,a6=FID,a0–a5=인자, 돌아오면a0=오류,a1=값. EID는 대개 이름의 ASCII 코드다(TIME =0x54494D45). - HSM 확장의
sbi_hart_start(hartid, start_addr, opaque)로 보조 하트를 깨운다. 깨어난 하트는 MMU가 꺼진 S 모드에서 a0=hartid, a1=opaque로 시작한다. - 타이머·IPI·원격 펜스는 SBI로 펌웨어를 거칠 수도, Sstc(
stimecmp)와 AIA IMSIC으로 하드웨어에서 직접 처리할 수도 있다. 추세는 후자다. - 인터럽트 구조는 CLINT/ACLINT(타이머·소프트웨어) → PLIC(선, claim/complete MMIO) → AIA(APLIC + IMSIC, MSI 쓰기와
*topeiCSR)로 발전했다. - IOMMU는 CPU와 같은 페이지 테이블 형식으로 장치 DMA를 2단계 변환해 격리하며, MSI도 게스트 인터럽트 파일로 보낼 수 있다.
- Debug 규격은 DTM → DMI → Debug Module 구조로 하트를 멈추고 추상 명령으로 레지스터에 접근한다. BRS·UEFI·ACPI·Server Platform 규격이 이 모든 부품을 소프트웨어 호환성 약속으로 묶는다.
확인 퀴즈
SBI 호출에서 확장 번호(EID)를 담는 레지스터는?
EID 0x48534D를 ASCII로 읽으면 어떤 확장인가?
0x48 'H', 0x53 'S', 0x4D 'M'. 하트를 시작·정지·일시 중지하는 HSM 확장이다.이미 STARTED 상태인 하트에 sbi_hart_start를 다시 부르면 돌아오는 오류 코드는?
Sstc 확장이 있을 때 타이머 이벤트 하나를 처리하는 데 필요한 M 모드 진입 횟수는?
PLIC에서 처리기가 “어느 장치의 인터럽트인가”를 알아내는 방법은?
AIA에서 장치가 하트 1의 S 모드에 인터럽트 식별자 42를 보내려면?
0x000 seteipnum_le에 식별자를 쓰면 그 대기 비트가 켜진다.Debug Module의 Access Register 명령으로 정수 레지스터 x10(a0)을 지정하는 regno 값은?
0x1000–0x101F가 정수 레지스터이므로 x10은 0x100a다. 0x0000–0x0FFF는 CSR(0x7b1은 dpc), 0x1020부터는 부동소수점 레지스터다.UEFI로 부팅하는 RISC-V 시스템에서 OS 로더가 부트 하트 번호를 얻는 표준 방법은?
더 읽을거리
- RISC-V Supervisor Binary Interface Specification v3.0 — 호출 규약, 오류 코드, 모든 확장
- The RISC-V Advanced Interrupt Architecture v1.0 — IMSIC, APLIC, 인터럽트 CSR
- RISC-V Platform-Level Interrupt Controller Specification v1.0.0
- RISC-V IOMMU Architecture Specification
- The RISC-V Debug Specification v1.0 — Debug Module, DTM, Sdext, Sdtrig
- Efficient Trace for RISC-V (E-Trace) · N-Trace · Semihosting
- RISC-V Boot and Runtime Services (BRS) v1.0 · RISC-V UEFI Protocol
- RISC-V Server Platform · Server SoC · RPMI · ACPI RIMT
- Volume II: Privileged Architecture — CSR 주소 목록 (stimecmp, dcsr, dpc, 트리거 CSR)
- OpenSBI 프로젝트 문서 (github.com/riscv-software-src/opensbi) · 리눅스 커널 문서 RISC-V Kernel Boot Requirements and Constraints