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 전체를 다룹니다.
핵심 요약
- 모듈형 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에서 실행.
단계별 이해
- 레지스터와 ABI 이름 파악
x0-x31의 ABI 이름(zero, ra, sp, a0-a7, s0-s11, t0-t6)과 용도를 먼저 익힙니다. - 기본 ISA(RV64I) 학습
Load/Store, 산술, 논리, 분기, 점프 명령어가 기본입니다. - 확장 모듈 이해
M(곱셈/나눗셈), A(원자적), F/D(부동소수점), C(압축), V(벡터)를 순서대로 학습합니다. - 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) |
| 기본 ISA | RV32I / 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 커널은 이를 기본으로 요구합니다.
- 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에 위임할 수 있습니다.
레지스터 셋
범용 레지스터 (x0-x31)
| 레지스터 | ABI 이름 | 용도 | 보존 |
|---|---|---|---|
| x0 | zero | 하드와이어 제로 (항상 0) | — |
| x1 | ra | 복귀 주소 (Return Address) | Caller |
| x2 | sp | 스택 포인터 | Callee |
| x3 | gp | 전역 포인터 | — |
| x4 | tp | 스레드(Thread) 포인터 | — |
| x5-x7 | t0-t2 | 임시 레지스터 | Caller |
| x8 | s0/fp | Saved/프레임 포인터 | Callee |
| x9 | s1 | Saved 레지스터 | Callee |
| x10-x17 | a0-a7 | 함수 인자 / 반환값 (a0-a1) | Caller |
| x18-x27 | s2-s11 | Saved 레지스터 | Callee |
| x28-x31 | t3-t6 | 임시 레지스터 | Caller |
레지스터 파일 시각적 맵
32개 범용 레지스터를 용도별로 그룹화하고 Caller/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 |
| satp | S-mode 주소 변환 (페이지 테이블(Page Table) 루트 + 모드) | S |
| mscratch / sscratch | 트랩 핸들러(Handler) 스크래치 레지스터 | M / S |
| mhartid | 하드웨어 스레드 ID | M |
| cycle / time / instret | 사이클/시간/명령어 카운터 (읽기 전용(Read-Only)) | U |
CSR 주소 공간 레이아웃
RISC-V CSR 주소는 12-bit (0x000-0xFFF)로 인코딩됩니다. 상위 비트들이 접근 권한과 특권 레벨을 결정하므로, CSR 번호만으로도 접근 가능 여부를 하드웨어가 즉시 판단할 수 있습니다.
| 비트 [11:10] | 비트 [9:8] | 의미 | 주소 범위 | 예시 |
|---|---|---|---|---|
| 00 | 00 (U) | User 읽기/쓰기 | 0x000-0x0FF | ustatus, fflags, frm |
| 00 | 01 (S) | Supervisor 읽기/쓰기 | 0x100-0x1FF | sstatus, sie, stvec, satp |
| 00 | 11 (M) | Machine 읽기/쓰기 | 0x300-0x3FF | mstatus, mie, mtvec |
| 01 | 00 (U) | User 읽기/쓰기 | 0x400-0x4FF | (예약) |
| 01 | 01 (S) | Supervisor 읽기/쓰기 | 0x500-0x5FF | (예약, Hypervisor) |
| 01 | 11 (M) | Machine 읽기/쓰기 | 0x700-0x7FF | mhpmcounterN (debug) |
| 10 | ** | 커스텀/디버그 읽기/쓰기 | 0x800-0xBFF | dscratch, dpc (debug) |
| 11 | 00 (U) | User 읽기 전용 | 0xC00-0xCFF | cycle, time, instret |
| 11 | 01 (S) | Supervisor 읽기 전용 | 0xD00-0xDFF | (예약) |
| 11 | 11 (M) | Machine 읽기 전용 | 0xF00-0xFFF | mvendorid, mhartid |
부동소수점 / 벡터 레지스터
| 확장 | 레지스터 | 크기 |
|---|---|---|
| F/D 확장 | f0-f31 | 32/64-bit 부동소수점 |
| F/D 제어 | fcsr (frm + fflags) | 라운딩 모드 + 예외 플래그 |
| V 확장 | v0-v31 | VLEN-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 + addi | PC + 상위 20 + 하위 12 | auipc a0, %pcrel_hi(sym); addi a0, a0, %pcrel_lo(.) |
| 의사 명령어 li | li rd, imm | 어셈블러가 lui+addi로 확장 | li a0, 0x12345678 |
| 의사 명령어 la | la rd, symbol | auipc+addi로 확장 | la a0, my_var |
| 주소 모드 | RISC-V | x86_64 | ARM64 |
|---|---|---|---|
| 기본 (레지스터+오프셋) | 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) |
데이터 전송 명령어
| 명령어 | 문법 | 설명 | 확장 |
|---|---|---|---|
| LB / LBU | lb a0, 0(a1) | 바이트 로드 (부호/제로 확장) | I |
| LH / LHU | lh a0, 0(a1) | 하프워드 로드 (부호/제로 확장) | I |
| LW / LWU | lw a0, 0(a1) | 워드 로드 (RV64: LWU=제로확장) | I |
| LD | ld a0, 0(a1) | 더블워드 로드 | RV64I |
| SB / SH / SW / SD | sw a0, 0(a1) | 바이트/하프/워드/더블 저장 | I |
| LUI | lui a0, 0x12345 | 상위 20-bit 즉시값 로드 | I |
| AUIPC | auipc a0, 0x12345 | PC + 상위 20-bit | I |
| FLW / FLD | flw fa0, 0(a1) | 부동소수점 로드 | F / D |
| FSW / FSD | fsw fa0, 0(a1) | 부동소수점 저장 | F / D |
| C.LW / C.LD | c.lw a0, 0(a1) | 압축 로드 (16-bit) | C |
| C.SW / C.SD | c.sw a0, 0(a1) | 압축 저장 (16-bit) | C |
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 /* 어셈블러가 위 보정을 자동 수행 */
산술 명령어
| 명령어 | 문법 | 설명 | 확장 |
|---|---|---|---|
| ADD | add a0, a1, a2 | 덧셈 | I |
| ADDI | addi a0, a1, 42 | 즉시값 덧셈 | I |
| SUB | sub a0, a1, a2 | 뺄셈 | I |
| ADDW / ADDIW / SUBW | addw a0, a1, a2 | 32-bit 연산 + 부호 확장 (RV64) | RV64I |
| MUL | mul a0, a1, a2 | 곱셈 (하위 XLEN bit) | M |
| MULH / MULHU / MULHSU | mulh a0, a1, a2 | 곱셈 상위 (부호/부호없는/혼합) | M |
| DIV / DIVU | div a0, a1, a2 | 나눗셈 (부호/부호없는) | M |
| REM / REMU | rem a0, a1, a2 | 나머지 (부호/부호없는) | M |
| MULW / DIVW / REMW | mulw a0, a1, a2 | 32-bit 곱셈/나눗셈/나머지 (RV64M) | RV64M |
| FADD.S/D | fadd.s fa0, fa1, fa2 | 부동소수점 덧셈 | F/D |
| FMUL.S/D | fmul.d fa0, fa1, fa2 | 부동소수점 곱셈 | F/D |
| FDIV.S/D | fdiv.s fa0, fa1, fa2 | 부동소수점 나눗셈 | F/D |
| FSQRT.S/D | fsqrt.d fa0, fa1 | 부동소수점 제곱근 | F/D |
| FMADD.S/D | fmadd.s fa0, fa1, fa2, fa3 | FMA: fa1*fa2 + fa3 | F/D |
논리/시프트/비트 조작 명령어
| 명령어 | 문법 | 설명 | 확장 |
|---|---|---|---|
| AND / ANDI | and a0, a1, a2 | 비트 AND | I |
| OR / ORI | or a0, a1, a2 | 비트 OR | I |
| XOR / XORI | xor a0, a1, a2 | 비트 XOR | I |
| SLL / SLLI | sll a0, a1, a2 | 논리 좌측 시프트 | I |
| SRL / SRLI | srl a0, a1, a2 | 논리 우측 시프트 | I |
| SRA / SRAI | sra a0, a1, a2 | 산술 우측 시프트 | I |
| SLT / SLTI | slt a0, a1, a2 | 부호 있는 < 비교 (1/0) | I |
| SLTU / SLTIU | sltu a0, a1, a2 | 부호 없는 < 비교 | I |
| SLLW / SRLW / SRAW | sllw a0, a1, a2 | 32-bit 시프트 (RV64) | RV64I |
| CLZ / CTZ / CPOP | clz a0, a1 | 선행/후행 제로, 팝카운트 | Zbb |
| ANDN / ORN / XNOR | andn a0, a1, a2 | AND-NOT / OR-NOT / XNOR | Zbb |
| MIN / MAX / MINU / MAXU | min a0, a1, a2 | 최솟값/최댓값 | Zbb |
| ROL / ROR | rol a0, a1, a2 | 순환 시프트 | Zbb |
| REV8 | rev8 a0, a1 | 바이트 순서(Byte Order) 반전 | Zbb |
| BCLR / BEXT / BINV / BSET | bext a0, a1, a2 | 단일 비트 조작 | Zbs |
비교/분기 명령어
| 명령어 | 문법 | 설명 |
|---|---|---|
| BEQ | beq a0, a1, label | 같으면 분기 (±4KB) |
| BNE | bne a0, a1, label | 다르면 분기 |
| BLT | blt a0, a1, label | 부호 있는 < |
| BGE | bge a0, a1, label | 부호 있는 >= |
| BLTU | bltu a0, a1, label | 부호 없는 < |
| BGEU | bgeu a0, a1, label | 부호 없는 >= |
| JAL | jal ra, label | 점프 + 링크 (±1MB) |
| JALR | jalr 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 어셈블러는 프로그래머 편의를 위해 다양한 의사 명령어를 제공합니다. 아래 표는 자주 사용되는 의사 명령어와 그것이 실제로 확장되는 기본 명령어를 보여줍니다.
| 의사 명령어 | 확장되는 기본 명령어 | 설명 |
|---|---|---|
nop | addi zero, zero, 0 | 아무 동작 없음 |
li rd, imm | lui rd, upper; addi rd, rd, lower | 즉시값 로드 (32/64-bit) |
la rd, symbol | auipc rd, delta_hi; addi rd, rd, delta_lo | 주소 로드 (PC 상대) |
mv rd, rs | addi rd, rs, 0 | 레지스터 복사 |
not rd, rs | xori rd, rs, -1 | 비트 반전 |
neg rd, rs | sub rd, zero, rs | 부호 반전 (2의 보수) |
negw rd, rs | subw rd, zero, rs | 32-bit 부호 반전 (RV64) |
sext.w rd, rs | addiw rd, rs, 0 | 32-bit 부호 확장 (RV64) |
seqz rd, rs | sltiu rd, rs, 1 | rs == 0이면 1 |
snez rd, rs | sltu rd, zero, rs | rs != 0이면 1 |
sltz rd, rs | slt rd, rs, zero | rs < 0이면 1 |
sgtz rd, rs | slt rd, zero, rs | rs > 0이면 1 |
beqz rs, label | beq rs, zero, label | 0이면 분기 |
bnez rs, label | bne rs, zero, label | 0이 아니면 분기 |
blez rs, label | bge zero, rs, label | rs <= 0이면 분기 |
bgez rs, label | bge rs, zero, label | rs >= 0이면 분기 |
bltz rs, label | blt rs, zero, label | rs < 0이면 분기 |
bgtz rs, label | blt zero, rs, label | rs > 0이면 분기 |
bgt rs, rt, label | blt rt, rs, label | rs > rt이면 분기 (피연산자 교환) |
ble rs, rt, label | bge rt, rs, label | rs <= rt이면 분기 (피연산자 교환) |
j label | jal zero, label | 무조건 점프 (링크 없음) |
jr rs | jalr zero, 0(rs) | 간접 점프 (링크 없음) |
ret | jalr zero, 0(ra) | 함수 복귀 |
call func | auipc ra, off_hi; jalr ra, off_lo(ra) | 원거리 함수 호출 |
tail func | auipc t1, off_hi; jalr zero, off_lo(t1) | 꼬리 호출 (tail call) |
- 파이프라인 의존성 제거: 플래그 레지스터는 모든 산술 명령어에 대한 암묵적 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) */
- 인자: a0-a7 (최대 8개 정수/포인터)
- 반환값: a0 (+ a1)
- Callee-saved: s0-s11, sp
- Caller-saved: t0-t6, a0-a7, ra
- SP 16-byte 정렬
시스템/특권 명령어
| 명령어 | 문법 | 설명 |
|---|---|---|
| ECALL | ecall | 환경 콜 (U→S→M 트랩) |
| EBREAK | ebreak | 디버그 브레이크포인트 |
| MRET | mret | M-mode 트랩 복귀 |
| SRET | sret | S-mode 트랩 복귀 |
| WFI | wfi | 인터럽트 대기 |
| CSRRW | csrrw a0, sstatus, a1 | CSR 읽기+쓰기 (atomic swap) |
| CSRRS | csrrs a0, sstatus, a1 | CSR 읽기+비트 세트 |
| CSRRC | csrrc a0, sstatus, a1 | CSR 읽기+비트 클리어 |
| CSRRWI / CSRRSI / CSRRCI | csrrsi a0, sstatus, 2 | 즉시값 버전 CSR 조작 |
| FENCE | fence rw, rw | 메모리 순서 펜스 |
| FENCE.I | fence.i | 명령어 펜스 (I-cache 동기화) |
| FENCE.TSO | fence.tso | TSO 메모리 순서 펜스 |
| SFENCE.VMA | sfence.vma a0, a1 | TLB 무효화 (주소, ASID) |
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 등)가 이 위임을 설정합니다.
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를 저장합니다.
원자적/동기화 명령어 (A 확장)
| 명령어 | 문법 | 설명 |
|---|---|---|
| LR.W / LR.D | lr.d a0, (a1) | Load-Reserved (독점 로드) |
| SC.W / SC.D | sc.d a2, a0, (a1) | Store-Conditional (a2=0이면 성공) |
| AMOSWAP.W/D | amoswap.d a0, a1, (a2) | 원자적 교환 |
| AMOADD.W/D | amoadd.d a0, a1, (a2) | 원자적 덧셈 |
| AMOAND.W/D | amoand.d a0, a1, (a2) | 원자적 AND |
| AMOOR.W/D | amoor.d a0, a1, (a2) | 원자적 OR |
| AMOXOR.W/D | amoxor.d a0, a1, (a2) | 원자적 XOR |
| AMOMAX.W/D | amomax.d a0, a1, (a2) | 원자적 MAX (부호) |
| AMOMIN.W/D | amomin.d a0, a1, (a2) | 원자적 MIN (부호) |
| AMOMAXU / AMOMINU | amomaxu.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 (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, 복잡한 원자적 갱신 |
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의 predecessor/successor 집합은 4가지 접근 타입을 조합합니다:
i(Input) — 장치 입력 (MMIO 읽기)o(Output) — 장치 출력 (MMIO 쓰기)r(Read) — 메모리 읽기w(Write) — 메모리 쓰기
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 분수값도 가능하여 좁은 요소를 효율적으로 처리합니다.
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
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) | 설명 |
|---|---|---|---|
| 0x10 | SBI_EXT_BASE | get_spec_version(0), get_impl_id(1), get_impl_version(2), probe_extension(3) | SBI 기본 정보 조회 |
| 0x54494D45 | SBI_EXT_TIME (TIME) | set_timer(0) | 타이머(Timer) 인터럽트 설정. 커널 tick 소스 |
| 0x735049 | SBI_EXT_IPI | send_ipi(0) | 프로세서 간 인터럽트 전송 |
| 0x52464E43 | SBI_EXT_RFENCE | remote_fence_i(0), remote_sfence_vma(1), remote_sfence_vma_asid(2) | 원격 TLB/Icache 무효화 |
| 0x48534D | SBI_EXT_HSM | hart_start(0), hart_stop(1), hart_get_status(2), hart_suspend(3) | Hart 상태 관리 (SMP 부팅) |
| 0x53525354 | SBI_EXT_SRST | system_reset(0) | 시스템 리셋/종료 |
| 0x504D55 | SBI_EXT_PMU | num_counters(0), counter_get_info(1), counter_start(2), counter_stop(3) | 성능 모니터링 카운터 |
| 0x4442434E | SBI_EXT_DBCN | write(0), read(1), write_byte(2) | 디버그 콘솔 (earlycon) |
| 0x535553 | SBI_EXT_SUSP | suspend(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]에 위치하여 디코딩을 단순화합니다.
인코딩 예제: add a0, a1, a2
add a0, a1, a2는 R-type 인코딩입니다. 각 필드가 어떻게 매핑되는지 구체적으로 보겠습니다.
| 필드 | 비트 범위 | 값 | 이진수 | 설명 |
|---|---|---|---|---|
| funct7 | [31:25] | 0x00 | 0000000 | ADD (SUB는 0100000) |
| rs2 | [24:20] | 12 (a2=x12) | 01100 | 소스 레지스터 2 |
| rs1 | [19:15] | 11 (a1=x11) | 01011 | 소스 레지스터 1 |
| funct3 | [14:12] | 0x0 | 000 | ADD 연산 |
| rd | [11:7] | 10 (a0=x10) | 01010 | 목적 레지스터 |
| opcode | [6:0] | 0x33 | 0110011 | OP (레지스터-레지스터 연산) |
/* 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 */
- 식별: 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, a1 | add a0, a0, a1 | 2바이트 (50%) |
c.li a0, 5 | addi a0, zero, 5 | 2바이트 |
c.lw a0, 4(a1) | lw a0, 4(a1) | 2바이트 |
c.sw a0, 4(a1) | sw a0, 4(a1) | 2바이트 |
c.beqz a0, label | beq a0, zero, label | 2바이트 |
c.j label | jal zero, label | 2바이트 |
c.mv a0, a1 | add a0, zero, a1 | 2바이트 |
c.nop | addi zero, zero, 0 | 2바이트 |
SBI (Supervisor Binary Interface)
SBI는 S-mode(리눅스 커널)가 M-mode(펌웨어, OpenSBI)에 서비스를 요청하는 표준 인터페이스입니다. x86의 BIOS/UEFI Runtime Services, ARM의 PSCI(Power State Coordination Interface)에 대응하며, RISC-V 커널이 하드웨어 추상화 없이 이식성을 확보하는 핵심 메커니즘입니다.
/* 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은 선택적으로 활성화됩니다.
| 비교 항목 | Sv39 | Sv48 | Sv57 | x86-64 4-Level |
|---|---|---|---|---|
| VA 비트 | 39 | 48 | 57 | 48 |
| 페이지 테이블 레벨 | 3 | 4 | 5 | 4 |
| VA 공간 | 512GB | 256TB | 128PB | 256TB |
| PA 비트 | 56 | 56 | 56 | 52 |
| 페이지 크기 | 4KB | 4KB | 4KB | 4KB |
| 대형 페이지 | 2MB, 1GB | 2MB, 1GB, 512GB | +256TB | 2MB, 1GB |
| PTE 크기 | 8B | 8B | 8B | 8B |
| ASID 비트 | 16비트 (최대 65536 프로세스) | 12비트 (PCID) | ||
| TLB 플러시 | sfence.vma | invlpg | ||
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)의 관계를 한눈에 파악할 수 있습니다.
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/sw | ld/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에 지정된 핸들러로 점프합니다. stvec는 Direct 모드(모든 트랩이 동일 주소)와 Vectored 모드(인터럽트는 원인별 오프셋 점프)를 지원합니다.
트랩 처리 흐름 다이어그램
트랩 벡터 설정 (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 ECALL | do_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 아키텍처 다이어그램
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 → 마이크로아키텍처 성능 힌트 */
주요 표준 확장 목록
| 확장 | 버전 | 설명 | 커널 지원 |
|---|---|---|---|
| Zicbom | 1.0 | Cache Block 관리 (cbo.clean/flush/inval) | RISCV_ISA_EXT_ZICBOM |
| Zicboz | 1.0 | Cache Block Zero (cbo.zero) | RISCV_ISA_EXT_ZICBOZ |
| Zba | 1.0 | 주소 생성 (sh1add, sh2add, sh3add) | RISCV_ISA_EXT_ZBA |
| Zbb | 1.0 | 기본 비트 조작 (clz, ctz, cpop, rev8) | RISCV_ISA_EXT_ZBB |
| Zbs | 1.0 | 단일 비트 연산 (bset, bclr, bext, binv) | RISCV_ISA_EXT_ZBS |
| Zicntr | 2.0 | 기본 카운터 (cycle, time, instret) | 기본 포함 |
| Svadu | 1.0 | HW A/D 비트 자동 갱신 | RISCV_ISA_EXT_SVADU |
| Svnapot | 1.0 | NAPOT 대형 페이지 PTE | RISCV_ISA_EXT_SVNAPOT |
| Svpbmt | 1.0 | PTE 기반 메모리 타입 (NC/IO/PMA) | RISCV_ISA_EXT_SVPBMT |
| Sstc | 1.0 | S-mode 타이머 비교 CSR | RISCV_ISA_EXT_SSTC |
| Sscofpmf | 1.0 | PMU 카운터 오버플로 인터럽트 | 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 엔트리 매칭 다이어그램
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단계 주소 변환 다이어그램
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는 아키텍처 수준에서 캐시 일관성(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.i와 sfence.vma로 I-cache를 동기화한 후 패칭된 코드가 실행됩니다.
디버그 모듈과 Trigger
RISC-V Debug 스펙은 외부 디버거(JTAG/OpenOCD)와 자체 트리거(hardware breakpoint/watchpoint) 두 가지 디버그 메커니즘을 정의합니다. Debug Module(DM)은 하트를 정지시키고 레지스터/메모리를 검사하는 외부 인터페이스이며, Trigger Module은 주소/데이터 매칭 시 브레이크포인트/워치포인트를 생성합니다. Linux 커널은 Trigger Module을 perf와 ptrace를 통해 하드웨어 브레이크포인트로 노출합니다.
디버그 모듈 아키텍처
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 공식 사양서 — RISC-V ISA Specifications (Unprivileged/Privileged)
- RISC-V ISA 매뉴얼 GitHub 저장소 — riscv/riscv-isa-manual
- 커널 공식 문서 — Linux kernel RISC-V architecture documentation
- 커널 소스 — arch/riscv 디렉터리 (Bootlin Elixir)
- 커널 소스 — arch/riscv/kernel/entry.S 트랩 엔트리 (Bootlin Elixir)
- 커널 소스 — arch/riscv/kernel/head.S 부팅 엔트리 (Bootlin Elixir)
- LWN.net RISC-V 아키텍처 기사 모음 — LWN Kernel Index: RISC-V
- LWN.net — An introduction to RISC-V
- LWN.net — RISC-V kernel support improvements
- RISC-V SBI 사양 — RISC-V Supervisor Binary Interface specification
- OpenSBI 구현체 — RISC-V Open Source Supervisor Binary Interface
- RISC-V 벡터 확장 사양 — RISC-V "V" Vector Extension specification
- RISC-V PLIC 사양 — Platform-Level Interrupt Controller specification
- RISC-V 머신 모드 레퍼런스 — Machine-Level ISA (Five EmbedDev)
관련 문서
- 어셈블리 종합 — GCC 인라인 어셈블리, 호출 규약
- SIMD 명령과 커널 개발 — 커널 벡터 사용
- GNU Assembler (as) — GAS 지시자
- x86_64 명령어셋 (ISA) — x86_64 CISC 비교
- ARM64 명령어셋 (ISA) — ARM64 RISC 비교
- MIPS 명령어셋 (ISA) — MIPS 전통 RISC