

Opcode : 명령어의 종류를 표현하는 부분
Operand : 피연산자
트랜지스터 속 $2의 자리에 $2의 값과 $4의 값을 더해서 집어넣어라
• x86 ISA 다양한 구현을 가짐: 286, 386, 486, Pentium, Pentium Pro, Pentium 4, Core, …
Microarchitecture는 회사마다 다양한 방법이 있을 수 있다
http://www.jidum.com/jidums/view.do?jidumId=401
자잘한걸 많이 호출해야하지만 그러나 일관적이고 간단하므로 ㄱㅊㄱㅊ
Most modern x86 processors are implemented using RISC techniques

CISC의 접근법: IC을 줄일려고함
RISC의 접근법: CPI를 줄일려고함

RISV-V : RISC 원칙에 기반한 오픈 스탠다드 ISA이다
Simplicity favors regularity
- 회로제작이 더 쉬워진다
- 잠재적인 수행능력에서 이득을 본다
C code: f = (g + h) - (i + j);
Compiled Assembly code:
레지스터는 CPU 내부에 있는 작은 메모리 공간으로, 매우 빠른 속도로 데이터를 저장하고 처리할 수 있습니다.
레지스터는 주로 연산을 위해 자주 사용되는 데이터를 저장하거나, 메모리 접근보다 빠른 데이터 접근을 가능하게 합니다
Operands of arithmetic instructions must be registers
(산술연산의 대상은 반드시 레지스터(안의 데이터)여야 한다. 레지스터의 크기는 보통 32비트)
RISC-V의 레지스터 크기 : 64 bits (doubleword)

► 레지스터가 아닌 메모리에 저장된 데이터를 피연산자로 사용하는 경우를 의미
► CPU는 메모리 주소를 참조하여 메모리에 있는 데이터를 읽어와 레지스터에서 연산한다
ld : load double wordld x2, 16(x3) :x3메모리에서 16바이트 떨어진 메모리의 값을 읽어 x2에 저장해라address 공간의 주소 
sd : store double wordsd x9, 96(x22) :x9 레지스터에 있는 값을 x22 레지스터에 저장된 메모리 주소에 96바이트 더한 위치에 저장하라는 명령어.A[12] = h + A[8];
ld x9, 64(x22) // 배열 A의 base adress가 들어있는 x22 레지스터에서 64바이트 떨어진 위치에서 64비트 (doubleword의 크기)만큼 읽어 x9에 저장해라
add x9, x21, x9 //x21에 있는값 (h)와 x9에 있는값(A[8])을 더해서 x9에 저장해라
sd x9, 96(x22) //x9 레지스터에 있는 값을 x22 레지스터에 저장된 메모리 주소에 96바이트 더한 위치에 저장하라는 명령어. (A[12]이니 A의 base address에서 12*8 떨어진 곳)
Memory Operand에 사용되는 명령어들은 레지스터에서 사용되는 add나 sub보다 수십배는 더 오래걸린다
물리적으로 CPU 밖으로 나가서 8바이트가 흘러들어와 저장이 되어야하기 때문
따라서 메모리로 가는 것은 최대한 피하는것이 좋다! (쩔수긴 함 ㅋ)
Big-endian과 Little-endian은 데이터를 메모리에 저장하는 두 가지 주요 방식입니다.최상위 바이트(Most Significant Byte, MSB)가 가장 낮은 메모리 주소에 저장됩니다.
사람이 숫자를 읽는 방식과 비슷하게 큰 값이 앞에 오도록 정렬됩니다.
예를 들어, 32비트 숫자 0x1234ABCD을 Big-endian 방식으로 저장하면:
주소 0x1000: 12
주소 0x1001: 34
주소 0x1002: AB
주소 0x1003: CD
최하위 바이트(Least Significant Byte, LSB)가 가장 낮은 메모리 주소에 저장됩니다.
작은 값이 앞에 오도록 정렬됩니다.
동일한 32비트 숫자 0x1234ABCD을 Little-endian 방식으로 저장하면:
주소 0x1000: CD
주소 0x1001: AB
주소 0x1002: 34
주소 0x1003: 12

