지난 글에서는 6개를 넘는 매개변수를 서브루틴에 넘기는 방법과 그 예제를 살펴보았다.
이번 글에서는 서브루틴의 값 반환 중 '구조체'를 반환하는 경우를 중점적으로 정리해보고자 한다.
서브루틴의 값 반환
서브루틴에서 값을 반환할 때, word 하나 사이즈의 데이터를 반환하는 것은 %i0 레지스터를 통해 값을 넘기면 되었다.
그리고 서브루틴을 호출한 함수 입장에서는 %o0 위치에서 반환 값을 읽어올 수 있었다.
그렇다면 아래 C 코드와 같이 구조체를 반환하는 경우는 어떻게 받아올 수 있을까?
struct point {
int x;
int y;
};
struct point zero() {
struct point local;
local.x = 0;
local.y = 0;
return local;
}
struct point x1;
main() {
x1 = zero();
}
구조체를 생성해서 0으로 초기화 한 뒤 반환하는 zero 함수를 선언하고, 이 함수를 이용해 전역으로 선언한 구조체를 초기화하는 코드이다.
전에 스택프레임 할당 시, 서브루틴 내에서 또 다른 서브루틴을 호출한다면 64 + 4 + 24 byte 의 스택 프레임 사이즈를 기본으로 가져야한다고 하였다.
이때 4 byte가 구조체를 위한 공간이었는데, 바로 이 공간을 이용해 구조체 포인터를 넘기도록 하여 위 코드를 어셈블리로 바꿀 수 있다.
이 공간에 접근할 때는 함수를 호출한 caller 기준 (즉, %sp 기준으로 연산한다.) 64 byte 위치로 접근할 수 있다.
따라서 %sp + 64 로 접근하면 구조체를 위한 스택 공간에 접근할 수 있다.
먼저 항상 하던대로, 스택프레임 사이즈를 계산해보자.
main 함수는 함수를 하나 호출하는 것만 하므로, 92byte 사이즈가 필요하다.
이를 8의 배수로 맞추면 96 byte 의 스택프레임을 선언하면 된다.
zero 함수는 내부에서 지역 구조체 변수를 갖는다.
(그런데 강의록 코드를 보면 전역에 선언한 구조체 변수에 zero 함수에서 직접 접근하고 있어 지역 구조체 변수는 필요가 없긴 하다.)
일단 그래도 계산을 해보자면, 함수 내에서 별도의 함수호출도 없고, 그에 따른 매개변수도 없으니 기본 64 byte 를 먼저 잡는다.
지역 변수로 선언하는 구조체의 경우, int 형 변수 2개를 가지므로, 8byte 를 가진다.
구조체의 offset 은 잘 맞춰져 있고, 전체 사이즈 역시 8byte 이며, 이는 구조체 내 제일 큰 변수 사이즈인 4byte word 의 배수 이므로 문제가 없다.
따라서 지역변수로 필요한 사이즈는 8byte 이다.
따라서 zero 함수는 64 + 8 = 72 byte 의 스택 프레임 사이즈가 필요하며, 이는 8의 배수 이므로 72byte 스택프레임을 할당하면 된다.
우선 주어진 코드를 위와 같이 어셈블리로 바꾸어 보았다.
main 함수에서는 zero 함수를 호출하고, 그 결과를 %o0 에서 받은 뒤,
그 값을 %l0에 저장한 x1 전역 구조체 주소로 넘기고 있다.
zero 함수도 위와 같이 작성하였는데, 여기에서 한가지 의문이 든다.
zero 함수의 스택프레임의 %fp - 8 이 구조체의 시작위치인 동시에 구조체 포인터이기도 한데,
엄밀하게 저 ld [%fp-8], %i0 코드는 구조체를 반환하는 것이 아니라 구조체의 x 속성 값을 반환한다.
그 결과 main 함수의 %o0 에는 0이 들어가고, 이 값을 x1 의 주소에 저장하면, 실질적으로 x1.x = 0 만 저장되고 나머지는 아무것도 저장되지 않는다.
그렇다면 이렇게 %fp-8 로 구조체 주소 자체를 넘기면 어떨까?
그래도 여전히 문제가 존재한다.
구조체 주소가 %o0 으로 넘어오는 것은 좋은데, 그 넘어온 주소값을 x1 의 주소에 해당하는 메모리에 넣고 있다.
즉, 지역 구조체 변수의 주소값을 x1.x 에 넣고 있는 셈이다.
그렇다면 어떻게 해야할까?
강의록은 어떻게 구현하였는지 찾아보았다.
우선 강의록의 main 함수 부분이다.
x1 전역 구조체 변수의 시작 주소를 %o0에 임시로 저장한 뒤
그 값을 %sp+64 구조체 반환 포인터 위치로 정해진 스택 프레임 공간에 할당한다.
그런데 할당하는 위치가 caller 의 스택 프레임 공간이다.
(새로 호출할 서브루틴의 struct return pointer 가 아니라 자신의 struct return pointer 이다.)
이렇게보면 zero 함수에서는 아무런 인자도 넘기지 않았다.
(%o0 을 임시변수로 쓰는 바람에, x1의 주소값이 %o0 을 통해 넘어가긴 하지만 사용하지 않았다.)
강의록에서 zero 함수는 위와 같이 작성하였다.
먼저 caller 함수의 %sp + 64 위치에 접근해야 하므로, callee 에서는 %fp + 64 로 프레임 포인터를 이용해 접근하였다.
이렇게 부모 서브루틴의 스택프레임에서 전역 구조체 변수의 주소값을 가져온 뒤, 해당 주소값을 기준으로 전역변수에 직접 접근하여 값을 채워 넣었다.
그런 다음은 아무런 반환도 하지 않고 그냥 서브루틴을 종료시켰다.
그럼 사실 아래와 같은 코드를 어셈블리로 작성한 것과 같다고도 볼 수 있지 않을까?
struct point x1;
struct point {
int x;
int y;
};
void zero() {
x1.x = 0;
x1.y = 0;
}
main() {
zero();
}
교수님께 여쭤봤을 때는 교수님도 예제가 사실 조금 이상한거라고 하셨다.
구조체 포인터로 전역 매개변수를 받아서(?) 사실 전역이니까 안 받아도 되긴 하지만, 아무튼 그런식으로 예제 코드를 작성하는 것이 맞긴 하다고 하셨다.
지역 구조체 예제
위 예제에서는 전역 구조체에 zero 함수를 이용해 x, y 를 0으로 초기화 하였다.
이번에는 main 함수에서 선언한 지역 구조체에 zero 함수를 이용해 값을 초기화 해보자.
struct point {
int x;
int y;
};
struct point zero() {
struct point local;
local.x = 0;
local.y = 0;
return local;
}
main() {
struct point x1, x2;
x1 = zero();
x2 = zero();
}
위와 같은 코드를 어셈블리로 바꿀 것이다.
이전 예제 코드와 바뀐 점은 전역 구조체가 사라지고, main 함수 안에 지역 구조체 변수 2개가 생겼다는 것이다.
항상 하던대로 스택프레임 사이즈부터 계산해보자.
main 함수부터 계산해보면
기본 64
zero 함수 호출이 있으므로 기본 (4 + 24)
함수에 넘기는 6개 초과 매개변수는 없으므로 0
지역변수는 구조체 2개가 있다.
구조체는 4byte 사이즈 변수 2개가 있으므로, 각 구조체의 offset을 고려한 사이즈는 8
변수 중 제일 큰 사이즈는 4byte 인데, offset 을 고려한 사이즈가 4의 배수이므로, 구조체 하나당 필요한 스택 공간은 8byte 이다.
구조체 변수가 2개 있으므로, 16 byte가 필요하다.
따라서 필요한 스택프레임의 사이즈는 64 + 4 + 24 + 0 + 16 = 108 byte 이다.
이를 8의 배수로 맞춰주면 112byte를 선언하면 된다.
zero 함수는
기본 64
내부 함수 호출 x, 넘기는 매개변수 x
내부에서 지역변수 사용도 x (어셈블리 코드에서 작성할 때 기준이다.)
따라서 64 byte 만 선언하면 된다.
위 내용을 토대로 위와 같이 기본 틀을 작성하였다.
이제 main 함수부터 채워보자.
강의록 코드를 토대로 구조체를 사용하는 방법을 정리해보면
1. 값을 쓸 구조체의 시작 주소를 %sp + 64 로 넘긴다.
2. 서브루틴 내에서는 %fp + 64 에서 그 시작 주소를 읽어온다.
그 뒤 그 내부에서 offset 을 계산하여 값을 쓴 뒤, 서브루틴을 종료한다.
아무리 생각을 해봐도, 그냥 별도 구조체 포인터 매개변수로 주소값을 넘겨서 처리하는 것과 차이가 없어보이는데
왜 스택 프레임 안에 구조체 포인터를 위한 공간을 따로 만든 건지 잘 모르겠다.
분명 이유가 있을 것 같은데..
암튼 이 내용을 토대로 %sp + 64 위치에 값을 설정할 구조체를 잘 넘겨보자.
이렇게 작성했다.
그리고 테스트를 위해 각 구조체 변수에 담긴 값을 출력해보았다.
이렇게 작성하였다.
출력 문자열은 위와 같이 설정하였다.
지금 실행해보면 이렇게 나온다.
이상한 값이 나오는데 이는 zero 함수를 아직 설정해주지 않아서 그렇다.
zero 함수는 위와 같이 작성하였다.
struct return pointer 에 넘어온 구조체 주소를 읽어서 값을 구조체 할당해주고 있다.
한번 프로그램을 실행해보면
여전히 값이 이상하게 나온다....
문제가 뭘까..?
출력 코드가 문제였다.
x1.y 와 x2.y를 출력할 때, %fp + x1/x2 + 4 가 아니라 %fp + 4 위치의 메모리 값을 출력해서 발생한 문제였다.
출력 값이 예상대로 잘 나온다.
'CS > 어셈블리' 카테고리의 다른 글
[SPARC] 35. Bubble Sort 구현 (2) | 2023.12.07 |
---|---|
[SPARC] 34. Leaf Subroutine & Pointer Type Argument (0) | 2023.12.06 |
[SPARC] 32. 서브루틴 매개변수 전달 (0) | 2023.12.03 |
[SPARC] 31. Register Set & Register Window Over/Underflow (2) | 2023.12.02 |
[SPARC] 30. 서브루틴 개요 & call, jmpl, ret, retl (0) | 2023.12.01 |