RISC-V Instructions

김민욱·2025년 6월 15일

Architecture

instruction set(명령어)과 operand locations(연산에 사용할 데이터의 위치 ; 레지스터, 메모리 등)로 구성되는 프로그래머 관점에서의 컴퓨터Architecture라고 한다.

RISC-V architecture
Kriste Asanovic과 David Patterson과 UC Berkeley 대학이 개발한 최초로 널리 상용화된 오픈소스 컴퓨터 아키텍쳐다.
RISC-V 아키텍쳐는 다음과 같은 설계 원칙(Design Principles) 아래에 개발되었다.

Architecture Design Principles of RISC-V
1. Regularity supports simplicity
2. Make the common case fast
3. Smaller is faster
4. Good design demands good compromises

Instruction

설계 원칙 1번에 따라서 명령어 포맷을 일관성 있게 유지한다.
(two sources and one destination)

CRISC-V(op, dest, src, src)
a = b + c;add a, b, c
a = b - c;sub a, b, c

복잡한 연산은 여러개의 명령어로 다룬다.
C

a = b + c - d;

RISC-V

add t, b, c
sub a, t, d

# 여담으로 주석은 '#'을 이용해서 쓴다

설계 원칙 2번에 따라서 RISC-V는 단순하고 자주 사용되는 명령어만 가지고 있다. 덕분에 하드웨어는 명령어를 단순하고 빠르게 해석하고 실행할 수 있다.
RISC-V의 RISC가 Reduced Instruction Set Computer인 점을 기억하자.

Operand

Operand location은 레지스터, 메모리, 상수(Constant; immediate) 같이 데이터가 저장되어 있는 컴퓨터 내부의 물리적인 위치이다.

Register Operand
RISC-V는 32개의 32-bit 레지스터를 가지고 있고, 레지스터는 메모리보다 빠르다.

레지스터는 이름(zero, ra 등)으로도 쓸 수 있고 번호(x0, x1 등)로도 사용 가능하다. 보통 이름으로 쓴다.

각 레지스터는 용도에 맞게 사용된다.

레지스터 종류용도
zero항상 상수값 0을 저장
saved registers(s0-11)변수 값 저장에 사용
temporary registers(t0-t6)큰 계산에서 상수값 저장에 사용

Memory Operand
32개의 레지스터에 모두 저장되기에는 데이터가 너무 많다. 남은 데이터들은 메모리에 저장되어야 하는데 메모리는 크지만 느리다. 따라서 자주 쓰는 변수들을 레지스터에 저장한다.

메모리에는 Word-addressable memory와 Byte-addressable memory가 있다.

  • Word-addressable : 메모리에 지정된 word 단위를 기준으로 주소 할당
  • Byte-addressable : 각 byte에 고유한 주소를 할당

문자 처리에 있어서 word-addressable 방식보다 byte-addressable 방식이 더 효율적이기 때문에 RISC-V는 byte-addressable memory를 채택했다.

(a) Byte address 예시, (b) 데이터 저장 방식

메모리 8번지에 있는 데이터 word를 s3 레지스터로 load하기

lw s3, 8(zero) # read word at address 8 into s3

t7 레지스터가 가진 값을 메모리 0x10번지에 저장하기

sw t7, 0x10(zero) # write t7 into address 16

위 코드를 보면 알 수 있듯이 메모리 주소는 zero+offset 방식으로 작성한다.

Constant; immediate
addi 명령어에는 12-bit의 signed constant를 담을 수 있다.
하지만 프로그램을 짜다보면 12-bit 이상의 상수가 필요할 때가 있다.
그럴 때는 lui 명령어와 addi명령어를 함께 사용한다.
lui 명령어는 load upper immediate의 약자로, immediate의 상위 20bit를 저장소에 넣고 하위 12bit는 0으로 채우는 명령어이다.

int a = 0xFEDC8765;

와 같은 코드가 RISC-V assembly code에서는 다음과 같이 해석된다.

# s0 = a
lui s0, 0xFEDC8
addi s0, s0, 0x765

이 때, addi는 12-bit의 immediate를 sign-extend함을 기억하라.