RISC-V에서는Little endian을 사용한다!
16진수의 한자리는 4비트를 가짐 (비트가 4개여야지 16개를 표현할 수 있기 때문)
1바이트는 8비트임 따라서 2자리수씩 끊는것 AB, CD,,,
Access speed
Capacity
Energy consumption

그래서 자주 쓰이는 변수는 레지스터에 저장하는 것이 좋다
Spilling: 레지스터에 공간이 부족할 때 덜 자주 쓰이는 변수를 메모리로 옮기는것
기존의 add명령어는 add x22, x22, x9 처럼 셋다 레지스터에서 갖고오는거라면
바로 4와 같은 상수(숫자)를 더하고 싶을 수도 있다.
그렇다면
var1: .dword 4 //어셈블리에서 변수 선언 . Var1은 변수명(주소이자 위치)을 의미 .dword 는 doubleword라는 타입, 4는 저장된 값(크기는 8바이트)
...
ld x9, var1(x3) # x3 + var1’s offset // x3이라는 base adress에다가 var1(4)이라는 offset을 더한 주소에서 값을 읽어 x9에다 저장해라
add x22, x22, x9
► 이러한 연산은 로딩하는 과정에서 메모리를 들럿다와 매우 비효율적이다
따라서 immediate operand를 사용해서 즉시 연산 처리를 함
addi x22, x22, 4 // 상수 4가 메모리 안에 있는 값이 아님, 명령어 안의 비트의 빈공간에 숫자 4를 바로 넣은거임 (0100)
▫ Immediate operand instructions are faster and use less energy
▫ 4 is included inside the instruction bits

