본문 바로가기
IT_Study/CS_Study

[Computer Architecture] (5) RISC-V Procedure Call

by 두번째얼룩 2024. 4. 28.

이번에 알아볼 내용은 Procedure Call이다. 

 

코드 내에서 다른 함수를 호출하여 사용하는 경우를 말한다. 일반적으로 함수는 Input과 Output으로 구성되어 있다. 따라서 함수를 호출하여 사용할 경우, Input을 전달해야 하며, 또한 Output을 전달받아야 한다. 앞서 RISC-V의 Register를 살펴보았을 때, Function Argument와 Return value를 위한 Register가 정해져 있었다. 

2024.04.27 - [IT_Study/CS_Study] - [Computer Architecture] (4-2) RISC-V Register 설명

 

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

각 Register의 기능을 소개하기 앞서, "Caller saved" register와 "Callee saved" register를 알아보자.Caller는 Call를 수행하는 주체이며, Callee는 Call를 당하는 주체를 이야기한다. 예를 들어 Main에서 function foo()를

secondspot.tistory.com

이를 활용하여 Input과 Output을 주고 전달 받는다. 그리고 함수 호출이 끝난 경우, 다시 원래의 Instruction으로 돌아와야 한다. 즉, Return address가 필요한데, 이 또한 지정된 RISC-V Register를 사용한다. 또 생각해보아야 할 부분은 불려진 함수 자기 자신이 수행할 때 메모리 영역이 필요할 수 있다. 따라 메모리 영역을 할당해서 사용해야 하며, 함수가 끝나면 할당을 해제해야 한다. 

위의 내용을 정리하면 아래와 같다.

1. 함수를 호출 하기 전에, 돌아갈 주소를 저장해 둔다. 

2. 함수를 호출 할 때, 함수의 Input을 전달한다.

3. 함수가 완료될 때, 함수의 Output을 전달받는다.

4. 함수가 수행될 때, 메모리 영역을 할당받아 사용하고, 끝나면 해제한다. 

코드로 생각하면 아래 그림과 같다.

Procedure Call

 

RISC-V에서는 Stack을 사용하기 위한 Stack Pointer Register가 존재한다. Stack은 Program에 필요한 데이터들을 할당하기 위하여 사용한다. 각 Program 별로 Arguments, local variables, return address 등을 저장해 두는 용도로 사용된다. 

아래 그림처럼 각 함수가 각자의 Stack 영역을 가지고, 종료가 되면 할당 해제를 하게 된다. 

 

 

 RISC-V에서 Stack은 Address가 감소하는 방향으로 크기가 커지고, Stack Pointer는 마지막 Element를 지정하고 있다. 

따라서 Stack에서 데이터를 Pop하거나 Push 할 때, 아래와 같이 Stack Pointer를 통해서 수행할 수 있다.

// Push, No explicit push instruction, push라는 inst.이 존재하지 않음.
sub sp, sp, 8 # sp = sp - 8 // 감소하므로 Stack 크기는 증가.
sd t5, 0(sp)  # M[sp] = t5  // 추가된 부분에 t5 데이터를 저장.

// Pop, No explicit push instruction, pop이라는 inst.이 존재하지 않음.
ld t4, 0(sp)  # t4 = M[sp]  // 마지막 데이터를 t4에 저장.
add sp, sp, 8 # sp = sp + 8 // 증가하므로 stack 크기는 감소.

 

구체적으로 RISC-V에서 Procedure Call를 수행하는 과정은 아래와 같다.

1. Place parameters in registers x10 to x17 (or a0 to a7)

2. Transfer control to procedure, saving the return address in ra

3. Acquire storage for procedure

4. Perform procedure's operaton

5. Place result in register a0(and a1) for caller

6. Return to the next instruction of call (address in ra)

 

처음부터 살펴보도록 하겠다. 

1. Place parameters in registers x10 to x17 (or a0 to a7)

처음에는 procedure에 argument를 넘겨주기 위하여 Register에 값을 적는다. 기본적으로 Input Argument는 처음 8개는 x10~x17의 register에 순차적으로 쓰이며, 그 이상의 값은 stack에 적게 된다. stack으로부터 값을 읽을 때, 9번째부터 접근하기 위하여 Stack에 넣는 순서는 반대로 수행된다. 이렇게 저장해 두면 바로 꺼내는 것이 9번째라는 것이 명확하다.

 Output은 기본적으로 x10-x11의 Register에 순차적으로 쓰이며, 그 이상의 값은 그 Output들을 structure로 만들어서 포인터를 return하거나, external variable(global variable)을 사용한다.

Procedure's argument

2. Transfer control to procedure, saving the return address in ra

Procedure를 호출하기 전에, 아래와 같이 해당 Procedure(bar(x))가 끝나고 다시 실행할 주소(z = y + 1)에 대해서 저장을 해야 한다. 

return address

'jump and link' 인 jal instruction을 사용한다. 이와 관련된 Instruction에 대한 설명은 아래 글을 참조하기 바란다.

2024.04.28 - [IT_Study/CS_Study] - [Computer Architecture] (4-5) RISC-V Control Transfer Operation

 

[Computer Architecture] (4-5) RISC-V Control Transfer Operation

Control Transfer Operation은 특정 조건에 따라 동작이 달라지는 것을 말한다. 대표적으로 Branch 동작이 있다. Branch 동작은 특정조건이 만족되면 지정된 위치로 이동하고, 만족되지 않으면 다음 Instructio

secondspot.tistory.com

3. Acquire storage for procedure

procedure를 위한 stack 공간을 할당하여 사용해야 한다. 이 공간에는 다시 돌아갈 정보, Arguments, Local variables & 임시 공간등이 저장된다. 관리는 'Set-up' Code/ 'Finish' Code로 이뤄지며 'Set-up' Code는 procedure에 진입하면 공간을 할당하고, 'Finish' Code는 procedure가 return을 수행하면 할당해제를 한다. 경우에 따라 frame pointer를 두고, 할당해제를 할 수 있다. 이 frame pointer는 이전 procedure(foo)의 sp 위치를 말한다. 즉, 아래 그림과 같이 현재 해당 procedure(bar)에 사용하고 있는 stack 영역은 frame pointer 에서부터 stack pointer까지 이다. 

Frame Pointer & Stack Pointer

4. Perform procedure's operaton

말 그대로 procedure의 동작을 수행한다.

그런데, procedure 동작을 수행할 때, register를 사용하게 되면 어떻게 될까? 아래와 같이 yoo가 사용하고 있던 'x5' register를 who가 사용하게 되면 저장되어 있는 데이터는 사라지게 된다. 

'x5' overwritten

, callee가 레지스터에 들어있는 caller의 데이터를 변경할 수 있기 때문에 문제가 발생하는 것이다.

이를 방지하려면, yoo가 who를 부르기 전에 register들 백업해두거나 who가 자신의 procedure를 수행하기 전에 register를 백업해둬야 한다. 누가 register를 백업하느냐에 따라서 Caller-saved Register 혹은 Callee-saved Register라 부른다. 

만약 Caller가 Register를 백업한다고 하면, 항상 Callee를 부르기 전에 백업을 수행해야 한다. 모든 Register를 항상 백업하는 건 낭비가 될 수 있다. 특정 Register는 Caller가 백업하여, Callee가 자유롭게 사용할 수 있게 하거나, 반대로 Callee가 백업하여 사용하는 Register는 Callee가 가능하면 사용하지 않도록 만들면 백업을 수행하지 않아도 될 수 있다. 

 즉, 일부는 Callee, 다른 일부는 Caller가 백업하는 Hybrid 방식은 주로 각 procedure가 일시적인 데이터 접근을 위해 오직 몇 개의 레지스터가 필요하기 때문입니다. 이러한 Register들이 Call procedure에 의해 수정될 수 있다는 규칙을 가지면 호출 시퀀스를 줄일 수 있으므로 procedure call를 조금 더 빠르고 간결하게 만들 수 있습니다.
한편, 일부 register를 "영구적"으로 간주하면 leaf procedure(다른 프로시저를 호출하지 않는 프로시저)에서 이러한 register를 사용하지 않고 이러한 register를 저장/복원하는 것을 피할 수 있습니다. CPU는 일반적으로 leaf procedure에서 대부분의 시간을 소비하므로, 이는 상당한 속도 향상을 의미합니다.

 

5. Place result in register a0(and a1) for caller

수행이 완료되면 return 할 output 값을 register에 적는다. 

 

6. Return to the next instruction of call (address in ra)

return을 하기 위해 return address, ra를 이용하여 아래와 같이 수행한다. 

ret, jalr

'jump and link register' jalr를 이용하며, 현재 PC 값은 바꾸지 않고, ra에 저장되어 있는 return address로 이동한다.

 

 

 

 

댓글