만약 32bit 중 하위 12bit의 msb가 1일 경우 addi의 특성에 의해 1로 extend 되어 음수가 되어버린다.
이를 방지하기 위해 해당 케이스에서는 lui에 있는 immediate에 1을 더한다.

int a = 0xFEDC8EAB;
# Note : 0xEAB = -341
# s0 = a
lui s0, 0xFEDC9 # s0 = 0xFEDC9000
addi s0, s0, 0xEAB # s0 = 0xFEDC9000 + 0xFFFFFEAB = 0xFEDC8EAB

그러나, addi가 아닌 or을 이용하면 더 간단하게 처리할 수 있다.

Programming for RISC-V

  • Logical instruction

immediate 명령어로는 andi, ori, xori가 있다.

  • Shift instruction
    sll(shift left logical), slli(sll immediate)
    왼쪽 logical shift 명령어. left shift는 logical과 arithmetic의 차이가 없기 때문에 이 명령어 하나로 통일된다.
    srl(shift right logical), srli(srl immediate)
    오른쪽 logical shift 명령어. 빈 자리를 0으로 채운다.
    sra(shift right arithmetic), srai(sra immediate)
    오른쪽 arithmetic shift 명령어. 빈 자리를 msb로 채운다.

  • Multiplication instruction
    32bit×32bit의 결과는 64bit이기 때문에 명령어가 두개로 나뉘었다.
    mul s3, s1, s2
    곱셈 결과의 하위 32bit를 s3에 저장한다.
    mulh s4, s1, s2
    곱셈 결과의 상위 32bit를 signed bit로 저장한다.

  • Division instruction
    32-bit 나눗셈의 결과는 32-bit 몫과 32-bit 나머지이다.
    div s3, s1, s2
    나눗셈의 몫 32bit를 s3에 저장한다.
    rem s4, s1, s2
    나눗셈의 나머지 32bit를 s4에 저장한다.

Branch & Jump

조건명령
조건부beq(equal), bne(not equal), blt(less than), bge(greater than or equal)
무조건j(jump), jr(jump register), jal(jump and link), jalr(jump and link register)

무조건 jump는 함수 호출에 주로 사용된다.

  • conditional branch 예시
addi s0, zero, 4 # s0 = 0 + 4 = 4
addi s1, zero, 1 # s1 = 0 + 1 = 1
slli s1, s1, 2	 # s1 = s1 << 2 = 4
beq s0, s1, target # s0 == s1 -> jump to target
addi s1, s1, 1
sub s1, s1, s0

target : # target is here
	add s1, s1, s0 # s1 = s1 + s0 = 8
  • unconditional jump 예시
j	target # jump to target
srai s1, s1, 2 # 실행 X
addi s1, s1, 1 # 실행 X
sub s1, s1, s0 # 실행 X

target : # target is here
	add s1, s1, s0 # excuted

Conditional statement & Loop

  • if statement
if (i == j) f = g + h;

f = f - i;

\darr

# s0 = f, s1 = g, s2 =h
# s3 = i, s4 = j
	bne s3, s4, L1 # i != j ?
    add s0, s1, s2 # f = g + h
 
L1 : # L1 is here
	sub s0, s0, s3 # f = f - i

High-level 프로그래밍 언어에서는 (==) 케이스를 검사했으나, assembly 언어에서는 (!=) 케이스로 검사를 하는 것을 알 수 있다. 이는 현재 상황에서 조건에 맞지 않을 때 jump하는 것이 더 자연스럽고 분기 구조를 단순하게 만들기 때문이다.

  • if/else statement
if (i==j) f = g + h;
else f = f - i;

\darr

# s0 = f, s1 = g, s2 =h
# s3 = i, s4 = j
	bne s3, s4, L1
    add s0, s1, s2
    j	L2

L1 :
	sub s0, s0, s3
L2 :
  • while loop
int pow = 1;
int x = 0;

while (pow != 128) {
	pow = pow * 2;
    x = x + 1;
}

\darr

# s0 = pow, s1 = x
	addi s0, zero, 1 # pow = 1
    add s1, zero, zero # x = 0
    
    addi t0, zero, 128 # t0 = 128

