RISC-V 명령어셋 레퍼런스 (ISA)

RISC-V 아키텍처의 명령어를 GAS 문법 기준으로 종합 정리합니다. 모듈형 ISA 설계 철학과 RV64I 기본 명령어셋, M(곱셈)/A(원자적(Atomic))/F(단정도)/D(배정도)/C(압축)/V(벡터) 표준 확장, 32개 범용 레지스터(Register)와 CSR 체계, R/I/S/B/U/J 6가지 인코딩 타입, 데이터 전송·산술·논리·분기·스택·시스템·원자적·벡터 명령어 카테고리 표, 인코딩 다이어그램, 커널 핵심 명령어(ECALL, SRET, LR/SC, SFENCE.VMA) 고급까지 Linux 커널 개발에 필요한 RISC-V ISA 전체를 다룹니다.

전제 조건: 어셈블리(Assembly) 종합을 먼저 읽으세요. RISC-V 명령어 레퍼런스는 RISC Load/Store 모델, 레지스터-레지스터 연산, 특권 모드에 대한 기본 이해가 필요합니다.
일상 비유: RISC-V는 오픈소스 레고 블록 시스템과 같습니다. 기본 블록 세트(RV64I)에 필요한 확장 팩(M, A, F, D, V)을 자유롭게 추가할 수 있으며, 설계도(ISA 스펙) 자체가 공개되어 누구나 구현할 수 있습니다.

핵심 요약

  • 모듈형 ISA — RV64I 기본 + M/A/F/D/C/V 확장. RV64G = RV64IMAFD (범용 세트).
  • 32개 범용 레지스터 — x0=zero(항상 0), x1=ra, x2=sp, x10-x17=a0-a7(인자).
  • 고정 32-bit 명령어 — C 확장 사용 시 16-bit 압축 명령어도 혼용 가능.
  • CSR 체계 — mstatus/sstatus, mtvec/stvec, mepc/sepc 등 특권 레벨별 제어/상태 레지스터.
  • 특권 모드 — Machine(M), Supervisor(S), User(U). Linux는 S-mode에서 실행.

단계별 이해

  1. 레지스터와 ABI 이름 파악
    x0-x31의 ABI 이름(zero, ra, sp, a0-a7, s0-s11, t0-t6)과 용도를 먼저 익힙니다.
  2. 기본 ISA(RV64I) 학습
    Load/Store, 산술, 논리, 분기, 점프 명령어가 기본입니다.
  3. 확장 모듈 이해
    M(곱셈/나눗셈), A(원자적), F/D(부동소수점), C(압축), V(벡터)를 순서대로 학습합니다.
  4. CSR과 특권 명령어
    ECALL, CSRRW, SFENCE.VMA 등 커널 관련 명령어를 학습합니다.

아키텍처 개요

RISC-V는 UC Berkeley에서 2010년 시작한 오픈소스 RISC ISA입니다. 모듈형 설계로 기본 정수 ISA(RV32I/RV64I)에 표준 확장을 선택적으로 추가합니다.

특성RISC-V
설계 철학모듈형 오픈소스 RISC
명령어 길이고정 32-bit (C 확장 시 16/32-bit 혼합)
엔디언리틀 엔디언
범용 레지스터32개 (x0=hardwired zero)
기본 ISARV32I / RV64I (RV32E는 임베디드 변형, RV128I는 현재 표준에 미포함)
표준 확장M(곱셈), A(원자적), F(단정도FP), D(배정도FP), C(압축), V(벡터), Zicsr, Zifencei
RV64G= RV64IMAFD (General-purpose 세트)
특권 모드Machine(M), Supervisor(S), User(U)
주소 공간(Address Space)Sv39(39-bit), Sv48(48-bit), Sv57(57-bit)

모듈형 ISA 확장 구조

RISC-V의 핵심 설계 철학은 모듈형(modular) ISA입니다. RV64I 기본 정수 명령어셋을 중심으로 필요한 확장을 선택적으로 추가하여 SoC 설계자가 목적에 맞는 최적의 프로세서를 구성할 수 있습니다. 표준 확장 조합인 RV64G(= RV64IMAFD)는 범용 운영체제 실행에 필요한 최소 세트이며, Linux 커널은 이를 기본으로 요구합니다.

RISC-V 모듈형 ISA 확장 구조 RV64G 구성 확장 (Linux 커널 기본) M 확장 곱셈/나눗셈 (MUL, DIV) A 확장 원자적 (LR/SC, AMO) F 확장 단정도 FP (32-bit) D 확장 배정도 FP (64-bit) 추가 표준 확장 C 확장 압축 16-bit 명령어 V 확장 가변 길이 벡터 (VLEN-bit) Zicsr + Zifencei CSR 접근 + I-fence RV64I — 기본 정수 명령어셋 RV32I 40개 + RV64 전용 명령어 Load/Store, ALU, Branch, Jump RV64G = RV64I + M + A + F + D (Linux 커널 기본 요구 사항)
RISC-V 모듈형 ISA 확장 구조 — RV64I 베이스에 표준 확장을 선택적으로 조합하는 개념 개요도입니다. RV64G(= RV64IMAFD)는 Linux 커널이 기본으로 요구하는 최소 세트입니다.
특권 모드 계층 구조: RISC-V는 3단계 특권 모드를 정의합니다.
  • Machine (M-mode) — 최고 권한. 하드웨어에 직접 접근. 모든 CSR 사용 가능. 펌웨어(Firmware)/SBI가 실행됨.
  • Supervisor (S-mode) — OS 커널 실행 모드. 가상 메모리(Virtual Memory), 인터럽트(Interrupt) 관리. Linux 커널이 이 모드에서 동작.
  • User (U-mode) — 최소 권한. 사용자 프로세스(Process) 실행. 특권 명령어 사용 시 트랩 발생.
모드 전환은 ECALL(상위 모드로 트랩), MRET(M→이전 모드), SRET(S→이전 모드)로 이루어집니다. 트랩 위임(delegation) 레지스터 medeleg/mideleg를 통해 M-mode가 특정 예외/인터럽트를 S-mode에 위임할 수 있습니다.
RISC-V 특권 모드 전환 (Trap / Return) Machine (M) SBI / Firmware / 최고 권한 Supervisor (S) Linux Kernel / OS User (U) 사용자 프로세스 ECALL (SBI 호출) MRET (트랩 복귀) ECALL (시스템 콜) SRET (트랩 복귀) medeleg/mideleg 트랩 위임 (M → S)
RISC-V 특권 모드 전환 — ECALL(상위 모드로 트랩)과 MRET/SRET(하위 모드로 복귀), medeleg/mideleg 트랩 위임을 나타낸 개념 개요도입니다.

레지스터 셋

범용 레지스터 (x0-x31)

레지스터ABI 이름용도보존
x0zero하드와이어 제로 (항상 0)—
x1ra복귀 주소 (Return Address)Caller
x2sp스택 포인터Callee
x3gp전역 포인터—
x4tp스레드(Thread) 포인터—
x5-x7t0-t2임시 레지스터Caller
x8s0/fpSaved/프레임 포인터Callee
x9s1Saved 레지스터Callee
x10-x17a0-a7함수 인자 / 반환값 (a0-a1)Caller
x18-x27s2-s11Saved 레지스터Callee
x28-x31t3-t6임시 레지스터Caller

레지스터 파일 시각적 맵

32개 범용 레지스터를 용도별로 그룹화하고 Caller/Callee-saved 여부를 색상으로 구분한 시각적 맵입니다. 함수 호출 시 어떤 레지스터를 저장해야 하는지 파악하는 데 유용합니다.

RISC-V 범용 레지스터 맵 (x0-x31) — 용도별 그룹 & Caller/Callee 구분 고정 zero 시스템 예약 gp·tp Caller-saved (호출자 저장) Callee-saved (피호출자 저장) 특수 (zero·gp·tp) 고정 1 · 시스템 예약 2 zero x0 gp x3 tp x4 제어 (ra·sp) ra=Caller · sp=Callee ra x1 sp x2 인자·반환 (a0-a7) 8개 · 모두 Caller-saved a0 x10 a1 x11 a2 x12 a3 x13 a4 x14 a5 x15 a6 x16 a7 x17 임시 (t0-t6) 7개 · 모두 Caller-saved t0 x5 t1 x6 t2 x7 t3 x28 t4 x29 t5 x30 t6 x31 저장 (s0-s11) 12개 · 모두 Callee-saved s0/fp x8 s1 x9 s2 x18 s3 x19 s4 x20 s5 x21 s6 x22 s7 x23 s8 x24 s9 x25 s10 x26 s11 x27 총 32개 — 고정(zero) 1 · 시스템 예약(gp·tp) 2 · Caller-saved(ra, t0-t6, a0-a7) 16 · Callee-saved(sp, s0-s11) 13 Linux 커널에서의 특수 용도 tp (x4): 커널 모드에서 current task_struct를 가리킴 (per-CPU 데이터 접근) gp (x3): 전역 포인터 — 커널 이미지의 .sdata/.sbss 접근 최적화 sscratch: 트랩 진입 시 tp와 교환하여 커널/유저 모드 전환에 사용
RISC-V 레지스터 파일 시각적 맵 — 32개 범용 레지스터(x0–x31)를 용도별 다섯 그룹으로 묶고 Caller-saved/Callee-saved를 색으로 구분한 개념 개요도입니다.

CSR (Control and Status Registers) — 주요

CSR설명모드
mstatus / sstatus머신/슈퍼바이저 상태 (인터럽트 활성화, 이전 모드 등)M / S
mtvec / stvec트랩 벡터 베이스 주소M / S
mepc / sepc예외 PC (복귀 주소)M / S
mcause / scause트랩 원인 (인터럽트/예외 코드)M / S
mtval / stval트랩 값 (폴트 주소 등)M / S
mie / sie인터럽트 활성화M / S
mip / sip인터럽트 펜딩M / S
satpS-mode 주소 변환(Address Translation) (페이지 테이블(Page Table) 루트 + 모드)S
mscratch / sscratch트랩 핸들러(Handler) 스크래치 레지스터M / S
mhartid하드웨어 스레드 IDM
cycle / time / instret사이클/시간/명령어 카운터 (읽기 전용(Read-Only))U

CSR 주소 공간 레이아웃

RISC-V CSR 주소는 12-bit (0x000-0xFFF)로 인코딩됩니다. 상위 비트들이 접근 권한과 특권 레벨을 결정하므로, CSR 번호만으로도 접근 가능 여부를 하드웨어가 즉시 판단할 수 있습니다.

비트 [11:10]비트 [9:8]의미주소 범위예시
00 (R/W)00 (U)User 읽기/쓰기0x000-0x0FFfflags, frm, fcsr, vstart
00 (R/W)01 (S)Supervisor 읽기/쓰기0x100-0x1FFsstatus, sie, stvec, satp
00 (R/W)10 (H)Hypervisor 읽기/쓰기0x200-0x2FFhstatus, hgatp, hvip, htinst
00 (R/W)11 (M)Machine 읽기/쓰기0x300-0x3FFmstatus, mie, mtvec, medeleg
01 (R/W)00 (U)User 표준 읽기/쓰기0x400-0x4FF(표준 할당 대기)
01 (R/W)01 (S)Supervisor 표준 + 커스텀 읽기/쓰기0x500-0x5FF0x5C0-0x5FF (커스텀)
01 (R/W)10 (H)Hypervisor 표준 + 커스텀 읽기/쓰기0x600-0x6FF0x6C0-0x6FF (커스텀)
01 (R/W)11 (M)Machine 표준/디버그 + 커스텀 읽기/쓰기0x700-0x7FFdcsr, dpc, dscratch / 0x7C0-0x7FF (커스텀)
10 (R/W)00 (U)User 커스텀 읽기/쓰기0x800-0x8FF(벤더 커스텀)
10 (R/W)01 (S)Supervisor 표준 + 커스텀 읽기/쓰기0x900-0x9FF0x9C0-0x9FF (커스텀)
10 (R/W)10 (H)Hypervisor 표준 + 커스텀 읽기/쓰기0xA00-0xAFF0xAC0-0xAFF (커스텀)
10 (R/W)11 (M)Machine 표준 + 커스텀 읽기/쓰기0xB00-0xBFFmcycle, minstret, mhpmcounterN / 0xBC0-0xBFF (커스텀)
11 (RO)00 (U)User 읽기 전용0xC00-0xCFFcycle, time, instret, hpmcounterN
11 (RO)01 (S)Supervisor 읽기 전용0xD00-0xDFF(예약)
11 (RO)10 (H)Hypervisor 읽기 전용0xE00-0xEFF(예약)
11 (RO)11 (M)Machine 읽기 전용0xF00-0xFFFmvendorid, marchid, mhartid

CSR 인코딩 규칙: 12-bit CSR 번호의 상위 4비트(csr[11:8])가 접근 권한과 최소 특권 레벨을 결정합니다. csr[11:10]는 읽기/쓰기 가능 여부를 나타내며 00/01/10은 읽기/쓰기 가능, 11은 읽기 전용입니다. csr[9:8]는 해당 CSR에 접근하기 위한 최소 특권 레벨을 인코딩합니다: 00=User(비특권), 01=Supervisor, 10=Hypervisor, 11=Machine. 상위 권한 모드는 항상 하위 레벨 CSR에도 접근할 수 있습니다. 같은 256바이트 블록 안에서 표준 할당과 커스텀 할당은 하위 니블(csr[7:4])로 구분합니다: csr[7:4]가 0XXX/10XX이면 표준, 11XX이면 커스텀입니다. 따라서 0xB00-0xBBF는 표준 카운터(mcycle, minstret, mhpmcounterN 및 그 상위 하프)이고 0xBC0-0xBFF만 커스텀입니다. 예를 들어 cycle(0xC00)은 User 읽기 전용이므로 커널 디버깅(Debugging)/프로파일링(Profiling)에서 유저 공간이 직접 성능 카운터를 읽을 수 있습니다. H 확장(하이퍼바이저) CSRs는 0x200(표준)과 0x600(0x600-0x6BF 표준, 0x6C0-0x6FF 커스텀) 범위에 위치하며 hgatp(게스트 주소 변환), hstatus, hvip 등이 0x600대에 속합니다.

CSR 12-bit 주소 인코딩 11 10 9 8 7 6 5 4 3 2 1 0 csr[11:10] 접근 권한 csr[9:8] 최소 특권 레벨 csr[7:0] 레지스터 식별자 (256개 슬롯) 비트 필드 이름 인코딩 / 의미 csr[11:10] 접근 권한 00·01·10 = 읽기/쓰기(RW), 11 = 읽기 전용(RO) csr[9:8] 최소 특권 레벨 00=U, 01=S, 10=H(하이퍼바이저), 11=M csr[7:0] 레지스터 식별자 256개 슬롯 · csr[7:4] 0xxx/10xx=표준, 11xx=커스텀 적용 예시 — 12-bit 주소를 세 필드로 분해 sstatus = 0x100 00 01 00000000 읽기/쓰기 S-mode 레지스터 #0 mhartid = 0xF14 11 11 00010100 읽기 전용 M-mode 레지스터 #20
CSR 12-bit 주소 인코딩 구조 — 상위 4비트(csr[11:8])의 접근 권한·최소 특권 레벨 필드와 하위 8비트(csr[7:0])의 레지스터 식별자 필드, 그리고 csr[7:4]에 따른 표준/커스텀 구분을 나타낸 개념 개요도입니다.

부동소수점 / 벡터 레지스터

확장레지스터크기
F/D 확장f0-f3132/64-bit 부동소수점
F/D 제어fcsr (frm + fflags)라운딩 모드 + 예외 플래그
V 확장v0-v31VLEN-bit (구현 의존, 128+)
V 제어vl, vtype, vstart, vxsat, vxrm벡터 길이/타입/시작/포화/라운딩

부동소수점과 벡터 레지스터는 정수 레지스터와 별도의 물리 레지스터 파일을 사용합니다. 따라서 정수 이름 공간(x0-x31)과 겹치지 않으며, 커널은 이 상태를 태스크별로 격리(Isolation)하여 보존합니다.

f0-f31은 호출 규약(Calling Convention)상 정수 레지스터와 마찬가지로 caller-saved/saved 그룹으로 나뉩니다. 인자 전달과 반환값은 fa0-fa7, 호출을 넘겨 유지하는 값은 fs0-fs11이 담당합니다.

그룹레지스터용도보존 여부
인자/반환fa0-fa7FP 인자 전달과 반환값Caller-saved
임시ft0-ft11중간 계산값Caller-saved
보존fs0-fs11호출을 넘겨 유지하는 값Callee-saved

NaN-boxing: F 확장만 구현한 프로세서에서도 f 레지스터의 물리 폭은 64-bit이며, 단정도 값은 상위 32-bit을 모두 1로 채워 저장합니다. D 확장이 있으면 배정도 값을 그대로 담습니다. 이 규칙 덕분에 하드웨어는 레지스터 폭을 통일한 채로 단정도/배정도 값을 구분할 수 있습니다.

fcsr 비트 구성: 비트 [7:5]는 라운딩 모드(frm: RNE/RTZ/RDN/RUP/RMM), 비트 [4:0]은 예외 플래그(fflags: NV/DZ/OF/UF/NX)입니다. 예외 플래그는 누적형(sticky)이므로 한 번 설정되면 소프트웨어가 지우기 전까지 유지됩니다.

커널의 FPU/Vector 상태 관리: 커널은 sstatus.FS 필드로 FPU 상태를 Off/Initial/Clean/Dirty로 추적하고, DIRTY일 때만 실제 레지스터를 저장합니다(fstate_save()). 벡터도 sstatus.VS 필드로 같은 방식으로 관리하며, 태스크 생성과 트랩 처리 과정에서 riscv_v_vstate_discard()로 벡터 레지스터를 무효화하여 이전 태스크의 값이 남지 않게 합니다. 이 방식은 FPU/벡터를 쓰지 않는 태스크의 컨텍스트 스위치 비용을 없애 줍니다.

주소 지정 모드

RISC-V는 매우 단순한 주소 모드를 사용합니다: 베이스 레지스터 + 12-bit 부호 확장 즉시값 오프셋(Offset)만 지원합니다.