• opcode: Basic operation (연산의 종류를 알려줌)
• rd: destination register
• funct3: additional opcode
• rs1: 1st source register (5bits인 이유는 레지스터가 32개기 때문이다 (2^5)
• rs2: 2nd source register
• funct7: additional opcode

위 표에서 add와 sub의 opcode 는 동일한데, 이는 연산이라는 공통된 명령어라는 것을 의미
이후 funct7에서 추가적으로 각각 더하기와 빼기임을 구분을해준다.
원래는 fuct7과 funct3 그리고 opcode는 모두 합쳐져 opcode의 역할을 하지만
rs2, rs1, rd의 효율적인 배치를 따지다보니 하나의 역할이 세 토막으로 나뉘어진것이다.
add 명령어:
opcode: 0110011
funct3: 000
funct7: 0000000
sub 명령어:
opcode: 0110011 (add와 동일)
funct3: 000 (add와 동일)
funct7: 0100000 (funct7을 통해 sub 연산이 구분됨)
lb (load byte):
opcode: 0000011
funct3: 000
lh (load halfword):
opcode: 0000011
funct3: 001
lw (load word):
opcode: 0000011
funct3: 010

rs1 = x20
rs2 = x21
rd = x9
R-type 명령어의 종류
1. 논리 연산(Logical Operations)
and
or
xor
sll
2. 산술 연산(Arithmetic Operations)
ADD: add rd, rs1, rs2
rd = rs1 + rs2 (덧셈)
SUB: sub rd, rs1, rs2
rd = rs1 - rs2 (뺄셈)
MUL: mul rd, rs1, rs2
rd = rs1 * rs2 (정수 곱셈, M-extension에 해당)
DIV: div rd, rs1, rs2
rd = rs1 / rs2 (정수 나눗셈, M-extension에 해당)
비교 연산(Comparison Operations)
SLT: slt rd, rs1, rs2
rd = (rs1 < rs2) ? 1 : 0 (부호 있는 비교)
SLTU: sltu rd, rs1, rs2
rd = (rs1 < rs2) ? 1 : 0 (부호 없는 비교)
시프트 연산(Shift Operations)
SLL: sll rd, rs1, rs2
rd = rs1 << rs2 (논리적 왼쪽 시프트)
SRL: srl rd, rs1, rs2
rd = rs1 >> rs2 (논리적 오른쪽 시프트)
SRA: sra rd, rs1, rs2
rd = rs1 >> rs2 (산술적 오른쪽 시프트)
곱셈 및 나눗셈 연산(M-extension)
MUL: mul rd, rs1, rs2
rd = rs1 rs2 (곱셈)
MULH: mulh rd, rs1, rs2
rd = 상위 비트(rs1 rs2) (정수 곱셈 상위 비트)
DIV: div rd, rs1, rs2
rd = rs1 / rs2 (정수 나눗셈)
REM: rem rd, rs1, rs2
rd = rs1 % rs2 (정수 나머지)

addi 와 같은 명령어는 rs2가 필요가 없음 -> 남는 부분 따라서
addi x5, x3, 4
이것의 경우 4같은 숫자를 할당하는 공간에 12비트가 할당이되니 2^12
들어갈 수 있는 최대 숫자는 -2048~2047 (2의보수이므로 한비트가 부호를 위해 빠져서 2^11)
또한, immediate의 값은 명령어의 종류에 따라 다르게 해석이 된다
예를 들어, ld x14, 8(x2)에서 immediate 값(8)은 오프셋(offset)으로 해석됩니다.
x2 레지스터에 저장된 주소에 immediate 값(8)을 더하여, 최종 메모리 주소를 계산한 뒤 해당 주소에서 값을 가져옵니다.
산술 연산 명령어에서 immediate 값은 연산에 사용되는 상수로 해석됩니다.
예를 들어, addi x22, x22, 4 명령어에서는 immediate 값(4)가 상수로 사용됩니다.
즉, x22 레지스터의 값에 4를 더한 결과를 다시 x22 레지스터에 저장합니다.
immediate는 분기할 주소의 오프셋으로 사용됩니다.
예를 들어 beq x1, x2, offset에서 offset은 현재 명령어로부터 얼마만큼 떨어진 주소로 분기할지를 나타냅니다.


(01111이 아닌 01110)
(27`30)
• Two registers and one immediate field
• Two immediate fields: 7bits+5bits
• rs1 and rs2 remain in the same position for all instructions

사실상 i-type과 내부적으론 똑같은데,,왜 저따우로 햇냐면
rd가 필요없어져서 rd부분을 immediate로 돌리고 원래 immmediate에서 5비트 때서 rs2할당
sd같은 ㄱ

사실상 0000000 + 01000 = 8

(32') 다시들어잉

Shift operation
00000000 00001001 = 9
00000001 00100000 = 9 x 32 (shift left 5)
Shift instructions (I-type)
(n칸 옮기라는 숫자가 immediate에 들어감)
- slli, srli – shift left (right) logical immediate
- sra, srai – shift right arithmetic (immediate): fill with sign-bit
1100 0011 → 1110 0001 (일종의 sign extension이다)
srl x1, x2, x3
x2 레지스터의 값을 x3 레지스터의 값만큼 오른쪽으로 시프트하고, 결과를 x1에 저장합니다.
srli x1, x2, 5
x2 레지스터의 값을 5비트만큼 오른쪽으로 시프트하고, 그 결과를 x1에 저장합니다.

[동작 예시:]
and x1, x2, x3
x2: 1010 1010₂
x3: 1100 1100₂
x1: 1000 1000₂ (각 비트를 AND 연산)
동작 예시
andi x1, x2, 15
x2: 1010 1010₂
15: 0000 1111₂
x1: 0000 1010₂ (각 비트를 AND 연산)

두개도 비슷혀~


(not이라는 명령어는 따로 없으며, xor 명령어로 그 역할을 대체함)
beq rs1, rs2, L1
► rs1과 rs2가 같으면 L1으로 이동해라
bne rs1, rs2, L1
► rs1과 rs2가 다르면 L1으로 이동해라
if (i==j)
f = g + h;
else
f = g – h;
bne x22, x23, Else # go to Else if i ≠ j
add x19, x20, x21 # f = g + h (skipped if i ≠ j)
beq x0, x0, Exit # if 0 == 0, go to Exit //항상 같으므로 무조건 exit ( else부분을 건너뛰기 위한 구문) (unconditional)
Else: sub x19, x20, x21 # f = g − h (skipped if i = j)
Exit:
While (save[i]==k)
i += 1;
Loop: slli x10, x22, 3 // (shift left logical 3 이니 2^3을 곱하란 의미 ) x10 = i x 8 (array지만 디폴트가 doubleword이므로 8byte로 가정, 따라서 8을 곱함)
add x10, x10, x25 // x10 = address of save[i], x25라는 베이스 주소에 offset인 x10 더하기
ld x9, 0(x10)// x9 = save[i] x10: 가져오고자하는 데이터의 주소값, 0은 offset
bne x9, x24, Exit // go to Exit if save[i] ≠ k (x9와 x24가 다르면 exit)
addi x22, x22, 1 // i = i+1
beq x0, x0, Loop // go to Loop
Exit:
[8][8][8][8]
blt rs1, rs2, L1
bge rs1, rs2, L1
int[] age = new int[5];
age[-2] = 12; // 겠냐고
age[2] = 4;
age[12] = 4; // 겠냐고
Need to check if index>0 and index<=max_length
if x20 >= x11 or x20 < 0, goto IndexOutOfBounds
bgeu x20, x11, IndexOutOfBounds
x20이 index, x11이 최대치
만약 x20이 -1이면 bgeu가 2의 보수로 봤을땐 -1인데 (11111111111~0) unsigned로 해석하면 엄청 큰수가 되서 암튼간에 Indexoutofbound로 처리가됨, 음수면 존나 커져서 아웃 ㅋㅋ
int main() {
if (a == 0)
b = f1(g, h);
else
b = f1(k, i);
return 0;
}
int f1(x,y) {
a = (x+y) * (x+2) * f2(y);
return a;
}
int f2(y) {
return y/2;
}

jal x1, ProcedureAddress (함수 호출 명령어)
• jal: “jump-and-link” (어디로 점프해서 마저 수행하라)
• Branch unconditionally and save the address of the next instruction (return
address) to the designated register (무조건 적으로 점프해라, 그리고 다음 명령어의 주소를 지정된 레지스터(x1) 저장해라(link) -> 함수로 점프하기 전에 돌아올 지점을 링크해놓는,,,
jal x0, Label
jalr x0, 0(x1)
마찬가지로 x0에 뭘 시도해도 의미가 없다.
기계적으로 리턴할땐 이 명령어를 쓰면됨.
• Branches to the address stored in register x1 (i.e., 0 + address in x1)
** x0 is hard-wired to zero, writing to x0 will have the effect of discarding the
value (e.g., return address)
(함수에서 8개 이상의 레지스터를 요구할 수 있다)
Compiler chooses to use other additional registers (8개의 레지스터말고 다른거 사용)
- Save the value of registers to memory → spill (쩔수없이 메모리 사용)
- Restore the value after procedure finishes
예를들어 x18을 사용했다면 convention을 넘어간 input 호출용 레지스터가 아니므로
x18에 원래 있던 값을 다른 곳에 저장한 후(spilling) 사용하고, 이후에 다시 복원해야함
만약 32개의 레지스터를 넘어간다면 어쩔 수 없이 포인터를 전달해야하는,,,그런 st
Last-in-first-out queue in memory
Memory area for spilling registers
Stack pointer, x2 (sp, 이것또한 레지스터임)
Stack grows from high address to low address
stack pointer is adjusted by one doubleword for each register that is saved (i.e., push) and restored (i.e., pop)


long long int f = local 변수

leaf_example:
addi sp, sp, -24 // 스택 포인터(sp)를 24만큼 감소하여 3개의 레지스터를 저장할 공간 확보
sd x5, 16(sp) // x5(임의) 레지스터를 스택에 저장 (sp기준 두칸 올리기)
sd x6, 8(sp) // x6(임의)레지스터를 스택에 저장 (sp기준 한칸 올리기)
sd x20, 0(sp) // x20 레지스터를 스택에 저장
add x5, x10, x11 // x5 = g + h
add x6, x12, x13 // x6 = i + j
sub x20, x5, x6 // x20 = (g + h) - (i + j)
addi x10, x20, 0 // 결과를 x10에 저장 (x10 = x20 + 0)
ld x20, 0(sp) // 스택에서 x20 레지스터 복원
ld x6, 8(sp) // 스택에서 x6 레지스터 복원
ld x5, 16(sp) // 스택에서 x5 레지스터 복원
addi sp, sp, 24 // 스택 포인터를 원래 위치로 복원
jalr x0, 0(x1) // 호출한 함수로 복귀
x5~x7, x28~x31 (애초에 중요한 값은 여기에 넣으면 안됨)x8,x9, x18~x27 (웬만하면 안쓰는데 썼으면 원상복구하는 레지스터)addi sp, sp, -24 // adjust stack to make room for 3 items
sd x20, 0(sp) // save register x20 for use afterwards (예시로 든거고 실제로는 x7같은걸 써야함)
add x5, x10, x11 // register x5 contains g + h
add x6, x12, x13 // register x6 contains i + j
sub x20, x5, x6 // f = x5 − x6, which is (g + h) − (i + j)
addi x10, x20, 0 // returns f (x10 = x20 + 0)
ld x20, 0(sp) // restore register x20 for caller
addi sp, sp, 24 // adjust stack to delete 3 items
jalr x0, 0(x1) // branch back to calling routine


[C Code]
long long int fact (long long int n)
{
if (n < 1) return (1);
else return (n * fact(n − 1));
}

recursive 가 몇번 진행됐는지 알 수 없기에 통용될수있는 코드를 짜야함
fact:
addi sp, sp, -16 // 스택 포인터를 16만큼 줄여 2개의 값을 저장할 공간 확보
sd x1, 8(sp) // 현재 리턴 주소 x1을 스택에 저장 (sp + 8 위치에 저장)
sd x10, 0(sp) // 인수 n이 저장된 x10을 스택에 저장 (sp + 0 위치에 저장)
addi x5, x10, -1 // n - 1을 계산하여 x5에 저장 (x5 = n - 1)
bge x5, x0, L1 // n-1이 0 이상이면 L1으로 분기, 그렇지 않으면 다음 명령어 실행
addi x10, x0, 1 // n == 1인 경우, 결과를 1로 설정 (x10에 1 저장) (return 1)
addi sp, sp, 16 // 스택을 원래대로 복구 (sp를 16만큼 더해서 복구)
jalr x0, 0(x1) // 리턴 주소로 복귀 (호출한 함수로 돌아감)
L1:
addi x10, x10, -1 // n = n - 1 (x10에 n - 1 저장)
jal x1, fact // fact(n-1)을 재귀적으로 호출 (리턴 주소는 x1에 저장)
addi x6, x10, 0 // 재귀 호출의 결과 fact(n-1)을 x6에 저장 (x10 값을 x6으로 복사)
ld x10, 0(sp) // 스택에서 n 값을 복원 (sp + 0 위치에서 x10에 저장)
ld x1, 8(sp) // 스택에서 리턴 주소를 복원 (sp + 8 위치에서 x1에 저장)
addi sp, sp, 16 // 스택 포인터를 원래대로 복구 (sp를 16만큼 더해서 원래 위치로)
mul x10, x10, x6 // n * fact(n-1)을 계산하여 x10에 저장 (최종 결과를 x10에 저장)
jalr x0, 0(x1) // 리턴 주소로 복귀 (호출한 함수로 돌아감)
stack 영역 : 자료구조의 형태가 아닌 사용 용도를 의미하는 것

High add
return을 할때 해당 값이 그대로 유지되긴하지만 레지스터에 는 Fp와 Sp의 (포인터)값을 바꾼다
-> 정보가 그대로 남아있기때문에 해킹 취약점이 될 수 있다.
-> 근데 왜 그러냐? : 메모리 영역을 초기화하는 것 또한 비효율적이기 때문
- It has nothing to do with the real physical memory
관행적으로 이렇게 쓰이고 있다.
▪ There are times when constants are too big to fit into 12 bits
▪ RISC-V provides lui (Load Upper Immediate) instruction
• U-type instruction
bne라는 instruction에서 branch의 타겟 대상은 12비트라서 2의 12승
2의 12승 비트 안에 들어가는 숫자의 의미는 개별 바이트 값이 아닌 instruction의 개수
값이 10이면 10바이트가 아닌 instruction의 개수 그래서 개당 4바이트니까 총 표현할 수 있는 바이트는 2의 12승 x 4(byte)
이후에 2바이트짜리 instruction도 지원하기 위해서 2만 곱해서 (다만 범위는 반이 줄어듬)
13비트의 표현효과를 가짐
왜 무조건 0인가? 2혹은 4를 표현하면 짝수니까 맨 끝자리는 걍 0이거덩여