RISC-V 명령어셋 레퍼런스
RISC-V 아키텍처의 명령어를 GAS 문법 기준으로 종합 정리합니다. 모듈형 ISA 설계 철학과 RV64I 기본 명령어셋, M(곱셈)/A(원자적)/F(단정도)/D(배정도)/C(압축)/V(벡터) 표준 확장, 32개 범용 레지스터와 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) |
| 주소 공간 | Sv39(39-bit), Sv48(48-bit), Sv57(57-bit) |
모듈형 ISA 확장 구조
RISC-V의 핵심 설계 철학은 모듈형(modular) ISA입니다. RV64I 기본 정수 명령어셋을 중심으로 필요한 확장을 선택적으로 추가하여 SoC 설계자가 목적에 맞는 최적의 프로세서를 구성할 수 있습니다. 표준 확장 조합인 RV64G(= RV64IMAFD)는 범용 운영체제 실행에 필요한 최소 세트이며, Linux 커널은 이를 기본으로 요구합니다.
- Machine (M-mode) — 최고 권한. 하드웨어에 직접 접근. 모든 CSR 사용 가능. 펌웨어/SBI가 실행됨.
- Supervisor (S-mode) — OS 커널 실행 모드. 가상 메모리, 인터럽트 관리. Linux 커널이 이 모드에서 동작.
- User (U-mode) — 최소 권한. 사용자 프로세스 실행. 특권 명령어 사용 시 트랩 발생.
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 | 스레드 포인터 | — |
| 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 주소 변환 (페이지 테이블 루트 + 모드) | S |
| mscratch / sscratch | 트랩 핸들러 스크래치 레지스터 | M / S |
| mhartid | 하드웨어 스레드 ID | M |
| cycle / time / instret | 사이클/시간/명령어 카운터 (읽기 전용) | 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(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— 전체 메모리 배리어. 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 재배치만 방지합니다. 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 | 바이트 순서 반전 | 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) 파이프라인에서 불필요한 직렬화를 방지합니다.
- 마이크로아키텍처 자유도: 플래그 레지스터 이름 변경(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는 인자에 따라 무효화 범위가 달라집니다. 불필요하게 넓은 범위를 플러시하면 성능 저하가 발생하므로, 커널은 가능한 한 좁은 범위의 무효화를 사용합니다.
| 형식 | rs1 (주소) | rs2 (ASID) | 효과 | 커널 사용 사례 |
|---|---|---|---|---|
sfence.vma zero, zero | 무시 | 무시 | 모든 TLB 엔트리 전체 플러시 | satp 변경 후, 전체 페이지 테이블 교체 |
sfence.vma addr, zero | 가상 주소 | 무시 | 해당 주소의 모든 ASID TLB 플러시 | 단일 페이지 매핑 변경 (커널 주소) |
sfence.vma zero, asid | 무시 | ASID | 해당 ASID의 모든 TLB 플러시 | 프로세스 전체 주소 공간 무효화 |
sfence.vma addr, asid | 가상 주소 | ASID | 해당 주소+ASID의 TLB만 플러시 | 단일 프로세스의 단일 페이지 (가장 세밀) |
Sv39 페이지 테이블 워크 구조
Linux RISC-V는 주로 Sv39 (39-bit 가상 주소, 3단계 페이지 테이블)를 사용합니다. satp CSR이 루트 페이지 테이블의 물리 주소와 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가 반복 실패할 수 있음 (단, 스펙은 결국 성공 보장) |
| 캐시 프로토콜 | 캐시 라인 수준 원자성 | 예약 세트 추적 필요 |
| 대표 용례 | atomic_add, 스핀락 | 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) | 타이머 인터럽트 설정. 커널 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/벡터 레지스터 — 지연 저장(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 핸들링 오버헤드를 제거합니다.
관련 문서
- 어셈블리 종합 — GCC 인라인 어셈블리, 호출 규약
- SIMD 명령과 커널 개발 — 커널 벡터 사용
- GNU Assembler (as) — GAS 지시자
- x86_64 명령어셋 (ISA) — x86_64 CISC 비교
- ARM64 명령어셋 (ISA) — ARM64 RISC 비교
- MIPS 명령어셋 (ISA) — MIPS 전통 RISC