패턴문법설명예제
베이스+오프셋offset(rs1)[rs1 + sign_ext(offset)]lw a0, 8(sp)
32-bit 절대lui + addi상위 20 + 하위 12 조합lui a0, %hi(sym); addi a0, a0, %lo(sym)
PC 상대auipc + addiPC + 상위 20 + 하위 12auipc a0, %pcrel_hi(sym); addi a0, a0, %pcrel_lo(.)
의사 명령어 lili rd, imm어셈블러가 lui+addi로 확장li a0, 0x12345678
의사 명령어 lala rd, symbolauipc+addi로 확장la a0, my_var
설계 철학 — 왜 주소 모드가 이렇게 단순한가? RISC-V는 의도적으로 base+offset 단일 주소 모드만 제공합니다. 이는 하드웨어 설계를 단순화하여 파이프라인(Pipeline) 스톨을 줄이고, 면적과 전력을 절약하기 위함입니다. 복잡한 주소 계산(인덱스 시프트, pre/post-increment 등)은 컴파일러가 별도 명령어로 분해하여 처리하므로, 결과적으로 하드웨어 복잡도는 낮추면서 컴파일러 최적화(Compiler Optimization)의 자유도를 높이는 설계입니다.
주소 모드 비교: RISC-V vs x86_64 vs ARM64
주소 모드RISC-Vx86_64ARM64
기본 (레지스터+오프셋)lw a0, 8(a1)mov eax, [rbx+8]ldr w0, [x1, #8]
레지스터+레지스터지원 안 함mov eax, [rbx+rcx]ldr w0, [x1, x2]
스케일드 인덱스지원 안 함mov eax, [rbx+rcx*4]ldr w0, [x1, x2, lsl #2]
Pre-increment지원 안 함지원 안 함ldr w0, [x1, #8]!
Post-increment지원 안 함지원 안 함ldr w0, [x1], #8
PC-상대auipc+addi (2개 명령어)mov eax, [rip+off]adr/adrp + ldr
절대 주소lui+addi (2개 명령어)mov eax, [addr]지원 안 함 (ADRP+ADD)
RISC-V는 주소 모드 수가 가장 적지만, 이로 인해 디코더 복잡도가 낮아 고주파 동작에 유리합니다.

데이터 전송 명령어

명령어문법설명확장
LB / LBUlb a0, 0(a1)바이트 로드 (부호/제로 확장)I
LH / LHUlh a0, 0(a1)하프워드 로드 (부호/제로 확장)I
LW / LWUlw a0, 0(a1)워드 로드 (RV64: LWU=제로확장)I
LDld a0, 0(a1)더블워드 로드RV64I
SB / SH / SW / SDsw a0, 0(a1)바이트/하프/워드/더블 저장I
LUIlui a0, 0x12345상위 20-bit 즉시값 로드I
AUIPCauipc a0, 0x12345PC + 상위 20-bitI
FLW / FLDflw fa0, 0(a1)부동소수점 로드F / D
FSW / FSDfsw fa0, 0(a1)부동소수점 저장F / D
C.LW / C.LDc.lw a0, 0(a1)압축 로드 (16-bit)C
C.SW / C.SDc.sw a0, 0(a1)압축 저장 (16-bit)C

로드의 부호/제로 확장: RV64에서 메모리에서 읽은 바이트·하프워드·워드는 레지스터의 64-bit 폭에 맞게 확장되어야 합니다. LB/LH/LW는 부호 확장, LBU/LHU/LWU는 제로 확장을 수행하며, 컴파일러가 정수 타입의 부호 유무에 따라 적절한 변형을 선택합니다. 반면 저장(SB/SH/SW/SD)은 대상 폭만 지정하고 부호 개념이 없습니다.

폭부호 확장제로 확장RV64 확장 결과
8-bitLBLBU비트 [63:8]을 부호 또는 0으로 채움
16-bitLHLHU비트 [63:16]을 부호 또는 0으로 채움
32-bitLWLWU비트 [63:32]을 부호 또는 0으로 채움
64-bitLD—확장 없음

로드/저장 예제

구조체(struct) 필드 접근은 로드 폭과 확장 규칙이 그대로 드러나는 대표적인 예다. 다음 C 코드는 필드 오프셋에 해당하는 로드 명령으로 전개됩니다.

struct packet {
    unsigned char  flag;   /* offset 0 */
    unsigned int   id;     /* offset 4 */
    unsigned long  addr;   /* offset 8 */
};

unsigned int packet_id(const struct packet *p) {
    return p->id;             /* unsigned → 제로 확장 */
}

signed char packet_flag(const struct packet *p) {
    return p->flag;           /* signed char → 부호 확장 */
}
/* packet_id: a0=p → a0 (unsigned int) */
packet_id:
    lwu     a0, 4(a0)              /* 32-bit 로드 후 제로 확장 */
    ret

/* packet_flag: a0=p → a0 (signed char) */
packet_flag:
    lb      a0, 0(a0)              /* 8-bit 로드 후 부호 확장 */
    ret

정렬(Alignment)과 misaligned 접근: RISC-V는 자연 정렬 접근을 권장하며, 정렬되지 않은 접근은 구현에 따라 트랩될 수 있습니다. Linux 커널은 CONFIG_RISCV_MISALIGNED로 misaligned 로드/저장을 소프트웨어로 에뮬레이션하고(handle_misaligned_load()/handle_misaligned_store()), SBI FWFT(Firmware Features)를 지원하는 펌웨어에서는 위임된 load/store address misaligned 예외를 커널이 직접 처리합니다. 커널 코드는 로드/저장 폭을 필요한 만큼만 쓰고 정렬을 지키는 형태를 선호합니다.

FENCE 명령어 변형과 메모리 순서:
  • FENCE iorw, iorw — 전체 메모리 배리어(Memory Barrier). predecessor 집합(i=입력, o=출력, r=읽기, w=쓰기)의 모든 연산이 successor 집합의 연산보다 먼저 관측됨을 보장합니다. 예: fence rw, rw는 이전 모든 읽기/쓰기가 이후 읽기/쓰기보다 먼저 완료됨을 보장.
  • FENCE.I — 명령어 페치 배리어 (Zifencei 확장). 이전 저장(store)이 이후 명령어 페치에 반영됨을 보장합니다. 자기 수정 코드(JIT, 모듈 로딩) 시 필수. Linux 커널의 flush_icache_range()에서 사용.
  • FENCE.TSO — TSO(Total Store Order) 배리어. FENCE rw, rw보다 약한 순서로, predecessor의 load를 successor의 모든 메모리 연산보다, predecessor의 store를 successor의 store보다 먼저 정렬합니다. 다만 store→load 순서는 보장하지 않으므로 store→load 재배치(Reordering)는 허용됩니다. x86 TSO 메모리 모델에 가깝게 동작합니다.

LUI + ADDI로 32-bit 상수 로딩

RISC-V 명령어의 즉시값은 최대 12-bit이므로, 더 큰 상수를 레지스터에 로드하려면 LUI(상위 20-bit)와 ADDI(하위 12-bit)를 조합해야 합니다. 여기서 주의할 점은 ADDI가 부호 확장을 수행하므로, 하위 12-bit의 MSB가 1이면 LUI에 1을 더해 보정해야 하는 것입니다.

/* 예: a0 = 0x12345678 로드 */
/* 상위 20-bit: 0x12345, 하위 12-bit: 0x678 */
/* 0x678의 MSB=0이므로 보정 불필요 */
    lui     a0, 0x12345           /* a0 = 0x12345000 */
    addi    a0, a0, 0x678         /* a0 = 0x12345678 */

/* 예: a0 = 0x12345800 로드 */
/* 하위 12-bit: 0x800, MSB=1 → ADDI가 -2048로 부호 확장 */
/* 보정: LUI에 1을 더함 (0x12345 + 1 = 0x12346) */
    lui     a0, 0x12346           /* a0 = 0x12346000 */
    addi    a0, a0, -0x800        /* a0 = 0x12346000 - 0x800 = 0x12345800 */

/* 어셈블러 의사 명령어 li가 이를 자동 처리: */
    li      a0, 0x12345800        /* 어셈블러가 위 보정을 자동 수행 */

산술 명령어

명령어문법설명확장
ADDadd a0, a1, a2덧셈I
ADDIaddi a0, a1, 42즉시값 덧셈I
SUBsub a0, a1, a2뺄셈I
ADDW / ADDIW / SUBWaddw a0, a1, a232-bit 연산 + 부호 확장 (RV64)RV64I
MULmul a0, a1, a2곱셈 (하위 XLEN bit)M
MULH / MULHU / MULHSUmulh a0, a1, a2곱셈 상위 (부호/부호없는/혼합)M
DIV / DIVUdiv a0, a1, a2나눗셈 (부호/부호없는)M
REM / REMUrem a0, a1, a2나머지 (부호/부호없는)M
MULW / DIVW / REMWmulw a0, a1, a232-bit 곱셈/나눗셈/나머지 (RV64M)RV64M
FADD.S/Dfadd.s fa0, fa1, fa2부동소수점 덧셈F/D
FMUL.S/Dfmul.d fa0, fa1, fa2부동소수점 곱셈F/D
FDIV.S/Dfdiv.s fa0, fa1, fa2부동소수점 나눗셈F/D
FSQRT.S/Dfsqrt.d fa0, fa1부동소수점 제곱근F/D
FMADD.S/Dfmadd.s fa0, fa1, fa2, fa3FMA: fa1*fa2 + fa3F/D

산술 명령어 C↔어셈블리 대응 예제

RISC-V 산술 명령어의 핵심 특징은 세 피연산자(Operand) 형식과 MOD 전용 명령어 존재입니다. ARM64와 달리 REM 명령어가 있어 나머지 계산이 1 명령어로 가능합니다. 또한 RV64에서 ADDW/SUBW는 32-bit 연산 후 자동으로 64-bit 부호 확장하여 하위 호환성을 유지합니다.

/* 예제 1: 기본 사칙연산 — RV64 W-suffix 명령어 */
int add32(int a, int b) {    /* 32-bit 정수 */
    return a + b;
}

long add64(long a, long b) { /* 64-bit 정수 */
    return a + b;
}

long div_rem(long a, long b, long *rem) {
    *rem = a % b;
    return a / b;
}
/* add32: a0=a, a1=b → a0 (32-bit 덧셈, 부호 확장) */
add32:
    addw    a0, a0, a1            /* 32-bit 덧셈 + 64-bit 부호 확장 */
    ret                           /* (= ra 점프) */

/* add64: a0=a, a1=b → a0 (64-bit 덧셈) */
add64:
    add     a0, a0, a1            /* 64-bit 덧셈 */
    ret

/* div_rem: a0=a, a1=b, a2=rem* → a0=몫, *a2=나머지 */
div_rem:
    rem     t0, a0, a1            /* t0 = a % b */
    div     a0, a0, a1            /* a0 = a / b (몫) */
    sd      t0, 0(a2)             /* *rem = 나머지 저장 */
    ret
/* 예제 2: 128-bit 곱셈 (암호화 연산에서 활용) */
typedef unsigned long u64;
typedef unsigned __int128 u128;

u128 mul128(u64 a, u64 b) {
    return (u128)a * b;
}
/* mul128: a0=a, a1=b → a1:a0 (a1=상위, a0=하위) */
mul128:
    mulhu   a2, a0, a1            /* a2 = 상위 64-bit (unsigned high) */
    mul     a0, a0, a1            /* a0 = 하위 64-bit */
    mv      a1, a2                 /* a1 = 상위 64-bit (반환 규약) */
    ret
/* 예제 3: FMA (Fused Multiply-Add) — D 확장 (배정도) */
double fma_op(double a, double b, double c) {
    return a * b + c;   /* IEEE 754 FMA: 중간 반올림 없음 */
}
/* fma_op: fa0=a, fa1=b, fa2=c → fa0 (D 확장, double) */
fma_op:
    fmadd.d fa0, fa0, fa1, fa2    /* fa0 = fa0*fa1 + fa2 (단일 명령어 FMA) */
    ret
RISC-V 산술 명령어 핵심 패턴 요약:
  • REM 전용 명령어: ARM64(msub 조합 필요)와 달리 rem a0, a1, a2로 나머지를 1 명령어로 계산
  • W 접미사(RV64): addw/subw/mulw는 32-bit 연산 후 64-bit 부호 확장 — C의 int(32-bit) 연산에 대응
  • MULH/MULHU/MULHSU: 128-bit 곱셈의 상위 64-bit를 추출하는 세 변형 — 부호 조합에 따라 선택
  • FMADD 성능: ML·DSP 연산에서 fmul + fadd 2-명령어 대신 fmadd 1-명령어로 처리하며, 중간 반올림이 없어 수치 정밀도도 향상됨

논리/시프트/비트 조작 명령어

명령어문법설명확장
AND / ANDIand a0, a1, a2비트 ANDI
OR / ORIor a0, a1, a2비트 ORI
XOR / XORIxor a0, a1, a2비트 XORI
SLL / SLLIsll a0, a1, a2논리 좌측 시프트I
SRL / SRLIsrl a0, a1, a2논리 우측 시프트I
SRA / SRAIsra a0, a1, a2산술 우측 시프트I
SLT / SLTIslt a0, a1, a2부호 있는 < 비교 (1/0)I
SLTU / SLTIUsltu a0, a1, a2부호 없는 < 비교I
SLLW / SRLW / SRAWsllw a0, a1, a232-bit 시프트 (RV64)RV64I
CLZ / CTZ / CPOPclz a0, a1선행/후행 제로, 팝카운트Zbb
ANDN / ORN / XNORandn a0, a1, a2AND-NOT / OR-NOT / XNORZbb
MIN / MAX / MINU / MAXUmin a0, a1, a2최솟값/최댓값Zbb
ROL / RORrol a0, a1, a2순환 시프트Zbb
REV8rev8 a0, a1바이트 순서(Byte Order) 반전Zbb
BCLR / BEXT / BINV / BSETbext a0, a1, a2단일 비트 조작Zbs

비교/분기 명령어

명령어문법설명
BEQbeq a0, a1, label같으면 분기 (±4KB)
BNEbne a0, a1, label다르면 분기
BLTblt a0, a1, label부호 있는 <
BGEbge a0, a1, label부호 있는 >=
BLTUbltu a0, a1, label부호 없는 <
BGEUbgeu a0, a1, label부호 없는 >=
JALjal ra, label점프 + 링크 (±1MB)
JALRjalr ra, 0(a0)간접 점프 + 링크
의사 명령어: j label = jal zero, label, call func = auipc ra, ...; jalr ra, ...(ra), ret = jalr zero, 0(ra), beqz a0, label = beq a0, zero, label, mv a0, a1 = addi a0, a1, 0, nop = addi zero, zero, 0

주요 의사 명령어(pseudo-instruction) 확장 표

RISC-V 어셈블러는 프로그래머 편의를 위해 다양한 의사 명령어를 제공합니다. 아래 표는 자주 사용되는 의사 명령어와 그것이 실제로 확장되는 기본 명령어를 보여줍니다.

의사 명령어확장되는 기본 명령어설명
nopaddi zero, zero, 0아무 동작 없음
li rd, immlui rd, upper; addi rd, rd, lower즉시값 로드 (32/64-bit)
la rd, symbolauipc rd, delta_hi; addi rd, rd, delta_lo주소 로드 (PC 상대)
mv rd, rsaddi rd, rs, 0레지스터 복사
not rd, rsxori rd, rs, -1비트 반전
neg rd, rssub rd, zero, rs부호 반전 (2의 보수)
negw rd, rssubw rd, zero, rs32-bit 부호 반전 (RV64)
sext.w rd, rsaddiw rd, rs, 032-bit 부호 확장 (RV64)
seqz rd, rssltiu rd, rs, 1rs == 0이면 1
snez rd, rssltu rd, zero, rsrs != 0이면 1
sltz rd, rsslt rd, rs, zerors < 0이면 1
sgtz rd, rsslt rd, zero, rsrs > 0이면 1
beqz rs, labelbeq rs, zero, label0이면 분기
bnez rs, labelbne rs, zero, label0이 아니면 분기
blez rs, labelbge zero, rs, labelrs <= 0이면 분기
bgez rs, labelbge rs, zero, labelrs >= 0이면 분기
bltz rs, labelblt rs, zero, labelrs < 0이면 분기
bgtz rs, labelblt zero, rs, labelrs > 0이면 분기
bgt rs, rt, labelblt rt, rs, labelrs > rt이면 분기 (피연산자 교환)
ble rs, rt, labelbge rt, rs, labelrs <= rt이면 분기 (피연산자 교환)
j labeljal zero, label무조건 점프 (링크 없음)
jr rsjalr zero, 0(rs)간접 점프 (링크 없음)
retjalr zero, 0(ra)함수 복귀
call funcauipc ra, off_hi; jalr ra, off_lo(ra)원거리 함수 호출
tail funcauipc t1, off_hi; jalr zero, off_lo(t1)꼬리 호출 (tail call)
왜 RISC-V에는 조건 플래그(condition flags)가 없는가? x86의 EFLAGS나 ARM의 NZCV처럼 별도의 조건 플래그 레지스터를 두지 않고, 비교-분기(compare-and-branch) 방식을 채택한 이유:
  • 파이프라인 의존성 제거: 플래그 레지스터는 모든 산술 명령어에 대한 암묵적 WAW(Write-After-Write) 의존성을 만듭니다. 비교-분기 방식은 분기 명령어가 직접 두 레지스터를 비교하므로, 비순차 실행(OoO) 파이프라인에서 불필요한 직렬화(Serialization)를 방지합니다.
  • 마이크로아키텍처 자유도: 플래그 레지스터 이름 변경(renaming) 없이도 효율적인 비순차 실행이 가능합니다.
  • 코드 간결성: blt a0, a1, label 한 명령어로 비교+분기를 수행하므로, CMP + Bcc 두 명령어 조합보다 명령어 수가 줄어들 수 있습니다.

스택/함수 호출 명령어

RISC-V에는 PUSH/POP이 없습니다. addi sp와 sd/ld로 수동 관리합니다.

/* 함수 프롤로그/에필로그 */
my_func:
    addi    sp, sp, -32           /* 스택 프레임 할당 */
    sd      ra, 24(sp)             /* 복귀 주소 저장 */
    sd      s0, 16(sp)             /* 프레임 포인터 저장 */
    addi    s0, sp, 32             /* 프레임 포인터 설정 */
    /* ... 함수 본문 ... */
    ld      s0, 16(sp)             /* 프레임 포인터 복원 */
    ld      ra, 24(sp)             /* 복귀 주소 복원 */
    addi    sp, sp, 32             /* 스택 프레임 해제 */
    ret                             /* jalr zero, 0(ra) */
RISC-V 호출 규약(Calling Convention):
  • 인자: a0-a7 (최대 8개 정수/포인터)
  • 반환값: a0 (+ a1)
  • Callee-saved: s0-s11, sp
  • Caller-saved: t0-t6, a0-a7, ra
  • SP 16-byte 정렬

시스템/특권 명령어

명령어문법설명
ECALLecall환경 콜 (U→S→M 트랩)
EBREAKebreak디버그 브레이크포인트
MRETmretM-mode 트랩 복귀
SRETsretS-mode 트랩 복귀
WFIwfi인터럽트 대기
CSRRWcsrrw a0, sstatus, a1CSR 읽기+쓰기 (atomic swap)
CSRRScsrrs a0, sstatus, a1CSR 읽기+비트 세트
CSRRCcsrrc a0, sstatus, a1CSR 읽기+비트 클리어
CSRRWI / CSRRSI / CSRRCIcsrrsi a0, sstatus, 2즉시값 버전 CSR 조작
FENCEfence rw, rw메모리 순서 펜스
FENCE.Ifence.i명령어 펜스 (I-cache 동기화)
FENCE.TSOfence.tsoTSO 메모리 순서 펜스
SFENCE.VMAsfence.vma a0, a1TLB 무효화(Invalidation) (주소, ASID)
CSR 의사 명령어: csrr rd, csr = csrrs rd, csr, zero (읽기), csrw csr, rs = csrrw zero, csr, rs (쓰기), csrs csr, rs = csrrs zero, csr, rs (비트 세트), csrc csr, rs = csrrc zero, csr, rs (비트 클리어)

트랩 위임(Trap Delegation) 메커니즘

기본적으로 모든 트랩은 M-mode로 전달되지만, medeleg(예외 위임)와 mideleg(인터럽트 위임) CSR을 설정하면 특정 트랩을 S-mode에서 직접 처리할 수 있습니다. Linux 부팅 시 SBI(OpenSBI 등)가 이 위임을 설정합니다.

RISC-V 트랩 위임: medeleg · mideleg에 따른 S/M-mode 라우팅 모든 트랩은 기본적으로 M-mode로 전달되며, 위임 비트가 1인 트랩만 S-mode에서 직접 처리합니다. 트랩 발생 (U-mode) 예외: ECALL(8), 페이지 폴트(12/13/15) 인터럽트: 타이머, 외부 대상 모드는 위임 비트가 결정 트랩 medeleg[N] / mideleg[N] = 1 인가? = 1 = 0 S-mode에서 처리 위임됨 — 비트 [N] = 1 stvec → 트랩 핸들러 scause / sepc / stval SRET으로 복귀 M-mode에서 처리 위임 안 됨 — 비트 [N] = 0 mtvec → 트랩 핸들러 mcause / mepc / mtval MRET으로 복귀 위임 비트 설정 위임 비트 설정 — M-mode(SBI / OpenSBI 등)가 부팅 시 기록 medeleg : 예외 위임 비트마스크 — 예) ECALL(8), 페이지 폴트(12/13/15) mideleg : 인터럽트 위임 비트마스크 — 예) S 타이머(5), S 외부(9) 머신 타이머(7)는 M-mode가 처리하며, 비정렬 접근(4·6)은 SBI FWFT 지원 시 위임됩니다.
RISC-V 트랩 위임 메커니즘 — U-mode 트랩이 medeleg/mideleg의 위임 비트[N] 값에 따라 S-mode(stvec·scause·SRET) 또는 M-mode(mtvec·mcause·MRET)로 라우팅되고, 그 비트를 M-mode의 SBI가 부팅 시 설정하는 흐름을 나타낸 개념 개요도입니다.

SFENCE.VMA 변형 (TLB 무효화)

SFENCE.VMA는 인자에 따라 무효화 범위가 달라집니다. 불필요하게 넓은 범위를 플러시(Flush)하면 성능 저하가 발생하므로, 커널은 가능한 한 좁은 범위의 무효화를 사용합니다.

형식rs1 (주소)rs2 (ASID)효과커널 사용 사례
sfence.vma zero, zero무시무시모든 TLB 엔트리 전체 플러시satp 변경 후, 전체 페이지 테이블 교체
sfence.vma addr, zero가상 주소(Virtual Address)무시해당 주소의 모든 ASID TLB 플러시단일 페이지(Page) 매핑(Mapping) 변경 (커널 주소)
sfence.vma zero, asid무시ASID해당 ASID의 모든 TLB 플러시프로세스 전체 주소 공간 무효화
sfence.vma addr, asid가상 주소ASID해당 주소+ASID의 TLB만 플러시단일 프로세스의 단일 페이지 (가장 세밀)

Sv39 페이지 테이블 워크 구조

Linux RISC-V는 주로 Sv39 (39-bit 가상 주소, 3단계 페이지 테이블)를 사용합니다. satp CSR이 루트 페이지 테이블의 물리 주소(Physical Address)와 ASID를 저장합니다.

Sv39 가상 주소 변환 — 3단계 페이지 테이블 워크 satp가 루트 L2 테이블의 물리 주소(PPN)를 보관하고, VA의 각 VPN 필드가 하위 테이블을 인덱싱합니다. 39-bit 가상 주소 분해 (9 + 9 + 9 + 12 = 39 bits) VPN[2] [38:30] VPN[1] [29:21] VPN[0] [20:12] Offset [11:0] 결합 satp CSR MODE=8(Sv39) ASID | 루트 PPN L2 (루트) VPN[2] 인덱스 512 PTE (4KB) L1 VPN[1] 인덱스 512 PTE (4KB) L0 VPN[0] 인덱스 512 PTE (4KB) 물리 페이지 PPN + Offset = 물리 주소 PPN PTE.PPN PTE.PPN PTE.PPN PTE 포맷 (64-bit, Sv39) Reserved [63:54] PPN [53:10] (44-bit) — 다음 테이블/물리 페이지 RSW [9:8] V R W X U G A D [7:0] V=Valid · R=Read · W=Write · X=eXecute · U=User · G=Global · A=Accessed · D=Dirty · RSW=커널 소프트웨어용 [9:8] 리프 PTE의 R/W/X 중 하나라도 1이면 리프 — 물리 페이지를 직접 가리킴 (L0=4KB, L1=2MB 메가페이지, L2=1GB 기가페이지)
Sv39 페이지 테이블 워크 — satp의 루트 PPN에서 시작해 VPN[2]→VPN[1]→VPN[0] 순으로 L2→L1→L0 페이지 테이블을 인덱싱하고, 최종 PTE.PPN과 Offset[11:0]을 결합해 물리 주소를 얻는 3단계 변환 과정과 64-bit PTE 필드 포맷을 함께 나타낸 Sv39 기준 구조 개요도입니다.

원자적/동기화 명령어 (A 확장)

명령어문법설명
LR.W / LR.Dlr.d a0, (a1)Load-Reserved (독점 로드)
SC.W / SC.Dsc.d a2, a0, (a1)Store-Conditional (a2=0이면 성공)
AMOSWAP.W/Damoswap.d a0, a1, (a2)원자적 교환
AMOADD.W/Damoadd.d a0, a1, (a2)원자적 덧셈
AMOAND.W/Damoand.d a0, a1, (a2)원자적 AND
AMOOR.W/Damoor.d a0, a1, (a2)원자적 OR
AMOXOR.W/Damoxor.d a0, a1, (a2)원자적 XOR
AMOMAX.W/Damomax.d a0, a1, (a2)원자적 MAX (부호)
AMOMIN.W/Damomin.d a0, a1, (a2)원자적 MIN (부호)
AMOMAXU / AMOMINUamomaxu.d a0, a1, (a2)원자적 MAX/MIN (부호 없음)
순서 지정자: .aq(acquire), .rl(release), .aqrl(순차 일관성). 예: lr.d.aq, sc.d.rl, amoswap.d.aqrl

A 확장의 세분화 (Zaamo / Zalrsc): 원래 단일 A 확장은 RISC-V International에 의해 두 개의 독립적인 하위 확장으로 분해되었습니다. Zaamo(Atomic Memory Operations)는 AMO 계열(swap/add/and/or/xor/min/max)만을, Zalrsc는 LR/SC(Load-Reserved / Store-Conditional)만을 각각 별도 확장으로 정의합니다. 즉 A = Zaamo + Zalrsc입니다. 이 분해는 임베디드/마이크로컨트롤러처럼 AMO는 필요하지만 LR/SC 루프는 불필요한 구현에서 선택적 채택을 가능하게 합니다. Linux 커널 6.15부터 RISCV_ISA_EXT_ZAAMO/RISCV_ISA_EXT_ZALRSC로 개별 감지·활성화하며, 표준 A 확장 문자열이 있으면 두 하위 확장을 모두 암시적으로 활성화합니다. 또한 v6.13에서 Zacas(단일 명령 CAS)와 Zabha(바이트/하프워드 원자 연산) 기반 원자 연산 가속이 추가되어, arch_cmpxchg()의 XLEN 폭 연산은 Zacas만으로 단일 amocas 명령이 되고, 바이트/하프워드처럼 좁은 폭은 Zabha가 함께 있어야 단일 원자 명령으로 처리됩니다.

LR/SC CAS 루프 패턴

/* compare_and_swap(addr=a0, expected=a1, desired=a2) → old in a0 */
cas_loop:
    lr.d.aq  a3, (a0)             /* Load-Reserved + Acquire */
    bne      a3, a1, 1f           /* expected와 다르면 실패 */
    sc.d.rl  a4, a2, (a0)          /* Store-Conditional + Release */
    bnez     a4, cas_loop          /* SC 실패 시 재시도 */
1:
    mv       a0, a3               /* 이전 값 반환 */
    ret
AMO vs LR/SC — 언제 어떤 것을 사용할까?
특성AMO (Atomic Memory Operation)LR/SC (Load-Reserved / Store-Conditional)
복잡도단일 명령어로 완결루프 필요 (최소 4개 명령어)
연산 유형swap, add, and, or, xor, min, max만 가능임의의 read-modify-write 가능
ABA 문제해당 없음 (단일 연산)면역 (예약이 깨지면 SC 실패)
진행 보장항상 완료 (하드웨어 보장)SC가 반복 실패할 수 있음 (단, 스펙은 결국 성공 보장)
캐시(Cache) 프로토콜캐시 라인(Cache Line) 수준 원자성예약 세트 추적 필요
대표 용례atomic_add, 스핀락(Spinlock)CAS, cmpxchg, 복잡한 원자적 갱신
일반적으로 단순한 원자적 갱신에는 AMO가 더 효율적이고, CAS나 복잡한 조건부 갱신에는 LR/SC가 필수입니다.

AMOSWAP 기반 스핀락 구현

/* 스핀락: lock=a0 (0=해제, 1=잠김) */
spin_lock:
    li       t0, 1
1:
    amoswap.w.aq t1, t0, (a0)      /* t1 = old, *a0 = 1 (acquire) */
    bnez     t1, 1b               /* 이미 잠겨있으면 재시도 */
    ret                             /* 획득 성공, acquire 의미론 보장 */

spin_unlock:
    amoswap.w.rl zero, zero, (a0)  /* *a0 = 0, release 의미론 */
    ret

/* 최적화된 버전: test-and-set with backoff */
spin_lock_optimized:
    li       t0, 1
1:  lw       t1, (a0)              /* 일반 로드로 먼저 확인 (캐시 친화적) */
    bnez     t1, 1b               /* 잠겨있으면 바쁜 대기 */
    amoswap.w.aq t1, t0, (a0)      /* 해제된 것 같으면 시도 */
    bnez     t1, 1b               /* 실패 시 다시 대기 */
    ret
FENCE 순서 의미론 상세: FENCE의 predecessor/successor 집합은 4가지 접근 타입을 조합합니다:
  • i (Input) — 장치 입력 (MMIO 읽기)
  • o (Output) — 장치 출력 (MMIO 쓰기)
  • r (Read) — 메모리 읽기
  • w (Write) — 메모리 쓰기
Linux 커널 매핑: mb() = fence iorw, iorw, rmb() = fence ir, ir, wmb() = fence ow, ow, smp_mb() = fence rw, rw, smp_rmb() = fence r, r, smp_wmb() = fence w, w. .aq/.rl 비트는 AMO/LR/SC에만 적용되며, 각각 acquire/release 의미론을 부여합니다.

벡터 명령어 (V 확장)

RISC-V V 확장은 가변 길이 벡터(VLEN: 구현 의존, 128-bit 이상)를 지원합니다. vsetvli로 벡터 길이와 요소 타입을 동적으로 설정합니다.

명령어설명
vsetvli rd, rs1, vtypei벡터 길이/타입 설정 (SEW, LMUL)
vsetivli rd, uimm, vtypei즉시값으로 벡터 길이 설정
VLE8/16/32/64.V vd, (rs1)벡터 로드 (8/16/32/64-bit 요소)
VSE8/16/32/64.V vs3, (rs1)벡터 저장
VLSE32.V vd, (rs1), rs2스트라이드 벡터 로드
VLUXEI32.V vd, (rs1), vs2인덱스 벡터 로드 (gather)
VADD.VV vd, vs2, vs1벡터 덧셈
VSUB.VV vd, vs2, vs1벡터 뺄셈
VMUL.VV vd, vs2, vs1벡터 곱셈
VAND.VV / VOR.VV / VXOR.VV벡터 논리 연산
VSLL.VV / VSRL.VV / VSRA.VV벡터 시프트
VMSEQ.VV / VMSNE.VV / VMSLT.VV벡터 비교 → 마스크
VREDSUM.VS vd, vs2, vs1벡터 리덕션 합계
VREDMAX.VS / VREDMIN.VS벡터 리덕션 최대/최소
VMAND.MM / VMOR.MM / VMXOR.MM마스크 논리 연산
VSLIDEUP.VI / VSLIDEDOWN.VI벡터 슬라이드
VRGATHER.VV vd, vs2, vs1벡터 인덱스 기반 재배치
VCOMPRESS.VM vd, vs2, vs1마스크 기반 압축

LMUL (Length MULtiplier) 개념

LMUL은 하나의 벡터 연산이 사용하는 레지스터 그룹의 크기를 결정합니다. LMUL=1이면 단일 레지스터, LMUL=2이면 연속 2개 레지스터를 하나의 벡터로 묶어 더 긴 벡터를 처리합니다. 반대로 LMUL=1/2, 1/4, 1/8 분수값도 가능하여 좁은 요소를 효율적으로 처리합니다.

LMUL (Length MULtiplier) — 레지스터 그룹핑 가정: VLEN = 256-bit (구현 예) LMUL=1: v0 256-bit = VLEN · 레지스터 32개 사용 가능, 벡터 길이 = VLEN/SEW LMUL=2: v0 v1 512-bit 논리 벡터 (레지스터 2개) v0-v1이 그룹 → 16개 벡터 그룹 사용 가능 (v0,v2,v4,...,v30) LMUL=4: v0 v1 v2 v3 1024-bit (레지스터 4개) v0-v3이 그룹 → 8개 벡터 그룹 사용 가능 (v0,v4,v8,...,v28) LMUL=8: v0 v1 v2 v3 v4 v5 v6 v7 2048-bit v0-v7이 그룹 → 4개 벡터 그룹 사용 가능 (v0,v8,v16,v24) 핵심 공식 VLMAX = (VLEN / SEW) * LMUL 예: VLEN=256, SEW=32, LMUL=4 → VLMAX = (256/32)*4 = 32개 요소 사용 가능 벡터 그룹 수 = 32 / LMUL LMUL이 커질수록 벡터 길이 증가, 레지스터 수 감소 (트레이드오프)
RISC-V V 확장 LMUL 개념 — 벡터 레지스터 그룹화(LMUL)에 따라 한 번에 처리하는 비트 폭이 어떻게 달라지는지 나타낸 개념 개요도입니다.

vsetvli와 VLEN/SEW/LMUL 관계

vsetvli 명령어는 벡터 연산 전에 반드시 호출하여 SEW(Selected Element Width)와 LMUL을 설정합니다. 하드웨어는 요청된 벡터 길이(AVL)와 VLMAX를 비교하여 실제 처리할 요소 수(VL)를 결정합니다.

/* vsetvli rd, rs1, vtypei */
/* rd: 실제 설정된 벡터 길이 (VL) */
/* rs1: 요청하는 벡터 길이 (AVL). rs1=zero이면 VL 변경 없이 vtype만 변경 */
/* vtypei: SEW, LMUL, ta(tail agnostic), ma(mask agnostic) 인코딩 */

/* 예: SEW=8, LMUL=1, tail-agnostic, mask-agnostic */
    vsetvli  t0, a2, e8, m1, ta, ma  /* t0 = min(a2, VLMAX) */

/* SEW 옵션: e8(8-bit), e16(16-bit), e32(32-bit), e64(64-bit) */
/* LMUL 옵션: mf8(1/8), mf4(1/4), mf2(1/2), m1(1), m2(2), m4(4), m8(8) */
/* 꼬리 정책: ta(tail agnostic) / tu(tail undisturbed) */
/* 마스크 정책: ma(mask agnostic) / mu(mask undisturbed) */

벡터화된 memcpy 예제

RISC-V V 확장의 강력한 점은 vsetvli 스트립마이닝 루프입니다. 하드웨어가 한 번에 처리할 수 있는 최대 요소 수를 자동으로 결정하므로, VLEN에 독립적인 코드를 작성할 수 있습니다.

/* void *memcpy_v(void *dst, const void *src, size_t n) */
/* a0=dst, a1=src, a2=n (바이트 수) */
memcpy_v:
    mv       a3, a0                /* 반환용 dst 주소 보존 */
.Lloop:
    vsetvli  t0, a2, e8, m8, ta, ma /* t0 = 이번에 복사할 바이트 수 */
                                      /* SEW=8(바이트), LMUL=8(최대 처리량) */
    vle8.v   v0, (a1)              /* src에서 t0 바이트 로드 */
    vse8.v   v0, (a0)              /* dst에 t0 바이트 저장 */
    add      a1, a1, t0            /* src += t0 */
    add      a0, a0, t0            /* dst += t0 */
    sub      a2, a2, t0            /* n -= t0 */
    bnez     a2, .Lloop            /* 남은 바이트가 있으면 반복 */
    mv       a0, a3                /* dst 주소 반환 */
    ret
스트립마이닝(strip-mining) 패턴의 장점: 위 memcpy_v 코드는 VLEN=128이든 VLEN=1024이든 수정 없이 동작합니다. vsetvli가 하드웨어 VLEN에 맞게 VL을 자동 조정하므로, ARM SVE처럼 벡터 길이 불가지(Vector Length Agnostic) 프로그래밍이 가능합니다. LMUL=8을 사용하면 한 번에 최대 8 * VLEN / 8 바이트를 처리합니다 (VLEN=256이면 256바이트).

커널 핵심 명령어

ECALL — SBI 호출 패턴

/* SBI 호출: a7=EID, a6=FID, a0-a5=인자 */
/* 반환: a0=error, a1=value */
    li      a7, 0x10              /* SBI_EXT_BASE */
    li      a6, 0                  /* SBI_BASE_GET_SPEC_VERSION */
    ecall                            /* S-mode → M-mode 트랩 */
    /* a0=error code, a1=spec version */

트랩 핸들러 진입

/* stvec → _handle_exception */
_handle_exception:
    csrrw   tp, sscratch, tp       /* tp ↔ sscratch 교환 */
    /* tp는 이제 커널 task_struct, sscratch는 유저 tp */
    sd      sp, TASK_TI_USER_SP(tp)    /* 유저 sp 저장 */
    ld      sp, TASK_TI_KERNEL_SP(tp)  /* 커널 스택으로 전환 */
    addi    sp, sp, -PT_SIZE       /* pt_regs 공간 할당 */
    /* 레지스터 저장... */
    sd      ra, PT_RA(sp)
    sd      gp, PT_GP(sp)
    /* ... a0-a7, s0-s11, t0-t6 저장 ... */
    csrr    a0, scause             /* 트랩 원인 */
    csrr    a1, sepc               /* 예외 PC */
    csrr    a2, stval              /* 트랩 값 */

SRET — 트랩 복귀

    /* 레지스터 복원 후 */
    csrw    sepc, a0               /* 복귀 PC 설정 */
    csrw    sstatus, a1            /* 상태 복원 */
    csrrw   tp, sscratch, tp       /* tp ↔ sscratch 복원 */
    sret                            /* PC←sepc, 모드←sstatus.SPP */

SFENCE.VMA — TLB 무효화

/* arch/riscv/include/asm/tlbflush.h */
static inline void local_flush_tlb_page(unsigned long addr)
{
    asm volatile("sfence.vma %0" :: "r"(addr) : "memory");
}
static inline void local_flush_tlb_all(void)
{
    asm volatile("sfence.vma" ::: "memory");
}

SBI (Supervisor Binary Interface) 확장 표

SBI는 S-mode(커널)가 M-mode(펌웨어)의 서비스를 호출하는 표준 인터페이스입니다. RISC-V에서 ARM의 PSCI나 x86의 BIOS 서비스에 해당하며, OpenSBI가 대표적 구현체입니다. 호출 규약은 a7=EID(Extension ID), a6=FID(Function ID), a0-a5=인자, 반환은 a0=error, a1=value입니다.

EID확장 이름주요 함수 (FID)설명
0x10SBI_EXT_BASEget_spec_version(0), get_impl_id(1), get_impl_version(2), probe_extension(3)SBI 기본 정보 조회
0x54494D45SBI_EXT_TIME (TIME)set_timer(0)타이머(Timer) 인터럽트 설정. 커널 tick 소스
0x735049SBI_EXT_IPIsend_ipi(0)프로세서 간 인터럽트 전송
0x52464E43SBI_EXT_RFENCEremote_fence_i(0), remote_sfence_vma(1), remote_sfence_vma_asid(2)원격 TLB/Icache 무효화
0x48534DSBI_EXT_HSMhart_start(0), hart_stop(1), hart_get_status(2), hart_suspend(3)Hart 상태 관리 (SMP 부팅)
0x53525354SBI_EXT_SRSTsystem_reset(0)시스템 리셋/종료
0x504D55SBI_EXT_PMUnum_counters(0), counter_get_info(1), counter_start(2), counter_stop(3)성능 모니터링 카운터
0x4442434ESBI_EXT_DBCNwrite(0), read(1), write_byte(2)디버그 콘솔 (earlycon)
0x53555350SBI_EXT_SUSPsuspend(0)시스템 서스펜드

완전한 컨텍스트 스위치 예제

Linux 커널의 __switch_to()는 프로세스 간 컨텍스트를 전환합니다. RISC-V에서는 callee-saved 레지스터(s0-s11, sp, ra)만 저장/복원하면 됩니다 (caller-saved는 C 호출 규약에 의해 호출자가 이미 처리).

/* arch/riscv/kernel/entry.S — __switch_to(prev, next) */
/* a0 = prev->thread (struct thread_struct *) */
/* a1 = next->thread (struct thread_struct *) */
__switch_to:
    /* === prev 컨텍스트 저장 === */
    sd      ra,  THREAD_RA(a0)     /* 복귀 주소 */
    sd      sp,  THREAD_SP(a0)     /* 스택 포인터 */
    sd      s0,  THREAD_S0(a0)     /* callee-saved s0 (fp) */
    sd      s1,  THREAD_S1(a0)
    sd      s2,  THREAD_S2(a0)
    sd      s3,  THREAD_S3(a0)
    sd      s4,  THREAD_S4(a0)
    sd      s5,  THREAD_S5(a0)
    sd      s6,  THREAD_S6(a0)
    sd      s7,  THREAD_S7(a0)
    sd      s8,  THREAD_S8(a0)
    sd      s9,  THREAD_S9(a0)
    sd      s10, THREAD_S10(a0)
    sd      s11, THREAD_S11(a0)

    /* === next 컨텍스트 복원 === */
    ld      ra,  THREAD_RA(a1)     /* next의 복귀 주소 */
    ld      sp,  THREAD_SP(a1)     /* next의 스택 */
    ld      s0,  THREAD_S0(a1)
    ld      s1,  THREAD_S1(a1)
    ld      s2,  THREAD_S2(a1)
    ld      s3,  THREAD_S3(a1)
    ld      s4,  THREAD_S4(a1)
    ld      s5,  THREAD_S5(a1)
    ld      s6,  THREAD_S6(a1)
    ld      s7,  THREAD_S7(a1)
    ld      s8,  THREAD_S8(a1)
    ld      s9,  THREAD_S9(a1)
    ld      s10, THREAD_S10(a1)
    ld      s11, THREAD_S11(a1)

    /* tp(스레드 포인터)를 next의 task_struct로 전환 */
    mv      tp, a2                 /* a2 = next task_struct */

    ret                             /* ra가 next의 복귀 주소이므로 next 프로세스로 점프 */
컨텍스트 스위치에서 저장하지 않는 레지스터:
  • t0-t6, a0-a7 — Caller-saved이므로 C 함수 호출 규약에 의해 switch_to() 호출 전에 이미 스택에 저장됨
  • gp — 커널 이미지 전체에서 동일한 값 (프로세스 간 공유)
  • zero — 항상 0
  • FP/벡터 레지스터 — 지연(Latency) 저장(lazy save): 커널에서 FP/벡터를 사용하기 직전에만 저장

SFENCE.VMA 커널 사용 패턴

/* 1. 단일 페이지 매핑 변경 후 */
static inline void local_flush_tlb_page(unsigned long addr)
{
    /* 해당 가상 주소의 TLB 엔트리만 무효화 */
    asm volatile("sfence.vma %0" :: "r"(addr) : "memory");
}

/* 2. 프로세스 전체 주소 공간 무효화 (특정 ASID) */
static inline void local_flush_tlb_all_asid(unsigned long asid)
{
    asm volatile("sfence.vma zero, %0" :: "r"(asid) : "memory");
}

/* 3. 전체 TLB 플러시 (satp 변경 후) */
static inline void local_flush_tlb_all(void)
{
    asm volatile("sfence.vma" ::: "memory");
}

/* 4. 원격 TLB 무효화 (SBI를 통해 다른 hart에 IPI 전송) */
void flush_tlb_range(struct vm_area_struct *vma,
                     unsigned long start, unsigned long end)
{
    /* SBI RFENCE 확장 사용 */
    /* 로컬 hart: sfence.vma */
    /* 원격 hart: sbi_remote_sfence_vma() IPI */
}

명령어 인코딩

RISC-V는 6가지 기본 인코딩 타입을 사용합니다. 모든 타입에서 opcode는 [6:0], rd는 [11:7]에 위치하여 디코딩을 단순화합니다.

RISC-V 32-bit 기본 인코딩 타입 (6종) 31 25 24 20 19 15 14 12 11 7 6 0 R: funct7 rs2 rs1 funct3 rd opcode I: imm[11:0] rs1 funct3 rd opcode S: imm[11:5] rs2 rs1 funct3 imm[4:0] opcode B: imm[12|10:5] rs2 rs1 funct3 imm[4:1|11] opcode U: imm[31:12] rd opcode J: imm[20|10:1|11|19:12] rd opcode opcode [6:0] rd [11:7] rs1/rs2 funct3 [14:12] funct7 [31:25] immediate R: ADD/SUB/AND/OR/XOR/SLL/SRL/SRA | I: ADDI/LW/JALR | S: SW/SD | B: BEQ/BNE/BLT/BGE | U: LUI/AUIPC | J: JAL
RISC-V 32-bit 명령어 인코딩 6가지 타입 — R/I/S/B/U/J 형식의 비트 필드 배치를 비트 31~0 기준으로 대조한 상세도입니다. 모든 타입이 opcode[6:0]와 rd[11:7] 위치를 공유하고, immediate만 타입별로 분할 위치가 달라집니다.

인코딩 예제: add a0, a1, a2

add a0, a1, a2는 R-type 인코딩입니다. 각 필드가 어떻게 매핑되는지 구체적으로 보겠습니다.

필드비트 범위값이진수설명
funct7[31:25]0x000000000ADD (SUB는 0100000)
rs2[24:20]12 (a2=x12)01100소스 레지스터 2
rs1[19:15]11 (a1=x11)01011소스 레지스터 1
funct3[14:12]0x0000ADD 연산
rd[11:7]10 (a0=x10)01010목적 레지스터
opcode[6:0]0x330110011OP (레지스터-레지스터 연산)

add a0, a1, a2의 32-bit 인코딩은 다음과 같습니다.

 funct7  | rs2   | rs1   | f3  | rd    | opcode
 0000000 | 01100 | 01011 | 000 | 01010 | 0110011
 = 0x00C58533

검증: objdump 출력에서 0: 00c58533 add a0, a1, a2로 확인할 수 있습니다.

비교: sub a0, a1, a2는 funct7만 다릅니다 (0100000).

 funct7  | rs2   | rs1   | f3  | rd    | opcode
 0100000 | 01100 | 01011 | 000 | 01010 | 0110011
 = 0x40C58533
C 확장 (압축 명령어) 16-bit 인코딩: C 확장은 자주 사용되는 명령어를 16-bit로 압축하여 코드 크기를 크게 줄입니다(공식 사양은 "substantially improves code density"로 규정하며, 실제 감소 폭은 컴파일러·워크로드에 따라 달라집니다).
  • 식별: 32-bit 명령어는 하위 2비트가 11, 16-bit 명령어는 00, 01, 10 중 하나
  • 제약: 압축 명령어는 레지스터 x8-x15(s0-s1, a0-a5)만 사용 가능 (3-bit 레지스터 필드)
  • 포맷: CR(레지스터), CI(즉시값), CSS(스택 저장), CIW(와이드 즉시값), CL(로드), CS(저장), CB(분기), CJ(점프) 8가지
압축 명령어확장되는 32-bit 명령어크기 절약
c.add a0, a1add a0, a0, a12바이트 (50%)
c.li a0, 5addi a0, zero, 52바이트
c.lw a0, 4(a1)lw a0, 4(a1)2바이트
c.sw a0, 4(a1)sw a0, 4(a1)2바이트
c.beqz a0, labelbeq a0, zero, label2바이트
c.j labeljal zero, label2바이트
c.mv a0, a1add a0, zero, a12바이트
c.nopaddi zero, zero, 02바이트
Linux 커널은 CONFIG_RISCV_ISA_C 옵션으로 C 확장 사용 여부를 결정합니다. 대부분의 RISC-V 리눅스 배포판은 RV64GC를 기본으로 사용합니다.

SBI (Supervisor Binary Interface)

SBI는 S-mode(리눅스 커널)가 M-mode(펌웨어, OpenSBI)에 서비스를 요청하는 표준 인터페이스입니다. x86의 BIOS/UEFI Runtime Services, ARM의 PSCI(Power State Coordination Interface)에 대응하며, RISC-V 커널이 하드웨어 추상화 없이 이식성을 확보하는 핵심 메커니즘입니다.

RISC-V SBI (Supervisor Binary Interface) 아키텍처 U-mode — 유저 프로세스 ecall → S-mode 트랩 S-mode — 리눅스 커널 ecall → M-mode 트랩 (SBI 호출) M-mode — 펌웨어(OpenSBI 등) SBI 서비스 처리 → sret 복귀 SBI 호출 규약 (ecall) a7 = Extension ID (EID) a6 = Function ID (FID) a0-a5 = 인자 (최대 6개) ecall 실행 → M-mode 트랩 반환: a0 = error, a1 = value struct sbiret { long error; long value; } error: 0=성공, -1=실패, -2=미지원, -4=DENIED SBI 확장(Extension) — EID · 용도 · 커널 진입점 EID 이름 용도 커널 진입점 0x10 BASE SBI 버전, 확장 탐지 sbi_probe_extension() 0x54494D45 TIME 타이머 설정 (Sstc 없을 때) sbi_set_timer() 0x735049 IPI hart 간 인터럽트 sbi_send_ipi() 0x52464E43 RFENCE 원격 TLB/I-cache 무효화 sbi_remote_sfence_vma_asid() 0x48534D HSM hart 시작/중지 (SMP 부팅) sbi_hsm_hart_start() 0x53525354 SRST 시스템 리셋/셧다운 sbi_shutdown() 0x504D55 PMU 성능 카운터 (perf) riscv_pmu_sbi (perf 드라이버) 0x4442434E DBCN 디버그 콘솔 (earlycon) sbi_debug_console_write() 0x53555350 SUSP 시스템 서스펜드 sbi_system_suspend_ops
RISC-V SBI 아키텍처 — U/S/M 권한 계층에서 커널(S-mode)이 ecall로 펌웨어(M-mode, OpenSBI)의 서비스를 호출하는 구조와, a7=EID·a6=FID·a0-a5=인자로 요청하고 a0=error·a1=value(struct sbiret)로 반환받는 호출 규약, 9개 확장의 EID·용도·커널 진입점을 대조한 개념 개요도입니다. 오류 코드는 커널 arch/riscv/include/asm/sbi.h 정의(0=SBI_SUCCESS, -1=SBI_ERR_FAILED, -2=SBI_ERR_NOT_SUPPORTED, -4=SBI_ERR_DENIED)를 따릅니다.
/* arch/riscv/kernel/sbi.c — SBI 호출 구현 */
struct sbiret sbi_ecall(int ext, int fid,
                       unsigned long arg0, unsigned long arg1,
                       unsigned long arg2, unsigned long arg3,
                       unsigned long arg4, unsigned long arg5)
{
    struct sbiret ret;
    register unsigned long a0 asm("a0") = arg0;
    register unsigned long a1 asm("a1") = arg1;
    register unsigned long a2 asm("a2") = arg2;
    register unsigned long a3 asm("a3") = arg3;
    register unsigned long a4 asm("a4") = arg4;
    register unsigned long a5 asm("a5") = arg5;
    register unsigned long a6 asm("a6") = fid;
    register unsigned long a7 asm("a7") = ext;

    asm volatile ("ecall"
        : "+r" (a0), "+r" (a1)
        : "r" (a2), "r" (a3), "r" (a4), "r" (a5),
          "r" (a6), "r" (a7)
        : "memory");

    ret.error = a0;  /* SBI_SUCCESS=0, SBI_ERR_FAILED=-1, ... */
    ret.value = a1;
    return ret;
}

/* SMP 부팅: HSM 확장으로 다른 hart 시작 */
/* sbi_hart_start(hartid, start_addr, opaque) */
/* → M-mode에서 해당 hart를 start_addr로 점프시킴 */
/* → secondary_start_sbi() → 커널 진입 */

가상 메모리 (Sv39/Sv48/Sv57)

RISC-V의 가상 메모리는 satp CSR로 제어되며, Sv39(39비트 VA, 3레벨), Sv48(48비트 VA, 4레벨), Sv57(57비트 VA, 5레벨) 모드를 지원합니다. 리눅스 커널은 RV64에서 하드웨어가 지원하는 최상위 모드(Sv57 → Sv48 → Sv39 순)를 기본으로 선택하며, no5lvl/no4lvl 부팅 옵션으로 제한할 수 있습니다.

RISC-V 가상 메모리 — satp MODE가 선택하는 Sv39 · Sv48 · Sv57 세 모드 모두 12비트 offset과 9비트 VPN 레벨을 쓰며, 레벨 수만 3·4·5로 늘어납니다. satp CSR (RV64) MODE [63:60] ASID [59:44] (16비트) PPN [43:0] — 루트 페이지 테이블 물리 페이지 번호 MODE 값: 0 = Bare(변환 없음) · 8 = Sv39 · 9 = Sv48 · 10 = Sv57 MODE가 선택하는 변환 모드 — 가상 주소(VA) 비트 분해와 페이지 테이블 레벨 VA 비트 경계 57 48 39 30 21 12 0 Sv39 VA 39비트 · 3레벨 · 512GB MODE = 8 L2 · VPN[2] VA[38:30] L1 · VPN[1] VA[29:21] L0 · VPN[0] VA[20:12] Offset VA[11:0] Sv48 VA 48비트 · 4레벨 · 256TB MODE = 9 L3 · VPN[3] VA[47:39] L2 · VPN[2] VA[38:30] L1 · VPN[1] VA[29:21] L0 · VPN[0] VA[20:12] Offset VA[11:0] Sv57 VA 57비트 · 5레벨 · 128PB MODE = 10 L4 · VPN[4] VA[56:48] L3 · VPN[3] VA[47:39] L2 · VPN[2] VA[38:30] L1 · VPN[1] VA[29:21] L0 · VPN[0] VA[20:12] Offset VA[11:0] 대형 페이지(leaf): L0 = 4KB · L1 = 2MB · L2 = 1GB · L3 = 512GB · L4 = 256TB Linux 커널: 부팅 시 하드웨어가 지원하는 최상위 모드를 선택합니다 (Sv57 → Sv48 → Sv39). no5lvl/no4lvl 옵션으로 제한합니다. 64비트 PTE 비트 필드 — 리프 판정과 물리 페이지 번호 Rsvd [60:54] PPN [53:10] — 44비트 물리 페이지 번호 D A G U X W R V [7:0] N[63] · PBMT[62:61] RSW [9:8] V = Valid · R = Read · W = Write · X = eXecute · U = User · G = Global · A = Accessed · D = Dirty R = W = X = 0 → 다음 레벨 포인터(비리프) · R 또는 X = 1 → 리프(최종 매핑) N = NAPOT(Svnapot 확장) · PBMT = 페이지 기반 메모리 타입(Svpbmt 확장) · RSW = 커널 소프트웨어 전용
RISC-V 가상 메모리 Sv39/Sv48/Sv57 — satp의 MODE·ASID·PPN 필드와 MODE 값(8/9/10)이 선택하는 세 변환 모드의 가상 주소 비트 분해(레벨당 9비트 VPN과 공통 12비트 offset)를 같은 눈금의 우측 정렬로 비교하고, 레벨 수·주소 공간 크기와 64비트 PTE 비트 필드, 리프 판정 규칙을 함께 나타낸 상세도입니다.
비교 항목Sv39Sv48Sv57x86-64 4-Level
VA 비트39485748
페이지 테이블 레벨3454
VA 공간512GB256TB128PB256TB
PA 비트56565652
페이지 크기4KB4KB4KB4KB
대형 페이지2MB, 1GB2MB, 1GB, 512GB+256TB2MB, 1GB
PTE 크기8B8B8B8B
ASID 비트16비트 (최대 65536 프로세스)12비트 (PCID)
TLB 플러시sfence.vmainvlpg

sfence.vma 변형: sfence.vma zero, zero(전체 TLB 플러시), sfence.vma addr, zero(VA 기반), sfence.vma zero, asid(ASID 기반), sfence.vma addr, asid(VA+ASID). 커널은 flush_tlb_range()에서 VA+ASID 기반 세밀한 플러시를 사용하여 불필요한 전체 플러시를 피합니다. Svadu 확장(v1.0 비준)은 하드웨어 A/D 비트 자동 갱신을 지원하여 소프트웨어 page fault 핸들링 오버헤드(Overhead)를 제거합니다.

RISC-V 호출 규약 (ilp32/lp64)

RISC-V의 호출 규약은 ABI(Application Binary Interface)의 핵심으로, 함수 호출 시 인자 전달, 반환값, 레지스터 보존 규칙을 정의합니다. RV32에서는 ilp32(int/long/pointer 32비트), RV64에서는 lp64(long/pointer 64비트) ABI를 사용하며, 부동소수점 확장에 따라 ilp32f/ilp32d, lp64f/lp64d 변형이 존재합니다. Linux 커널은 RV64에서 lp64/lp64d를 사용합니다.

레지스터 사용 규약 다이어그램

아래 다이어그램은 RISC-V 호출 규약에 따른 32개 범용 레지스터의 역할 분류를 보여줍니다. 인자 레지스터(a0-a7), 반환값(a0-a1), Callee-saved(s0-s11), Caller-saved(t0-t6), 그리고 특수 용도 레지스터(ra, sp, gp, tp)의 관계를 한눈에 파악할 수 있습니다.

RISC-V 호출 규약 레지스터 분류 RISC-V 호출 규약 — 레지스터 역할 분류 특수 용도 (x0-x4) x0 (zero) — 하드와이어 0 x1 (ra) — 복귀 주소 x2 (sp) — 스택 포인터 x3 (gp) — 전역 포인터 x4 (tp) — 스레드 포인터 ra는 호출자 보존, sp 유지·gp·tp 고정 인자/반환 (Caller-saved) a0 (x10) — 인자1/반환1 a1 (x11) — 인자2/반환2 a2-a5 (x12-x15) — 인자3-6 a6 (x16) — 인자7 / SBI FID a7 (x17) — 인자8 / SBI EID 9번째 이상 인자는 스택 전달 Callee-saved (s0-s11) s0/fp (x8) — 프레임 포인터 s1 (x9) s2-s11 (x18-x27) 피호출 함수가 반드시 보존 Temporary (Caller-saved) t0-t2 (x5-x7) t3-t6 (x28-x31) 호출자가 필요 시 저장 Caller-saved (호출자가 저장) a0-a7 · t0-t6 · ra — 호출 전에 스택 등에 저장 Callee-saved (피호출 함수가 보존) s0-s11 · sp — 호출 후에도 유지, gp·tp는 고정 함수 호출 흐름 caller: a0-a7 인자 설정 jal ra, func callee: s0-s11 저장·실행 a0-a1 반환, ret 스택 프레임 레이아웃 (lp64) caller의 스택 인자 (9번째 이상) 높은 주소 ↑ 저장된 ra (8바이트) 저장된 s0/fp (8바이트) 지역 변수 · s1-s11 저장 sp (낮은 주소)
RISC-V 호출 규약 레지스터 분류 — 32개 범용 레지스터의 역할(특수 용도·인자/반환·Callee-saved·Temporary)과 Caller-saved/Callee-saved 보존 규칙, 함수 호출 흐름, 스택 프레임(Stack Frame) 레이아웃을 나타낸 개념 개요도입니다.

C 함수 호출 규약 어셈블리 예제

/* C: long result = my_func(arg1, arg2, arg3, arg4); */
/* lp64 ABI: 인자를 a0-a7에 전달, 반환값은 a0 (128비트는 a0:a1) */

    /* caller 측: 인자 설정 */
    mv      a0, s2                 /* arg1 */
    mv      a1, s3                 /* arg2 */
    mv      a2, s4                 /* arg3 */
    mv      a3, s5                 /* arg4 */
    jal     ra, my_func            /* 함수 호출, ra ← 복귀 주소 */
    mv      s6, a0                 /* 반환값 저장 */

/* callee 측: my_func */
my_func:
    addi    sp, sp, -32           /* 스택 프레임 할당 */
    sd      ra, 24(sp)            /* ra 저장 */
    sd      s0, 16(sp)            /* s0/fp 저장 */
    sd      s1, 8(sp)             /* s1 저장 (사용하는 경우) */
    addi    s0, sp, 32            /* 프레임 포인터 설정 */

    /* 함수 본체: a0-a3에 인자 접근 */
    add     a0, a0, a1            /* 연산 */

    /* 에필로그 */
    ld      s1, 8(sp)
    ld      s0, 16(sp)
    ld      ra, 24(sp)
    addi    sp, sp, 32
    ret                            /* jalr zero, ra, 0 */

ilp32 vs lp64 ABI 차이

특성ilp32 (RV32)lp64 (RV64)
int 크기32비트32비트
long 크기32비트64비트
포인터 크기32비트64비트
레지스터 너비32비트 (XLEN=32)64비트 (XLEN=64)
load/store 워드lw/swld/sd
스택 정렬16바이트16바이트
64비트 인자 전달레지스터 쌍 (a0:a1)단일 레지스터 (a0)
부동소수점 변형ilp32f (F), ilp32d (F+D)lp64f (F), lp64d (F+D)
/* arch/riscv/include/asm/asm.h */
/* ABI에 따라 load/store 매크로가 자동 선택됨 */
#ifdef CONFIG_64BIT
#define REG_S      sd          /* 64비트: store doubleword */
#define REG_L      ld          /* 64비트: load doubleword */
#define REG_SIZE   8
#else
#define REG_S      sw          /* 32비트: store word */
#define REG_L      lw          /* 32비트: load word */
#define REG_SIZE   4
#endif

커널에서의 ABI: Linux 커널은 RV64에서 lp64d ABI를 사용하지만, 커널 내부에서는 부동소수점 레지스터를 사용하지 않습니다. 커널 진입 시 sstatus의 FS/VS 필드(SR_FS_VS)를 클리어하여 FP/벡터 유닛을 비활성화하고, 유저 공간 복귀 시에만 FP 상태를 복원합니다. 커널 모드에서 부동소수점/벡터가 필요하면 kernel_fpu_begin()/kernel_vector_begin()으로 명시적으로 활성화해야 합니다. 9번째 이상의 인자는 스택으로 전달되지만, 커널 코드에서 8개 초과 인자를 가진 함수는 거의 없습니다. 구조체(Struct) 반환은 a0에 포인터로 전달합니다.

트랩/예외 처리 흐름

RISC-V의 트랩(trap) 메커니즘은 예외(exception)와 인터럽트(interrupt)를 통합적으로 처리합니다. S-mode에서 트랩이 발생하면 하드웨어가 자동으로 관련 CSR들을 설정하고 stvec에 지정된 핸들러로 점프합니다. stvec는 Direct 모드(모든 트랩이 동일 주소)와 Vectored 모드(인터럽트는 원인별 오프셋 점프)를 지원합니다.

트랩 처리 흐름 다이어그램

RISC-V 트랩 처리 흐름 RISC-V S-mode 트랩 처리 흐름 트랩 발생 예외 / 인터럽트 / ecall 하드웨어 자동 동작 ① sepc ← PC (트랩 발생 주소) ② scause ← 트랩 원인 코드 ③ stval ← 부가 정보 (접근 주소 등) ④ sstatus.SPP ← 이전 특권 모드 ⑤ sstatus.SIE ← 0 (인터럽트 차단) PC ← stvec stvec.MODE에 따라 진입점 결정 Direct (MODE=0) 모든 트랩 → BASE 주소로 점프 Linux 커널 기본 모드 Vectored (MODE=1) 인터럽트 → BASE + 4×cause 예외는 BASE로 점프 (오프셋 없음) 소프트웨어 핸들러 — handle_exception sscratch 교환 · pt_regs에 컨텍스트 저장 (sepc/scause/stval) scause 최상위 비트로 인터럽트/예외 분기 인터럽트 (최상위 비트 1) do_irq() 호출 타이머(0x80..05) · 외부(0x80..09) 소프트웨어(0x80..01) IPI 처리 예외 (최상위 비트 0) excp_vect_table[cause]로 분기 페이지 폴트(13·15) · U-mode ecall(8) ecall은 sepc += 4 후 복귀 복귀 — ret_from_exception → sret pt_regs에서 컨텍스트 복원 후 sret 실행 PC ← sepc, sstatus.SPP로 이전 특권 모드 복원 scause 비트 레이아웃 (RV64) bit[63]: Interrupt 플래그 bit[62:0]: Exception Code 예) 0x80..05: 타이머 인터럽트 예) 13: Load 페이지 폴트 scause 최상위 비트로 판별 0이면 예외, 1이면 인터럽트 예외 코드는 pt_regs의 cause 필드로 저장되어 핸들러 분기 인덱스 사용
RISC-V 트랩 처리 흐름 — 트랩 발생 시 하드웨어가 기록하는 CSR(sepc·scause·stval·sstatus), stvec.MODE(Direct/Vectored)에 따른 진입점 선택, 커널 핸들러의 scause 분기, sepc 조정과 sret 복귀까지의 처리 경로, scause 비트 레이아웃을 나타낸 개념 개요도입니다.

