지난 글까지 실수에서의 산술연산 방법과 FPU의 개념 및 FPU에서 사용 가능한 연산들에 대해 정리하였다.
이번 글에서는 정리한 명령어를 직접 사용해보면서 명령어에 익숙해져보려고 한다.
단정밀도 예제 1
다음 코드를 어셈블리로 컴파일해보자.
void main() {
static float a = 1.5, b=10.5, c=0.0, d=3.5;
if (c == d) c = a+b;
else c = a-b;
}
간단히 float 형 (단정밀도) 정적 변수 4개를 선언해 값을 할당하고, 비교해서 연산 결과 값을 저장하는 코드이다.
먼저 static 정적 공간에 데이터를 생성해야 하므로, ".data" 섹션을 사용하면 된다.
.data 섹션에 단정밀도 공간을 할당하는 방법은 아래와 같이 하면 된다.
선언하는 사이즈는 .single 로 지정하면 되고, 32bit 공간, 즉, 1 word 사이즈를 선언하는 것과 같다.
값을 넣을 때는 0r 형태로 선언해서 넣어주면 된다.
같은 4byte라고 이렇게 선언하면 안된다.
에러가 발생하며 어셈블이 안된다.
참고로 만약 레지스터에 실수값을 바로 쓰고 싶다면 절대 mov 1.5, %f1 과 같은 형태로 쓰면 안된다.
1.5 라는 실수 자체가 32bit 를 모두 온전히 사용하고 있으므로, mov 명령어 포맷 속 상수공간인 13bit 안에 못 들어가기 때문이다.
궁금하면 직접 해보면 된다.
이렇게 문법오류를 내준다.
"그래서 이럴 때 쓰라고 fmovs 가 있는거 아니었나요?"
한번 해보자.
이렇게 바꾼 뒤 실행해보면
여전히 에러가 난다.
이렇게 고친다음 실행하면 에러가 발생하지 않는다.
즉, fmovs는 FPU register 에 있는 값을 다른 register 에 옮길 때 사용하는 명령어라는 것을 알 수 있다.
그렇다면 실수 상수값 1.5 는 어떻게 레지스터나 메모리에 쓸 수 있을까?
이럴 때 바로 ld, st 명령어를 사용하면 된다.
ld, st 명령어는 읽고 쓰는 그 '사이즈' 자체가 정해져있을 뿐, 그 안의 형식이 정해져 있는 것은 아니라서 실수든 정수든 상관없이 사용할 수 있다.
따라서 위에서 선언한 정적 메모리 속 데이터는 아래와 같이 f-register 에 저장할 수 있다.
이제 값을 비교하고 그 결과에 따라 다른 연산을 수행해보자.
위와 같이 코드를 작성하였다.
결과를 확인해보니 c != d 이므로 else 문으로 잘 들어갔고, else 문의 fsubs 명령어 실행 결과로 -9 도 c에 잘 저장되었다.
Integer <-> Floating Point Transfer
이쯤에서 CPU 의 Integer Unit (Integer Register) 과 FPU (Floating point register) 사이에 데이터를 주고 받는 방법을 정리해보자.
함수를 호출할 때 인자를 넘기고자 한다면 o-register 를 사용하고, 넘어온 인자를 받을 때는 i-register 를 사용한다.
이는 실수에서도 마찬가지인데, FPU 는 f-register를 이렇게 구분해두지 않았다.
따라서 서브루틴에 실수값을 인자로 넘기려고 할 때는 %o 레지스터에 실수값을 넘겨야 한다.
그렇다면 어떻게 정수형 레지스터에 실수형 포맷 값을 쓸 수 있을까?
이는 아까 위에서 잠깐 언급하였듯, 그리고 data 섹션에 작성한 값을 fpu register 로 읽어올 때 사용하였듯 메모리를 이용하면 된다.
메모리를 경유하여 st, ld 명령어를 통해 값을 읽고 쓰면 실수값을 integer register 에 저장할 수 있다.
이 그림을 보면 이해가 간단히 된다.
이제 이 내용을 참고하여 두번째 예제 코드를 살펴보자.
단정밀도 예제 2
이번 예제는 C 코드 없이 바로 어셈블리 코드를 작성한다.
아래와 같은 어셈블리 코드를 살펴보자.
먼저 읽기 전용으로 text 영역에 1.5, 3.0, 5.0 단정밀도 소수를 선언한다.
그리고 o0 에는 정수형에서 설정했던 비트 형식으로 3을 표현하여 넣었다.
이 값을 메모리를 거쳐 f1 에 옮겨보았다.
f1 에는 저 00000003 16진수 비트가 그대로 들어가게 되는데, f - register 는 이 비트를 실수표현으로 번역하므로 값을 출력해보면 다른 값이 나올 것이다.
한번 gdb 를 이용해 메모리 값을 직접 찍어보았다.
우선 o0 에 3을 저장하고나서
첫 st 명령어가 실행된 직후, %fp-4 위치의 값을 찍어보면 이렇게 0x00000003 비트가 찍혀서 저장되어 있음을 알 수 있다.
다음 명령어인 ld [%fp-4], %f1 를 실행하고 나면, f1 레지스터에 이 비트값이 그대로 들어가게 된다.
이를 출력해보면
이렇게 전혀 다른 값이 들어가게 된다.
0x00000003 은 부동소수점 표현으로 3.0 이 아니라 위 이미지 속 값을 나타내기 때문이다.
(이전 내용을 복습해보자면, 이 값은 exponent 의 값이 0 이고, fraction 이 0이 아니므로 subnormal 표현이다.)
그 다음 명령어인 fitos %f1, %f2 를 실행한 뒤, %f2 를 찍어보면
이렇게 이번엔 정수값 3이 잘 저장되어있다는 것을 알 수 있다.
만약 이렇게 fitos 에 i 라고 진짜 정수형 레지스터를 넣으면
이런 에러가 발생한다.
f 가 붙은 명령어는 꼭 f - register 끼리 연산하자.
다음으로 a 의 주소를 o1에 넣고, o1위치의 메모리 값을 f3 에 저장하였다.
그렇다면 f3 의 값은 a에 저장했던 1.5가 들어있는 것이 맞다.
마지막으로 3.0의 부동소수점 표현을 %o0에 저장한 뒤, 이를 메모리를 거쳐 f4에 저장하면
이렇게 3이 저장되어 있는 것이 맞다.
단정밀도 예제 3
지금까지 예제는 부동소수점 활용을 실제로 어떻게 하는지 설명하는 예제에 가까웠다.
이번엔 C 코드를 어셈블리로 옮겨보면서 진짜 예제를 구현해보자.
float a[] = { 1.0, 1.5, -1.5, 1.0, -1.0, 1.0, 3.2E1, 2.1E2, 1.3E-3};
float b[] = { 5.0, 4.0, 10.1, 0.25, 3.0, 5.0, -2.5, 3.6, 9.0 };
float result = 0.0;
void main() {
register float f1;
register int i, k = 0;
f1 = result;
for (i = 9; i > 0; i--, k++) {
f1 += a[k]*b[k];
}
result = sqrt(f1);
}
반복문을 굉장히 독특하게 도는데, 원소가 9개인 두 백터의 내적을 구하는 코드이다.
(원래 내적을 구하고 마지막에 루트까지 취하던가..? 싶긴하지만)
이 코드를 한번 어셈블리로 직접 컴파일해보자.
메인 함수 안에서는 우선 함수 호출도 없고, 별도 지역변수도 없기에 메모리는 필요하지 않다.
그러나 실수 연산을 위해 메모리를 경유하는 일이 필요할 수 있으니 64 + a 라고 생각을 하고 ,일단 64 byte 만 선언해보자.
전역에 저렇게 배열을 선언한 것은 data 영역에 데이터를 선언했다고 볼 수 있다.
우선 data 영역에 전역 변수를 위와 같이 선언해주었다.
main 함수가 시작하면, 각 변수 값을 초기화해준다.
반복문 코드는 위와 같다.
gdb로 찍어보면 이렇게 계산 결과 값이 잘 들어있음을 알 수 있다.
.global main
main:
save %sp, 64, %sp
! %f1 = f1
! %l0 = i, %l1 = k
mov %g0, %l1 ! k = 0
! f1 = result;
set result, %o0
ld [%o0], %f1
! for loop
mov 9, %l0 ! i = 9
loop:
cmp %l0, 0
ble break
nop
! address calculate
sll %l1, 2, %l3 ! l3 = k * 4
set a, %l4
set b, %l5
ld [%l4 + %l3], %f2
ld [%l5 + %l3], %f3
fmuls %f2, %f3, %f4 ! f4 = f3 * f4 = a[k] * b[k]
fadds %f1, %f4, %f1 ! f1 += f4
dec %l0 ! i--
ba loop
inc %l1 ! k++, delay slot
break:
fsqrts %f1, %f5 ! f5 = sqrt(f1)
set result, %o0
st %f5, [%o0]
test:
mov 1, %g1
ta 0
.data
a: .single 0r1.0, 0r1.5, 0r-1.5, 0r1.0, 0r-1.0, 0r1.0, 0r3.2E1, 0r2.1E2, 0r1.3E-3
b: .single 0r5.0, 0r4.0, 0r10.1, 0r0.25, 0r3.0, 0r5.0, 0r-2.5, 0r3.6, 0r9.0
result: .single 0r0.0
예제 3의 전체 코드이다.
강의록과 비교해서 한가지 재미있는 차이점을 보자면, 강의록에서는 cmp 대신 subcc %l0, 1, %l0 를 사용했다.
subcc 로 i의 값을 1 빼는 연산을 수행함과 동시에 cc 코드를 발생시키고, 그 cc를 보면서 반복문을 계속 돌아야 하는지 체크하는 것이다.
다음 글에서는 배정밀도 명령어를 살펴보고, 예제를 정리한 뒤, 함수 매개변수로 실수를 넘기는 방법에 대해 정리하는 것으로 어셈블리 정리를 마무리 하고자 한다.
'CS > 어셈블리' 카테고리의 다른 글
[SPARC] 40. 기말고사 대비 정리 (0) | 2023.12.08 |
---|---|
[SPARC] 39. FPU Instructions 사용 예제 (배정밀도, 매개변수) (0) | 2023.12.08 |
[SPARC] 37. 실수 계산 & FPU & FPU Instructions (0) | 2023.12.07 |
[SPARC] 36. Floating Point (0) | 2023.12.07 |
[SPARC] 35. Bubble Sort 구현 (2) | 2023.12.07 |