본문 바로가기
IT_Study/CS_Study

[Computer Architecture] (4-2) RISC-V Register 설명

by 두번째얼룩 2024. 4. 27.
RISC-V Register Set[1]

 
각 Register의 기능을 소개하기 앞서, "Caller saved" register와 "Callee saved" register를 알아보자.
Caller는 Call를 수행하는 주체이며, Callee는 Call를 당하는 주체를 이야기한다. 예를 들어 Main에서 function foo()를 부르게 되면 Caller는 Main, Callee는 foo()가 된다. 그런데 동작을 수행할 때, Caller와 Callee 모두 Register를 사용한다. 그런데 사용할 수 있는 Register는 32개로 한정적이기 때문에 문제가 발생한다. 이를 Register saving problem이라 한다. 이 문제는 Caller가 사용 중이던 Register들을 Callee가 사용하게 되면 기존에 저장되어 있는 데이터가 사라지게 된다. 그러므로 사용하기 전에 백업을 해두고, 사용한 후 다시 복원시켜야 한다.
 누가 이 작업을 할 것인가에 따라 'Caller saved''Callee saved'로 나뉜다. 만약 Caller가 모든 Register를 백업하고, Callee를 수행하고, 다시 복원할 수도 있다. 반대로 Callee가 Call을 받으면 모든 Register를 백업하고, 작업을 수행하고, 다시 복원할 수 도 있다. 그런데 잘 살펴보면 각 Register 별로 값이 변하지 않아야 되는 시점이 다르다. 예를 들어 Return address는 Caller가 Callee를 부를 때 지정하고, Callee에 들어가면 변하면 안 된다. 그러므로 Caller가 백업해 두고, Callee는 백업하지 않아도 된다. 반대로 Stack Pointer는 Callee 안에서도 계속 변할 수 있으므로 stack pointer에 대한 백업을 callee가 수행하도록 한다. 이렇듯 각 Register를 구분하여 'Caller-saved'와 'callee-saved'로 구분하여 진행하여 효율을 높일 수 있다. 
 
정리하자면 
"Caller saved" Register는 Caller가 저장을 해야하는 Register이며, Callee는 해당 Register를 사용할 수 있기 때문에 Caller가 Call을 수행하기 전에 백업을 해둬야 한다.
"Callee saved" Register는 Callee가 저장을 해야하는 Register(call을 수행하는 동안 보존되어야 함)이며, Callee는 return 하기 전에 반드시 복원을 해둬야 한다. 
 
1. x0, Hard-wired zero
x0로 지정된 Register는 '0'으로 고정된 값이다. 해당 주소를 참조하면 '0'을 읽어오게 되고, 이 주소의 값은 변경할 수 없다. 
32개 중 하나로 지정한 이유는 '0'을 이용하면 효율적인 Instruction 생성이 가능하다. 몇 가지 예시를 들어 살펴보겠다.
 
첫 번 째는 메모리의 특정 주소[0(x10)]에 '0'을 쓰는 동작이다. 이를 명령어 레벨로 구현하면 아래와 같다. 
1. Register에 '0' 값을 저장한다.
2. 해당 Register 값을 메모리 주소에 저장한다. 
2가지 동작이 필요하지만, hardwired zero reigster를 활용하면 아래와 같이 하나의 명령어로 수행가능하다.
1. x0 값을 메모리 주소에 저장한다. --> sw x0,0(x10)
추가적으로 'Register에 특정 값을 저장한다'는 따로 명령어를 만들지 않고, 'addi'를 통해서 구현이 가능하다. 
예를 들어 '3'이라는 값을 x1에 적으려고 하면 다음과 같이 표현하면 된다. 
addi x1, x0, 3(imm12) --> x1 = x0(0) + 3  = 3 
Instruction을 사용하는 횟수 뿐만아니라 Instruction의 종류도 줄여줄 수 있다. 
 
다음 예제는 'jal'(Jump & Link)와 'jalr'(Jump & Link Register)에서 'x0'를 활용하여 Link를 하지 않고 Jump를 하는 명령어를 만들 수 있다. 
1. jar x0, offset -> x0 = PC + 4; PC = PC + offset; -> PC = PC + offset
이 때, x0는 저장불가이므로 PC 값만 변경된다. -> PC + Offset 값으로 Jump 동작
2. jalr x0, rs, 0(imm) -> x0 = PC + 4; PC = RS + 0;  -> PC = RS;
마찬가지로 x0는 저장불가이므로 PC 값만 변경된다. -> PC는 RS 값으로 Jump 동작.
3. jalr x0, x1, 0         -> x0 = PC + 4; PC = x1 + 0;  -> PC = x1;
마찬가지로 x0는 저장불가이므로 PC 값만 변경된다. -> PC는 x1 값으로 Jump 동작.
다음 Register인 x1은 return address이므로 해당 address로 return 하는 동작을 수행함.
 
마지막 예제는 아래와 표처럼 일부 Branch 명령어를 통해서 여러 가지 '0'과 비교하는 Branch를 만들 수 있다.
'beqz'(branch equal zero)                           <= 'beq'(Branch Equal) 
'bnez'(branch not equal zero)                    <= 'bne'(Branch Not Equal)
'bgez'(Branch greater than or Equal zero) <= 'bge'(Branch Greather than or Eequal)
'blz(Branch less than zero)                        <= 'blt'(Branch less than)
'bgtz(Branch Greater than zero)                <= 'blt'(Branch less than)