트랩 벡터 설정 (stvec)

/* arch/riscv/kernel/head.S — stvec 초기 설정 */
    la      t0, handle_exception   /* 핸들러 주소 로드 */
    csrw    stvec, t0              /* Direct 모드 (MODE=0) */
    /* Vectored 모드 설정 시: */
    /* ori t0, t0, 1 → stvec에 MODE=1 설정 */

/* arch/riscv/kernel/entry.S — Direct 모드 핸들러 */
SYM_CODE_START(handle_exception)
    /* sscratch에는 커널 모드면 0, 유저 모드면 task_struct 포인터 */
    csrrw   tp, CSR_SCRATCH, tp    /* tp ↔ sscratch 교환 */
    bnez    tp, _save_context      /* 유저 모드에서 온 경우 */
    /* 커널 모드에서 온 경우: tp 복원 */
    csrr    tp, CSR_SCRATCH        /* 원래 tp 복원 */
_save_context:
    /* pt_regs 프레임 할당 및 레지스터 저장 */
    REG_S   ra, PT_RA(sp)
    REG_S   gp, PT_GP(sp)
    /* ... 나머지 레지스터 저장 ... */
SYM_CODE_END(handle_exception)

예외 디스패치(Dispatch) 테이블

excp_vect_table은 C 배열이 아니라 arch/riscv/kernel/entry.S에 정의된 어셈블리 포인터 테이블입니다. handle_exception은 scause를 인덱스로 삼아 테이블 항목을 읽고, 범위 검사를 통과한 뒤 해당 핸들러로 직접 점프합니다. 테이블 범위를 벗어난 원인은 do_trap_unknown으로 넘기며, 인터럽트는 테이블을 거치지 않고 do_irq()로 곧장 갑니다.