while :
	beq s0, t0, done # pow == 128 ?
    slli s0, s0, 1 # pow = pow * 2
    addi s1, s1, 1 # x = x + 1
    j	while # repeat loop

done :

위에서 봤던 if와 마찬가지로 현재 코드에서는 ==을 검사하여 jump하는 것이 더 자연스럽고 분기 구조를 단순하게 만들기 때문에 bne로 바뀌었다.

  • for loop
    assembly의 for 구현을 알아보기 전에 High-level 언어에서 for 문의 구성을 먼저 살펴 보자.
for (init; condition; loop operation)
	statement;

init : loop 진입 전에 실행되는 구문
condition : 매 반복 전에 실행되는 구문
loop operation : 매 반복 끝에 실행되는 구문
statement : 매 반복 중 조건이 맞을 때 실행되는 구문

int sum = 0;
int i = 0;

for (i=0; i!=10; i=i+1) {
	sum = sum + i;
}

\darr

# s0 = i, s1 = sum
	addi s1, zero, 0 # sum = 0
    addi s0, zero, 0 # i = 0 ; init
    addi t0, zero, 10

for :
	beq s0, t0, done # i==10 ? ; condition
    add s1, s1, s0 # sum += i ; statement
    addi s0, s0, 1 # i = i + 1 ; loop operation
    j	for		   # repeat

done :
  • Less Than 처리 방법
int sum = 0;
int i = 0;

for (i=0; i < 101; i = i + 1) {
	sum = sum + i;
}

ver.1

# s0 = i, s1 = sum
	addi s1, zero, 0 # sum = 0
    addi s0, zero, 0 # i = 0 ; init
    addi t0, zero, 101

for :
	bge s0, t0, done # i >= 101 ? ; condition
    add s1, s1, s0 # sum += i ; statement
    addi s0, s0, 1 # i = i + 1 ; loop operation
    j	for		   # repeat

done :

ver.2

# s0 = i, s1 = sum
	addi s1, zero, 0 # sum = 0
    addi s0, zero, 0 # i = 0 ; init
    addi t0, zero, 101

for :
	slt t2, s0, t0 # if (s0<t0) t2=1, else t2=0
	beq t2, zero, done # t2==0 ? ; condition
    add s1, s1, s0 # sum += i ; statement
    addi s0, s0, 1 # i = i + 1 ; loop operation
    j	for		   # repeat

done :

Array
배열(Array)을 이용하면 큰 양의 유사한 데이터를 index를 이용해 접근할 수 있다.

int arr[5];
arr[0] = arr[0] * 2;
arr[1] = arr[1] * 2;

\darr

# s0 = arr base address ; 0x123B4780
lui s0, 0x123B4
addi s0, s0, 0x780 # s0 = 0x123B4780

lw t1, 0(s0) # t1 = arr[0]
slli t1, t1, 1 # t1 = t1 * 2
sw t1, 0(s0) # arr[0] = t1

lw t1, 4(s0) # t1 = arr[1]
slli t1, t1, 1 # t1 = t1 * 2
sw t1, 4(s0) # arr[1] = t1

accessing array using for loop

int arr[1000];
int i;

for (i=0; i<1000; i=i+1)
	arr[i] = arr[i] * 8;

\darr

# s0 = arr base address ; 0x23B8F400
# s1 = i
	lui s0, 0x23B8F    # s0 = 0x23B8F000
    ori s0, s0, 0x400  # s0 = 0x23B8F400
    addi s1, zero, 0   # i = 0
    addi t2, zero, 1000 # t2 = 1000

loop :
	bge s1, t2, done	# i >= 1000 goto done
    slli t0, s1, 2		# t0 = i * 4
    add t0, t0, s0		# arr[i] 주소
    lw t1, 0(t0)		# t1 = arr[i]
    slli t1, t1, 3		# t1 = t1 * 8
    sw t1, 0(t0)		# arr[i] = t1
    addi s1, s1, 1		# i = i + 1
    j	loop			# repeat

done :

<참고자료>
Harris & Harris, Digital Design and Computer Architecture, RISC-V Edition, 2022.

0개의 댓글