RISC-V의 모든 instruction은 32-bit, 1 WORD 길이로 일반화돼있다. 아래를 보면 모든 Instruction이 우리가 읽기에 친숙한 구조가 아닌데, 왜냐하면 RISC-V가 Little endian이기 때문이다.
opcode
: Instruction operation coderd
: 연산결과가 저장될 레지스터 번호 (Destination register number)funct3
: 3-bit 짜리 function coders1
: 연산에 사용될 operand register number 1rs2
: 연산에 사용될 operand register number 2funct7
: 7-bit 짜리 function code, funct3
과 합친 10-bit가 operation의 세부적인 기능을 결정한다.rd
영역을 상수를 표현하기 위한 추가 5-bit 영역으로 활용한다. rs2
에 있는 데이터를 rs1
에 저장한다.Shift 연산은 operand가 2개일 필요가 없으므로 rs2
가 immed
로 대체됐다.
immed
는 몇 bit shift 할 지 나타내는 상수이며 rs2
가 5-bit 였던 것에 비해 immed
는 6-bit다.
immed
가 1bit 더 필요하게 됐다. 남은 칸은 6-bit 이므로 funct6
라는 이름의 칸이 있다.
왼쪽 shift는 slli
라고 표현하고 i
-bit 만큼 왼쪽으로 이동한다.
오른쪽 shift는 srli
라고 표현하고 i
-bit 만큼 오른쪽으로 이동한다.
beq
(= Branch if equal, ==)과 bne
(=Branch if not equal, !=)이 있다.blt
(= Branch less than), bgt
(= Branch greater than)도 있다.ble
(= Branch less than or equal), bge
(= Branch greater than or equal)도 있다.u
를 붙힌 명령어를 사용하면 된다.if (i == j) f = g + h;
else f = g - h;
위와 같은 C언어 코드에서 f
부터 차례대로 x19
에 저장돼있다고 가정하면,
bne x22, x23, Else
add x19, x20, x21
beq x0, x0, Exit ;;; 무조건 진입한다.
Else: sub x19, x20, 21
Exit: ...
x22
(= i
)와 x23
(= j
)를 비교하고, 같지 않다면 Else
라는 이름의 분기문으로 진입한다.Else
에서는 x19
(= f
)에 x20
(=g
)에서 x21
(=h
)를 뺀 결과를 저장한 뒤 Exit
로 진입한다.x19
(= f
)에 x20
(=g
)에서 x21
(=h
)를 더한 결과를 저장한다.beq x0, x0
는 무조건 참이다. 따라서 Else
라벨을 지나서 바로 Exit
로 넘어간다.while (A[i] == k) i++;
위와 같은 C언어 코드에서 i
는 x22
에, k
는 x24
에, A
는 x25
에 저장돼있다고 가정하면,
Loop: slli x10, x22, 3
add x10, x10, x25
ld x9, 0(x10)
bne x9, x24, Exit
addi x22, x22, 1
beq x0, x0, Loop
Exit: ...
i
를 1
증가시켜주는 이유는 A
의 다음 인덱스를 가리키기 위해서다.i
를 2³만큼, 3-bit만큼 shift left 해줘야 한다.i
를 8
증가시켜준 값을 x10
이라는 temporary register에 저장한다.x25
(=A = A[0]
)에 x10
을 더해주면 다음 인덱스의 주소가 되고 이를 다시 x10
에 저장한다.x10
으로 부터 0bit 떨어진 곳의 값을 x9
에 저장한다.x10
에 저장된 주소 안의 값 (=A[i]
)을 x9
에 저장한다.x9 != x24
라면, while문을 그만해야 하므로 Exit:
로 넘어가서 반복문을 종료한다.1
을 i
에 더한다.beq x0, x0
은 무조건 진입하는 unconditional statement이므로 다시 Loop:
로 돌아가서 위 과정을 반복한다.함수같은 프로시저를 호출할 때 발생하는 operation을 의미하고 과정은 다음과 같다.
x10 ~ x17
register에 input parameter들을 저장한다.x1
에는 프로시저를 호출한 (= 돌아갈) 주소가 저장돼있으므로 그쪽으로 return 한다.위 과정을 컴파일하면 다음과 같다.
jal x1, label
로 x1
에 PC + 1
값을 저장해서 돌아갈 주소를 확보하고 label
로 branch한다.jalr x0, 0(x1)
으로 x1
에 저장된 돌아갈 주소로 branch 한다.x0
는 rd
역할을 하므로 경우에 따라 상수값을 넣어서 다른 곳으로 이동할 수도 있다.long long procedure (long long g, long long h,long long i, long long j) {
long long f = (g + h) - (i + j);
return f;
}
위 코드를 컴파일하면 아래와 같다.
;;; g부터 x10에 저장된다. f는 x20에 저장된다. x5, 6은 임시공간.
procedure: addi sp, sp, -24 ;;; Stack pointer를 3-Byte 확보.
sd x5, 16(sp) ;;; 임시공간 x5 공간확보
sd x6, 8(sp) ;;; 임시공간 x6 공간확보
sd x20, 0(sp) ;;; local variable f를 위한 공간확보
add x5, x10, x11 ;;; g + h
add x6, x12, x13 ;;; i + j
sub x20, x5, x6 ;;; f 구함
addi x10, x20, 0 ;;; x10에 return value 저장
ld x20, 0(sp) ;;; 저장해뒀던 x20값 백업
ld x6, 8(sp) ;;; 저장해뒀던 x6값 백업
ld x5, 16(sp) ;;; 저장해뒀던 x5값 백업
addi sp, sp, 24 ;;; Stack pointer 복구
jalr x0, 0(x1) ;;; 돌아갈 주소로 RETURN
x8, x9, x18~x27
)는 사용 후 다시 원래 값으로 복원시켜줘야 함. 이때 위와 같이 stack을 사용할 수 있음.마이크로 아키텍처를 설계할 때 어떤 부분에 대해 초점을 맞췄고, 설계된 ISA가 어떤 목적에 따라 만들어졌는지 위주로 공부하자.