/* arch/riscv/kernel/entry.S — handle_exception의 인덱싱과 범위 검사 */
    /* scause 최상위 비트가 0이면 예외, 1이면 인터럽트 */
    bge     s4, zero, 1f
    call    do_irq                 /* 인터럽트: 테이블 미사용 */
    j       ret_from_exception
1:  /* 예외: scause에 포인터 크기를 곱해 바이트 오프셋으로 변환 */
    slli    t0, s4, RISCV_LGPTR
    la      t1, excp_vect_table
    la      t2, excp_vect_table_end
    add     t0, t1, t0
    bgeu    t0, t2, 3f            /* 테이블 범위 검사 */
    REG_L   t1, 0(t0)
2:  jalr    t1                    /* 원인별 핸들러 호출 */
    j       ret_from_exception
3:  la      t1, do_trap_unknown   /* 범위 밖 원인: 폴백 핸들러 */
    j       2b
/* arch/riscv/kernel/entry.S — 원인 번호별 핸들러 테이블 */
/* 인덱스 = scause 예외 코드 (arch/riscv/include/asm/csr.h EXC_*) */
    .align  LGREG
SYM_DATA_START_LOCAL(excp_vect_table)
    RISCV_PTR do_trap_insn_misaligned          /* 0 */
    ALT_INSN_FAULT(RISCV_PTR do_trap_insn_fault)    /* 1 */
    RISCV_PTR do_trap_insn_illegal              /* 2 */
    RISCV_PTR do_trap_break                      /* 3 */
    RISCV_PTR do_trap_load_misaligned            /* 4 */
    RISCV_PTR do_trap_load_fault                 /* 5 */
    RISCV_PTR do_trap_store_misaligned           /* 6 */
    RISCV_PTR do_trap_store_fault                /* 7 */
    RISCV_PTR do_trap_ecall_u                    /* 8: system call */
    RISCV_PTR do_trap_ecall_s                    /* 9 */
    RISCV_PTR do_trap_unknown                    /* 10 */
    RISCV_PTR do_trap_ecall_m                    /* 11 */
    ALT_PAGE_FAULT(RISCV_PTR do_page_fault)          /* 12: instruction page fault */
    RISCV_PTR do_page_fault                      /* 13: load page fault */
    RISCV_PTR do_trap_unknown                    /* 14 */
    RISCV_PTR do_page_fault                      /* 15: store page fault */
    RISCV_PTR do_trap_unknown                    /* cause=16 */
    RISCV_PTR do_trap_unknown                    /* cause=17 */
    RISCV_PTR do_trap_software_check             /* cause=18: software check */
    RISCV_PTR do_trap_hardware_error             /* hardware error (19) */
