RISC-V 명령어셋 레퍼런스

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 / 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 확장 구조 RV64I 기본 정수 명령어셋 (47개) Load/Store, ALU, Branch, Jump 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 RV64G = RV64I + M + A + F + D (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 (시스템 콜) ECALL (SBI 호출) SRET (트랩 복귀) MRET (트랩 복귀) medeleg 트랩 위임 PRV=3 PRV=1 PRV=0

레지스터 셋

범용 레지스터 (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 구분 특수 용도 (고정) Caller-saved (호출자 보존) Callee-saved (피호출자 보존) 시스템 예약 x0 (zero) 항상 0 x1 (ra) 복귀 주소 x2 (sp) 스택 포인터 x3 (gp) 전역 포인터 x4 (tp) 스레드 포인터 임시 (Caller-saved): x5 (t0) 임시 x6 (t1) 임시 x7 (t2) 임시 x28 (t3) 임시 x29 (t4) 임시 x30 (t5) 임시 x31 (t6) 임시 보존 (Callee-saved): x8 (s0/fp) 프레임 ptr x9 (s1) saved x18-x27 (s2-s11) — 10개 Saved 레지스터 함수 호출 전후로 값이 보존됨 (피호출자가 저장/복원 책임) 인자/반환 (Caller-saved): x10 (a0) 인자1/반환 x11 (a1) 인자2/반환 x12 (a2) 인자3 x13 (a3) 인자4 x14 (a4) 인자5 x15 (a5) 인자6 x16 (a6) 인자7 x17 (a7) 인자8 총 32개: 고정(x0) 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와 교환하여 커널/유저 모드 전환에 사용

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 주소 변환 (페이지 테이블(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]의미주소 범위예시
0000 (U)User 읽기/쓰기0x000-0x0FFustatus, fflags, frm
0001 (S)Supervisor 읽기/쓰기0x100-0x1FFsstatus, sie, stvec, satp
0011 (M)Machine 읽기/쓰기0x300-0x3FFmstatus, mie, mtvec
0100 (U)User 읽기/쓰기0x400-0x4FF(예약)
0101 (S)Supervisor 읽기/쓰기0x500-0x5FF(예약, Hypervisor)
0111 (M)Machine 읽기/쓰기0x700-0x7FFmhpmcounterN (debug)
10**커스텀/디버그 읽기/쓰기0x800-0xBFFdscratch, dpc (debug)
1100 (U)User 읽기 전용0xC00-0xCFFcycle, time, instret
1101 (S)Supervisor 읽기 전용0xD00-0xDFF(예약)
1111 (M)Machine 읽기 전용0xF00-0xFFFmvendorid, mhartid
CSR 12-bit 주소 인코딩 bit [11:10] bit [9:8] bit [7:0] 읽기/쓰기 권한 11 = 읽기 전용 (RO) 최소 특권 레벨 00=U, 01=S, 11=M 레지스터 식별자 256개 슬롯 (같은 권한/레벨 내) 예시: sstatus = 0x100 = 0b 00_01_00000000 ^^=RW ^^=S-mode ^^^^^^^^=레지스터 #0 예시: mhartid = 0xF14 = 0b 11_11_00010100 ^^=RO ^^=M-mode ^^^^^^^^=레지스터 #20

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

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

주소 지정 모드

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
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보다 약한 순서로, store→load 재배치(Relocation)만 방지합니다. x86 메모리 모델을 에뮬레이션할 때 유용합니다.

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

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

명령어문법설명확장
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 spsd/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 무효화 (주소, 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 등)가 이 위임을 설정합니다.

트랩 위임 메커니즘: medeleg / mideleg User (U-mode) ECALL / 페이지 폴트 / 예외 발생 Supervisor (S-mode) 위임된 트랩 처리: stvec → 핸들러 scause, sepc, stval 사용 SRET으로 복귀 Machine (M-mode) 위임되지 않은 트랩: mtvec → 핸들러 MRET으로 복귀 위임됨 medeleg[N]=1 위임 안 됨 medeleg[N]=0 위임 CSR (M-mode가 설정) medeleg: 예외 위임 비트마스크 mideleg: 인터럽트 위임 비트마스크 예: 페이지 폴트(13,15), 시스콜(8) 위임 일반적으로 Linux 부팅 시: 페이지 폴트, 유저 ECALL, 타이머/외부 S-인터럽트를 S-mode에 위임. 머신 타이머, 비정렬 접근은 M-mode에서 처리.

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단계 페이지 테이블 워크) 39-bit 가상 주소 분해: VPN[2] [38:30] VPN[1] [29:21] VPN[0] [20:12] Offset [11:0] 9 + 9 + 9 + 12 = 39 bits satp CSR MODE=8(Sv39) | ASID | PPN L2 페이지 테이블 512 PTE (VPN[2] 인덱스) L1 페이지 테이블 512 PTE (VPN[1] 인덱스) L0 페이지 테이블 512 PTE (VPN[0] 인덱스) 물리 페이지 PPN + Offset PPN PTE.PPN PTE.PPN PTE.PPN PTE 포맷 (64-bit): Reserved [63:54] PPN[2:0] [53:10] = 44 bits RSW [9:8] D A G U X W R V [7:0] V=Valid, R=Read, W=Write, X=eXecute, U=User, G=Global, A=Accessed, D=Dirty, RSW=커널 소프트웨어용 4KB 페이지(L0), 2MB 메가페이지(L1), 1GB 기가페이지(L2) — 리프 PTE의 R/W/X 중 하나라도 1이면 리프

원자적/동기화 명령어 (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

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 논리 벡터 v0-v1이 그룹 → 16개 벡터 그룹 사용 가능 (v0,v2,v4,...,v30) LMUL=4: v0 v1 v2 v3 1024-bit v0-v3이 그룹 → 8개 벡터 그룹 사용 가능 (v0,v4,v8,...,v28) LMUL=8: v0-v7 (8개 레지스터 = 2048-bit) → 4개 벡터 그룹 가능 핵심 공식 VLMAX = (VLEN / SEW) * LMUL 예: VLEN=256, SEW=32, LMUL=4 → VLMAX = (256/32)*4 = 32개 요소 사용 가능 벡터 그룹 수 = 32 / 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)
0x535553SBI_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 f3 rd opcode I: imm[11:0] rs1 f3 rd opcode S: imm[11:5] rs2 rs1 f3 imm[4:0] opcode B: imm[12|10:5] | rs2 | rs1 | f3 | 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 immediate funct7 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

인코딩 예제: 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)  */
/*  0100000 | 01100 | 01011 | 000 | 01010 | 0110011 */
/*  = 0x40C58533                                    */
C 확장 (압축 명령어) 16-bit 인코딩: C 확장은 자주 사용되는 명령어를 16-bit로 압축하여 코드 크기를 ~25-30% 줄입니다.
  • 식별: 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 / BBL SBI 서비스 처리 → sret 복귀 SBI 호출 규약 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=미지원, -3=DENIED SBI 확장 (Extension) 목록 EID 이름 용도 커널 함수 0x10 BASE SBI 버전, 확장 탐지 sbi_get_spec_version() 0x54494D45 TIME 타이머 설정 (stimecmp) sbi_set_timer() 0x735049 IPI Inter-Processor Interrupt sbi_send_ipi() 0x52464E43 RFENCE 원격 SFENCE.VMA (TLB 플러시) sbi_remote_sfence_vma() 0x48534D HSM Hart 시작/중지/상태 (SMP 부팅) sbi_hart_start() 0x53525354 SRST 시스템 리셋/셧다운 sbi_system_reset() 0x504D55 PMU 성능 카운터 관리 perf 서브시스템 연동 0x44424E43 DBCN 디버그 콘솔 입출력 earlycon sbi 콘솔 0x535553 SUSP 시스템 서스펜드 (절전) suspend_ops
/* 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레벨) 모드를 지원합니다. 리눅스 커널은 Sv48을 기본으로 사용하며, Sv57은 선택적으로 활성화됩니다.

RISC-V 가상 메모리 (Sv39/Sv48/Sv57) satp CSR (Supervisor Address Translation and Protection) MODE [63:60] ASID [59:44] PPN [43:0] — 루트 페이지 테이블 물리 페이지 번호 (PPN × 4KB = 물리 주소) MODE: 0=Bare(물리), 8=Sv39, 9=Sv48, 10=Sv57 Sv39 (3레벨, 512GB) VA[38:30] → VPN[2] (L2) VA[29:21] → VPN[1] (L1) VA[20:12] → VPN[0] (L0) VA[11:0] → offset VA 공간: ±256GB Megapage: 2MB (L1 leaf) Gigapage: 1GB (L2 leaf) 초기 RISC-V Linux 기본 Sv48 (4레벨, 256TB) VA[47:39] → VPN[3] (L3) VA[38:30] → VPN[2] (L2) VA[29:21] → VPN[1] (L1) VA[20:12] → VPN[0] (L0) VA[11:0] → offset VA 공간: ±128TB + Terapage: 512GB (L3 leaf) 현재 Linux 기본 모드 Sv57 (5레벨, 128PB) VA[56:48] → VPN[4] (L4) VA[47:39] → VPN[3] (L3) ... VA[11:0] → offset VA 공간: ±64PB + Petapage: 256TB x86 LA57에 대응 CONFIG_64BIT 필수 RISC-V PTE (Page Table Entry) 비트 필드 N [63] PBMT[62:61] Rsvd[60:54] PPN [53:10] — 44비트 물리 페이지 번호 RSW[9:8] D A G U X W R V [7:0] V(Valid) R(Read) W(Write) X(eXecute) U(User) G(Global) A(Accessed) D(Dirty) R=W=X=0 → 비리프(다음 레벨 포인터) | R|X ≠ 0 → 리프(최종 매핑) N(NAPOT): 자연 정렬 파워-of-2 대형 페이지 (Svnapot 확장) PBMT: Page-Based Memory Types (00=PMA, 01=NC, 10=IO) — Svpbmt 확장 RSW: 소프트웨어 예약 비트 (커널이 더티/young 추적에 사용)
비교 항목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의 호출 규약(Calling Convention)은 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 (zero) — 하드와이어 0 x1 (ra) — 복귀 주소 x2 (sp) — 스택 포인터 x3 (gp) — 전역 포인터 x4 (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 Callee-saved s0/fp (x8) — 프레임 포인터 s1 (x9) s2-s11 (x18-x27) 피호출 함수가 반드시 보존 Temporary (Caller-saved) t0-t2 (x5-x7) t3-t6 (x28-x31) 호출자가 필요 시 저장 함수 호출 흐름 caller: a0-a7 설정 jal ra, func callee: s-reg 저장, 실행 a0-a1 반환, ret 스택 프레임 레이아웃 (lp64) 높은 주소 ↑ caller의 스택 인자 (9번째~) 저장된 ra (8바이트) 저장된 s0/fp (8바이트) 지역 변수 / s1-s11 저장 ← sp (낮은 주소) ← s0/fp

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를 사용하지만, 커널 내부에서는 부동소수점 레지스터를 사용하지 않습니다. 커널 진입 시 kernel_nofpstate로 FP/벡터 컨텍스트를 비활성화하며, 유저 공간 복귀 시에만 FP 상태를 복원합니다. 9번째 이상의 인자는 스택으로 전달되지만, 커널 코드에서 8개 초과 인자를 가진 함수는 거의 없습니다. 구조체(Struct) 반환은 a0에 포인터로 전달합니다.

트랩/예외 처리 흐름

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

트랩 처리 흐름 다이어그램

RISC-V 트랩 처리 흐름 RISC-V S-mode 트랩 처리 흐름 트랩 발생 예외/인터럽트/ecall 하드웨어 자동 동작 ① sepc ← PC (예외 발생 주소) ② scause ← 트랩 원인 코드 ③ stval ← 부가 정보 (주소 등) ④ sstatus.SPP ← 이전 모드 ⑤ sstatus.SIE ← 0 (인터럽트 비활성) PC ← stvec 핸들러 진입점 stvec.MODE ? Direct(0): BASE Vectored(1): BASE+4×cause 소프트웨어 핸들러 레지스터 저장 → scause 분석 → 처리 예외: page fault 등 인터럽트: 타이머/외부 syscall: ecall sret → PC=sepc, 모드 복원 scause 비트 레이아웃 bit[63]: Interrupt 플래그 bit[62:0]: Exception Code 예: 0x8..05 = 타이머 IRQ 예: 0x0..0D = load page fault

트랩 벡터 설정 (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) 테이블

/* arch/riscv/kernel/traps.c */
static void (*excp_vect_table[])(struct pt_regs *) = {
    [EXC_INST_MISALIGNED]    = do_trap_insn_misaligned,
    [EXC_INST_ACCESS]        = do_trap_insn_fault,
    [EXC_INST_ILLEGAL]       = do_trap_insn_illegal,
    [EXC_BREAKPOINT]         = do_trap_break,
    [EXC_LOAD_ACCESS]        = do_trap_load_fault,
    [EXC_STORE_ACCESS]       = do_trap_store_fault,
    [EXC_SYSCALL]            = do_trap_ecall_u,
    [EXC_INST_PAGE_FAULT]    = do_page_fault,
    [EXC_LOAD_PAGE_FAULT]    = do_page_fault,
    [EXC_STORE_PAGE_FAULT]   = do_page_fault,
};

void do_trap_unknown(struct pt_regs *regs)
{
    unsigned long cause = regs->cause;
    if (cause < ARRAY_SIZE(excp_vect_table) && excp_vect_table[cause])
        excp_vect_table[cause](regs);
    else
        die(regs, "unhandled 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 — 시스템 콜 처리 */
/* 유저 모드에서 ecall 실행 시 scause=8로 트랩 발생 */
handle_syscall:
    /* sepc는 ecall 명령어를 가리킴 → 다음 명령어로 이동 */
    addi    a0, a0, 4              /* sepc += 4 (ecall 다음) */
    REG_S   a0, PT_EPC(sp)         /* 수정된 sepc 저장 */

    /* syscall 번호 확인 (a7에 있음) */
    li      t0, __NR_syscalls       /* 최대 syscall 번호 */
    bgeu    a7, t0, .Lillegal_syscall

    /* syscall 테이블에서 핸들러 조회 */
    la      t0, sys_call_table
    slli    t1, a7, 3              /* t1 = a7 * 8 (포인터 크기) */
    add     t0, t0, t1
    ld      t0, 0(t0)              /* 핸들러 함수 포인터 */

    /* a0-a5에 syscall 인자가 이미 설정됨 */
    jalr    t0                     /* 핸들러 호출 */
    REG_S   a0, PT_A0(sp)          /* 반환값 저장 */

컨텍스트 스위치 (__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 에지/레벨 변환 중복 억제 Pending IP[1..1023] Priority 0-7 (소스별) 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 쓰기 → 재활성 PLIC MMIO 레지스터 맵 0x000000 Priority (4B × 1024) 0x001000 Pending (128B) 0x002000 Enable (하트별 128B) 0x200000 Threshold (하트별 4B) 0x200004 Claim/ Complete

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 */
static int riscv_clock_next_event(unsigned long delta,
                                  struct clock_event_device *ce)
{
    /* SBI를 통해 타이머 설정 (S-mode에서 mtimecmp 직접 접근 불가) */
    sbi_set_timer(get_cycles64() + delta);
    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/NAPOT/NA4) PMP 주소 매칭 모드 pmpcfg[i] 비트 레이아웃 (8비트) L Rsv A[1:0] X W R bit[0] Lock 모드 권한 TOR (A=01) Top Of Range 범위: pmpaddr[i-1] ≤ addr < pmpaddr[i] addr[i-1] 보호 영역 addr[i] NAPOT (A=11) Naturally Aligned Power-of-Two 크기: pmpaddr의 하위 연속 1 비트로 인코딩 yyyy...y01111 → 64B 영역 yyyy...y0111 → 32B 영역 NA4 (A=10) Naturally Aligned 4-byte 고정 4바이트 단위 보호 범위: pmpaddr[i] × 4 ~ +4 가장 세밀한 단위 PMP 매칭 우선순위 PMP0 (최고) PMP1 ... PMP15 매칭 없음 → M-mode만 접근 * 번호가 낮은 PMP 엔트리가 높은 우선순위 (첫 매칭 규칙) * L(Lock) 비트가 설정되면 M-mode에서도 해당 규칙 적용, 리셋 전까지 변경 불가

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단계 주소 변환 다이어그램

RISC-V H 확장 2단계 주소 변환 H 확장 2단계 주소 변환 (Two-Stage Translation) 게스트 (VS/VU-mode) Guest VA (GVA) VS-stage vsatp (게스트 페이지 테이블) Guest PA (GPA) 하이퍼바이저 (HS-mode) G-stage hgatp (호스트 페이지 테이블) Host PA (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 → COVH (기밀 VM)

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 확장을 필수로 요구합니다. hgatpVMID 필드로 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는 아키텍처 수준에서 캐시 일관성(coherency)을 보장하지 않습니다. 멀티코어 시스템에서 하드웨어 일관성 프로토콜(예: 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.isfence.vma로 I-cache를 동기화한 후 패칭된 코드가 실행됩니다.

디버그 모듈과 Trigger

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

디버그 모듈 아키텍처

RISC-V 디버그 모듈 아키텍처 RISC-V 디버그 아키텍처 외부 디버거 GDB + OpenOCD JTAG/cJTAG DTM Debug Transport Module Debug Module (DM) dmcontrol — 하트 정지/재개 dmstatus — 하트 상태 abstracts — 레지스터 R/W progbuf — 프로그램 버퍼 Hart Debug I/F D-mode (Debug Mode) dcsr — Debug CSR dpc — Debug PC dscratch0/1 Trigger Module (소프트웨어 디버그) Type 2: mcontrol 주소/데이터 매칭 BP / WP Type 6: mcontrol6 확장 매칭 (체인) vs/vu 모드 지원 Type 5: icount 명령어 카운트 싱글 스텝 tselect → tdata1(type+config) → tdata2(match value) → tdata3(textra) Action: 브레이크포인트 예외 (cause=3) Action: Debug Mode 진입 (외부 디버거용)

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로 지원하는 트리거 타입을 확인할 수 있습니다.

참고 자료

다음 학습: