[컴퓨터구조] RISC-V Instructions (1)
RISC-V는 대표적인 RISC (Reduce Instruction Set Computer) 구조로, 레지스터의 비트 수에 따른 여러 버전과 Extension이 존재한다. 본 글에서는 32개의 32-bit 레지스터를 가진 rv32i 버전을 기준으로 설명한다.
RISC-V에서 32개의 레지스터들에게는 미리 정해진 역할이 있는데, Instruction을 알아보기 전 간략히 알아보자.
RISC-V Registers
- x0 (zero): Hard-wired zero
- x1 (ra): Return address (procedure call에서 사용)
- x2 (sp): Stack pointer (procedure 내 local variable 접근)
- x3 (gp): Global pointer (global variable 접근)
- x4 (tp): Thread pointer
- x5~7 (t0~2): Caller-save registers
- x8 (s0/fp): Callee-save register / Frame pointer
- x9 (s1): Callee-save register
- x10, 11 (a0, a1): Function arguments / Return Values
- x12~17 (a2~7): Function arguments
- x18~27: Callee-save registers
- x28~x31: Caller-save registers
x0는 언제나 그 값이 각종 연산에 빈번히 쓰이는 0으로 고정되어 있는 레지스터이다.
x1부터 x4까지는 procedure 및 메모리 관리에 쓰이는 포인터에 해당하고, 그 이후에는 caller-save, callee-save, arguments등 데이터 값들을 저장하는 레지스터로 구성되어 있음을 알 수 있다.
이외에도 PC(Program Counter) 등 위 32개에 포함되지 않는 특수 레지스터들이 존재한다.
이제 본격적으로 RISC-V의 Instruction Set을 알아보자.
(다른 RISC 프로세서들이 그렇듯) RISC-V의 CPU는 기본적으로 다음 사이클을 반복하며 동작한다.
Fetch Instruction -> Decode -> Execute -> Update PC
여기에서 프로세서가 실행하는 Instruction들은 역할에 따라 다음과 같이 분류할 수 있다.
RISC-V Instruction (기능적 분류)
- Perform arithmetic/logic operation on register data
- Transfer data between memory and register
- Transfer control (jump, branch, procedure call and return)
위 기능 및 세부 기능에 따라 Instruction을 encoding하는 format도 여러 가지가 있는데, 이는 다음 포스팅에서 다루겠다.
Arithmetic/Logic operations
먼저, operand(레지스터 혹은 immediate value)에 대해 산술 및 논리 연산을 하여 저장하는 instruction들의 목록은 다음과 같다.
RISC-V Arithmetic operations
- add
- sub (subtract)
- addi (add immediate)
- slt (set less than)
- slti (set less than immediate)
- sltu (set less than unsigned)
- sltui (set less than unsigned immediate)
- lui (load upper immediate)
- auipc (add upper immediate to pc)
위 instruction들의 사용 예시와 그 의미는 다음과 같다.
add rd, rs1, rs2
## R[rd] = R[rs1] + R[rs2]
sub rd, rs2, rs2
## R[rd] = R[rs1] - R[rs2]
addi rd, rs1, imm12
## R[rd] = R[rs1] + SignExt(imm12)
slt rd, rs1, rs2
## R[rd] = (R[rs1] < R[rs2]) ? 1 : 0
slti rd, rs1, imm12
## R[rd] = (R[rs1] < SignExt(imm12)) ? 1 : 0
lui rd, imm20
## R[rd] = SignExt(imm20 << 12)
auipc rd, imm20
## R[rd] = PC + SignExt(imm20 << 12)
u(unsigned)가 붙은 operation의 경우, 비교하는 데이터를 unsigned 값으로 읽어 비교한다는 의미이다.
또한 'upper immediate' operation이 필요한 이유는, 모든 instruction이 32-bit로 인코딩되기 때문에, 한번에 레지스터에 로드할 수 있는 immediate value의 자릿수에 한계가 있기 때문이다.
예를 들어 x5에 1000 0000 0000 0001(2)이라는 값을 저장하고 싶다고 하자. 이때 addi와 같은 operation만으로는 쉽게 값을 저장할 수 없다. immediate의 자릿수가 최대 12자리이기 때문이다.
이때 다음과 같이 lui를 이용하여, 12자리보다 높은 자릿수를 먼저 채운 다음 아래 12자리는 addi를 이용해 채울 수 있다.
lui x5, 16
addi x5, x5, 1
RISC-V Logical Operations
- and
- or
- xor
- andi (and immediate)
- ori (or immediate)
- xori (xor immediate)
- sll (shift left logical)
- srl (shift right logical)
- sra (shift right arithmetic)
- slli (shift left logical immediate)
- srli (shift right logical immediate)
- srai (shift right arithmetic immediate)
위 instruction들의 사용 예시와 그 의미는 다음과 같다.
and rd, rs1, rs2
## R[rd] = R[rs1] & R[rs2]
or rd, rs1, rs2
## R[rd] = R[rs1] | R[rs2]
xor rd, rs1, rs2
## R[rd] = R[rs1] ^ R[rs2]
sll rd, rs1, rs2
## R[rd] = R[rs1] << R[rs2]
sli rd, rs1, shamt
## R[rd] = R[rs1] << shamt
사용 예시와 기능을 쉽게 유추할 수 있는 것은 생략했다.
참고로 logical right shift와 arithmetic right shift의 차이는, 기존 비트를 오른쪽으로 shift한 후 남은 자리를 0으로 채울 것인지 most significant bit로 채울 건지의 차이이다.
Data Transfer Operations
메모리로부터 데이터를 레지스터에 불러오고, 반대로 레지스터의 데이터를 메모리에 저장하는 instruction들을 살펴보자.
RISC-V의 메모리는 Byte-addressed 방식이다. 즉, 하나의 메모리 주소는 메모리 상에서 하나의 Byte를 가리킨다. 한편 레지스터 하나는 8byte에 해당하는 64-bit를 저장하는데, 따라서 Data Transfer Operation은 byte, halfword (2byte), word (4byte), doubleword (8byte) 버전이 존재하여, 한 번의 메모리 주소 몇 개에 해당하는 데이터를 이동시킬지 선택할 수 있다.
또한 RISC-V는 Little Endian 방식으로, 한 word를 이루는 byte의 주소들 중 가장 작은 것이 해당 word의 주소를 나타낸다.
RISC-V Data Transfer Operations
- ld (load doubleword)
- lw (load word)
- lh (load halfword)
- lb (load byte)
- lwu (load word unsigned)
- lhu (load halfword unsigned)
- lbu (load byte unsigned)
- sd (store doubleword)
- sw (store word)
- sh (store halfword)
- sb (store byte)
unsigned가 붙은 load operation은 메모리에서 불러온 데이터에 대해 sign extension 대신 zero extension을 거쳐 레지스터에 저장한다.
위 instruction들의 사용 예시와 그 의미는 아래와 같다. 이번에도 예시와 의미를 쉽게 유추할 수 있는 것들은 생략했다.
ld rd, imm12(rs1)
## R[rd] = Mem8[ R[rs1] + SignExt(imm12) ]
lw rd, imm12(rs1)
## R[rd] = SignExt( Mem4[ R[rs1] + SignExt(imm12) ] )
lwu rd, imm12(rs1)
## R[rd] = ZeroExt( Mem4[ R[rs1] + SignExt(imm12) ] )
sd rs2, imm12(rs1)
## Mem8[ R[rs1] + SignExt(imm12) ] = R[rs2]
sw rs2, imm12(rs1)
## Mem4[ R[rs1] + SignExt(imm12) ] = R[rs2](31:0)
Control Transfer Operations
마지막으로, 프로그램의 실행 흐름을 제어하는 Instruction들에 대해 알아보자. 이들은 조건이 만족되었을 때 PC를 설정한 위치로 이동시키는 branch와 jump로 구성되어 있다.
RISC-V Control Transfer Operations
- beq (branch equal)
- bne (branch not equal)
- bge (branch greater than or equal)
- bgeu (branch greater than or equal unsigned)
- blt (branch less than)
- bltu (branch less than unsigned)
- jal (jump and link)
- jalr (jump and link register)
이들의 사용 예시와 그 의미는 다음과 같다.
beq rs1, rs2, imm12
## if(R[rs1] == R[rs2]) PC += SignExt(imm12 << 1)
bne rs1, rs2, imm12
## if(R[rs1] != R[rs2]) PC += SignExt(imm12 << 1)
bge rs1, rs2, imm12
## if(R[rs1] >= R[rs2]) PC += SignExt(imm12 << 1)
jal rd, imm20
## R[rd] = PC + 4
## PC += SignExt(imm20 << 1)
jalr rd, imm12(rs1)
## R[rd] = PC + 4
## PC += (R[rs1] + SignExt(imm12)) & (~1)
또한 실제 Instruction set에는 포함되지 않지만 프로그래밍 후 어셈블러가 자동으로 instruction으로 변환해 주는 pseudo-instruction도 있는데, 이들은 procedure call 등에 유용하다.
jal imm20
## -> jal x1, imm20
ret
## -> jalr x0, 0(x1)
다음 포스팅에서는 이러한 instruction들이 인코딩되는 포맷의 구조와 종류에 대해 정리해 보겠다.
References
- D. Patterson, J. Hennessy (2021). Computer Organization and Design: RISC-V edition (2nd ed). Morgan Kaufmann. pp.66-178