SYM_DATA_END_LABEL(excp_vect_table, SYM_L_LOCAL, excp_vect_table_end)
/* ALT_* 항목은 errata 설정에 따라 SiFive 전용 핸들러로 대체된다 */
/* arch/riscv/kernel/traps.c — 디스패치가 아닌 미처리 예외 핸들러 */
/* do_trap_unknown은 테이블을 인덱싱하지 않는다. DO_ERROR_INFO 매크로가 */
/* 생성한 핸들러로, 유저 모드 예외에는 SIGILL/ILL_ILLTRP 시그널을 보내고, */
/* 커널 모드 예외에는 fixup_exception() 실패 시 die()를 호출한다. */
DO_ERROR_INFO(do_trap_unknown,
    SIGILL, ILL_ILLTRP, "unknown exception");

scause 예외/인터럽트 코드

코드유형설명커널 핸들러
0예외명령어 주소 미정렬do_trap_insn_misaligned
1예외명령어 접근 폴트do_trap_insn_fault
2예외비정상 명령어do_trap_insn_illegal
3예외브레이크포인트do_trap_break
5예외Load 접근 폴트do_trap_load_fault
7예외Store/AMO 접근 폴트do_trap_store_fault
8예외U-mode ECALLdo_trap_ecall_u (syscall)
12예외명령어 페이지 폴트(Page Fault)do_page_fault
13예외Load 페이지 폴트do_page_fault
15예외Store/AMO 페이지 폴트do_page_fault
0x80..01인터럽트S-mode 소프트웨어 인터럽트IPI 처리
0x80..05인터럽트S-mode 타이머 인터럽트riscv_timer_interrupt
0x80..09인터럽트S-mode 외부 인터럽트PLIC/APLIC 핸들러

Vectored vs Direct 모드: Linux 커널은 기본적으로 Direct 모드를 사용합니다. Vectored 모드는 각 인터럽트 원인에 대해 별도 진입점(Entry Point)을 제공하여 디스패치 오버헤드를 줄일 수 있지만, 예외(exception)는 여전히 BASE로 점프합니다. RISC-V AIA(Advanced Interrupt Architecture)의 IMSIC과 함께 사용하면 더 세분화된 인터럽트 라우팅(Routing)이 가능합니다.

커널 어셈블리 실전 예제

RISC-V Linux 커널의 핵심 어셈블리 코드를 분석합니다. 시스템 콜(System Call) 진입, 컨텍스트 스위치, 원자적 연산(Atomic Operation), 유저 공간 접근, TLB 플러시 등 커널 동작의 근간을 이루는 어셈블리 구현을 실전 코드 기반으로 상세히 다룹니다.

시스템 콜 진입 (entry.S)

