RISC-V환경에서 32-bit 명령어로 구성되었다고 가정하면서 머신 코드가 무엇인지 알아보자.
R포맷 명령어는 계산을 수행하는 데 쓰인다.
| funct7 | rs2 | rs1 | funct3 | rd | opcode |
|---|---|---|---|---|---|
| 7bits | 5bits | 5bits | 3bits | 5bits | 7bits |
opcode
명령어의 핵심 기능은 opcode(operation code)가 전달한다. 그러나 7비트 opcode만으로 명령을 모두 정의하기 어려울 때 7비트, 3비트인 funct7과 funct3를 보조 opcode로 사용한다.
RICS-5는 명령어 개수가 많기 때문에 funct7/3이 필요하지만 다른 포맷에서는 필요하지 않은 경우도 있다.
rd(destination)
연산을 저장할 레지스터 번호이다.
rs1, rs2(source)
각각 연산의 첫 번째, 두 번째 소스가 되는 레지스터 번호이다.
addi나 lw처럼 즉시 수행하는 명령어를 작성할 때 쓰인다.
I 포맷에는 명령어가 몇 개 없어서 funct7 없이도 명령어를 완성할 수 있다.
| immediate | rs1 | funct3 | rd | opcode |
|---|---|---|---|---|
| 12bits | 5bits | 3bits | 5bits | 7bits |
이처럼 RISC-V에는 다양한 명령어가 존재하지만 funct7처럼 필요할지 필요하지 않을지 모르는 비트를 두는 이유는, 각 함수에게 가장 적합한 형태가 있더라도 함수끼리 포맷을 맞춰야 좋은 프로그램이라고 할 수 있기 때문이다.
R포맷과 I포맷도 서로 다른 점이 많지만, 일부러 같은 위치에 동일한 기능의 비트를 배치해서 프로그램 복잡도를 낮췄다.
addi x4, x3, 7 / funct3=000, opcode=0010011
2진수 : 0000 0000 0111 0001 1000 0010 0001 0011
16-bit : 00718213
store를 하기 위한 명령어이다.
| imm[11:5] | rs2 | rs1 | funct3 | imm[4:0] | opcode |
|---|---|---|---|---|---|
| 7bits | 5bits | 5bits | 3bits | 5bits | 7bits |
rs2에 저장된 데이터를 rs1레지스터에 sw한다. immed는 오프셋 용도이고, rd는 없다.
immed가 왜 두 개로 나뉘어있을까?
S-format에는 rd가 없기 때문에 다른 명령어와 일관성을 지키기 위해 immed를 쪼개서 rd 자리를 채웠다.
논리 연산자는 비트를 논리적으로 조작하는 데 쓰인다.
and x9, x10, x11
or x9, x10, x11
xor x9, x10, x11
위 명령어들은 비트를 줄세워놓고 각각 비교한다.
비트의 위치를 이동하는 명령어이다. (곱셈 아님)
SLL, SRL, SRA는 R-format이다.
SLLI, SRLI, SRAI는 I-format과 비슷한데, imm[11:5]는 고정 값이고 imm[4:0]을 활용해서 i값을 정한다. 이 때 몇 비트 shift할지 나타내는 수를 shamt 라고 한다.
Shift Left Logical(SLL)
비트를 왼쪽으로 옮기고 오른쪽 빈 자리에 0을 넣는다.
i-bit로 slli를 수행하면 기존 값에 2^i를 곱한 값이 된다.
Shift Right Logical(SRL)
비트를 오른쪽으로 옮기고 왼쪽 빈 자리에 0을 넣는다.
i-bit로 srli를 수행하면 SLL과 반대로 기존 값을 2^i로 나눈 값이 된다.
srl명령어는 sign bit를 바꾸기 때문에 부호가 있는(특히 음수) 숫자를 대상으로 오류를 일으킬 수 있다.
Shift Right Arithmetic(SRA)
SRL의 한계를 해결하기 위한 연산자이다.
SRL과 비슷하지만 빈 공간을 0이 아닌 sign bit로 채워서 부호를 유지한다.
shamt는 왜 5비트일까?
컴퓨터가 32비트이기 때문에 2^5 이상 shift하면 숫자가 원래대로 돌아온다. 만약 64비트 환경이었으면 6비트 shamt를 사용했을 것이다.
rd는 레지스터가 32개이기 때문에 5비트, shamt는 컴퓨터가 32비트이기 때문에 5비트이다.
조건 명령어와 밀접한 개념은 branch 이다. 명령어는 보통 순서대로 처리되지만 branch가 발생하면 순서를 뛰어넘을 때가 있다.
beq rs1, rs2, L1 : if(rs1==rs2) go to L1
bne rs1, rs2, L1 : if(rs1!=rs2) go to L1
blt rs1, rs2, L1 : if(rs1<rs2) go to L1
bge rs1, rs2, L1 : if(rs1>=rs2) go to L1 (great or equal)
>나 <=기호가 없는 이유는 명령어를 만들기보다 rs1, rs2 위치를 바꾸는 것이 효율적이기 때문이다. 비교연산자에서는 rs1, rs2 순서가 정해져있지 않다.
if(i==j) 라는 분기가 있다고 가정해보자
단순하게 생각하면 beq 명령어를 사용해야 하는 것처럼 보인다.
하지만 코드 상으로, i와 j가 같으면 그냥 순서대로 진행하고,
같지 않으면 else로 건너뛰어야 하기 때문에bne를 사용하는 것이 맞다.만약 순서대로 진행했다면?
어셈블리 상으로 명령어 순서는 bne - L1 - Else 이다.
만약 bne에서 조건을 통과하여 L1을 수행했다면 그 다음으로 수행하면 안 되는 Else가 온다.
따라서beq x0, x0, Exit처럼 항등식 브랜치 명령어를 L1 다음에 넣어, L1을 수행한 이후 Else를 뛰어넘도록 한다.
bne - L1 - beq - Else - Exit 순서로 구성하면 이상적이다.
unsigned 환경에서 비교 조건 사용
만약 rs1, rs2가 unsigned라면 blt, bge대신 bltu, bgeu를 사용한다.
숙제 2.9, 2.13, 2.18, 2.25
0100_0000_0101_0011_1000_0011_0011_0011
010011