지난 글에서는 함수를 호출하는 call 명령어와, 이와 비슷하게 코드의 실행을 조정하는 jmpl 명령어,
그리고 이전 주소로 복귀하는 ret 명령어와 retl 명령어에 대해 정리하였다.
이번 글에서는 call 명령어를 호출한 이후, 함수의 시작을 알리는 역할로 불리는 save 명령어에 대해 정리하고자 한다.
'Save' Instruction
save 명령어는 서브루틴을 위한 공간을 할당하는 명령어로서, 함수의 시작을 나타낸다.
save 명령어는 2가지 기능을 한다.
첫번째 기능은 스택 메모리에 사용자가 지정한 사이즈 (스택 프레임의 사이즈) 만큼 공간을 할당한다.
공간을 할당하는 것은 %fp, %sp 의 이동과 같다.
%fp 은 스택 프레임의 시작점, %sp 는 스택 프레임의 끝 점을 의미한다.
스택 공간에 새로운 공간을 할당하면, 기존 %sp 가 새로운 함수의 %fp 가 되고,
사용자가 지정한 값만큼 기존 %sp 가 감소한다.
그래서 save 명령어를 호출하면 %sp 의 값을 감소시켜서 새로운 %sp 에 할당하는 형태로 작동한다.
두번째 기능은 레지스터를 새로 할당하는 일이다.
SPARC 에서는 각 서브루틴마다 24개의 레지스터를 할당받는다.
이때 o-register 는 자신의 자식 서브루틴의 i-register가 되어 자식 서브루틴에 의해 값이 바뀔 수 있기 때문에,
함수 하나의 관점에서 자신이 독점적으로 점유하는 공간은 i-register, l-register 이렇게 16 byte 공간이라고 할 수 있다.
SPARC 에서는 이렇게 서브루틴 하나가 독점적으로 점유하는 공간을 'register set' 이라고 한다.
save 명령어는 함수를 호출하면서 새로운 register-set 도 같이 할당한다.
(그런데 여기에서 나도 이해가 잘 안가는 부분이 하나 있다.
최초로 존재하는 main 함수에서 이미 o-register 를 사용하고 있으니, 최초에는 32개 register가 할당되어 있고, 이후로는 기존 o-register 를 새로운 서브루틴의 i-register 로 사용하고, 실질적으로는 새로운 서브루틴의 l-register, o-register 를 할당하여 스택의 공간이 늘어나는 것이 맞지 않을까?
그러면 register-set 은 l-register, o-register 가 되는 것이 맞지 않을까? 하는 생각도 든다.
우선 위에 적은대로 서브루틴이 독점적으로 '점유' 한다는 개념으로 이해했을 때는 i, l -register가 맞는 것 같기도 하다.
교수님께 여쭤봤을 때는, 개념적으로는 i, l 레지스터만 할당되지만, 실제로는 o까지 할당된다고 보면 된다고 설명해주셨던 것 같다.)
Register Set
SPARC 에는 128개의 일반 레지스터와 8개의 global 레지스터로 총 136개의 레지스터가 있다.
128개의 일반 레지스터는 16개씩 묶어 위에 설명한대로 register set 이라는 단위로 나누며 총 8개의 register set 이 있다.
각 register set 은 함수가 호출 될 때마다 함수를 위한 레지스터 공간으로 할당된다.
각 register set 은 함수가 호출 될 때마다, (꼭 7번이 시작일 필요는 없지만) 높은 곳부터 낮은 곳을 향해 감소하는 방향으로 할당된다. (global 레지스터가 있는 곳이 높은 쪽이다.)
그런데, 만약 함수를 계속 호출하다가 아래 그림과 같이 레지스터 공간이 모자라게 되면 어떻게 할까?
out-register 는 register-set 개념에는 포함되지는 않아도, 실제로는 할당이 되어야 하는 레지스터인데,
더 이상 할당할 공간이 모자라다.
그런 경우에는, 위 그림과 같이 o-register를 제일 처음 레지스터 공간으로 활용한다.
그러면 기존에 호출했던 서브루틴의 i-register 는 어떻게 되는걸까?
바로 이때 기존에 스택에 할당했던 64 byte의 공간을 사용한다.
잉렇게 레지스터 공간이 모자라서, 기존 서브루틴이 사용하던 공간을 침범할 수 밖에 없는 경우,
스택에서 l-register, i-register 값을 백업하기 위해 할당했던 64 byte 공간에 기존 값들을 복사하여 저장해두고,
레지스터의 값은 새로 호출된 서브루틴의 o-register 공간으로 덮어쓴다.
이를 Register Window Overflow 라고 한다.
그런데 이런 로직을 처리하려면 컴퓨터에게 지금 레지스터가 가득 찼으니 기존 레지스터 값을 백업하고 새로 덮어써야한다는 것을 알려줄 수 있어야 한다.
CWP 와 WIM
그래서 SPARC 에서 레지스터가 가득찼다는 걸 알려주는 용도로 사용하는 값이 있다.
첫번째는 CWP (Current Window Pointer).이다.
현재 실행중인 함수의 register set 이 register window 인덱스의 몇번째 위치인지를 나타낸다.
save 명령어가 호출되면 위에서 기술한대로, register set 이 감소하는 방향으로 추가되므로 CWP는 1 감소한다.
반대로 restore 명령어가 호출되면 register set이 반환되어 CWP 는 1 증가한다.
두번째는 WIM (Window Lnvalid Mask) 이다.
유효하지 않은 window 의 값을 설정해둔다.
만약 save 명령어를 쭉쭉 호출하면서 CWP가 감소하다가 WIM과 만나면 인터럽트가 발생하면서 운영체제에 모든 register set을 사용하였음을 알려준다. (이때 Register Window Overflow가 발생했다고 인식한다.)
WIM의 값은 사용하지 않은 register set 중 가장 마지막 번호를 가진다.
(만약 CWP의 시작 위치가 3이라면 WIM의 값이 4가 되는걸까 궁금해서 질문을 해보았는데, 꼭 그렇지만은 않다고 한다.
WIM의 값은 그때그때 달라서 매번 확인을 해봐야 한다고 한다.)
Register Window Overflow
위 그림과 같이, CWP = 1, WIM = 0 인 상황에서 save 명령어를 한번 더 실행하면 어떻게 될까?
그 다음에는 CWP와 WIM이 겹치면서 오버플로우가 발생한다.
비록 0번째 register set 은 비어있지만, 새로 호출한 서브루틴의 out-register 를 위한 공간은 기존에 사용중이던 7번째 register set 을 침범할 수 밖에 없기 때문이다.
그래서 이 경우, 7번 register set 의 내용 (l -register, i-register) 을 스택에 백업한 뒤, 새로운 공간을 0번째 register set 을 사용하는 서브루틴의 o-register 공간으로 사용한다.
그리고 WIM의 값은 7로 바뀐다.
이 상황에서 한번더 save 명령어를 호출하면 CWP 가 감소하면서 (처음으로 돌아와) 7이 되고, 위 과정을 반복할 것이다.
위와 같이, CWP = 2, WIM = 3 인 상황에서 서브루틴 G() 호출 이후, 그 내부에서 H() 를 호출하면 오버플로우가 발생하고,
이때는 A() 서브루틴의 i-register, l-register 값을 A() stack 에 백업해둔 뒤, H() 서브루틴의 스택을 생성한다.
Register Window Underflow
언더플로우는 반대로 생각할 수 있다.
함수를 여러번 호출하면서 A() 서브루틴의 i, l 레지스터 값들을 스택에 백업을 해두었는데, 함수가 줄줄이 return 되면서 다시 A() 서브루틴으로 돌아오는 상황이다.
다르게 말하면 CWP = 1 인데, restore 명령어가 호출되어 CWP가 증가하여 WIM = 2 와 만나는 상황이다.
이 경우, 스택에 백업되어있던 i, l 레지스터의 값을 register window 2번 인덱스로 복원을 한 뒤, CWP, WIM 을 모두 1씩 증가 시킴으로서 언더플로우를 처리한다.
Register Window Overflow / Underflow Handling
그렇다면 이 레지스터 윈도우 오버플로우 / 언더플로우는 누가 처리할까?
바로 OS 운영체제가 처리한다.
save, restore 명령어가 호출되면, CPU는 CWP, WIM 값을 읽어 오버플로우, 언더플로우 여부를 확인한다.
만약 오버플로우/언더플로우가 확인되면(CWP == WIM 이면), trap (또는 시스템콜)이 발생한다.
그러면 운영체제가 이에 대한 제어권을 가지고, 오버플로우와 언더플로우를 처리한 뒤, 다시 프로그램에 제어권을 돌려주게 된다.
따라서 서브루틴을 사용할 때 프로그램은 '스택 메모리 공간'만 잇으면 된다.
모자란 레지스터의 공간은 운영체제가 알아서 스택을 활용해 관리해준다.
'CS > 어셈블리' 카테고리의 다른 글
[SPARC] 33. 구조체 return 하기 (0) | 2023.12.05 |
---|---|
[SPARC] 32. 서브루틴 매개변수 전달 (0) | 2023.12.03 |
[SPARC] 30. 서브루틴 개요 & call, jmpl, ret, retl (0) | 2023.12.01 |
[SPARC] 29. 구조체 (0) | 2023.11.30 |
[SPARC] 28. 다차원 배열과 이진수 곱셈 계산 (2) | 2023.11.29 |