/* arch/riscv/kernel/entry.S — handle_exception (트랩 진입) */
/* 유저 모드에서 ecall 실행 시 scause = 8 (EXC_U_ECALL)로 트랩 발생 */
SYM_CODE_START(handle_exception):
    /* sepc(트랩을 유발한 명령어 주소)를 pt_regs에 저장 */
    csrr    s2, CSR_EPC
    REG_S   s2, PT_EPC(sp)
    csrr    s4, CSR_CAUSE          /* scause */
    move    a0, sp                 /* pt_regs 포인터 전달 */

    /* scause 최상위 비트가 0이면 예외, 1이면 인터럽트 */
    bge     s4, zero, 1f
    call    do_irq                 /* 인터럽트 처리 */
    j       ret_from_exception
1:  /* 예외: scause를 인덱스로 벡터 테이블 조회 */
    slli    t0, s4, RISCV_LGPTR
    la      t1, excp_vect_table
    add     t0, t1, t0
    REG_L   t1, 0(t0)
    jalr    t1                     /* scause=8 → do_trap_ecall_u */
/* arch/riscv/kernel/traps.c — 시스템 콜 처리는 C로 구현 */
void do_trap_ecall_u(struct pt_regs *regs)
{
    long syscall = regs->a7;

    regs->epc += 4;              /* ecall 명령어 다음 주소로 복귀 */
    regs->orig_a0 = regs->a0;      /* 원본 a0 보존 (재시작용) */
    regs->a0 = -ENOSYS;            /* 기본 반환값 */

    if (syscall >= 0 && syscall < NR_syscalls)
        syscall_handler(regs, syscall);   /* sys_call_table[syscall] 호출 */
}

컨텍스트 스위치 (__switch_to)

/* arch/riscv/kernel/entry.S — __switch_to */
/* a0 = prev task_struct, a1 = next task_struct */
SYM_FUNC_START(__switch_to)
    /* prev 태스크의 callee-saved 레지스터 저장 */
    sd      ra,  TASK_THREAD_RA(a0)
    sd      sp,  TASK_THREAD_SP(a0)
    sd      s0,  TASK_THREAD_S0(a0)
    sd      s1,  TASK_THREAD_S1(a0)
    sd      s2,  TASK_THREAD_S2(a0)
    sd      s3,  TASK_THREAD_S3(a0)
    sd      s4,  TASK_THREAD_S4(a0)
    sd      s5,  TASK_THREAD_S5(a0)
    sd      s6,  TASK_THREAD_S6(a0)
    sd      s7,  TASK_THREAD_S7(a0)
    sd      s8,  TASK_THREAD_S8(a0)
    sd      s9,  TASK_THREAD_S9(a0)
    sd      s10, TASK_THREAD_S10(a0)
    sd      s11, TASK_THREAD_S11(a0)

    /* next 태스크의 callee-saved 레지스터 복원 */
    ld      ra,  TASK_THREAD_RA(a1)
    ld      sp,  TASK_THREAD_SP(a1)
    ld      s0,  TASK_THREAD_S0(a1)
    ld      s1,  TASK_THREAD_S1(a1)
    /* ... s2-s11 복원 생략 ... */

    /* tp를 next 태스크로 전환 */
    mv      tp, a1                 /* current task = next */

    /* sscratch에 next task 포인터 저장 (트랩 시 사용) */
    csrw    CSR_SCRATCH, tp

    ret                            /* next 태스크의 ra로 복귀 */
SYM_FUNC_END(__switch_to)

원자적 연산: LR/SC 루프와 AMO

/* arch/riscv/include/asm/atomic.h — atomic_add */
/* LR/SC (Load-Reserved / Store-Conditional) 패턴 */
atomic_add:                       /* a0=val, a1=&atomic_t */
1:
    lr.w    t0, (a1)               /* Load-Reserved: t0 ← *a1, reservation 설정 */
    add     t1, t0, a0             /* t1 = 기존 값 + 추가 값 */
    sc.w    t2, t1, (a1)           /* Store-Conditional: *a1 ← t1 (조건부) */
    bnez    t2, 1b                 /* sc 실패(≠0) 시 재시도 */
    ret

/* AMO (Atomic Memory Operation) — 더 간단한 패턴 */
atomic_add_amo:                   /* a0=val, a1=&atomic_t */
    amoadd.w zero, a0, (a1)       /* *a1 += a0, 원래 값 버림 */
    ret

/* atomic_fetch_add: 이전 값 반환 */
atomic_fetch_add:
    amoadd.w.aqrl a0, a0, (a1)   /* a0 = 이전 값, acquire+release */
    ret

/* cmpxchg: LR/SC 기반 compare-and-exchange */
cmpxchg:                          /* a0=&val, a1=expected, a2=new */
1:
    lr.d.aq t0, (a0)               /* Load-Reserved with acquire */
    bne     t0, a1, 2f             /* expected와 불일치 시 실패 */
    sc.d.rl t1, a2, (a0)           /* Store-Conditional with release */
    bnez    t1, 1b                 /* sc 실패 시 재시도 */
2:
    mv      a0, t0                 /* 이전 값 반환 */
    ret

유저 공간 접근 (copy_to_user)

/* arch/riscv/lib/uaccess.S — __asm_copy_to_user */
/* a0=dst(user), a1=src(kernel), a2=len */
SYM_FUNC_START(__asm_copy_to_user)
    /* 유저 주소 범위 확인은 C 래퍼에서 수행 */
    beqz    a2, 3f                 /* len=0이면 즉시 리턴 */
1:
    lb      t0, 0(a1)              /* 커널 메모리 로드 */
2:
    sb      t0, 0(a0)              /* 유저 메모리 저장 (fault 가능) */
    addi    a0, a0, 1
    addi    a1, a1, 1
    addi    a2, a2, -1
    bnez    a2, 1b
3:
    mv      a0, a2                 /* 남은 바이트 수 반환 (0=성공) */
    ret

    /* fixup: 유저 접근 fault 시 여기로 점프 */
    _ASM_EXTABLE(1b, 3b)            /* 로드 fault → 남은 바이트 반환 */
    _ASM_EXTABLE(2b, 3b)            /* 저장 fault → 남은 바이트 반환 */
SYM_FUNC_END(__asm_copy_to_user)

TLB 플러시 (sfence.vma)

/* arch/riscv/mm/tlbflush.c — SBI 기반 원격 TLB 플러시 */
/* 로컬 TLB 플러시: 어셈블리 인라인 */

/* sfence.vma rs1, rs2 변형: */
    sfence.vma  zero, zero         /* 전체 TLB 플러시 (모든 ASID) */
    sfence.vma  a0, zero           /* VA 기반: 주소 a0의 TLB 엔트리 */
    sfence.vma  zero, a1           /* ASID 기반: a1 ASID의 모든 엔트리 */
    sfence.vma  a0, a1             /* VA+ASID: 가장 세밀한 플러시 */

/* 리모트 TLB 플러시: SBI IPI 사용 */
/* flush_tlb_range()는 SBI_EXT_RFENCE로 다른 하트에 IPI 전송 */
/*   → sbi_remote_sfence_vma(cpumask, start, size) */
/*   → 각 타겟 하트에서 sfence.vma 실행 */

_ASM_EXTABLE 메커니즘: RISC-V 커널은 _ASM_EXTABLE(from, to) 매크로(Macro)로 유저 접근 폴트 복구 테이블을 구성합니다. 유저 메모리 접근(2번 레이블)에서 페이지 폴트가 발생하면, 폴트 핸들러가 __exception_table을 검색하여 fixup 코드(3번 레이블)로 점프합니다. 이 방식은 x86의 _ASM_EXTABLE_UA와 동일한 원리이며, -EFAULT 대신 미복사 바이트 수를 반환하는 것이 RISC-V 관례입니다.

PLIC/CLINT/ACLINT 인터럽트 컨트롤러(Interrupt Controller)

RISC-V 시스템의 인터럽트 컨트롤러 아키텍처는 세 가지 주요 구성 요소로 이루어집니다. CLINT(Core-Local Interruptor)는 타이머와 소프트웨어 인터럽트(IPI)를 처리하고, PLIC(Platform-Level Interrupt Controller)는 외부 장치 인터럽트를 하트(hart)로 라우팅합니다. 최신 스펙에서는 CLINT를 대체하는 ACLINT(Advanced CLINT)와 PLIC를 대체하는 APLIC(Advanced PLIC) + IMSIC(Incoming MSI Controller) 조합이 정의되어 있습니다.

PLIC 아키텍처 다이어그램

RISC-V PLIC 아키텍처 PLIC (Platform-Level Interrupt Controller) 아키텍처 인터럽트 소스 UART (IRQ 10) Ethernet (IRQ 11) GPIO (IRQ 12) ... (최대 1023) Gateway 에지/레벨 변환 중복 억제 PLIC 코어 Pending IP[1..1023] Priority 소스별 우선순위 (0=비활성) Context Enable 비트맵 Threshold Claim/Complete 하트×모드별 독립 context Hart 0 S-mode / M-mode Hart 1 S-mode / M-mode Hart N S-mode / M-mode Claim/Complete 프로토콜 ① 외부 인터럽트 수신 ② Claim 읽기 → 최우선 IRQ 번호 ③ IRQ 핸들러 실행 ④ Complete 쓰기 → Gateway 재활성화 PLIC MMIO 레지스터 맵 (base + 오프셋, context 0 기준) 0x000000 Priority (4B × 1024) 0x001000 Pending (1024비트 = 128B) 0x002000 Enable (context별 128B) 0x200000 Threshold (context별 4B) 0x200004 Claim/Complete (4B)
RISC-V PLIC 아키텍처 — 외부 인터럽트 소스가 게이트웨이와 PLIC 코어(Pending·우선순위(Priority)·Context)를 거쳐 각 Hart로 전달되는 경로, Claim/Complete 프로토콜, MMIO 레지스터 배치를 함께 나타낸 개념 개요도입니다.

PLIC 레지스터 접근

/* drivers/irqchip/irq-sifive-plic.c */
struct plic_priv {
    void __iomem *regs;
    unsigned long plic_quirks;
};

/* 인터럽트 Claim (읽기) */
static u32 plic_claim(struct plic_handler *handler)
{
    return readl(handler->hart_base + CONTEXT_CLAIM);
}

/* 인터럽트 Complete (쓰기) */
static void plic_complete(struct plic_handler *handler, u32 hwirq)
{
    writel(hwirq, handler->hart_base + CONTEXT_CLAIM);
}

/* PLIC IRQ 핸들러 — chained handler */
static void plic_handle_irq(struct irq_desc *desc)
{
    struct plic_handler *handler = this_cpu_ptr(&plic_handlers);
    u32 hwirq;

    chained_irq_enter(chip, desc);
    while ((hwirq = plic_claim(handler))) {
        int irq = irq_find_mapping(handler->priv->irqdomain, hwirq);
        if (unlikely(irq <= 0))
            pr_warn("unexpected irq %d\n", hwirq);
        else
            generic_handle_irq(irq);
        plic_complete(handler, hwirq);
    }
    chained_irq_exit(chip, desc);
}

CLINT 타이머/IPI 설정

/* CLINT MMIO 레지스터 레이아웃 */
/* 0x0000: msip[hart]     — 소프트웨어 인터럽트 (IPI) */
/* 0x4000: mtimecmp[hart] — 타이머 비교 레지스터 */
/* 0xBFF8: mtime          — 글로벌 타이머 카운터 */

/* drivers/clocksource/timer-riscv.c — riscv_clock_next_event() */
static int riscv_clock_next_event(unsigned long delta,
                                  struct clock_event_device *ce)
{
    u64 next_tval = get_cycles64() + delta;

    if (static_branch_likely(&riscv_sstc_available)) {
        /* Sstc 확장: stimecmp CSR을 직접 기록 (S-mode 타이머) */
        csr_write(CSR_STIMECMP, next_tval);
    } else {
        /* SBI 경로: S-mode는 mtimecmp에 직접 접근할 수 없어 SBI 호출 사용 */
        sbi_set_timer(next_tval);
    }
    return 0;
}

/* Sstc 확장이 있으면 SBI 없이 stimecmp CSR 직접 사용 */
static int riscv_clock_next_event_sstc(unsigned long delta,
                                       struct clock_event_device *ce)
{
    csr_write(CSR_STIMECMP, get_cycles64() + delta);
    return 0;
}

/* IPI 전송: ACLINT MSWI 또는 SBI 사용 */
static void riscv_send_ipi(const struct cpumask *mask)
{
    if (ipi_ops && ipi_ops->ipi_inject)
        ipi_ops->ipi_inject(mask);  /* ACLINT 직접 접근 */
    else
        sbi_send_ipi(mask);         /* SBI 경유 */
}

PLIC에서 AIA로의 진화: 기존 PLIC는 최대 1023개 소스만 지원하고 MSI를 직접 지원하지 않는 한계가 있습니다. AIA(Advanced Interrupt Architecture)는 APLIC(Wired → MSI 변환) + IMSIC(MSI 수신, 하트별)으로 구성되어 PCIe MSI-X를 네이티브로 지원합니다. Linux 커널은 drivers/irqchip/irq-riscv-aplic-*과 irq-riscv-imsic-*에서 AIA를 지원합니다. Sstc 확장은 S-mode에서 stimecmp CSR로 타이머를 직접 제어할 수 있게 하여 SBI 호출 오버헤드를 제거합니다.

커스텀 확장과 Discovery

RISC-V의 가장 큰 특징은 모듈형 확장(extension) 시스템입니다. 표준 확장(M, A, F, D, C, V 등) 외에도 벤더별 커스텀 확장을 자유롭게 추가할 수 있으며, 커널은 부팅 시 하드웨어가 지원하는 확장을 감지하여 런타임에 최적화된 코드 경로를 선택합니다. ISA 문자열 파싱, riscv_hwprobe() syscall, AT_HWCAP 등 다양한 디스커버리 메커니즘이 존재합니다.

ISA 문자열과 /proc/cpuinfo

/* arch/riscv/kernel/cpu.c — /proc/cpuinfo 생성 */
/* DT의 riscv,isa 또는 riscv,isa-extensions 프로퍼티에서 파싱 */

/* 예시 /proc/cpuinfo 출력: */
/* processor    : 0                                */
/* hart         : 0                                */
/* isa          : rv64imafdc_zicntr_zicsr_zifencei  */
/*                _zihpm_zba_zbb_zbs_svadu          */
/* mmu          : sv39                             */

/* arch/riscv/kernel/cpufeature.c — ISA 문자열 파싱 */
static void riscv_fill_hwcap_from_isa_string(unsigned long *isa2hwcap)
{
    struct device_node *node;
    const char *isa;

    for_each_possible_cpu_node(node) {
        if (of_property_read_string(node, "riscv,isa-extensions", &isa))
            of_property_read_string(node, "riscv,isa", &isa);

        /* "rv64" 접두사 건너뛰기 */
        /* 단일 문자 확장(i,m,a,f,d,c,v) 파싱 */
        /* 멀티 문자 확장(_zba_zbb_zbs...) 파싱 */
        /* 벤더 확장(_xtheadba_xtheadbb...) 파싱 */
    }
}

riscv_isa_extension API

/* arch/riscv/include/asm/hwcap.h */
/* 확장 존재 여부를 비트맵으로 검사 */

/* 정적 키 기반 빠른 검사 (핫 패스용) */
static __always_inline bool
riscv_has_extension_likely(unsigned int ext)
{
    if (IS_ENABLED(CONFIG_RISCV_ALTERNATIVE))
        return __riscv_isa_extension_available(NULL, ext);
    return __riscv_isa_extension_available(riscv_isa, ext);
}

/* 사용 예시 */
if (riscv_has_extension_likely(RISCV_ISA_EXT_ZBB)) {
    /* Zbb (기본 비트 조작) 확장 사용 */
    count = __builtin_popcountl(val);  /* cpop 명령어 활용 */
} else {
    /* 소프트웨어 대체 구현 */
    count = hweight_long(val);
}

/* riscv_hwprobe() syscall — 유저 공간 디스커버리 */
/* struct riscv_hwprobe { __s64 key; __u64 value; }; */
/* RISCV_HWPROBE_KEY_IMA_EXT_0 → Zba, Zbb, Zbs 등 비트마스크 */
/* RISCV_HWPROBE_KEY_CPUPERF_0 → 마이크로아키텍처 성능 힌트 */

주요 표준 확장 목록

확장버전설명커널 지원
Zicbom1.0Cache Block 관리 (cbo.clean/flush/inval)RISCV_ISA_EXT_ZICBOM
Zicboz1.0Cache Block Zero (cbo.zero)RISCV_ISA_EXT_ZICBOZ
Zba1.0주소 생성 (sh1add, sh2add, sh3add)RISCV_ISA_EXT_ZBA
Zbb1.0기본 비트 조작 (clz, ctz, cpop, rev8)RISCV_ISA_EXT_ZBB
Zbs1.0단일 비트 연산 (bset, bclr, bext, binv)RISCV_ISA_EXT_ZBS
Zicntr2.0기본 카운터 (cycle, time, instret)기본 포함
Svadu1.0HW A/D 비트 자동 갱신RISCV_ISA_EXT_SVADU
Svnapot1.0NAPOT 대형 페이지 PTERISCV_ISA_EXT_SVNAPOT
Svpbmt1.0PTE 기반 메모리 타입 (NC/IO/PMA)RISCV_ISA_EXT_SVPBMT
Sstc1.0S-mode 타이머 비교 CSRRISCV_ISA_EXT_SSTC
Sscofpmf1.0PMU 카운터 오버플로 인터럽트RISCV_ISA_EXT_SSCOFPMF

벤더 확장 네이밍: RISC-V 스펙은 벤더별 커스텀 확장에 X 접두사를 사용합니다. 예를 들어 T-Head(Alibaba)는 Xtheadba, Xtheadbb, Xtheadcondmov 등을 정의하며, 커널은 이를 감지하여 alternatives 패칭으로 표준 구현을 벤더 최적화 코드로 교체합니다. 멀티 문자 확장은 Z(표준), S(supervisor), H(hypervisor), X(벤더) 접두사로 구분합니다.

PMP (Physical Memory Protection)

PMP(Physical Memory Protection)는 RISC-V의 하드웨어 기반 물리 메모리(Physical Memory) 보호 메커니즘입니다. M-mode 펌웨어(OpenSBI)가 PMP 레지스터를 설정하여 S-mode(커널)와 U-mode(유저)의 물리 메모리 접근 범위를 제한합니다. PMP가 없으면 S-mode에서 전체 물리 메모리에 접근할 수 있어 보안상 위험합니다. PMP 엔트리는 pmpcfg(구성)과 pmpaddr(주소) CSR 쌍으로 구성됩니다.

