지난 글에서는 구조체가 메모리에 어떻게 저장되는지를 정리하였다.
구조체도 배열과 마찬가지로, 먼저 선언된 변수가 낮은 주소의 값 (%fp에서 먼 쪽)을 가진다.
그래서 먼저 선언된 변수의 위치를 기준으로 직접 경계 정렬을 하여 전체 구조체가 가지는 사이즈를 계산한 뒤,
구조체의 시작 위치를 구조체 내 변수중 가장 큰 사이즈의 배수로 맞추어 선언하면 되었다.
이번 글부터는 서브루틴에 대해 좀 더 자세하게 정리하고자 한다.
서브루틴
서브루틴은 '특정 작업을 여러번 수행하는 연속된 명령어의 나열' 으로 볼 수 있다.
서브루틴의 실행 과정을 한번 정리해보자.
1. 인자값 설정
o-register 에 값을 할당하여 매개변수로 넘길 값을 설정한다.
(인자값은 o0 ~ o5 까지 6개 레지스터 범위에서 사용가능하며, 이를 넘어가는 경우, 스택을 활용한다.)
2. 함수 호출
call 명령어로 함수를 호출하여 해당 함수 위치로 코드를 이동(jump) 한다.
3. 함수 실행
함수 실행은 다음과 같은 단계로 실행된다.
a) 레지스터 set 과 스택 할당
b) i-register 에 있는 인자값을 활용한 함수 프로세스 실행
c) 레지스터와 스택 반환
4. 리턴값 설정
i-register 에 return 값을 설정한다.
5. 복귀(리턴)
ret 명령어로 함수를 호출했던 위치 다음, 다음줄로 이동한다.
(함수 호출 직후 delay slot이 있기 때문이다.)
프로그램 카운터의 입장으로 보면, 함수를 호출했던 위치 + 8 (명령어 하나당 4byte 이므로) 위치로 돌아간다.
복귀하는 것도 실행위치가 바뀌는 일종의 '제어'문이라서, 지연 주기가 존재한다.
restore 명령어는 ret 명령어의 지연주기에 위치하며, 이 명령어로 스택과 레지스터를 반환한다.
함수 호출 명령어
함수 호출 명령어는 크게 call과 jmpl 2가지가 있다.
이 두 명령어 모두 명령어의 실행 순서를 바꾸는 제어 명령어 이므로 delay slot이 존재한다.
각 명령어의 사용 방법과 기능에 대해 정리해보자.
call label
label 이 가리키는 주소로 프로그램 카운터를 이동시킨다.
프로그램 카운터를 이동시키기 전에, %o7 위치에 기존 프로그램 카운터 값을 저장한다. (return 시 돌아올 주소)
사실 정확하게는 return 시 %o7에 저장했던 주소+8 위치로 이동한다.
call 명령어의 포맷을 보면, Displacement 를 저장하게 된다.
(이동할 명령어와, 현재 명령어 사이에 몇 줄 간격 인지를 저장한다.)
즉, ( target address - %pc ) / 4 를 저장한다. ( 4로 나누는 이유는 명령어 하나가 4byte 메모리 공간을 차지하기 때문)
나중에 %pc 값을 이동시킬 때는 기존 %pc 값에 이 displacement << 4 한 값을 더하여 이동할 명령어 주소를 역 계산한다.
jmpl R+A, S
현재 프로그램 카운터(%pc) 값을 레지스터 S 에 저장한 뒤, R+A 주소로 이동한다.
만약 jmpl %o0 + 16, %o7 과 같이 작성하였다면
%o7 메모리에는 jmpl 명령어가 위치한 주소가 저장되고,
%pc가 %o0 + 16 위치로 이동하며 해당 위치부터 명령어를 실행한다.
call 명령어의 작동방식을 알고 있으므로, 이를 jmpl 명령어를 통해서도 구현할 수 있다.
call label 명령어는 아래 명령어와 같다.
set label, %o0
jmpl %o0, %o7
call 명령어가 복귀할 주소를 %o7에 저장하므로 그에 맞게 작성해주면 된다.
jmpl 명령어는 함수 포인터, C++의 virtual function, switch-case 와 관련이 있다.
이중에서 switch-case 문을 jmpl 을 통해 구현해보자.
함수 반환 명령어
반환 명령어 (복귀 명령어) 도 ret, retl 으로 2가지가 있다.
ret
함수를 호출하는 위치를 %o7에 저장하면서 함수를 호출하였으므로, 함수 내에서는 이 값을 %i7에서 읽어온다.
이 명령어를 사용하여 복귀할 때는 %i7 + 8 위치로 복귀한다.
+8이 붙는 이유는 delay slot 이 있기 때문에 한 줄 더 건너 뛰어서 이동하는 것이다.
ret 명령어도 명령어 실행 위치가 달라지는 제어 명령어 이므로, 마찬가지로 delay slot이 존재한다.
만약 jmpl 명령어를 사용하여 ret 명령어와 똑같은 기능을 하도록 구현한다면
jmpl %i7 + 8, %g0
으로 사용하면 된다. 복귀할 주소가 없으니 %g0의 값을 복귀 주소값으로 넣어주었다.
retl
이 명령어는 리프 서브루틴에서 사용하는 명령어이다.
이 명령어는 %o7 + 8 위치로 복귀하는 명령어인데, 자세한 내용은 리프 서브루틴에 대해 정리할 때 다룬다.
(지금 간단히 정리하자면, 리프 서브루틴은 자신의 스택프레임과 레지스터 set 을 가지고 있지 않아서, o-레지스터의 있는 값을 사용해야 하기 때문에 %i7 + 8 대신 %o7 + 8 을 사용한다.)
jmpl 명령어로 swtich-case 구현하기
다음과 같은 C 코드를 이전 분기 파트를 정리할 때, 분기명령어로 구현하였다.
switch(a) {
case 0:
printf("+2: %d\n", a+2);
break;
case 1:
printf("-2: %d\n", a-2);
break;
case 2:
printf("*2: %d\n", a*2);
break;
default:
printf("%d\n");
break;
}
이제 이 코드를 jmpl 명령어를 사용하여 구현해보자.
jmpl 명령어로 구현하는 원리는 간단하다.
주어진 코드를 보면 입력값과 분기하는 case 문의 순번사이에 규칙이 있다.
0을 입력하면 case_0, 1을 입력하면 case_1, 이런식으로 입력값과 분기할 case가 직접적으로 1:1 매칭이 된다.
jmpl 명령어는 특정 줄의 위치로 건너뛰는 명령어이므로 이를 이용하면 입력한 값에 따라 건너뛸 위치를 계산해낼 수 있다.
우선 각 case 별 분기명령어를 한 곳에 모아둔다.
이를 branch table 이라고 한다.
branch table 을 보면, 각 케이스별 분기문이 2줄 간격으로 나뉘어져 있다.
이제 ba case_0 명령어의 위치 주소를 address_case_0 이라고 하면 이를 기준으로 하여 아래와 같이 입력값에 따라 건너뛸 주소를 계산할 수 있다.
입력값이 0이면 address_case_0 + 8 * 0
입력값이 1이면 address_case_0 + 8 * 1
입력값이 2이면 address_case_0 + 8 * 2
8 단위로 증가하는 이유는 각 케이스가 2줄간격으로 나타나는데, 각 줄마다 명령어의 크기가 4byte 이기 때문이다.
이제 원리를 이해했으니 구현을 시작해보자.
a 값은 scanf로 입력받아서 정적 메모리에 저장하는 것으로 생각하면 필요한 스택 프레임 사이즈는 함수 호출을 위한 기본 사이즈인 92 byte 에 4 byte를 더한 값을 8의 배수로 맞춘 96 byte 이다.
먼저 0보다 작거나 2보다 크면 default 쪽으로 분기하도록 설정한다.
dfeault case 코드는 아래와 같이 작성하였다.
이제 branch_table 을 작성하고, 각 case 라벨을 설정한다.
각 케이스마다 입력값에 적절한 연산을 거쳐서 출력하는 과정은 아래와 같이 작성할 수 있다.
이제 이 글에서 제일 중요한, jmpl 할 주소를 계산하여 jmpl 을 수행하는 명령어를 작성해보자.
먼저 아까 작성한 원리에서 처음으로 계산할 것은 branch_table 의 주소이다.
이는 data 섹션에 작성한 라벨의 주소를 가져오는 것과 동일하게 set 명령어로 가져올 수 있다.
한가지 유의할 점은, branch_table 의 라벨링이 이 코드보다 뒤에 되어있어도 어셈블러가 알아서 잘 계산해준다는 것이다.
실행결과는 위와 같다.
그런데 여기에서 이 코드를 더 개선할 수 있다.
지금 위 코드는 case_0, case_1, case_2 의 주소값을 직접 case_0 으로부터 계산했지만, 이를 어셈블러에게 맡길 수 있다.
라벨링을 이용하여 주소값을 메모리에 미리 할당해두는 것이다.
지금까지 SPARC의 라벨을 그저 break point 를 거는 용도나 분기를 위한 용도로만 사용했는데, text 섹션에서도 data 섹션에서처럼 아래와 같이 쓸 수 있다.
이 코드는 data section 이 아니라 text section에 작성된 코드이다.
읽기 전용이라고 하더라도 이렇게 text 영역에 라벨링의 주소값을 메모리를 사전에 할당해둘 수 있다.
(이 코드도, 라벨링 이전에 사용할 수 있다. 어셈블하는 과정에서 어셈블러가 직접 할당한다.)
이제 주소값을 직접 계산하는 것이 아니라, text 섹션에 할당된 메모리에서 읽어오면 된다.
아래와 같이 코드를 작성할 수 있다.
이제 실행결과를 비교해보자.
동일한 결과가 나오는 것을 확인할 수 있다.
여기에 더해 한 가지 실험을 해보고자 한다.
분명 교수님은 "jmpl 명령어에 들어가는 displacement 는 건너뛸 위치와의 줄 간격" 이라고 하셨었다.
이 "줄 간격" 에는 공백줄이나 라벨링 줄도 포함이 될까?
그래서 이렇게 branch_table 내부 명령어 사이에 공백줄을 하나씩 추가하였다.
실행결과는 동일하게 나온다.
따라서 엄밀하게는, "소스 코드 사이에서 줄간격" 이 아니라 "명령어와 명령어 사이 간격" 을 의미하는 것을 알 수 있다.
명령어가 아닌 단순 라벨링, 주석, 빈 줄은 모두 무시된다.
이것으로 서브루틴의 수행 과정과 호출/반환 명령어에 대해 정리하였다.
다음 글에서는 스택 프레임을 할당하고 반환하는 save 명령어와 restore 명령어에 대해 정리해보고자 한다.
'CS > 어셈블리' 카테고리의 다른 글
[SPARC] 32. 서브루틴 매개변수 전달 (0) | 2023.12.03 |
---|---|
[SPARC] 31. Register Set & Register Window Over/Underflow (2) | 2023.12.02 |
[SPARC] 29. 구조체 (0) | 2023.11.30 |
[SPARC] 28. 다차원 배열과 이진수 곱셈 계산 (2) | 2023.11.29 |
[SPARC] ld: fatal: relocation error: ~~ symbol .data (section): value ~ does not fit 해결 방법 (0) | 2023.11.26 |