beqz rs, offsetbeq rs, x0, offset
bnez rs, offsetbne rs, x0, offset
blez rs, offsetbge x0, rs, offset
bgez rs, offsetbge rs, x0, offset
bltz rs, offsetblt rs, x0, offset
bgtz rs, offsetblt x0, rs, offset

 
2. x1, Return address, Caller
x1은 Return address를 저장하는 용도로 사용한다. 이 Register는 Instruction을 수행하다가 Jump가 발생하여 해당 영역의 동작을 수행하고 다시 돌아오기 위해 사용된다. 예를 들어 'Main' 안에서 어떤 함수 'foo()'를 수행하기 위해 Jump를 수행하였고, foo() 함수가 다 실행되면 다시 Main으로 돌아와야 한다. 다시 돌아와야 할 주소는 Jump 다음 주소이므로 Jump를 수행할 때, x1 register에 Jump 다음 명령어 주소(PC+4) 값을 저장하도록 한다. 이때 쓰이는 명령어 중 하나는 아래와 같다.
jal x1, offset  -> x1 = PC +4; PC = PC + offset;
foo()함수에서 다시 돌아오려면 함수 마지막에 아래와 같은 명령어를 실행하면 된다.
ret -> jalr x0, x1, 0(offset) -> x0 = PC+4; PC = x1 + 0(imm);
 
3. x2, Stack pointer, Callee
x2는 stack pointer로 현재 stack의 마지막 주소를 저장한다. Stack은 last-in first-out으로 가장 늦게 입력된 것이 제일 먼저 나오게 된다. 
Stack은 주소가 증가하는 방향으로 커지는 방식과 주소가 작아지는 방향으로 커지는 방식이 있다. RISC-V에서는 주소가 작아지는 방식을 사용한다. 또한 Stack Pointer가 제일 마지막에 들어온 element를 선택하느냐 아니면 다음 비어있는 공간을 선택하느냐에 따라 방식이 달라진다. RISC-V에서는 제일 마지막에 들어온 element를 선택하는 방식을 택한다.

stack 표현 방식
# Allocate space on the stack
addi sp, sp, -32  # Subtract 32 bytes from the stack pointer

# Release stack space
addi sp, sp, 32  # Add 32 bytes to the stack pointer

따라서 stack에 공간을 할당하기 위해서는 sp의 주소를 감소시키고, 공간을 반환하기 위해서는 sp의 주소를 증가시킨다.
 
4. x3, Global pointer
Application 내에서 메모리에 저장되어 있는 데이터를 global variable로 선언하여 사용할 수 있다. 이 때, global variable의 주소들을 PC값을 활용한 방법이나 절대 주소값을 사용하면 해당 주소를 계산하기 위한 추가적인 Instruction이 필요하다. 코드 크기를 줄이기 위해 RISC-V에서는 모든 global variables를 특정 영역에 두고 사용한다. 이 특정 영역을 가리키는 포인터를 저장하는 Register로 x3를 사용한다. 즉, x3 register는 global variables이 저장되어 있는 base address를 가지고 있다. 
 
5. x4, Thread pointer
x4는 Thread pointer로 현재 스레드의 스레드 로컬 스토리지(Thread-local storage)의 주소를 저장하고 있다. 스레드 로컬 스토리지는 멀티 스레드 프로그램에서 각 스레드가 개별적으로 사용하는 메모리 공간이다. 각 스레드는 각자 자기 자신만의 스레드 local variables를 가지고 있고, 그 위치를 표현하기 위해 x4를 사용한다. 
 
** Temporary registers / Saved registers 구분
Callee 입장에서 해당 값을 백업할 필요가 있는 지 없는지에 따라 구분할 수 있다.
> Temporary registers : 백업 하지 않아도 됨.
>> x5-7, Temporaries, Caller
>> x28-31, Temporaries, Caller
> Saved registers : 백업을 해야함.
>> x8, Saved register/frame pointer, Callee
>> x9, Saved register, Calle
>> x18-27, Saved register, Callee
 
6. x8, Saved register/frame pointer, Callee
Stack frame은 현재 Procedure가 사용하는 저장공간을 의미한다. Stack frame에는 Caller로 돌아가기 위한 정보와 Caller로부터 전달받은 Arguments, Local variables, 임시 저장 공간이 포함되어 있다. Caller에서 Callee를 호출할 때의 Caller에서 사용한 값으로 x2, stack pointer로 다시 돌려줘야 한다. 이 stack pointer를 제어하여 원래 값으로 갈 수도 있고, x8, frame pointer에 이전 stack pointer 값을 저장하여, 해당 값을 이용해서 돌아갈 수 도 있다. 
아래 예시에서 fp는 bar를 수행하기 이전 stack pointer을 저장하고 있으므로, bar를 종료할 때 fp 값을 가져와서 sp를 업데이트할 수도 있다. 

Frame Pointer &amp; Stack Pointer

 
 
7/8. x10-11, Function arguments/return values, Caller // x12-17, Function arguments, Caller
Caller가 x10, x11, x12-17을 통해서 Calee로 Fuction arguments를 전달할 수 있다. 또한 Callee는 수행이 완료된 다음, 결과값을 x10, x11을 통해서 돌려줄 수도 있다. 

Function argument &amp; return

 
 
 
 
[1]: http://csg.csail.mit.edu/6.S983/recs/riscv_recitation/

댓글