PMP 엔트리 매칭 다이어그램

PMP 엔트리 매칭 모드 (TOR/NA4/NAPOT) PMP 주소 매칭 모드 pmpcfg[i] 비트 레이아웃 (8비트) 7 6 5 4 3 2 1 0 L Rsv A[1:0] X W R Lock 예약 매칭 모드 실행 쓰기 읽기 A[1:0] 값이 매칭 모드를 선택 TOR A = 01 Top of Range pmpaddr[i-1] ≤ 주소 < pmpaddr[i] 보호 영역 pmpaddr[i-1] pmpaddr[i] NA4 A = 10 Naturally Aligned 4-byte 고정 4바이트 단위 보호 주소: pmpaddr[i] × 4 부터 4바이트 G≥1이면 선택 불가 (G=0에서만) NAPOT A = 11 Naturally Aligned Power-of-Two 크기: pmpaddr의 하위 연속 1 비트로 인코딩 yyyy...y0111 → 64B 영역 yyyy...y01111 → 128B 영역 PMP 매칭 우선순위 (첫 매칭 규칙) PMP0 (최고) PMP1 … PMP15 매칭 없음 → M-mode만 접근 * 번호가 낮은 PMP 엔트리가 높은 우선순위 (첫 매칭 규칙) * L(Lock) 비트가 설정되면 M-mode에서도 해당 규칙 적용, 리셋 전까지 변경 불가
PMP 엔트리 매칭 모드 — pmpcfg 8비트 필드(L·예약·A[1:0]·X·W·R)와 A[1:0] 값(01/10/11)이 선택하는 TOR/NA4/NAPOT 주소 매칭 방식, 그리고 PMP0부터 PMP15까지의 매칭 우선순위를 나타낸 상세도입니다.

OpenSBI PMP 설정

/* OpenSBI: lib/sbi/sbi_hart.c — PMP 초기 설정 */
/* S-mode(커널)가 접근할 수 있는 메모리 영역 정의 */

/* PMP 설정 예시: */
/* PMP0: OpenSBI 펌웨어 영역 보호 (S-mode 접근 금지) */
/* PMP1: 나머지 전체 메모리 접근 허용 (S-mode RWX) */

static int sbi_hart_pmp_configure(struct sbi_scratch *scratch)
{
    /* PMP0: OpenSBI 펌웨어 영역을 S-mode에서 접근 불가로 설정 */
    unsigned long fw_start = scratch->fw_start;
    unsigned long fw_size  = scratch->fw_size;

    /* TOR 모드로 펌웨어 영역 보호 */
    pmp_set(0, 0, fw_start,
            PMP_A_TOR | PMP_L);            /* Locked, no access */
    pmp_set(1, PMP_R | PMP_W | PMP_X,
            fw_start + fw_size,
            PMP_A_TOR | PMP_L);            /* Locked, no RWX */

    /* PMP2: 나머지 전체 메모리를 S-mode에 RWX 허용 */
    pmp_set(2, PMP_R | PMP_W | PMP_X,
            0xFFFFFFFFFFFFFFFFUL,
            PMP_A_NAPOT);                  /* 전체 주소 공간 */

    return 0;
}

커널에서의 PMP 활용

/* arch/riscv/kernel/cpufeature.c — PMP 엔트리 수 감지 */
/* RISC-V 스펙: 최소 0개, 최대 64개 PMP 엔트리 지원 */

/* 커널은 PMP를 직접 설정하지 않음 (M-mode 전용 CSR) */
/* 대신 SBI를 통해 PMP 관련 정보를 조회 */

/* Smepmp (Machine-mode enhancement for PMP): */
/* - M-mode 코드의 실행 권한을 PMP로 제한 가능 */
/* - mseccfg CSR로 MML/MMWP/RLB 비트 제어 */
/* - MML=1: Machine Mode Lockdown — M-mode도 PMP 적용 */
/* - MMWP=1: 기본 거부 정책 (PMP 미매칭 시 M-mode도 거부) */
/* - RLB=1: Rule Locking Bypass (디버그용) */

/* ePMP (enhanced PMP) 지원 확인 */
if (sbi_probe_extension(SBI_EXT_FWFT)) {
    /* Firmware Feature 확장으로 Smepmp 상태 조회 */
}

PMP 보안 주의사항: PMP 엔트리 수가 부족하면 보안 취약점(Vulnerability)이 발생할 수 있습니다. 엔트리 0개인 구현체는 S-mode에서 전체 물리 메모리에 접근 가능합니다. Smepmp(ePMP) 확장은 M-mode 코드도 PMP 규칙에 따르게 하여 보안을 강화합니다. Lock(L) 비트가 설정된 PMP 엔트리는 리셋 전까지 변경할 수 없으므로, OpenSBI가 부팅 시 설정한 펌웨어 보호 영역은 커널이 우회할 수 없습니다.

H 확장 (하이퍼바이저(Hypervisor))

RISC-V H 확장(Hypervisor Extension)은 효율적인 하드웨어 가상화(Virtualization)를 지원합니다. HS-mode(Hypervisor-extended Supervisor)에서 하이퍼바이저가 동작하고, VS-mode(Virtual Supervisor)/VU-mode(Virtual User)에서 게스트 OS와 유저 프로그램이 실행됩니다. 2단계 주소 변환(VS-stage + G-stage)으로 게스트 물리 주소를 호스트 물리 주소로 변환하며, KVM이 이를 활용합니다.

2단계 주소 변환 다이어그램

H 확장 2단계 주소 변환 (Two-Stage Translation) H 확장 2단계 주소 변환 (Two-Stage Translation) 게스트 (VS/VU-mode) 하이퍼바이저 (HS-mode) GVA 게스트 가상 주소 ① VS-stage vsatp 게스트 페이지 테이블 GPA 게스트 물리 주소 ② G-stage hgatp 호스트 페이지 테이블 HPA 호스트 물리 주소 물리 메모리 (DRAM) H 확장 주요 CSR 가상 S-mode CSR vsstatus — VS 상태 vsie/vsip — VS 인터럽트 vstvec — VS 트랩 벡터 vsscratch — VS 스크래치 vsepc/vscause/vstval vsatp — VS 주소 변환 하이퍼바이저 CSR hstatus — H 상태 제어 hedeleg — 예외 위임 hideleg — 인터럽트 위임 hgatp — G-stage 페이지 테이블 htval — 게스트 PA (폴트 시) htinst — 트래핑 명령어 Fence 명령어 hfence.vvma — VS TLB 플러시 hfence.gvma — G-stage 플러시 hlv/hsv — 게스트 메모리 접근 hfence.vvma: 게스트 ASID별 hfence.gvma: VMID별 KVM 활용 VM entry: hstatus.SPV=1 VM exit: 트랩 → HS-mode hgatp ← 게스트 PT 루트 VMID: TLB 태깅 SBI EXT_COVH (기밀 VM)
RISC-V H 확장 2단계 주소 변환 — 게스트 가상 주소(GVA)가 vsatp가 지시하는 VS-stage를 거쳐 게스트 물리 주소(GPA)가 되고, 다시 hgatp가 지시하는 G-stage를 거쳐 호스트 물리 주소(HPA)로 변환되어 물리 메모리에 도달하는 흐름을 중심으로, H 확장 CSR·hfence 명령어·KVM 활용 방식을 함께 정리한 개념 개요도입니다.

VS-mode CSR 접근과 VM 진입/종료

/* arch/riscv/kvm/vcpu.c — KVM vCPU 실행 */
int kvm_arch_vcpu_ioctl_run(struct kvm_vcpu *vcpu)
{
    struct kvm_run *run = vcpu->run;

    /* 게스트 CSR 복원 */
    kvm_riscv_vcpu_swap_in_guest_csrs(vcpu);

    /* G-stage 페이지 테이블 설정 */
    csr_write(CSR_HGATP, vcpu->arch.guest_context.hgatp);

    /* hstatus.SPV = 1 → sret 시 VS-mode로 진입 */
    csr_set(CSR_HSTATUS, HSTATUS_SPV);

    /* 게스트 레지스터 복원 후 sret → VM entry */
    __kvm_riscv_switch_to(&vcpu->arch);

    /* VM exit: 트랩이 HS-mode로 복귀 */
    unsigned long scause = csr_read(CSR_SCAUSE);
    unsigned long htval  = csr_read(CSR_HTVAL);

    /* htval: 게스트 물리 주소 (GPA) — 상위 비트 2만큼 시프트 */
    unsigned long gpa = htval << 2;

    /* exit 원인에 따라 처리 */
    switch (scause) {
    case EXC_VIRTUAL_INST_FAULT:
        kvm_riscv_vcpu_virtual_insn(vcpu, run, trap);
        break;
    case EXC_INST_GUEST_PAGE_FAULT:
    case EXC_LOAD_GUEST_PAGE_FAULT:
    case EXC_STORE_GUEST_PAGE_FAULT:
        kvm_riscv_vcpu_exit(vcpu, run, trap);
        break;
    }
    return 0;
}

hfence 명령어

/* 게스트 TLB 관리 명령어 */

/* hfence.vvma — VS-stage TLB 플러시 (현재 VMID) */
    hfence.vvma zero, zero          /* 전체 VS TLB 플러시 */
    hfence.vvma a0, zero             /* 특정 GVA의 VS TLB 플러시 */
    hfence.vvma zero, a1             /* 특정 게스트 ASID의 VS TLB */
    hfence.vvma a0, a1               /* GVA + 게스트 ASID */

/* hfence.gvma — G-stage TLB 플러시 (GPA→HPA 매핑) */
    hfence.gvma zero, zero          /* 전체 G-stage TLB 플러시 */
    hfence.gvma a0, zero             /* 특정 GPA의 G-stage TLB */
    hfence.gvma zero, a1             /* 특정 VMID의 G-stage TLB */
    hfence.gvma a0, a1               /* GPA + VMID */

/* hlv/hsv — HS-mode에서 게스트 메모리 직접 접근 */
    hlv.d   t0, (a0)                 /* 게스트 가상 주소에서 8바이트 로드 */
    hsv.d   t0, (a0)                 /* 게스트 가상 주소에 8바이트 저장 */
    /* hlv/hsv는 VS-stage + G-stage 변환을 모두 수행 */

RISC-V KVM 특징: RISC-V KVM은 H 확장을 필수로 요구합니다. hgatp의 VMID 필드로 TLB 태깅하여 VM 전환 시 TLB 플러시를 최소화합니다. hedeleg/hideleg를 통해 게스트가 처리할 예외/인터럽트를 VS-mode로 위임하여 VM exit를 줄입니다. AIA(Advanced Interrupt Architecture)의 IMSIC과 결합하면 게스트에 가상 인터럽트 파일을 제공하여 인터럽트 관련 VM exit를 추가로 제거할 수 있습니다.

CMO (Cache Management Operations)

RISC-V CMO(Cache Management Operations) 확장은 소프트웨어가 캐시를 명시적으로 관리할 수 있는 명령어를 제공합니다. Zicbom(Cache Block Management: clean/flush/inval)과 Zicboz(Cache Block Zero)로 구성되며, DMA 일관성 유지, 보안을 위한 메모리 제로화, 성능 최적화에 활용됩니다. 기존 RISC-V 기본 ISA에는 캐시 관리 명령어가 없어 벤더별 비표준 확장에 의존했으나, CMO 확장으로 표준화되었습니다.

CBO 명령어

/* Zicbom — Cache Block Management Operations */
/* 캐시 블록 크기는 구현체에 따라 다름 (보통 64바이트) */

/* cbo.clean — 캐시 라인을 메모리에 쓰기 (dirty → clean) */
    cbo.clean  (a0)                 /* a0 주소의 캐시 블록 clean */
    /* 용도: DMA 전송 전 더티 데이터 메모리 반영 */

/* cbo.flush — clean 후 무효화 (dirty → memory, then invalidate) */
    cbo.flush  (a0)                 /* a0 주소의 캐시 블록 flush */
    /* 용도: DMA 전송 후 수신 버퍼의 stale 데이터 제거 */

/* cbo.inval — 캐시 라인 무효화 (dirty 데이터 버림) */
    cbo.inval  (a0)                 /* a0 주소의 캐시 블록 invalidate */
    /* 주의: dirty 데이터가 버려짐 — 주로 DMA 수신 전 사용 */

/* Zicboz — Cache Block Zero */
    cbo.zero   (a0)                 /* a0 주소의 캐시 블록을 0으로 채움 */
    /* 용도: 페이지 초기화 (allocate 없이 직접 저장) */
    /* clear_page()에서 memset 대신 사용 → 성능 향상 */

/* 캐시 블록 크기 순회 루프 */
flush_cache_range:                /* a0=start, a1=end */
1:  cbo.flush (a0)
    add     a0, a0, a2             /* a2 = cbom_block_size */
    bltu    a0, a1, 1b
    ret

DMA 캐시 관리 시퀀스

/* arch/riscv/mm/dma-noncoherent.c — 비캐시 일관성 DMA 관리 */
void arch_sync_dma_for_device(phys_addr_t paddr, size_t size,
                              enum dma_data_direction dir)
{
    void *vaddr = phys_to_virt(paddr);

    switch (dir) {
    case DMA_TO_DEVICE:
        /* CPU → 디바이스: dirty 캐시를 메모리에 반영 */
        ALT_CMO_OP(clean, vaddr, size, riscv_cbom_block_size);
        break;
    case DMA_FROM_DEVICE:
        /* 디바이스 → CPU: stale 캐시 무효화 */
        ALT_CMO_OP(inval, vaddr, size, riscv_cbom_block_size);
        break;
    case DMA_BIDIRECTIONAL:
        /* 양방향: flush (clean + invalidate) */
        ALT_CMO_OP(flush, vaddr, size, riscv_cbom_block_size);
        break;
    }
}

/* ALT_CMO_OP 매크로: Zicbom이 없으면 대체 구현 사용 */
/* alternatives 메커니즘으로 부팅 시 CBO 명령어 패칭 */
/* riscv_cbom_block_size: DT cbom-block-size 프로퍼티에서 파싱 */

/* cbo.zero를 clear_page에 활용 */
void clear_page(void *page)
{
    if (riscv_has_extension_likely(RISCV_ISA_EXT_ZICBOZ)) {
        for (int i = 0; i < PAGE_SIZE; i += riscv_cboz_block_size)
            cbo_zero(page + i);
    } else {
        memset(page, 0, PAGE_SIZE);
    }
}

CMO와 캐시 일관성(Cache Coherency): RISC-V는 아키텍처 수준에서 캐시 일관성을 보장하지 않습니다. 멀티코어 시스템에서 하드웨어 일관성 프로토콜(예: MOESI)을 구현할 수도 있지만, 비일관성(non-coherent) DMA를 사용하는 SoC도 많습니다. Zicbom은 비일관성 DMA에서 필수이며, ALT_CMO_OP 매크로를 통해 Zicbom 지원 여부에 따라 런타임에 CBO 명령어 또는 대체 구현(캐시 전체 플러시 등)을 선택합니다.

alternatives 패칭과 errata

RISC-V Linux 커널의 alternatives 프레임워크는 부팅 시 CPU 확장 지원 여부에 따라 코드를 동적으로 패칭합니다. x86의 alternatives와 유사한 개념으로, 기본(fallback) 코드와 최적화된 대체(alternative) 코드를 준비하고, 런타임에 하드웨어 기능이 감지되면 대체 코드로 교체합니다. 벤더별 errata 수정에도 동일한 메커니즘이 사용됩니다.

ALTERNATIVE 매크로

/* arch/riscv/include/asm/alternative-macros.h */
/* ALTERNATIVE(old_content, new_content, vendor_id, patch_id, enable) */

/* 예시: Zbb 확장이 있으면 최적화된 명령어 사용 */
static __always_inline int arch_fls(unsigned int x)
{
    int r;
    asm volatile(
        ALTERNATIVE(
            /* 기본 구현: 소프트웨어 FLS */
            "li %0, 32\n"               /* fallback: 루프 기반 */
            "beqz %1, 1f\n"
            "clz %0, %1\n"              /* pseudo: SW clz */
            "1:\n",
            /* Zbb 대체: 하드웨어 CLZ 명령어 */
            "clz %0, %1\n"              /* HW clz 명령어 */
            "neg %0, %0\n"
            "addi %0, %0, 32\n",
            0,                           /* vendor_id: 0 = 표준 */
            RISCV_ISA_EXT_ZBB,           /* patch_id */
            CONFIG_RISCV_ISA_ZBB)        /* Kconfig */
        : "=r"(r) : "r"(x));
    return r;
}

패칭 엔진

/* arch/riscv/kernel/alternative.c */
/* 부팅 시 __alt_start ~ __alt_end 섹션을 순회하며 패칭 */

struct alt_entry {
    void *old_ptr;       /* 원본 코드 주소 */
    void *alt_ptr;       /* 대체 코드 주소 */
    unsigned long vendor_id;  /* 0: 표준, >0: 벤더별 */
    unsigned int alt_len;    /* 대체 코드 길이 */
    unsigned int old_len;    /* 원본 코드 길이 (NOP 패딩용) */
};

void riscv_cpufeature_patch_func(struct alt_entry *begin,
                                struct alt_entry *end,
                                unsigned int stage)
{
    for (struct alt_entry *a = begin; a < end; a++) {
        if (!riscv_cpufeature_patch_check(a->vendor_id, a->alt_len))
            continue;

        /* 대체 코드를 원본 위치에 복사 */
        patch_text_nosync(a->old_ptr, a->alt_ptr, a->alt_len);

        /* 원본이 더 길면 나머지를 NOP으로 채움 */
        if (a->old_len > a->alt_len)
            patch_text_nosync(a->old_ptr + a->alt_len,
                             nops, a->old_len - a->alt_len);
    }

    /* 패칭 완료 후 I-cache 동기화 */
    local_flush_icache_all();
}

벤더 errata 패칭 (T-Head, SiFive)

/* arch/riscv/errata/thead/errata.c — T-Head 에라타 */
/* Alibaba T-Head C906/C910 CPU의 하드웨어 버그 수정 */

static bool errata_probe_cmo(unsigned int stage,
                             unsigned long arch_id,
                             unsigned long impid)
{
    /* T-Head C9xx는 표준 Zicbom 대신 자체 캐시 명령어 사용 */
    /* th.dcache.civa (clean+inval by VA) */
    /* th.dcache.iva  (inval by VA) */
    /* th.dcache.cva  (clean by VA) */
    if (arch_id == THEAD_VENDOR_ID &&
        impid == THEAD_IMPID_C9XX) {
        return true;  /* T-Head CMO 에라타 적용 */
    }
    return false;
}

