명령어 형식과 인코딩
add a0, a1, a2는 CPU에게 0x00c58533이라는 32비트 숫자 하나로 전달된다. 이 32비트 안 어디에 “무슨 명령인지”, “어느 레지스터인지”, “어떤 상수인지”가 들어 있을까? RISC-V의 기본 명령어는 R·I·S·B·U·J 여섯 가지 형식만으로 모든 것을 표현한다. 이 장에서는 그 비트 배치를 하나하나 해부하고, 언뜻 보면 이상하게 뒤섞인 즉시값 비트가 사실은 하드웨어를 단순하게 만들기 위한 치밀한 설계라는 것을 확인한다.
- 32비트 명령어의 하위 2비트가 왜 항상
11인지, 명령어 길이를 어떻게 알아내는지 설명한다. - R·I·S·B·U·J 여섯 형식의 비트 필드를 그리고, 명령마다 어느 형식을 쓰는지 안다.
- rs1·rs2·rd가 모든 형식에서 같은 자리에 있고, 부호 비트가 언제나 inst[31]인 이유를 하드웨어 관점에서 설명한다.
- B·J 형식의 즉시값 비트가 뒤섞인 규칙을 외우지 않고 “다른 형식과 최대한 겹치게”라는 원리로 이해한다.
- opcode·funct3·funct7로 명령을 고르는 과정을 알고, 직접 인코딩·해독해 본다.
lui+addi로 32비트 상수를 만들 때 필요한 +1 보정과,auipc로 pc 상대 주소를 만드는 법을 익힌다.
32비트 명령어와 길이 표시 비트
RISC-V 베이스 ISA의 명령어는 모두 정확히 32비트이고, 메모리에서 4바이트 경계에 정렬되어야 한다. 규격은 이 정렬 단위를 IALIGN(instruction-address alignment)이라 부르며 베이스 ISA에서는 32비트다. 길이가 고정되어 있으니 CPU는 다음 명령이 어디서 시작하는지 고민할 필요가 없다. x86처럼 1바이트에서 15바이트까지 길이가 제각각인 ISA에서는 명령의 경계를 찾는 일 자체가 큰 회로가 된다.
그렇다고 RISC-V가 길이를 영원히 32비트로 묶어 둔 것은 아니다. 처음부터 16비트 압축 명령(C 확장, 13장)과 더 긴 명령이 들어올 자리를 남겨 두었다. 그 표시가 바로 하위 2비트다. 베이스 ISA의 32비트 명령은 모두 하위 2비트가 11이고, 압축 명령은 00·01·10 중 하나다.
11이 아니면 16비트 압축 명령, 11이면 32비트 이상이다. opcode 지도에서 inst[4:2]=111 열은 32비트보다 긴 명령을 위해 비워 두었다. 오른쪽: 명령어는 메모리 엔디언과 상관없이 리틀 엔디언 16비트 조각으로 저장되므로, 길이를 결정하는 비트는 언제나 가장 먼저 읽히는 바이트에 있다.32비트 중 2비트를 길이 표시에 쓰면 베이스 ISA가 쓸 수 있는 공간은 그만큼 좁아진다. 대신 얻는 것이 많다. 규격의 해설에 따르면 압축 명령을 지원하지 않는 구현은 명령어 캐시에 하위 2비트를 저장하지 않아도 되어 6.25%를 아낄 수 있고, RV32I 전체가 32비트 인코딩 공간의 8분의 1도 쓰지 않기 때문에 나머지는 표준 확장과 사용자 확장의 몫으로 남는다.
규격은 하위 16비트가 모두 0인 인코딩을 불법 명령으로 정했다. 그래서 소프트웨어는 “여기서 반드시 예외를 내고 싶다”는 자리에 0을 써 넣을 수 있다. 모든 비트가 1인 ILEN 길이 인코딩도 불법이다. 이 두 값은 지워진 메모리, 연결이 끊긴 버스, 프로그래밍하지 않은 플래시에서 흔히 읽히는 값이기 때문이다.
여섯 가지 형식: R·I·S·B·U·J
규격은 베이스 ISA의 형식을 핵심 네 가지(R·I·S·U)와, 즉시값을 다루는 방식만 조금 다른 변형 두 가지(B·J)로 설명한다. 이름은 쓰임새에서 왔다. Register(레지스터끼리 연산), Immediate(즉시값 연산과 적재), Store(저장), Branch(분기), Upper immediate(상위 즉시값), Jump(점프)다.
imm[x]는 “명령어의 몇 번째 비트”가 아니라 만들어질 즉시값의 몇 번째 비트인지를 뜻한다(규격 그림과 같은 표기). opcode는 늘 맨 아래 7비트, funct3은 늘 [14:12]에 있고, 레지스터 번호 칸(청록)은 형식이 달라도 자리가 움직이지 않는다.| 형식 | 들어 있는 것 | 쓰는 명령 (RV32I) | 예 |
|---|---|---|---|
| R | rd, rs1, rs2, funct3, funct7 | add, sub, sll, slt, sltu, xor, srl, sra, or, and | add a0, a1, a2 |
| I | rd, rs1, 12비트 즉시값, funct3 | addi 등 즉시값 연산, 시프트(즉시값 자리에 shamt), 적재 lb…lhu, jalr, fence, ecall, ebreak | lw a0, 8(sp) |
| S | rs1, rs2, 12비트 즉시값(둘로 나뉨), funct3 | sb, sh, sw | sw a0, 8(sp) |
| B | rs1, rs2, 13비트 오프셋(짝수), funct3 | beq, bne, blt, bge, bltu, bgeu | beq a0, zero, loop |
| U | rd, 상위 20비트 즉시값 | lui, auipc | lui a0, 0x12345 |
| J | rd, 21비트 오프셋(짝수) | jal | jal ra, func |
한 명령을 손으로 풀어 보기
add a0, a1, a2를 R 형식에 채워 넣어 보자. a0·a1·a2는 레지스터 x10·x11·x12이고(3장), add의 opcode는 0110011(OP), funct3과 funct7은 모두 0이다.
비트를 이어 붙이면 0000 0000 1100 0101 1000 0101 0011 0011, 16진수로 0x00c58533이다. 어셈블러가 하는 일의 대부분은 이렇게 칸을 채우는 것이다. 이 장 가운데의 인코더 시뮬레이터에서 다른 명령도 직접 채워 볼 수 있다.
레지스터 칸을 고정한 이유
그림 4-2에서 청록색 칸이 위아래로 가지런히 줄 선 것은 우연이 아니다. 규격은 rs1·rs2·rd를 모든 형식에서 같은 자리에 둔다고 못 박았다. 이유는 하드웨어의 시간표에 있다. 명령어를 받은 CPU는 가능한 한 빨리 레지스터 파일에서 피연산자를 읽어야 다음 단계(연산)로 넘어갈 수 있다. 만약 형식마다 rs1의 자리가 다르다면, 먼저 opcode를 해독해 형식을 알아낸 다음에야 어느 비트를 레지스터 번호로 쓸지 고를 수 있다. 해독과 레지스터 읽기가 직렬로 이어지는 것이다.
RISC-V에서는 형식을 몰라도 inst[19:15]는 rs1, inst[24:20]은 rs2라고 믿고 일단 레지스터를 읽어 버린다. I 형식처럼 rs2가 없는 명령이라면 그 자리에 있던 즉시값 비트로 엉뚱한 레지스터를 읽게 되지만 상관없다. 해독이 끝난 뒤 그 값을 쓰지 않으면 그만이다. 이렇게 해독·레지스터 읽기·부호 확장 세 가지가 같은 클럭 안에서 동시에 시작된다.
“Decoding register specifiers is usually on the critical paths in implementations, and so the instruction format was chosen to keep all register specifiers at the same position in all formats at the expense of having to move immediate bits across formats …”
Volume I, RV32I · Base Instruction Formats — 레지스터 칸을 지키는 대가로 즉시값 비트를 형식마다 옮겨 다니게 했다는 고백이다. 이 대가가 다음 두 절의 주제다.
MIPS의 R 형식은 목적지 레지스터를 rd(비트 15–11)에 두지만, I 형식은 목적지를 rt(비트 20–16)에 둔다. 그래서 MIPS 하드웨어는 “이 명령의 목적지가 어느 칸인가”를 opcode로 골라 주는 멀티플렉서가 필요했다. RISC-V는 rd를 언제나 inst[11:7]에 두어 이 선택 자체를 없앴다.
즉시값: 부호 비트는 언제나 inst[31]
명령어 안에 박힌 상수를 즉시값(immediate)이라 한다. RISC-V의 즉시값에는 두 가지 큰 규칙이 있다. 첫째, CSR 명령의 5비트 즉시값을 빼면 모든 즉시값은 부호 확장된다. MIPS처럼 어떤 명령은 0 확장, 어떤 명령은 부호 확장하는 식의 구분이 없다. 둘째, 부호 비트는 어느 형식에서나 inst[31]이다.
32비트 즉시값의 상위 비트 20여 개는 모두 부호 비트의 복사본이다. 부호 비트의 자리가 고정되어 있으니, 하드웨어는 형식을 해독하기 전부터 inst[31] 한 가닥을 상위 비트 전체로 퍼뜨려 둘 수 있다. RV64에서는 이 복사가 52비트 이상으로 늘어나므로 더 중요해진다(6장).
그림 4-4를 세로 열 하나씩 보면, 즉시값의 한 비트를 만들기 위해 하드웨어가 고를 후보가 몇 개인지 셀 수 있다. 예를 들어 imm[0]은 I에서 inst[20], S에서 inst[7], B·U·J에서 상수 0이므로 후보가 세 개다. 이 후보 수가 바로 즉시값 생성기 안의 멀티플렉서 입력 수다. 후보가 적을수록 회로가 작고, 명령어 비트 하나가 연결되는 곳(팬아웃)도 줄어든다.
“Sign extension is one of the most critical operations on immediates (particularly for XLEN>32), and in RISC-V the sign bit for all immediates is always held in bit 31 of the instruction to allow sign extension to proceed in parallel with instruction decoding.”
일반 명령의 즉시값은 12비트, lui·auipc만 20비트다. 규격은 “실제 프로그램의 즉시값은 대부분 아주 작거나, 아니면 XLEN 비트가 통째로 필요하다”는 관찰을 근거로 든다. 작은 값은 12비트로 충분하고, 큰 값은 어차피 두 명령으로 만들어야 하니, 일반 명령의 즉시값을 줄여 opcode 공간을 넓히는 쪽을 택했다.
B형과 J형: 뒤섞인 비트 풀기
B 형식은 처음 보면 가장 당혹스럽다. imm[12|10:5] … imm[4:1|11]이라니, 왜 11번 비트가 맨 끝에 붙어 있을까? 답은 S 형식과의 차이를 최소로 만들기 위해서다.
분기 오프셋은 늘 2의 배수다(명령어가 최소 16비트 단위로 놓이므로). 그러니 imm[0]은 언제나 0이고 인코딩할 필요가 없다. 비트 하나를 아꼈으니 S 형식과 같은 12비트 칸으로 imm[12:1], 즉 ±4 KiB를 표현할 수 있다. 흔한 방법은 “S의 12비트 값을 하드웨어에서 왼쪽으로 한 칸 민다”이다. 그러면 S와 B의 즉시값 비트가 모두 한 칸씩 어긋난다. RISC-V는 대신 S와 똑같이 두고, S에서 imm[0]을 담던 inst[7] 단 한 자리에 imm[11]을 넣었다.
J와 U의 관계도 똑같다. U 형식은 inst[31:12]를 즉시값의 imm[31:12]로 그대로 쓴다. J 형식은 20비트 칸으로 imm[20:1]을 표현해야 하는데, 이때 imm[19:12]는 U와 같은 inst[19:12]에, imm[10:1]은 I 형식과 같은 inst[30:21]에 두었다. 남은 imm[11]은 I 형식에서 imm[0]이 있던 inst[20]으로, 부호 비트 imm[20]은 inst[31]로 갔다. 결과적으로 J 형식에서 “새로 생긴 연결”은 거의 없다.
아래 애니메이션은 명령어 비트가 즉시값 자리로 날아가 모이는 과정을 보여 준다. 묶음 하나가 움직일 때마다, 그 연결이 다른 어느 형식과 똑같은지(즉 하드웨어가 이미 가진 배선을 다시 쓰는지) 아래 설명에 나타난다.
즉시값 비트 모으기
뒤섞인 배치는 16진수 덤프를 눈으로 읽는 사람과, 실행 중에 기계어를 만들어 내는 JIT 컴파일러에게는 조금 번거롭다. 규격도 이 점을 인정하면서, 정적 컴파일에는 무시할 만한 비용이고 가장 흔한 “짧은 앞쪽 분기”는 인코딩이 단순하다고 덧붙인다. 하드웨어는 수십억 번 해독하지만 어셈블러는 한 번만 인코딩한다. RISC-V는 그 비대칭에 맞춰 비용을 소프트웨어 쪽에 실었다.
opcode, funct3, funct7: 명령을 고르는 세 단계
32비트 명령의 하위 7비트가 opcode(major opcode)다. 그중 하위 2비트는 11로 정해져 있으니 실제로 고를 수 있는 것은 inst[6:2] 다섯 비트, 즉 32가지다. 규격은 이를 inst[6:5]를 행, inst[4:2]를 열로 하는 4×8 표로 그린다.
11까지 포함한 7비트 opcode 값이다. RV32I의 40개 명령은 파란 칸 11개에 모두 들어간다. custom-0~3은 앞으로도 표준 확장이 쓰지 않기로 약속한 자리여서 회사·연구실이 자기 명령을 넣기 좋고, reserved 칸은 미래의 표준 확장 몫이므로 피해야 한다.opcode가 큰 무리를 고르면, funct3(inst[14:12])이 그 안에서 연산을 고르고, R 형식에서는 funct7(inst[31:25])이 한 번 더 고른다. RV32I에서 funct7이 실제로 쓰는 값은 0000000과 0100000 둘뿐이고, 둘의 차이는 inst[30] 한 비트다. add와 sub, srl과 sra, srli와 srai가 이 비트 하나로 갈린다. 덧셈기는 inst[30]을 “두 번째 피연산자를 뒤집고 1을 더하라”는 신호로, 시프터는 “빈자리를 부호로 채워라”는 신호로 그대로 쓸 수 있다.
| 명령 | 형식 | opcode | funct3 | funct7 / imm[11:5] |
|---|---|---|---|---|
| lui | U | 0110111 LUI | — | — |
| auipc | U | 0010111 AUIPC | — | — |
| jal | J | 1101111 JAL | — | — |
| jalr | I | 1100111 JALR | 000 | — |
| beq · bne · blt · bge · bltu · bgeu | B | 1100011 BRANCH | 000 001 100 101 110 111 | — |
| lb · lh · lw · lbu · lhu | I | 0000011 LOAD | 000 001 010 100 101 | — |
| sb · sh · sw | S | 0100011 STORE | 000 001 010 | — |
| addi · slti · sltiu · xori · ori · andi | I | 0010011 OP-IMM | 000 010 011 100 110 111 | — |
| slli | I | 0010011 OP-IMM | 001 | 0000000 |
| srli · srai | I | 0010011 OP-IMM | 101 101 | 0000000 0100000 |
| add · sub | R | 0110011 OP | 000 000 | 0000000 0100000 |
| sll · slt · sltu · xor · or · and | R | 0110011 OP | 001 010 011 100 110 111 | 0000000 |
| srl · sra | R | 0110011 OP | 101 101 | 0000000 0100000 |
| fence | I | 0001111 MISC-MEM | 000 | (fm·pred·succ) |
| ecall · ebreak | I | 1110011 SYSTEM | 000 | imm = 0 / 1 |
표를 세어 보면 4 + 6 + 5 + 3 + 9 + 10 + 1 + 2 = 40개로, 규격이 말하는 “RV32I의 40개 고유 명령”과 맞는다. 각 명령의 뜻은 5장에서 하나씩 실행하며 살펴본다.
32비트 값을 31칸보다 더 밀 일은 없으므로 RV32I의 slli·srli·srai는 I 형식의 12비트 즉시값 중 하위 5비트만 시프트 양(shamt)으로 쓴다. 남는 상위 7비트는 R 형식의 funct7처럼 쓰여서, 오른쪽 시프트의 종류를 역시 inst[30]으로 고른다. RV64에서는 shamt가 6비트로 늘어난다(6장).
직접 인코딩하고 해독하기
이제 손으로 해 보자. 첫 번째 시뮬레이터는 어셈블러가 하는 일, 두 번째는 CPU의 해독기가 하는 일이다. 즉시값 범위를 벗어나거나 분기 오프셋을 홀수로 주면 어셈블러처럼 오류를 낸다.
명령어 인코더
beq를 고르고 오프셋을 -8, 2048, 4094, 4096, 7로 바꿔 보자. 범위를 넘거나 홀수면 오류가 난다. addi에서 rd만 바꾸면 inst[11:7]만, rs1만 바꾸면 inst[19:15]만 바뀌는 것도 확인하자.명령어 디코더
| 필드 | 비트 | 2진수 | 값 | 뜻 |
|---|
add가 sub로 바뀐다. inst[0]을 0으로 만들면 32비트 명령이 아니게 된다. 무작위 명령을 여러 번 눌러 형식마다 즉시값이 어떻게 흩어지는지 살펴보자.32비트 상수 만들기: lui + addi, 그리고 auipc
12비트 즉시값으로는 −2048부터 2047까지밖에 못 쓴다. 그보다 큰 상수는 두 명령으로 만든다. lui rd, imm20은 20비트 값을 레지스터의 상위 20비트에 놓고 하위 12비트를 0으로 채운다. 이어서 addi rd, rd, imm12로 하위 12비트를 더한다. 어셈블러의 li 의사 명령이 바로 이 일을 한다.
함정이 하나 있다. addi의 즉시값은 부호 확장된다. 하위 12비트의 맨 위 비트(bit 11)가 1이면 addi는 그것을 음수로 보고, 결과적으로 상위 부분에서 4096을 빼는 셈이 된다. 그래서 그런 경우에는 lui에 줄 상위 값을 미리 1 늘려 둔다. 식으로 쓰면 상위 = (값 + 0x800) >> 12, 하위 = 값 − (상위 << 12)이다.
32비트 상수 빌더
0x800, 0x7FFFF800, 0xFFFFF000처럼 하위 12비트가 경계에 걸린 값을 넣어 보자. 하위 12비트의 맨 위 비트(밑줄)가 1일 때만 +1 보정이 생긴다. 마지막 칸은 같은 li를 이 책의 RV32 에뮬레이터로 실제 실행해 a0 값이 목표와 같은지 확인한 결과다.auipc: pc를 기준으로 한 주소
auipc rd, imm20(add upper immediate to pc)는 lui와 같은 U 형식이지만, 20비트 값을 상위에 놓은 뒤 이 명령 자신의 주소(pc)를 더한다. 여기에 12비트 즉시값을 가진 명령을 하나 더 붙이면 pc에서 32비트 범위 안의 어디든 가리킬 수 있다. addi를 붙이면 주소를 만들고(la), 적재·저장 명령을 붙이면 그 주소를 바로 읽고 쓰며, jalr를 붙이면 그곳으로 점프한다(call). 프로그램이 메모리 어디에 올라가도 상대 거리는 변하지 않으므로 위치 독립 코드를 만드는 핵심 도구다.
la t0, msg가 펼쳐진 모습. auipc는 pc에 0x3000을 더해 목표 근처로 크게 뛰고, addi가 남은 거리(−1584)를 조정한다. 여기서도 하위 12비트의 bit 11이 1이어서 상위 값에 +1 보정이 들어갔다. 어셈블러는 %pcrel_hi(msg)·%pcrel_lo(...) 같은 재배치 연산자로 이 계산을 링커에게 맡긴다.절대 주소를 만들 때 쓰는 lui a0, %hi(symbol) · addi a0, a0, %lo(symbol)에서 %hi는 단순히 상위 20비트를 잘라 오는 것이 아니라, %lo가 음수가 될 것을 감안해 0x800을 더한 뒤 자른 값이다. 이 보정 규칙은 psABI의 재배치 정의에 들어 있다(7장).
분기와 점프는 얼마나 멀리 갈 수 있나
즉시값 비트 수가 곧 도달 거리다. B 형식은 imm[12:1]의 12비트에 암묵적인 imm[0]=0을 더한 13비트 부호 있는 값이므로 −4096부터 +4094바이트까지, 규격 표현으로 ±4 KiB다. J 형식은 imm[20:1]로 −1,048,576부터 +1,048,574바이트, ±1 MiB다. 두 경우 모두 오프셋은 분기·점프 명령 자신의 주소를 기준으로 한다.
| 명령 | 즉시값 | 단위 | 범위 | 기준 |
|---|---|---|---|---|
beq 등 분기 | B: 12비트 + 암묵 0 | 2바이트 | −4096 … +4094 | 분기 명령의 주소 |
jal | J: 20비트 + 암묵 0 | 2바이트 | −1,048,576 … +1,048,574 | jal 명령의 주소 |
jalr | I: 12비트 | 1바이트 (결과의 bit 0은 0으로) | rs1 −2048 … rs1 +2047 | 레지스터 rs1 |
auipc + jalr | U 20비트 + I 12비트 | 1바이트 | 32비트 pc 상대 전 범위 | auipc의 주소 |
jalr는 즉시값을 2배 하지 않는다. 규격은 그 이유를 “즉시값 형식을 하나 더 늘리지 않기 위해서”라고 설명한다. jalr는 대개 즉시값이 0이거나 lui·auipc와 짝을 이루므로 범위가 조금 줄어도 문제가 없다. 대신 계산된 주소의 bit 0을 0으로 지워, 함수 포인터의 최하위 비트를 다른 정보에 쓸 여지를 남겼다.
인코딩은 2바이트 단위 오프셋을 허용하지만, 압축 명령이 없는 구현(IALIGN=32)에서는 명령이 4바이트 경계에 있어야 한다. 이때 분기가 실제로 성립해서 4의 배수가 아닌 주소로 뛰려 하면 명령어 주소 정렬 예외(instruction-address-misaligned)가 난다. 규격은 이 예외를 목표 명령이 아니라 분기·점프 명령 자신에게서 보고하도록 정했고, 성립하지 않은 분기는 예외를 내지 않는다.
컴파일러는 함수가 얼마나 멀리 있을지 모르므로 call을 일단 auipc ra, … + jalr ra, …(ra) 두 명령으로 내보낸다. 링크할 때 실제 거리가 ±1 MiB 안이면 링커가 이를 jal ra, … 한 명령으로 줄인다. 즉시값 범위가 코드 크기와 직결되기 때문에 생긴 최적화다.
핵심 정리
- 베이스 ISA의 명령은 모두 32비트이고 하위 2비트가
11이다. 이 비트들로 16비트 압축 명령과 더 긴 명령의 자리를 미리 확보했다. - 형식은 R·I·S·U 네 가지와, 즉시값 처리만 다른 변형 B·J 두 가지다. opcode는 언제나 [6:0], funct3은 [14:12]에 있다.
- rs1·rs2·rd는 모든 형식에서 같은 자리에 있어서, 해독이 끝나기 전에 레지스터 파일을 읽을 수 있다.
- CSR의 5비트 즉시값을 빼면 즉시값은 모두 부호 확장되며, 부호 비트는 언제나 inst[31]이어서 부호 확장을 해독과 동시에 할 수 있다.
- B는 S와, J는 U·I와 최대한 겹치도록 비트를 “회전”시켰다. S와 비교해 B에서 뜻이 바뀐 것은 부호 비트를 빼면 inst[7](imm[11]) 하나이고, imm[0]은 언제나 0이라 인코딩하지 않는다.
- opcode가 큰 무리를, funct3과 funct7이 세부 연산을 고른다. add/sub, srl/sra는 inst[30] 한 비트로 갈린다.
- 32비트 상수는
lui+addi로 만들고, 하위 12비트의 bit 11이 1이면 상위 값에 +1 한다.auipc는 같은 방식으로 pc 상대 주소를 만든다. - 분기는 ±4 KiB, jal은 ±1 MiB, auipc+jalr는 32비트 pc 상대 전 범위에 닿는다.
확인 퀴즈
메모리에서 읽은 명령의 첫 바이트가 0x13이다. 이 명령의 길이는?
rs1·rs2·rd를 모든 형식에서 같은 자리에 둔 가장 큰 이유는?
B 형식에서 inst[7]이 담는 것은?
모든 즉시값의 부호 비트를 inst[31]에 둔 이유로 가장 알맞은 것은?
li a0, 0x00012800을 lui+addi로 펼치면 lui의 즉시값은?
add와 sub를 구별하는 비트는?
beq로 바로 갈 수 있는 가장 먼 앞쪽 목표는 분기 명령으로부터 몇 바이트인가?
opcode 지도에서 custom-0~3으로 표시된 칸의 뜻은?
더 읽을거리
- The RISC-V Instruction Set Manual, Volume I — RV32I Base Integer Instruction Set (v20260120): Base Instruction Formats, Immediate Encoding Variants, Control Transfer Instructions
- Volume I — RV32/64G Instruction Set Listings: opcode 지도와 전체 인코딩 표
- Volume I — Introduction: Base Instruction-Length Encoding, 명령어 저장 순서
- RISC-V psABI: %hi·%lo·%pcrel_hi·%pcrel_lo 재배치와 링커 완화
- Andrew Waterman, “Design of the RISC-V Instruction Set Architecture”, UC Berkeley 박사 논문 (2016) — 명령어 형식 설계 결정의 배경