지난 글에서는 서브루틴에서 구조체를 다루는 방법을 정리하였다.
%sp+64 위치에 구조체 시작 주소를 넘겨서 직접 해당 구조체를 다루는 것이 포인트였다.
서브루틴 내에서는 %fp+64로 접근하는 것이 차이점이었다.
이번 글에서는 리프 서브루틴에 대해 정리하고자 한다.
Leaf Subroutine
트리에서 자식 노드가 없는, 즉, degree = 0 인 노드를 leaf 노드라고 한다.
함수의 call graph 를 그렸을 때, leaf 노드에 해당하는 함수는 가장 마지막에 호출되어 더 이상 함수를 추가로 호출하지 않는 함수를 의미한다.
리프노드는 그냥 평범한 서브루틴과 비슷하게 구현하면 된다.
그런데 만약 리프노드에서 수행하는 작업이 너무 간단해서 새 스택 프레임을 할당할 필요가 없어 save, restore 명령어가 필요가 없다면 어떨까?
이 경우 현재 Register Window 를 유지한채로, 사용 가능한 레지스터 범위를 O-Register 로 제한한 채 사용하는 서브루틴이 된다. (i, l 레지스터는 부모 서브루틴의 점유 공간이므로 건들지 않는 것이 불문율이다.)
그래서 이때 leaf subroutine 의 return address 는 %o7 + 8 이 된다. (자신의 i register 가 없기 때문)
서브루틴 매개변수 파트에서 살펴보았던 4개의 숫자를 더하는 예시를 리프 서브루틴으로 작성해보자.
.global main
sum = -4
main:
save %sp, -96, %sp
mov 1, %o0
mov 2, %o1
mov 3, %o2
call add4
mov 4, %o3
st %o0, [%fp + sum]
set str, %o0
ld [%fp + sum], %o1
call printf
nop
mov 1, %g1
ta 0
add4:
save %sp, -64, %sp
add %i0, %i1, %i0
add %i0, %i2, %i0
! add %i0, %i3, %i0
restore %i0, %i3, %o0
retl
nop
.data
str: .asciz "sum is %d\n"
기존 코드는 위와 같았다.
여기에서 add4 함수 부분만 leaf subroutine 으로 바꿀 것이다.
그러면 위와 같이 바꿀 수 있다.
원리만 놓고 보면, 그냥 %o0 ~ %o3 레지스터에 더할 값을 저장해두고, 저장하는 코드 조각을 add4 라벨링으로 표시해둔 다음 그냥 goto 명령어로 main 함수 안에서 왔다갔다 하는 것과 똑같아 보이기도 한다.
실행결과는 동일하게 나온다.
그렇다면 매개변수가 8개인 버전은 어떨까?
.global main
sum = -4
main:
save %sp, -104, %sp
mov 1, %o0
mov 2, %o1
mov 3, %o2
mov 4, %o3
mov 5, %o4
mov 6, %o5
mov 7, %l0
mov 8, %l1
st %l0, [%sp + 92] ! 7th argument
st %l1, [%sp + 96] ! 8th argument
call add8
nop
st %o0, [%fp + sum]
set str, %o0
ld [%fp + sum], %o1
call printf
nop
mov 1, %g1
ta 0
add8:
save %sp, -64, %sp
add %i0, %i1, %i0
add %i0, %i2, %i0
add %i0, %i3, %i0
add %i0, %i4, %i0
add %i0, %i5, %i0
ld [%fp + 92], %l0
ld [%fp + 96], %l1
add %i0, %l0, %i0
add %i0, %l1, %i0
ret
restore
.data
str: .asciz "sum is %d\n"
이 코드가 기존에 작성했던 8개 매개변수를 받아 더하는 코드이다.
이 코드에서 add 8 함수만 리프 서브루틴으로 바꿔보자.
그러면 이렇게 작성할 수 있다.
그냥 i 레지스터를 모두 o 레지스터로 바꾼 다음, caller 의 추가 매개변수 공간에 접근할 때 %fp 대신 %sp 로 접근한다.
추가 매개변수 값을 저장하는 임시 공간도 l 레지스터 대신 사용하지 않는 o register 를 재사용한다.
그리고 return 을 할 때는 retl 명령어를 통해 돌아가면 된다.
실행 결과는 동일하게 36이 나온다.
Pointer Type Argument
이번에는 매개변수에 포인터 타입으로 메모리 공간의 주소값을 넘기는 예제를 살펴보자.
이거는 그냥 set 명령어로 주소값을 가져와서 저장한 값을 그대로 넘기면 될 것 같다.
void main() {
int i, j;
i = 5;
j = 7;
swap(&i, &j);
print("i : %d, j : %d", i, j);
}
void swap(int *x, int * y) {
register int temp = *x;
*x = *y;
*y = temp;
}
위 C 코드를 어셈블리로 옮겨보자.
항상 하던대로 스택 프레임 사이즈부터 빠르게 계산해보자.
main 함수의 스택 프레임 사이즈는
기본 64
함수 호출이 있으므로 4 + 24
6개를 넘는 매개변수는 없으므로 0
지역 변수는 int 형 변수 2개가 있으므로 4 * 2 = 8
따라서 100 byte 의 공간이 필요하며, 이를 8의 배수로 맞추면 104 byte 를 스택 프레임 사이즈로 선언하면 된다.
swap 함수의 스택 프레임 사이즈는
기본 64
함수 호출이 없으므로 0
6개를 넘는 매개변수도 없으므로 0
지역 변수도 없으므로 0
따라서 64 byte 의 8의 배수인 64 byte 를 선언하면 되는데, 한번 이 함수는 리프 서브루틴으로 구현해보자.
먼저 기본틀을 간단하게 잡아주었다.
i, j 변수에 접근할 때 코드를 읽기 좋게 상수값을 선언해주고, printf 문을 위한 문자열도 선언해주자.
다음으로 main 함수부터 작성해보자.
main 함수 틀 안에 작성한 코드이다.
먼저 i, j 변수를 초기화 해주고, 변수의 주소값을 매개변수로 넘긴 뒤 swap 함수를 호출하고 있다.
그 뒤, 메모리의 값을 printf 함수로 넘겨 값을 확인해본다.
다음으로 swap 함수를 작성해보자.
위와 같이 작성하였다.
아주 간단하다.
C코드에서 temp 레지스터 변수를 사용하길래 하나만 사용해서 하고 싶었으나, 어차피 메모리에서 읽어온 값을 레지스터에 임시로 저장해야해서 레지스터 변수가 2개 필요하였다.
(x 값 임시 저장용 레지스터와, x에 y의 값을 저장하려고 해도, y의 값을 읽어서 레지스터에 보관해야한다.)
nop 를 없애는 최적화 부분만 제외하면, 강의록 코드도 나와 비슷하다.
아래는 전체 코드이다.
.global main
i = -4
j = -8
main:
save %sp, -104, %sp
mov 5, %l0
mov 7, %l1
st %l0, [%fp + i]
st %l1, [%fp + j]
add %fp, i, %o0 ! set argu
add %fp, j, %o1 ! set argu
call f_swap
nop
set str, %o0
ld [%fp + i], %o1
ld [%fp + j], %o2
call printf
nop
mov 1, %g1
ta 0
f_swap:
! register temp -> %o2
! temporary register -> %o3
ld [%o0], %o2
ld [%o1], %o3
st %o3, [%o0]
st %o2, [%o1]
retl
nop
.data
str: .asciz "i : %d, j : %d\n"
이것으로 서브루틴에 대한 개념은 정리가 모두 끝났다.
다음 글에서는 지금까지 공부한 내용을 토대로 bubble sort 를 구현해보고자 한다.
버블 소트를 구현하는 과정에서 다양한 C Library 함수를 어셈블리에서 사용해 볼 것이다.
'CS > 어셈블리' 카테고리의 다른 글
[SPARC] 36. Floating Point (0) | 2023.12.07 |
---|---|
[SPARC] 35. Bubble Sort 구현 (2) | 2023.12.07 |
[SPARC] 33. 구조체 return 하기 (0) | 2023.12.05 |
[SPARC] 32. 서브루틴 매개변수 전달 (0) | 2023.12.03 |
[SPARC] 31. Register Set & Register Window Over/Underflow (2) | 2023.12.02 |