/* arch/riscv/errata/sifive/errata.c — SiFive 에라타 */
/* SiFive U74 (FU740) 에라타: CIP-1200 */
/* 특정 조건에서 I-cache가 stale 데이터 반환 */
/* 수정: sfence.vma 전후 fence.i 삽입 */

static bool errata_probe_cip1200(unsigned int stage,
                                 unsigned long arch_id,
                                 unsigned long impid)
{
    if (arch_id != SIFIVE_VENDOR_ID)
        return false;
    /* U74 코어에서만 sfence.vma에 fence.i 추가 */
    return (impid == SIFIVE_IMPID_U74);
}

alternatives 패칭 시점: 패칭은 두 단계로 수행됩니다. Stage 1(EARLY)은 SMP 초기화 전 부트 CPU에서만 실행되며, 기본 ISA 확장(M, A, Zbb 등)을 감지합니다. Stage 2(BOOT)는 모든 CPU가 온라인된 후 공통으로 지원하는 확장만 패칭합니다. 이종(heterogeneous) 하트 구성에서는 모든 하트가 공통으로 지원하는 확장만 활성화됩니다. fence.i와 sfence.vma로 I-cache를 동기화한 후 패칭된 코드가 실행됩니다.

디버그 모듈과 Trigger

RISC-V Debug 스펙은 외부 디버거(JTAG/OpenOCD)와 자체 트리거(hardware breakpoint/watchpoint) 두 가지 디버그 메커니즘을 정의합니다. Debug Module(DM)은 하트를 정지시키고 레지스터/메모리를 검사하는 외부 인터페이스이며, Trigger Module은 주소/데이터 매칭 시 브레이크포인트/워치포인트를 생성합니다. Linux 커널은 Trigger Module을 perf와 ptrace를 통해 하드웨어 브레이크포인트로 노출합니다.

디버그 모듈 아키텍처

RISC-V 디버그 모듈 아키텍처 RISC-V 디버그 아키텍처 외부 디버거 GDB + OpenOCD JTAG/cJTAG DTM Debug Transport Module (JTAG 드라이버/스캐너) Debug Module (DM) dmcontrol — 하트 정지/재개/리셋 dmstatus — 하트 상태/접속 command — 추상 명령 실행 progbuf/data — 레지스터 접근 Hart Debug I/F D-mode (Debug Mode) dcsr — Debug Control/Status dpc — Debug PC dscratch0/1 Trigger Module (Sdtrig) Type 2: mcontrol 주소/데이터 매칭 BP/WP (상세한 match 제어) Type 6: mcontrol6 확장 매칭 + chain VS/VU 모드 지원 Type 3: icount 명령어 카운터 기반 단일 스텝/조건적 중단 Type 4: itrigger 인터럽트 트리거 Type 5: etrigger tselect → tdata1(type+config) → tdata2(match value) → tdata3(context) Action=0: 브레이크포인트 예외 발생 Action=1: Debug Mode 진입 Trace/기타 액션
RISC-V 디버그 모듈 아키텍처 — 외부 디버거-JTAG/DTM-DM-하트 디버그 인터페이스 경로와 Trigger Module(Sdtrig)의 주요 트리거 타입과 tdata CSR 구성을 나타낸 개념 개요도입니다.

Trigger CSR 설정

/* arch/riscv/kernel/hw_breakpoint.c */
/* Trigger CSR을 사용한 하드웨어 브레이크포인트/워치포인트 */

/* Trigger 관련 CSR: */
/* tselect  — 활성 트리거 인덱스 선택 (0, 1, 2, ...) */
/* tdata1   — 트리거 타입 및 구성 (mcontrol/mcontrol6) */
/* tdata2   — 매칭 값 (주소 또는 데이터) */
/* tdata3   — 추가 매칭 (mhvalue, sselect 등) */
/* tinfo    — 지원하는 트리거 타입 비트마스크 (읽기 전용) */

static int riscv_install_hw_breakpoint(struct perf_event *bp)
{
    struct arch_hw_breakpoint *info = counter_arch_bp(bp);
    int idx = info->trigger_idx;

    /* 트리거 인덱스 선택 */
    csr_write(CSR_TSELECT, idx);

    /* tdata1: mcontrol6 타입 설정 */
    unsigned long tdata1 = 0;
    tdata1 |= MCONTROL6_TYPE;              /* Type 6 */
    tdata1 |= MCONTROL6_DMODE;             /* D-mode에서만 변경 가능 */

    if (info->type == HW_BREAKPOINT_X) {
        tdata1 |= MCONTROL6_EXECUTE;       /* 실행 브레이크포인트 */
    } else {
        if (info->type & HW_BREAKPOINT_R)
            tdata1 |= MCONTROL6_LOAD;      /* 읽기 워치포인트 */
        if (info->type & HW_BREAKPOINT_W)
            tdata1 |= MCONTROL6_STORE;     /* 쓰기 워치포인트 */
    }
    tdata1 |= MCONTROL6_S;                 /* S-mode에서 트리거 */
    tdata1 |= MCONTROL6_U;                 /* U-mode에서 트리거 */
    tdata1 |= MCONTROL6_ACTION_BKPT;       /* 브레이크포인트 예외 발생 */

    /* tdata2: 매칭할 주소 설정 */
    csr_write(CSR_TDATA2, info->address);
    /* tdata1: 구성 적용 (마지막에 써야 활성화) */
    csr_write(CSR_TDATA1, tdata1);

    return 0;
}

하드웨어 브레이크포인트 API

/* arch/riscv/kernel/hw_breakpoint.c — 커널 API */

/* 트리거 수 감지 (부팅 시) */
static int riscv_get_trigger_count(void)
{
    int count = 0;
    for (int i = 0; i < MAX_TRIGGERS; i++) {
        csr_write(CSR_TSELECT, i);
        if (csr_read(CSR_TSELECT) != i)
            break;  /* 지원하지 않는 인덱스 */
        unsigned long tinfo = csr_read(CSR_TINFO);
        if (tinfo & (TINFO_TYPE2 | TINFO_TYPE6))
            count++;  /* mcontrol 또는 mcontrol6 지원 */
    }
    return count;
}

/* 브레이크포인트 예외 핸들러 (scause=3) */
void do_trap_break(struct pt_regs *regs)
{
    /* 커널 KGDB 브레이크포인트 확인 */
    if (kgdb_handle_exception(regs))
        return;

    /* perf 하드웨어 브레이크포인트 확인 */
    if (riscv_hw_breakpoint_handler(regs))
        return;

    /* BUG()/WARN() 매크로의 ebreak 명령어 확인 */
    if (report_bug(regs->epc, regs) == BUG_TRAP_TYPE_WARN) {
        regs->epc += 4;  /* C 확장 시 2 */
        return;
    }

    /* 유저 공간 SIGTRAP */
    force_sig(SIGTRAP);
}

Sdtrig 확장: 디버그 트리거 기능은 Sdtrig(Debug Trigger) 확장으로 별도 정의됩니다. Type 2(mcontrol)는 기본적인 주소/데이터 매칭을, Type 6(mcontrol6)은 체인(chain) 모드와 VS/VU-mode 트리거를 추가 지원합니다. 체인 모드는 연속된 트리거를 AND 조건으로 결합하여 "특정 주소에서 특정 값을 쓸 때만" 트리거하는 복합 조건을 구현합니다. 트리거 수는 구현체에 따라 다르며(보통 2-8개), tinfo CSR로 지원하는 트리거 타입을 확인할 수 있습니다.

최신 RISC-V 동향 (v6.15~v7.0, 2025-2026)

RISC-V는 2025~2026년 기간 동안 가장 빠르게 확장된 아키텍처입니다. 신규 비준 확장의 커널 병합, 병렬 CPU 핫플러그(Hotplug), 제어 흐름 무결성(CFI / Integrity) 도입 등 현대 ISA가 갖춰야 할 핵심 기능이 대거 추가되었습니다.

버전변경상세
v6.15 (2025-05) BFloat16 / Zaamo / Zalrsc / Zicbom / Zicntr / Zihpm BF16 부동소수점(Zfbfmin 스칼라, Zvfbfmin/Zvfbfwma 벡터), 원자 연산 분리 확장(Zaamo=AMO, Zalrsc=LR-SC — 기존 A 확장을 세분화), 캐시 관리(Zicbom)·카운터(Zicntr)·성능 모니터(Zihpm) 확장 추가. 또한 재배치 가능(relocatable) non-MMU 커널 빌드, huge PFNMAP(TLB 활용 개선), 런타임 상수(runtime constants) 지원 추가
v6.16 (2025-07) SG2042 Pinctrl/SPI, cv18xx DTS 리팩터 SG200X 시리즈는 RISC-V + ARM64 듀얼 코어 SoC로, DT 재구성을 통해 혼합 아키텍처 기반을 마련
v6.17 (2025-09) SBI FWFT 정렬되지 않은(misaligned) 예외 위임 / Sophgo 확장 / IMSIC IPI 제어 SBI FWFT(Firmware Features) 기반 misaligned 예외 위임이 추가되었고, Sophgo EVB v1/v2 보드와 SG2042 ISA 확장, SG2044 Ziccrse 확장이 반영되었습니다. IMSIC IPI를 비활성화하는 커널 파라미터와 KVM ring 기반 dirty memory tracking도 함께 들어갔습니다.
v6.18 (2025-11) SBI v3.0 PMU / ACPI IOMMU / KVM 게스트 FWFT·Zicbop·BFloat16 SBI v3.0 PMU 강화와 ACPI IOMMU 지원이 추가되었습니다. KVM은 SBI_FWFT_POINTER_MASKING_PMLEN, 게스트용 Zicbop/BFloat16 확장, SBI FWFT ONE_REG 인터페이스를 지원하고, BPF riscv64 load-acquire/store-release와 SIMD syndrome 계산도 포함되었습니다.
v6.19 (2026-02) 보조 CPU 병렬 핫플러그 x86/ARM64에 있던 parallel bringup이 RISC-V에 도입. SBI HSM START를 병렬 발행하여 부팅 시간 단축
v6.19 Zicbop 유저스페이스 노출 + kselftest 프리페치 힌트 확장 탐지를 hwprobe()/HWCAP로 제공, 회귀 테스트 통합
v6.19 RISC-V 사이드 채널 방어 프레임워크 마이크로아키텍처 취약점에 대응하는 커널 측 안전장치 추가. AS/VUS 계열 공격 벡터 고려
v6.19 Tenstorrent Blackhole 초기 지원 Tenstorrent AI 가속기의 기본적인 RISC-V 호스트 지원
v7.0 (2026-04) Zicfiss (Shadow Stack) + Zicfilp (Landing Pad) 제어 흐름 무결성(CFI) 확장 메인라인 병합. x86 CET, ARM64 GCS/BTI와 동급 기능

Zicfiss / Zicfilp — RISC-V CFI 상세

Linux 7.0에 병합된 Zicfiss(Shadow Stack)와 Zicfilp(Indirect Branch Landing Pad)는 RISC-V의 제어 흐름 무결성을 하드웨어에서 강제하는 확장입니다. 두 확장은 RISC-V International이 v1.0으로 비준(ratified)한 표준 확장이며, x86 CET(Shadow Stack + IBT), ARM64 GCS/BTI와 동급의 기능을 제공합니다.

주요 하드웨어 현황

비준 확장 동향: Zaamo/Zalrsc, Zicbom/Zicboz/Zicbop, Zacas, Zicfiss/Zicfilp 등 2024~2026년에 비준된 확장이 커널에 꾸준히 흡수되고 있습니다. 새로운 보드를 지원할 때 device tree 또는 ACPI에서 riscv,isa-extensions와 riscv,isa-base 프로퍼티를 정확히 기술해야 커널이 각 확장을 활성화합니다.

커널 v6.13~v6.14 핵심 변경사항

v6.13~v6.14는 RISC-V의 보안 강화와 하드웨어 다양성 확대를 이끈 중요한 버전입니다.

v6.13 — 포인터 마스킹 확장 및 KVM 개선

Linux 6.13에서 RISC-V에 포인터 마스킹(Pointer Masking) 확장 3종이 추가되었습니다.

확장적용 모드설명
SmmpmM-mode머신 모드 포인터 마스킹. 최상위 비트를 태그로 활용 가능
SmnpmM→S/U 위임머신이 슈퍼바이저/유저로 포인터 마스킹 위임
SsnpmS→U 위임슈퍼바이저가 유저 공간으로 포인터 마스킹 위임. 하드웨어 태그 기반 sanitizer 구현에 활용

포인터 마스킹은 ARM64 TBI(Top-Byte Ignore), x86 LAM(Linear Address Masking)과 유사하게 포인터 상위 비트를 메타데이터 저장 공간으로 사용할 수 있게 합니다. ASAN/HWASAN 스타일의 태그 기반 메모리 안전성 도구 구현에 핵심적입니다.

또한 v6.13에서는 Zacas + Zabha 확장을 이용한 원자 연산 가속이 추가되었습니다. arch_cmpxchg()에서 XLEN 폭 연산은 Zacas(원자적 CAS)만 존재하면 LR/SC 루프 대신 단일 amocas 명령으로 컴파일되고, 바이트/하프워드처럼 XLEN보다 좁은 폭은 Zabha(바이트/하프워드 원자 연산) 확장이 함께 있어야 단일 원자 명령으로 처리됩니다.

/* arch/riscv/include/asm/cmpxchg.h — arch_cmpxchg() */
if (IS_ENABLED(CONFIG_RISCV_ISA_ZACAS) &&
    IS_ENABLED(CONFIG_TOOLCHAIN_HAS_ZACAS) &&
    riscv_has_extension_unlikely(RISCV_ISA_EXT_ZACAS)) {
    /* Zacas: 단일 원자 CAS 명령으로 교체 */
    __asm__ __volatile__ (
        "	.option push\n"
        "	.option arch, +zacas\n"
        "	amocas.d %0, %z2, %1\n"
        "	.option pop\n"
        : "+&r" (r), "+A" (*(p))
        : "rJ" (n)
        : "memory");
} else {
    /* Zacas가 없으면 LR/SC(원자 예약/조건부 저장) 루프로 폴백 */
}

부팅 시에는 riscv_spinlock_init()(arch/riscv/kernel/setup.c)이 Zabha + Zacas가 모두 감지되거나 Ziccrse가 감지되면 qspinlock을 선택하고, 어느 쪽도 없으면 CONFIG_RISCV_COMBO_SPINLOCKS 설정에 따라 티켓 스핀락(Ticket Spinlock)으로 전환합니다. qspinlock 슬로우 패스 자체는 아키텍처 공통 구현(kernel/locking/qspinlock.c)을 공유하고, RISC-V는 위 arch_cmpxchg() 원자 연산만 제공합니다.

KVM 개선으로 Svade/Svadu (Accessed/Dirty bit) 확장의 호스트·게스트 양쪽 지원이 추가되었습니다. Svadu가 있으면 하드웨어가 A/D 비트를 자동 갱신하여 OS의 페이지 폴트 오버헤드가 줄어듭니다. KVM은 게스트에 Svadu HWCAP을 노출하고 HGATP 설정 시 자동 활성화합니다.

v6.14 — T-Head XTheadVector 및 GhostWrite 완화

Linux 6.14에서 Alibaba T-Head 칩셋에서 사용하는 XTheadVector 확장이 메인라인에 병합되었습니다. 이 확장은 RISC-V Vector 0.7.1을 기반으로 하는 비표준 벡터 확장으로, 표준 V 확장과 혼용이 불가능합니다.

GhostWrite 취약점 (CVE-2024-44067): T-Head XuanTie C910(TH1520 SoC)·C920(SG2042)의 xtheadvector 명령어 중 일부가 권한 검사 없이 임의 물리 주소에 쓰기를 수행할 수 있는 마이크로아키텍처 결함입니다. Linux 6.14는 영향받는 하드웨어를 부팅 시 프로브(Probe)해 xtheadvector를 자동 비활성화하는 CONFIG_ERRATA_THEAD_GHOSTWRITE(errata) 완화책을 도입했습니다. (공식 문서: 커널 RISC-V 벡터 문서)

v6.14에서는 또한 SpacemiT K1 SoC(RISC-V 8코어, RV64GCVB)의 pinctrl 드라이버가 기본 활성화되어 Banana Pi BPI-F3 등 보드에서 데스크톱 리눅스 배포판 사용이 본격화되었습니다.

커널 6.14부터: XTheadVector 지원 + GhostWrite 완화, SpacemiT K1 기본 지원

v6.19 — Zalasr, Zilsd/Zclsd, Zicbop 사용자 공간(User Space) 노출

Linux 6.19에서 새로운 RISC-V 비준 확장 지원이 추가되었습니다.

확장기능비고
Zalasr 원자적 Load-Acquire / Store-Release RISC-V International 비준 완료. SC/acquire 페어 없이 단일 명령으로 순서 보장(Ordering)
Zilsd RV32 64비트 Load (레지스터 페어) RV32 한정. 레지스터 쌍으로 64비트 데이터 한 번에 적재
Zclsd RV32 C(압축) 64비트 Load Zilsd의 16비트 압축 인코딩 버전
Zicbop 캐시 프리페치 힌트 (prefetch.i/r/w) v6.19에서 hwprobe()/HWCAP 경유 유저스페이스 노출 + kselftest 통합

또한 v6.19에서 보조 CPU 병렬 핫플러그가 RISC-V에 도입되었습니다. 기존에는 CPU를 순차적으로 부팅했지만, 이제 SBI HSM(Hart State Management) START 요청을 병렬로 발행하여 다수 코어 시스템(예: SG2042 64코어)의 부팅 시간이 대폭 단축됩니다.

/* arch/riscv/kernel/smpboot.c — 병렬 핫플러그 (v6.19+) */
static int __init smp_init_cpus(void)
{
    /* 모든 hart에 SBI HSM START 병렬 발행 */
    for_each_possible_cpu(cpu)
        riscv_sbi_hart_start(cpu);  /* 동기 대기 없음 */
    wait_for_completion(&smp_cpus_done);
    return 0;
}

커널 6.19부터: Zalasr/Zilsd/Zclsd 지원, Zicbop 유저스페이스 노출, 병렬 CPU 핫플러그

참고자료

다음 학습: