RISC-V에서 함수를 호출하는 규약이 존재한다. 꼭 지켜야 하는 규칙은 아니지만 대부분 이렇게 사용한다고 보면 된다. 함수 호출 과정은 약간 복잡하다. 아래와 같은 함수가 있을 때 main()에서 f1, f2로 argument x와 y를 적절하게 전달해야 한다. 메모리 공간을 할당해 함수를 실행하고, 실행 후에는 기존의 PC 값으로 돌아와야 한다.
일반 분기문과 달리, 명령어의 진행 순서가 연속적이지 않고 함수를 수행하고 나서는 꼭 원래 위치로 돌아가야 한다. 또한 아래 예시처럼 함수 수행 후 return 값이 main 함수에서 다시 사용될 수 있다. 이러한 복잡한 과정에서 아키텍쳐에서는 어떻게 구현되어 있는지 알아보자.
아래는 main에서 j 명령어를 사용해 myfn으로 이동하는 과정이다. myfn에서는 main 이후에 실행되어야 할 함수인 after1의 시작 주소로 j하는 것으로 마무리하고 있다. 이런 경우에는 우리가 의도한 대로 프로그램이 흘러간다.
그러나 myfn을 여러 번 호출하면 어떻게 될까? main에서 myfn을 호출하고 after1으로 돌아간다. 다시 after1에서 myfn을 호출하면 j after1으로 인해서 다시 after1으로 돌아가 무한 반복된다. 원래라면 after1에서 myfn을 호출하고 나면 after2로 이동하는게 맞다. 그러나 myfn을 여러 번 사용하게 되면 올바르게 작동하지 않는다.
그래서 jal, jalr 명령어를 사용해 돌아올 메모리 주소 값을 저장하고 확인한다. jal myfn을 호출하면 x1에 돌아올 주소, 즉 다음 실행할 명령어 주소 PC+4 값을 x1 레지스터에 저장한 후에 myfn으로 이동한다. myfn을 수행하고 나서 다시 돌아올 때는 jalr이라는 명령어로 x1에 저장된 주소 값을 확인해 해당 주소로 이동한다. 이렇게 하면 여러 번 myfn을 호출해도 아무 문제가 없다.
여기에서 또 문제가 하나 있다. 만약에 myfn에서 자기 자신을 호출하게 되면 어떻게 될까? main에서 jal로 이동한 후에 myfn에서 자신을 호출했다. x1에 after2 주소가 쓰여지고 after2에서 jalr을 하면 x1에 after2 주소가 쓰여있어 무한 반복된다. 이러한 문제를 해결하려면 레지스터에 값을 저장하는 것과 더불어 메모리 영역에 다음 실행할 주소 값을 저장해야 하면 된다.
메모리 영역 중 Stack 영역에 return address를 담아두면 된다. addi sp, sp, -8을 통해 스택의 일부 공간을 확보한 다음 sd t5, 0(sp)을 통해 레지스터 값을 메모리에 저장하면 된다. 만약에 스택에서 pop 하고 싶으면 ld t4, 0(sp)으로 메모리에 저장된 값을 레지스터에 올리고, addi sp, sp, 8로 스택 포인터를 올려주면 된다.
아래 예시처럼 myfn에서 스택 공간을 확보하고 after2에서 확보된 스택 공간을 free 시킨다.
그렇다면 main 함수에서 서로 다른 함수로 arguments는 어떻게 전달하는 것일까? 레지스터에 저장해두고 필요할 때 접근해 사용한다. 그런데, 서로 다른 함수에서 같은 레지스터의 값을 바꾸면 원치 않은 결과가 발생한다. caller는 함수를 호출하는 것을 뜻하고 callee는 호출당한 함수를 말하는데, caller와 callee는 서로 다른 레지스터를 사용해야 한다. 그래서 caller는 한번에 8개의 레지스터를 사용해 callee로 argument를 전달한다. 만약에 8개보다 많은 argument를 전달해야 하면 나머지 argument는 메모리의 스택 영역에 올려 전달하고 사용하고 싶다면 메모리에 접근해야 한다. 참고로 작은 숫자부터 스택에 넣어야 한다.
그런데 space for x17 부분은 왜 존재할까? 재귀가 아닌 이상 크게 필요가 없다. 그러나 재귀 함수를 호출하면 같은 레지스터를 사용해야 하는데, 값이 바뀔 수 있기 때문이다. 그래서 메모리에 레지스터의 원래 값을 저장해둔다.
정리하면 아래와 같은 blue, pink 함수가 있을 때, pink의 argument 6개를 레지스터로 전달하고 재귀적으로 사용하거나 필요한 경우에는 스택에 레지스터 값을 저장해두고 사용할 수 있다. 만약에 argument가 8개보다 많은 경우는 성능이 좋을까? 그렇지 않다. 해당 변수를 사용하기 위해서 메모리에 주기적으로 접근해야 하기 때문에 성능에 큰 영향을 미친다.
blue() {
pink(0,1,2,3,4,5);
}
pink(int a, int b, int c, int d, int e, int f) {
int*g;
long A[2];
}
실제로 메모리에 올라갈 때는 이러한 방식으로 올라간다. 프로그램의 가상 메모리 상에서 stack에는 local variable, Heap에는 malloc으로 할당된 변수들, data 영역에는 static variable, global variable이 저장된다. 마지막으로 text 영역에는 실행할 프로그램 코드가 저장된다. data와 text 영역은 프로그램 실행 동안에 내용이 바뀌지는 않는다. heap과 stack은 프로그램의 실행 중에 크기와 내용이 바뀌는 영역이다.
정리하면, Calling Convention에서 중요한 것은 조건문에서 적절하게 분기되어 해당 주소로 잘 이동하고 다시 돌아와야 한다. 또 함수에 필요한 argument를 잘 전달하기 위해서 레지스터를 사용하고 레지스터의 오염을 방지하기 위해서 메모리의 stack 영역에 저장해두고 필요할 때 load해서 사용한다.
'CS > 컴퓨터구조' 카테고리의 다른 글
[컴퓨터구조] 2-4. Linking (1) | 2024.12.18 |
---|---|
[컴퓨터구조] 2-2. RISC-V ISA (0) | 2024.12.01 |
[컴퓨터구조] 2-1. General Instruction Set Architecture (0) | 2024.10.23 |
[컴퓨터구조] 1. Computer Architecture (1) | 2024.10.22 |