Computer Science/컴퓨터 구조

[컴퓨터 구조] Procedure Call

바보1 2022. 10. 22. 00:03


앞의 글을 읽으시면 이해에 도움이 됩니다.

 

 

2022.10.21 - [Computer Science/컴퓨터 구조] - [컴퓨터 구조] Procedure call, jal, jalr (프로시저 콜을 위한 명령어들)

 

[컴퓨터 구조] Procedure call, jal, jalr (프로시저 콜을 위한 명령어들)

앞의 글을 읽으시면 이해에 도움이 됩니다. 2022.10.01 - [Computer Science/컴퓨터 구조] - [컴퓨터 구조] Type of Instruction (명령어의 타입) [컴퓨터 구조] Type of Instruction (명령어의 타입) 앞의 글을..

hi-guten-tag.tistory.com


1. Using More Register

 

 

앞 글 그대로 복사한 내용

(이해를 위해 가져왔음)

더보기

우선 Procedure을 부르는 함수를 Caller라고 하고, 부름에 응답하는 함수를 Callee라고 합니다.

(통상적으로 main함수가 Caller, 다른 function이 Callee)

 

procedure calling을 위해서는 우선 파라미터가 들어갈 공간과 리턴을 위한 공간,

그리고 마지막으로 다시 권한을 반납할 때 돌아가야 할 장소를 저장하는 공간 이 세 가지가 필요합니다.

 

아래의 표를 보시면 자세히 나와있습니다.

x10 ~ x17 파라미터 혹은 return value를 위한 8개의 파라미터 레지스터를 저장
(통상적으로 x10과 x11은 return value를 위해 사용된다.)
x1 origin으로 돌아가기 위한 하나의 return 주소를 저장

이해되시나요?

 

Caller는 8개의 파라미터를 x10~x17에 저장하고 Callee는 자신이 수행한 작업에 대한 return value를 x10 혹은 x11에 저장한 후, 

x1에 저장되어 있는 주소로 권한을 반납합니다.

 

근데 만약에 Callee에게 넘겨줘야 할 파라미터가 8개가 넘어가면 어떻게 될까요?

 

이때는 register spilling을 통하여 메모리에 초과된 파라미터를 저장합니다.|

 

따라서 overhead를 줄이기 위해서는 파라미터를 8개 이하로 주는 게 현명하겠죠?

 

Register Spilling이란 어떤 값을 Register에서 Memory로 저장하는 과정입니다.

 

비단 8개 이상의 parameter만 넘길 때 쓰는 것이 아니라, 

연산 과정에서 지금 사용하지 않거나, 덜 중요한 값을 memory로 넣는 과정도 포함합니다.

 

통상적으로 Procedure call을 통하여 callee를 부를 때,

caller의 local value는 register 개수의 한계로 인해 메모리로 넣어야 합니다.

따라서 register spilling의 대부분이 procedure call에서 사용됩니다.

 

callee가 아무 register을 사용했는데, 만약 그 register가 caller가 사용하던 register라면 대참사가 발생하겠죠?

그렇다고 callee가 caller가 사용하던 위치를 일일이 기억해서 비어있는 register만 사용할 수도 없는 노릇이고, 

그렇게 한다고 하더라도 callee는 32개보다 더 적은 갯수의 register를 사용하기 때문에 overhead가 커집니다.

 

따라서 이때 caller가 callee에게 control을 넘기고, 다시 control을 받을 때, 

caller 본인의 local value는 보장되어야 합니다.

 

더욱 자세히 알아보기 전에 stack pointer를 먼저 보고 가겠습니다.


1.1 Stack Pointer

 

 

register spilling을 통해서 메모리로 간 데이터는 어떤 자료구조를 사용해야 효율적일까요?

 

queue, stack, linked list 등등이 있겠지만, 아무래도 last-in-first-out 개념인 stack이 효율적이겠네요

그래야 나중에 들어간 값(예를 들면 callee의 값)이 먼저 나오고, 그 이후에 맨 처음에 들어간 값(caller의 값)이 나오니까요

 

stack의 연산에는 push, pop이 있습니다.

load instruction은 pop, store instruction은 push겠네요

하지만 stack에는 push, pop 같은 작업이 명시되어 있지 않습니다.

 

sp(stack pointer)는 push, pop의 작업을 해결하는 pointer입니다.

sp를 움직임으로써 push와 pop을 표현합니다.

 

stack의 진행 방향

메모리 안에서 stack은 high address에서 low address로 자랍니다.

따라서 sp에 -8을 한다는 것은 8Byte의 공간을 확보한다는 뜻(push)이고, 

sp에 +8을 한다는 것은 8Byte의 공간을 제거하겠다는 뜻(pop)입니다.

 

이러한 sp는 register x2가 담당하고 있습니다.

참고로 sp는 word 단위로 움직입니다.

 

코드와 어셈블리어 예시를 보겠습니다.

leaf_example 함수에서 사용하는 register는 총 3개이고,

따라서 3개의 register를 쓰기 위해 저장 공간을 확보합니다.

(sp를 -12함으로써 3개의 word에 대한 공간을 확보함)

따라서 미리 register spilling을 통해 값을 저장합니다.

(x5, x6, x20은 caller가 쓰던 register일 수도 있기 때문에)

 

이후에는 return하기 전에 값을 복구함으로써 caller의 local value를 보장합니다.


2. Register Saving Convention

 

 

그렇다고 쓰인지 안 쓰인지도 모르는 register를 저장하는 것은 너무나 비효율적입니다.

따라서 RISC-V에서는 이러한 비효율을 방지하기 위하여 register spilling을 두 개의 그룹으로 나누어서 관리(저장)합니다.

 

  • Caller saved registers
    • x5~x7, x28~x31 : callee 호출 이후에 보장이 되지 않는 temporary register들
    • caller가 call을 하기 전에 스택에 미리 저장해야 한다.
    • 해당 register들은 procedure call 이후에 변경되어 있을 수 있다.
    • procedure call 이후에 복구한다.
  • Callee saved registers
    • x8~x9, x18~x27 : procedure call 이후에 필수로 보장되어야 하는 register들
      • 만약 사용했다면 callee는 반드시 저장하고 복구해야 한다.
    • callee는 반드시 해당 변수들을 스택에 저장해야 한다.
    • callee는 caller에게 return 하기 전에 해당 값들을 복구해야 한다.
    • 해당 register들은 procedure call 이후에 보장되어야 한다.

1.1의 그림에서 x5, x6은 callee가 저장하지 않아도 됩니다.

 

풀어서 설명하자면 caller saved registers는 호출 이후에 변경되어도 상관이 없는 register들이므로, 저장을 하는 것은 caller의 선택입니다.

하지만 callee saved registers는 호출 이후에도 절대 변경되면 안 되는 register들이기 때문에, 반드시 저장되어야 합니다.

 

caller 입장에서는 callee saved registers가 매우 중요한 register라고 봐도 될 것 같습니다.


3. Nest Procedures

 

 

main 함수에서 a 함수를 호출하고, a 함수에서 b 함수를 호출해본 경험이 있으신가요?

callee가 caller 되는 입장이 되기도 하는데, 이런 형태를 nested call이라고 합니다.

 

예를 들어서 

  • Main 프로그램이 A를 call, argument는 3
    • x10에 3을 넣고, x1에 return address(Main) 저장
  • 이후 A가 B를 call, argument는 7
    • x10에 7을 넣고, x1에 return address(A) 저장

이런 경우에는 x10과 x1은 overriding이 됩니다.

 

B의 procedure call이 끝난 이후에, A의 procedure call이 끝났다고 가정하면 돌아갈 장소(x1)과 파라미터(x10)이 다른 값으로 덮어씌워진 상태인데 어떻게 해야 할까요?

 

한 가지 해결책은 x10~x17과 x1 또한 스택에 저장하는 것입니다.

 

그렇기 때문에 caller는 x10~x17을 저장해야 하고,

callee는 x1을 저장해야 합니다.

 

따라서 이를 표로 그리면 다음과 같습니다.

callee, caller saved registers

callee saved registers는 Preserved가 되고, caller saved registers는 Not Preserved가 됩니다.

 

그렇다면 fp(frame pointer)는 뭘까요?


4. Frame pointer

 

 

메모리 안의 stack에는 saved register, local variable뿐만 아니라 크기가 큰 array, structure등이 들어가 있습니다.

이러한 stack은 procedure frame 혹은 activation record라고 합니다.

 

이때 fp(frame pointer, x8)은 frame의 첫 번째 word를 가리킵니다.

 

이게 왜 필요할까요?

이유는 다음과 같습니다.

 

  • sp는 함수 내에 변수를 위하여 공간을 만든다. (sp에 값을 더하던가 빼던가)
  • 그러고 나서 함수가 끝난다면 sp는 값을 더하여 해당 procedure과 관련된 정보를 지운다.
  • 이때 sp는 어디서부터 해당 procedure이 시작되었는지 모르기 때문에, fp가 해당 procedure frame의 첫 번째 word에 있는 것이다.
  • 따라서 sp = fp를 하면 procedure frame은 모두 pop이 되어버린다.

이래서 fp가 필요합니다.

 

하지만 여기서도 문제점이 발생하는데, 이유는 다음과 같습니다.

  • procedure call이 연속적으로 발생할 때 fp 또한 overriding 된다는 것
  • 연속적으로 발생한다면, 또 다른 procedure frame(a)이 생긴다.
  • 따라서 fp는 a의 first word를 가리키고 있다.
  • 그렇게 되면 이전의 procedure frame의 fp는?

그렇기 때문에 fp 또한 procedure frame에 저장합니다.

 

글로는 이해하기 복잡하니 간단한 그림을 그려보겠습니다.

 

fp saved

음..이해가 되시나요?

이해가 됐으면 좋겠네요..

 

 

이제 모든 명령어와 기본적인 내용은 다 본 것 같습니다.

 

이제부터는 실제 연산이 어떻게 이루어지는지 알아보겠습니다.

 

감사합니다.

 

지적 환영합니다.