amd64 또는 x64 에서의 기계어 분석 #3 (fpu #2) asm

  • 실수를 레지스터 스택으로 읽어들이는 명령과 스택에서 메모리로 저장하는 명령
  • Load and Store 
  • fld는 st(0) 가 목적지가 된다
  • fst는 소스가 st(0)다.
  • 끝자리에 p가 붙은건 연산후 pop을 한다
d9/0fld m32fp
dd/0fld m64fp
db/5fld m80fp
d9 c0+ifld st(i)
d9/2fst m32fp
dd/2fst m64fp
dd d0+ifst st(i)
d9/3fstp m32fp
dd/3fstp m64fp
db/7fstp m80fp
dd d8+ifstp st(i)
d9 c8+ifxch st(i)
d9 c9fxch

fxch 는  st(0)의 내용과 st(1)의 내용을 바꾸는 명령이고
fxch st(i) 는 st(0)의 내용과 st(i)의 내용을 바꾸는 명령이다.

  • 정수나 bcd를 레지스터 스택으로 읽어들이는 명령과  스택을 메모리에 저장하는 명령
  • load integer and bcd and store 
  • st(0)를 대상으로 한다
  • 끝자리에 p가 붙은건 연산후 pop을 한다
df/0fild m16int
db/0fild m32int
df/5fild m64int
df/4fbld m80dcd
df/2fist m16int
db/2fist m32int
df/3fistp m16int
db/3fistp m32int
db/7fistp m64int
df/6fbstp m80bcd

  • 더하기 fadd
  • 왼쪽 += 오른쪽
  • 단항은 왼쪽에 st(0)가 있다고 여긴다
  • 끝자리에 p가 붙은건 연산후 pop을 한다
d8/0  fadd m32fp   
dc/0fadd m64fp
db c0+ifadd st(0), st(i)
dc c0+ifadd st(i),st(0)
de c0+ifaddp st(i),st(0)
de c1faddp
da/0fiadd m32int
de/0fiadd m16int

  • 빼기 subtract
  • 왼쪽 -= 오른쪽
  • 단항은 왼쪽에 st(0)가 있다고 여긴다
  • 끝자리에 p가 붙은건 연산후 pop을 한다
d8/4fsub m32fp
dc/4fsub m64fp
db e0+ifsub st(0), st(i)
dc e8+ifsub st(i),st(0)
de e8+ifsubp st(i),st(0)
de e9fsubp
da/4fisub m32int
de/4fisub m16int

  • 자리를 바꾸어 빼기 reverse substract
  • 단항은 왼쪽에 st(0)가 있다고 여긴다
  • 왼쪽 = 오른쪽 - 왼쪽
  • 끝자리에 p가 붙은건 연산후 pop을 한다
d8/5fsubr m32fp 
dc/5fsubr m64fp
db e8+ifsubr st(0), st(i)
dc e0+ifsubr st(i),st(0)
de e0+ifsubrp st(i),st(0)
de e1fsubrp
da/5fisubr m32int
de/5fisubr m16int

  • 곱하기 multifly
  • 왼쪽 *= 오른쪽
  • 단항은 왼쪽에 st(0)가 있다고 여긴다
  • 끝자리에 p가 붙은건 연산후 pop을 한다
d8/1fmul m32fp
dc/1fmul m64fp
d8 c8+ifmul st(0), st(i)
dc c8+ifmul st(i),st(0)
de c8+ifmulp st(i),st(0)
de c9fmulp
da/1fimul m32int
de/1fimul m16int
 
  • 나누기 divide
  • 왼쪽 /= 오른쪽
  • 단항은 왼쪽에 st(0)가 있다고 여긴다
  • 끝자리에 p가 붙은건 연산후 pop을 한다
d8/6fdiv m32fp
dc/6fdiv m64fp
d8 f0+ifdiv st(0), st(i)
dc f8+ifdiv st(i),st(0)
de f8+ifdivp st(i),st(0)
de f9fdivp
da/6fidiv m32int
de/6fidiv m16int

  • 자리를 바꾸어 나누기 reverse divide
  • 단항은 왼쪽에 st(0)가 있다고 여긴다
  • 왼쪽 = 오른쪽 ÷ 왼쪽
  • 끝자리에 p가 붙은건 연산후 pop을 한다
d8/7fdivr m32fp 
dc/7fdivr m64fp
d8 f8+ifdivr st(0), st(i)
dc f0+ifdivr st(i),st(0)
de f0+ifsdivp st(i),st(0)
de f1fdivrp
da/7fidivr m32int
de/7fidivr m16int

  • 순서있는비교와 순서없는비교 
  • compare floating point values and unordered compare
  • st(0)와 비교
  • st(0)와 오퍼랜드를 비교
  • 끝자리에 p가 붙은건 비교후 pop을 한다
d8/2fcom m32fp
dc/2fcom m64fp
d8 d0+ifcom st(i)
d8 d1fcom
d8/3fcomp m32fp
dc/3fcomp m64fp
d8 d8+ifcomp st(i)
d8 d9fcomp
de d9 fcompp

unordered 비교
dd e0+ifucom st(i)
dd e1fucom
dd e8+ifucomp st(i)
dd e9fucomp
da e9fucompp

위의 비교결과 fpu 상태워드의 code flag의 변화가 다음과 같다.

      조건    c3(zf)c2(pf)c0(cf)
st(0) > src000
st(0) < src001
st(0) = src100
unordered111

비교를 하기 전에 그 대상들이 어떤 숫자들인지를 먼저 확인한다.
내부적으로 fxam 명령을 실행하는것으로 봐도 좋다. 숫자의 종류가 NaN이나 지원하지 형식이라면 IE를 발생시킨다. IE의 마스크가 벗겨져있으면 에러처리루틴으로 가버리니까 code flags 를 건드리지 않는다. 그러나 마스크를 쓰고 있으면 code flage를 111 로 만들어 unordered가 발생했음을 알린다. 여기서 unordered의 의미는 다음과 같다.

비교의 대상이 순서를 매길수 없는 또는 크기를 비교할 수 없는 값인 Qnan 일때 시끄럽게 떠들지 않고 슬쩍 넘어가라는 의미이다.

비교는 unordered 비교와 ordered비교로 나눌수 있는데 unordered일때만 명령어에 u 라는 문자가 붙는다. 

QNan을 만났을때 IE를 발생시키며 시끄럽게 구는 놈이 ordered 비교이고 조용히 있는 놈이 unorderd 비교라고 이해하면 된다.


이 코드플래그는 직접 읽을수는 없으니까 다음의 그림처럼 처리한다.

fcomp                 ; 비교를 한다
fstsw ax               ; 비교의 결과가 담긴 상태워드를 ax로 복사한다
sahf                   ; ax의 상위바이트인 ah를 eflag의 하위바이트로 복사한다
jz st0_st1_same      ; 플래그관련 명령을 실행한다.

  • 왼쪽과 오른쪽을 비교
  • 비교한뒤 결과를 cpu 상태워드에 저장(명령어끝에 i가 붙어있다)
  • compare floating point values and set cpu eflags
  • 끝자리에 p가 붙은건 비교후 pop을 한다
  • 이 명령을 사용하면 위처럼 fpu상태워드를 ax 에 옮긴뒤 ah 를 eflag로 보내는걸 할 필요가 없다.
    비교결과를 eflag에 직접 적는다.
  • u 가 붙은건 unorderd 비교를 한다는 뜻이다. 
db f0+ifcomi st,st(i)
df f0+ifcomip st, st(i)
db e8+ifucomi st,st(i)
df e8+ifucomip st,st(i)


      조건    zfpfcf
st(0) > src000
st(0) < src001
st(0) = src100
unordered111

  • cpu상태워드(eflag)의 따라 mov 한다.
  • conditional move
da c0+ifcmovb st(0),st(i)
below(cf=1)   
da c8+ifcmove st(0),st(i)
equal(zf=1)
da d0+ifcmovbe st(0),st(i)
below or equal
da d8+ifcmovu st(0),st(i)
unordered(pf=1)
 db c0+i fcmovnb st(0),st(i)
not below(cf=0)
db c8+ifcmovne st(0),st(i)
not equal(zf=0)
db d0+ifcmovnbe st(0),st(i)
above
db d8+ifcmovnu st(0),st(i)
not unordered(pf=0)

이것으로 fpu관련 명령어는 다 적었다.
이제 조금 원론적인 이야기를 할 차례가 되었다. 부동소수점 자체에 관한것이다.
ieee 754 또는 부동소수점 또는 floating point 로 검색을 해보면 훨씬 더 자세하고 놀라운 정보를 만나게 되는데 이곳에 적는건 일종의 맛보기같은거다.
  • 십진수 실수를 이진수 실수로 바꾸기
정수부분은 2로 나누고 소수부분은 2를 곱한다.
소수점을 사이에 두고 반대되는 행동을 한다. 한쪽이 곱하면 다른쪽은 나누고, 한쪽이 밑에서 부터 읽으면 다른쪽은 위에서 부터 읽으며 한쪽이 나머지를 선호하는 반면 다른쪽은 올림수를 선호한다.

123.45 

123은 정수부분이니까 2로 나누고, 45는 소수부분이니까 2를 곱한다.



나누기 나머지
1123 ÷ 2611
261 ÷ 2301
330 ÷ 2150
415 ÷ 271
57 ÷ 231
63 ÷ 211
71 ÷ 201
7번,6번순으로 나머지를 읽는다
1111011


곱하기결과올림
145 × 2900
290 × 2801
380 × 2601
460 × 2201
520 × 2400
640 × 2800
780 × 2601
7번부터는 3번의 반복이다.
영원히 계속된다.
1번부터 차례대로 읽는다
01 1100 1100 1100 1100 .....
1100의 연속이다 공백없이 적으면
0111001100110011001100.....

정수부분와 소수부분을 합쳐적으면 

1111011.01110011001100....

알아보기 힘드니까 소수점을 기준으로 네자리마다 끊어서 적기로 한다.

111 1011.0111 0011 0011 0011....( 7b.7333... )

위숫자가 십진수 123.45를 이진수로 바꾼 숫자이다.

십진수에서 과학적 표기법이라는게 있는데 이런거.

123.45 = 1.2345 x  (10^2) =  1.2345e2 
이렇게 바꾸는걸 normalize 라고한다.

111 1011.0111 0011 0011 00....
1.1110 1101 1100 1100 1100....   x (2^6)
1.1110 1101 1100 1100 1100.... E6

1.xxxxxxEy 에서 
xxxxx를 가수(mantissa,significant)라고 부르고
y 를 지수(exponent)라고 부른다.

모든 실수가 다 이렇게 무한한 수로 나타나는건 아니다.
십진수 0.5 의 5처럼 2로 곱해서 딱 떨어지는수는 유한한 수가 된다.

0.5 = 0.1 = 1.0e-1
0.25 = 0.01 = 1.0e-2

이 이진수 실수를 저장할때 가수를 몇비트에 어떤식으로 저장하고 지수를 몇비트에 또 어떤식으로 저장할것인가하는 문제와 함께 자리수를 맞출때 어떤 방식으로 반올림을 할것인가, 자리수의 한계를 벗어난 숫자들은 어떻게 할것인가. 등 온갖 문제에 대한 국제표준이 ieee 754에 녹아있다. 여기서는 인텔 fpu 연산에 필요한 것만 다룬다. 

single = dword = real4 = 32bit 
부호(1) + 지수(8) + 가수(23)

double = qword = real8 = 64bit 
부호(1) + 지수(11) + 가수(52)

double extended = tbyte = real10 = 80bit
부호(1) + 지수(15) + 가수(64)

위 그림에서 integer와 implied integer 가 있는데 주목할 것.

1.xxxxxxEy 가 저장될때 정수부 1는 저장되지 않는게 표준인데 그걸 표현한게  implied integer,즉 암묵적으로 1이 저장되어있으니까 계산할때 1을 집어넣어서 해라는 것인데 extended double 에서는 명시적으로 b63에 1을 집어넣었다. 
b63이 1이면 정수이고 b63이 0 이면 0과 1사이의 엄청 작은 수(최소값보다 더 작은수)를 나타내는 용도로 사용한다. 이 비트때문에 정밀도를 표현하는데 일관성이 없어졌다. 싱글은 가수부가 23비트이니까 암묵적인 비트하나를 합쳐서 정밀도는 24비트이고 더블은 가수부가 52비트이니까 비트 하나를 더해 정밀도는 53비트이고 확장더블은 가수부 64비트가 그대로 정밀도가 된다.
가수부는 정수부와 분수부로 나눌 수 있다. 예를 들어 1.xxxxEy 에서 소수점 왼쪽에 있는 숫자1이 정수부분이고 소수점 오른쪽에 있는 비트의 배열을 분수부분이다. 싱글과 더블에서는 정수1을 저장하지 않고 분수부분만 저장된다. 더블확장에서는 b63에 정수1이 저장되고 분수부분은 b62 부터 저장된다. 그래서 가수의 분수 최상위비트는 싱글에서는 b22 이고 더블에서는 b51, 더블확장에서는 b62가 된다. b63은 가수부의 정수비트라고 부른다. 


위 그림을 보면 Normalized Finite 부분이 있는데 그게 정상적인 숫자이고 다른건 0 이외에는 다 비정상적인 숫자에 대한 표기다.
숫자 0 도 +0과 -0이 있고 무한대도 +∞ 가 있고 -∞ 가 있다. 부호비트가 있으니까 그런건 편한다.

0을 기준으로 대칭이니까 한쪽만 살펴본다.
위의 그림은 싱글정밀도의 32비트,  dword를 대상으로 한것이지만 더블과 확장더블도 마찬가지다.

지수의 자리수가 8비트이니까 최대로 표현할 수 있는 수의 갯수는 (2^8)= 256 개 이고 이중 2개는 딴 용도로 사용한다. 모든 비트가 0으로 채워진것과 1로 채워진것은 특별취급한다. 그러면 사용할 수 있는게 254개 이고 이것과 가수부 23비트로 표현가능한 수를 Normalized Finite라 부른다. 갯수가 무한하지 않고 최소값이 있고 최대값이 있다.

  • 0 은 지수부와 가수부 둘다 0으로 채워서 표현한다.
  • 최소값보다 작은수는 Denormalized Finite 또는 subnornal 이라고 부르며 지수부는 0 으로 채우고 가수값은 0 이 아닌수를 넣는다. 가수값까지 0으로 되면 그냥 0 이 되어버리기기 때문이다. 0과 1사이의 엄청 작은 수이다.  10바이트, 80비트인 double extended ( long double ) 에서는 b63(일명 정수비트, J bit )가 따로 있기때문에 이 비트가 0 이 되어야 디노말이 된다. 이 비트가 1 이면 지수전체가 0 이 되더라도 아직 디노말이 아니다. 그런데 b63이 0 인데도 불구하고 지수값이 0 이 아니면 지원하지 않는 형식( unsupported format ) 이 된다. 정수비트를 가진 더블확장 자료형에서만 나타날 수 있는 이 지원하지 않는 형식의 숫자를 계산에서 만나면 Invalid Exception ( IE ) 을 일으킨다.
  • 최대값보다 큰수는 infinite(무한대)라고 부르며 지수부분은 1로 채우고 가수부분은 0으로 채운다.
  • NaN Not a Number : 숫자가 아니다 ) 는 QNaN(Quite NaN : 조용한 Nan) 과 SNaN(Signaling NaN : 시끄러운 NaN) 이 있는데 문자 그대로 실수라인상에 있는 숫자가 아니다. 무한대끼리 나누기를 하면 이 기호가 나타난다 
  • QNaN : 지수부는 1로 채우며 가수의 정수비트는 1이고 가수의 분수 최상위비트가 1 이며 가수전체의 값이 0 이 되면 안된다.
  • SNaN : 지수부는 1로 채우며 가수의 정수비트는 1이고 가수의 분수 최상위비트가 0 이며 가수전체의 값이 0 이 되면 안된다.

너무 엉뚱한 이야기만 했나?
우리의 데이타로 돌아가자.

1.1110 1101 1100 1100 1100.... E6 를 32비트 공간에 채워본다.

0
양수이니까 첫번째 부호비트는 0

0
1000 0101
그 다음 8비트에 지수를 넣는다.
지수는 6 이니까 110 인데 여기서는 1000 0101을 넣었다.
뒤에서 설명

0
1000 0101
1110 1101 1100 1100 1100 110
그다음 23개의 비트에 가수를 넣는다. 정수부분 1은 넣지않는다.  implied integer 이다.
무한이 계속되는 숫자이니까 반올림이 발생했는데 여기서는 기본값인 near to even을 사용했다.

부호, 지수, 가수를 이어서 적어보면

1000 0101 1110 1101 1100 1100 1100 110

처음부터 네자리씩 끊어서 다시 적으면

0100 0010 1111 0110 1110 0110 0110 0110

십육진수로 다시 표현하면

42 f6 e6 66 


지수를 넣을때 6을 넣지 않고 다른값을 넣었는데 그건 사연이 좀 있다.

먼저 정수에서 음수에 관해 생각해보자.

회로 설계상의 잇점으로 음수를 표현할때 2의 보수표현법을 사용한다고 배웠는데 거기에 문제가 숨어있다. 
음수를 2의 보수법으로 표현하면 표현할때는 좋았는데 숫자비교를 할려면 그 음수를 양수로 바꾼다음 비교를 해야하므로 이게 또 약점으로 부각된 모양이다. 

간단히 3비트만 가지고 살펴보자

3비트로 표현할 수 있는 숫자의 갯수는 (2^3)= 8. 
음수를 2의 보수로 나타낼려면 양수의 비트를 반전시킨후 1를 더하면 된다.

세자리 양수 010 (십진수 2)의 음수를 구해보면

010    (반전)->    101   (+1)-> 110 (-2) 

음수는 최상위 비트가 1이다.

8개의 숫자를 크기 순서대로 늘어놓으면 다음과 같다

30113
20102
10011
00000
-11117
-21106
-31015
-41004

이진수 111은 양수로 보면 7 이지만 음수로 보면 -1 이 된다. 그렇기에 이진수의 크기를 비교할려면 먼저 부호를 살펴야한다. 부호없는 비교를 하는데 필요한 자원이 1이라고 하면 부호있는 비교는 3정도는 되지 않을까? 어느정도 차이가 나는지는 잘 모르겠고 여하튼 분명 차이는 존재할테니까 이게 첫번째 부담이고 두번째 부담은 정수는 0과 1사이의 숫자를 다루지 않지만 실수는 이 영역을 밥먹듯이 다루니까 부호있는 비교를 밥먹듯이 한다는 것이다. 0.5는 1 E-1이고 0.25는 1E-2 이다. 이처럼 0과 1사이의 숫자는 지수에 다 음수를 가지고있다. 실수에서의 비교는 먼저 지수의 비교에서 부터 시작한다. 가수가 2 인 수와 100 인 수사이의 비교에서 지수를 내버려두고 먼저 가수끼리 비교하는게 무슨 의미를 가지겠는가? 가수2의 지수가 100이면 이 2가 그냥 2가 아니라 2 x 2의 백승이다. 그런 이유로 실수에서는 덧셈을 하더라도 먼저 지수의 크기를 비교해서 작은쪽에서 큰쪽의 크기에 맞게 가수를 소수점이동시킨 다음 덧셈을 하게된다.

결론은 지수에는 음수를 저장하지 않고 양수로 바꾸어 저장한다는 것이다. 

3비트 시스템에서 아래표처럼 두개의 경우를 생각해볼 수 있다


1번2번
0000예약예약
1001-2-3
2010-1-2
30110-1
410010
510121
611032
7111예약예약
 
모든비트가 0으로 채워진것과 1로 채워진것은 딴용도로 사용한다고 했을때 1번 방안과 2번 방안을 생각해볼 수 있다.
2번은 1번보다 음수의 갯수가 하나 더 많다. 
2의 보수를 사용한 음수시스템에서는 양수의 갯수보다 음수의 갯수가 하나 더 많다. 3비트에서는 -4에서 3까지, 8비트에서는 -128에서 127까지, 하나 더 적어볼까. 16비트에서는 -32768부터 32767까지.
일반화시키면 비트의 갯수를 n 이라 했을때
지금까지의 방식이 이러니까 아마도 2번 방식을 택했을것 같으나 ieee 754 에서는 1번 방안을 택했다. 이유야 당연히 수백가지 있어서 그랬겠지만 찾아볼정도로 궁금하지 않다. 그럴려니 한다.

1번 방식을 찬찬히 살펴보면

양수의 갯수가 음수보다 하나 더 많다
최상위비트가 1 이면 양수이다
최상위비트가 0 이고 나머지 비트가 1로 채워진수가 0 이다.

실제로 x32 또는 x64에서 사용하고 있는 8비트,11비트,15비트로 표를 만들어보자

오렌지라인에 적힌곳이 원점이다. 그 곳의 이진수배열은 011...111 이런 모습이고 십진수로 읽으면 127, 1023, 16383 이 된다. 이값이 더해지는 값이다. bias 값이라고 한다.

지수 6 대신에 1000 0101 을 넣었는데 그건 6 + 127 를 한결과였다.

0111 1111 + 0110 = 1000 0101 이 된다.

복습삼아 32비트 싱글에서 최소값과 최대값을 구해보자

양수 최소값
부호는 0
지수는 가장 작은값인 1 (0은 딴 용도로 사용하므로)
가수는 가장 작은값으로. 23자리전부를 0으로 채운다

0
0000 0001
0000 0000 0000 0000 0000 000

다 이어붙이면 
0 0000 0001 0000 0000 0000 0000 0000 000
0000 0000 1000 0000 0000 0000 0000 0000

00800000

최대값은 같은방식으로 하면
부호는 0, 지수는 가장 큰값에서 1작은값으로(가장 큰값은 예약되어있으므로. 무한대)
가수는 가장 큰값. 몽땅 1로 채운다
0
1111 1110
1111 1111 1111 1111 1111 111

이어붙여서 16진수로 바꾸면

7F7FFFFF

하는 김에 더블과 더블확장까지 다 해보자.

64비트의 더블은 
부호1 + 지수11+ 가수52

최소값은
0010000000000000

최대값은
7FEFFFFFFFFFFFFF

80비트의 더블확장은
부호1 + 지수15 + b63(1) + 가수63

최소값은
00018000000000000000

최대값은
7FFEFFFFFFFFFFFFFFFF

확인해보자.

.model flat
.data
dwMin     dword  00800000h
dwMax     dword  7F7FFFFFh
qwMin     qword  0010000000000000h
qwMax     qword  7FEFFFFFFFFFFFFFh
tMin        tbyte  00018000000000000000h
tMax       tbyte  7FFEFFFFFFFFFFFFFFFFh
.code
main       proc
    fld     dwMin
    fld     dwMax
    fld     qwMin
    fld     qwMax
    fld     tMin
    fld     tMax
    ret
main       endp
end        main
싱글 최소값
+1.1754943508222875e-0038

싱글 최대값
+3.4028234663852886e+0038

더블 최소값
+2.2250738585072013e-0308

더블 최대값
+1.7976931348623157e+0308

더블 확장 최소값
+3.3621031431120935e-4932 

더블 확장 최대값
+1.1897314953572317e+4932

  • 예외처리
이제 부터 예외(Exception)에 대해 알아본다. 
이런 저런 이유로 잘못된 연산이 일어났을때 cpu 또는 fpu가 어떻게 동작하는가에 관한 것이다

아래 그림은 fpu의 상태워드와 제어워드의 일부분을 그려본것이다.
초기값은 상태워드는 0이고 제어워드는 037f 이다 
0으로 나누는 경우가 알기가 쉬우니까 그걸 가지고 이야기해본다.

0으로 나누면 b2의 ZE 가 0 에서 1로 바뀐다. Zero divide 가 발생했음을 알리는 비트다
그걸로 끝나느냐, 즉 알려만 주고 그 다음 명령을 차분하게 수행하느냐?
절대 그렇지 않다.
제어워드의 같은곳의 비트인 b2의 ZM이 0인지 1인지를 살핀다.

0은 unmask
1은 mask

마스크 되어있으면 에러처리루틴으로 가지않는다. 입다물고 있으라고 마스크를 씌워둔것이다. 잘못된 결과값인 무한대(지수를 1로 채우고 가수는 0으로 채움)를 목적지에 넣고 그 다음 명령을 수행한다. 0에서 1로 바뀐 ZE는 따로 0 으로 만들어 주지 않으면 앞으로도 계속 1을 유지한다. 다른 플래그들도 다 이런 성질을 가지고 있다. 그 다음 명령에서 정상적인 나누기를 하더라도 1이 0으로 바뀌지 않는다. 이런 성질을 sticky 라고 부른다. 끈적끈적하다는 말인데 한번 붙으면 잘 떨어지지 않는것에 비유한듯.

마스크가 되어있지 않으면 목적지에 잘못된 결과값인 무한대를 넣지않는다. 그러니까 목적지의 데이타가 온전히 보전된다. 입에 마스크가 씌워져 있지 않으니까 마음껏 떠들어댄다. 즉, 에러처리루틴으로 가서 무시무시한 경고창을 띄우게도 할 수 있다. 그 다음 명령이 실행되지않고(이 명령이 에러상황에 관계없이 무조건 명령을 수행하라는 명령일 경우 실행될 수도 있다) 어딘가로 분기한다는게 핵심이다. 점검해보자. 

.model flat
.code
main proc
finit   ; fpu를 초기화한다
fldpi  ; 파이값을 스택에 넣는다
fldz   ; 0.0 을 스택에 넣는다
fdiv   ; 파이값 나누기 0.0 을 한다
fld1   ; 스택에 1.0을 넣는 명령인데 실행될까?
ret
main endp
end  main

st0에 1이 들어가있다. 8번 명령이 실행된것이다
STAT( 상태워드 : Status Word)의 값이 3004 이다. 

3004 = 0011 0000 0000 0100

ze 비트인 b2가 1이다.

st1에 무한대(#INF Infinite)가 들어가 있다. 

CTRL( 제어워드 : Control Word)의 값이 037f 이다. 

037f = 0000 0011 0111 1111

zm비트인 b2 가 1이다. 즉 마스크된 상태

이제 마스크를 풀어주고 실행해보자.

.model flat
.data
wCtrl    word   0
.code
main proc
finit  ; fpu를 초기화한다
fstcw    wCtrl ; 제어워드를 메모리로 복사
and      wCtrl, 0fffbh ; b2를 0으로 만든다.
fldcw    wCtrl ; 바뀐 값을 제어워드로 복사
fldpi           ; 파이값을 스택에 넣는다
fldz            ; 0 을 스택에 넣는다
fdiv            ; 파이값 나누기 0 을 한다
fld1  ;스택에 1을 넣는 명령인데 실행될까?
ret
main endp
end  main

에러박스가 떠있다.
13번 명령은 실행이 되지 않았다.
제어워드의 값이 037b, 

037b = 0000 0011 0111 1011

b2가 0으로 zm이 마스크가 벗겨진 상태(언마스크)

상태워드의 값이 b084, 

b084 = 1011 0000 1000 0100

b2가 1이다. 
참고로 b7 b15까지 1로 되어있는데 b7은 ES, b15는 Busy 이다.
에러가 발생했고 그 처리루틴을 실행중이면 이 두개의 비트가 1로 변한다. 
기계어분석 #2 에서 말한적이 있다.
fdiv 명령의 결과값이 저장되지 않고 무시되었기때문에 그 전까지의 스택이 깨끗하게 보전되어있다.

모든 예외가 이런식으로 처리된다. b0 부터 하나하나 살펴보자.

  • 유효하지 않은 연산 예외 (IE : Invalid operation exception)
  • Internet Explorer가 아니다.
유효하지 않는 연산이라는 말 자체가 너무 광범위하게 느껴진다.
방금 살펴본 0으로 나눈 연산도 유효하지 않은 연산처럼 보이고 모든 에러를 발생시키는 명령은 다 여기에 포함되는듯처럼 보인다.그러나 여기서 말하는 invalid는 두개의 범주로 나누어져있다.

1. 레지스터스택이 넘치거나(overflow) 마를때(underflow)
2. 유효하지 않은 산술오퍼랜드.

0으로 나눌때 발생하는 일련의 과정이 여기서도 유사하게 일어난다. 이 예외가 발생하면 b0(IE)가 1로 변하고 끈적끈적한(sticky) 성질을 가지며 제어워드의 b0(IM)의 상태에 따라 그다음명령이 실행되거나 아니면 예외처리루틴으로 분기하기도한다.

스택이 마르거나 넘치는건 따로 설명을 할 필요가 없을정도로 명확하다. 레지스터스택의 갯수가 8개밖에 없으니까 프로그램에서 잠시 방심하면 넘치도록 채우든가 마를정도로 빼쓰던가 하게 마련이다. 배열의 인덱스오류와 같은거니까 친숙하다. 
이 예외는 상태워드의 b6인 스택폴트(stack fault)와 같은 운명이다. 이 비트가 1로 변한다. 그리고 넘쳤는지 말랐는지를 표시하는 비트도 있다. 상태워드의 b9인 c1 이 그걸 표시한다. 0이면 말랐고 1이면 넘쳤다는걸 의미한다.
정말 말처럼 그렇게 동작하는지 살펴보자
마스크를 쓰고 있는게 초기값이니까 따로 벗기지 않는다.
fpu를 fninit명령으로 초기화하면 스택이 다 비게된다. 여기다대고 pop을 하면 당연 예외가 발생한다.

.model flat
.data
dwPop   dword  ?
.code
main proc
    fninit
    fst     dwPop
    ret
main endp
end main
명령포인터가 8번 라인을 가리키고 있으니까 7번까지 실행되었을때의 상황이다.
상태워드의 값이 
0041 = 0000 0000 0100 0001
b0 (IE) 와 b6 (SF)이 예상대로 1로 변해있다. 
스택이 파괴된 유효하지 않은 명령이 있었다는 뜻이다.
b9인 c1이 0이다. 즉 말랐다는 뜻이다.(underflow)
pop한 결과값이 잘못된 데이타인데 그래도 읽어보면 ffc00000 이다. 이 값의 의미는 NaN 에서 살펴본다.

push 실험

.model flat
.data
dwPush  dword ?
.code
main proc
    fninit; 초기화한다
    fldpi; 파이(π)를 스택에 넣는다
    fldpi; 2번
    fldpi; 3번
    fldpi; 4번
    fldpi; 5번
    fldpi; 6번
    fldpi; 7번
    fldpi; 8번
    fldpi; 넘친다.
    fst    dwPush
    ret
main endp
end main
15번 라인까지 실행했을때의 모습이다.
상태워드의 값이 

3A41 = 0011 1010 0100 0001

b0 (IE) 와 b6 (SF)이 1로 변했고
이제는 b9의 c1의 값이 1로 되었다. 이건 넘쳤다는 뜻이다(overflow)

14번 명령에서 스택이 찼고 15번 명령으로 넘친거니까 파이값이 스택에 들어가지 못하고 1.#IND(Indefinite:정의 되지않은값)이 스택의 꼭대기에 있다고 표시되어있다. 

스택에 있는 그 값을 읽어보자.


16라인이 실행된 모습이다.

상태워드의 값이 3841 이다.

15라인까지 실행했을때는 3A41 이었으니 b9 가 1에서 0으로 바뀐것이다.

b9는 sticky성질이 없다. 명령의 결과에 따라 계속 변한다.

1.#IND의 값이 ffc00000 이다.


IE를 더 살펴보기전에 필수적으로 요구되는게 NaN에 대한 이해라서 항목을 하나 마련했다.

  • NaN (Not a Number)
이름 짓는 센스가 탁월하다. dada 만큼 멋진 용어이다.

그림을 다시 한번 찬찬히 들여다보자.

+∞ 와 -∞ 위에 NaN이 있는데 수가 아니니까 공중에 붕 떠있다. 그럼에도 실수코드표에 포함되어 있는걸 보면 숫자처럼 사용된다는 의미이다. 숫자가 아니지만 숫자가 사용되는 곳에 대신 사용될 수 있다. 코딩하는 법은 위 그림근처에서 설명되어있다.

위에서 빈스택에서 값을 읽었을때 읽힌값과 스택이 넘쳤을때 스택에 저장된 값이 ffc00000 이었고 st0에 표현된 값은  #IND( Indefinite )  이었다. 즉 정의되지 않았다 라는 의미있데 실제 이 값을 분석해보면 NaN 임을 알 수 있다.

ffc00000
1111 1111 1100 0000...
1 111 1111 1 100 0000..

지수부 8비트가 1로 채워져 있고 가수부 분수비트가 1 이니까 QNan이다.

QNan을 만들때 부호비트는 0 이든 1 이든 관계없었고 가수부 나머지 비트도 0 이든 1 이든 관계가 없었는데 여기서 처럼 부호가 1이고 가수부 나머지 비트가 다 0으로 되어있는 수를 특별히 QNan Floating Point Indefinite value ( QNan FP ) 즉, 부동소수점 연산을 했는데 그 결과가 정의되지 않은 값이면 발생하는 QNan이라고 말할 수 있다. 
4바이트 싱글에서는 ff c0 00 00 이고 8바이트 더블에서는 ff f8 80 00 00 00 00 00 이다 

몇개 예를 들어보면, 

log(-1), √-1, ∞ ÷ ∞, 0 ÷ 0, 0 × ∞ , ∞ + (-∞), sin(∞) 등이 있다.

오퍼랜드 자리에 NaN 이 쓰일때가 있다.

add QNan , QNan
add QNan , 1.3 

이때도 결과는 Nan인데 QNan FP 는 아닌 다른 QNan이 만들어진다.

계산의 대상으로 NaN이 쓰일때의 모든 경우의 수를 따져보고 그 결과값도 같이 표시해본다.

1) Qnan 단독 : Qnan
2) Snan 단독 : Qnan
3) Qnan , 실수 : Qnan
4) Snan, 실수 : Qnan
5) Qnan1, Qnan2 : 가수가 큰 Qnan
6) Snan1, Snan2 : 가수가 큰 Qnan
7) Qnan, Snan : Qnan

결과값은 다 Qnan 이다.
결론을 염두에 두고 Qnan의 비교와 Snan의 변신에 관해 조금 더 알아보자.

5번의 경우)
Qnan1 : 7f c0 00 01
Qnan2 : 7f c0 00 12

원칙적으로는 이것들은 수가 아니기 때문에 크기를 비교할 수 없지만 편의상  부호와 지수는 버리고 가수만 가지고 크기를 비교할 수 있다. 위에서는 1과 12를 비교할 수 있다. 12가 커기 때문에 Qnan2 가 결과값이 된다.

6번의 경우)
Snan1 : 7f 80 00 10
Snan2 : 7f 80 00 25

이때도 위에서 처럼 가수끼리 비교하여 Snan2 가 선택된다.

7번의 경우)
Qnan : 7f c0 00 12
Snan : 7f 80 00 25

두개가 섞여 있을때는 가수 비교 필요없다. Qnan을 선택하면 된다.

2번의 경우)
Snan : 7f 80 10 45

이때는 가수부의 분수비트만 1로 만들면 Qnan으로 변한다. 그래서 7f c0 10 45 이 결과값이 된다. 최소의 비용으로 Qnan을 만들어 냈는데 가수부의 숫자들이 다 살아남았다는 사실이 중요하다. 

Qnan 과 Snan의 차이점이 뭘까?

Q가 Quiet 이고 S가 Signaling 임을 감안하면 S는 어딘가로 신호를 보내는 작용을 할 듯하다. 그대로다. IE(Invalid Exception)을 발생시킨다. 10바이트의 더블확장값은 예외지만 싱글과 더블의 Snan은 fld 로 스택에 불러들이기만 해도 IE가 발생한다. 산술연산의 오퍼랜드로 쓰여도 당연 IE가 발생한다.

1)번 Qnan 단독

sin함수의 인자로 QNan이 단독으로 쓰인경우인데 IE가 발생하지 않았고 그 값이 그대로 반환되었다.

2) Snan 단독
Snan 이 Qnan으로 바뀌어서  dwS에 저장되어 있고 IE가 발생했다.

3) Qnan , 실수
파이값과 Qnan을 더했다. Qnan이 그대로 반환되었고 IE가 발생하지 않았다.

4) Snan , 실수
파이값과 Snan을 더했다. IE가 발생했고 Qnan으로 바뀌었다.

5) Qnan1, Qnan2
Qnan1 + Qnan2 했는데  IE발생하지 않았고 가수부의 숫자가 더 큰 Qnan2가 반환되었다.

6) Snan1, Snan2

여기서는 10바이트 double extended를 쓸 수 밖에 없다.  딴 데이타타입은 스택에 올라올때 Qnan으로 바뀌지만 이 타입은 변환없이 그대로 스택으로 올라온다. Snan이지만 load 만으로는 IE가 발생하지 않는다. 

라인11의 faddp 가 실행되니까 예상대로 IE가 발생했고 Qnan으로 바뀌었다. 그러나 어떤 Snan이 바뀌었는지는 아직 까지는 알 수 없다. 메모리로 읽어온 다음 직접 읽어야된다.

tSnan1 의 번지가 녹색으로 박스친 114000h 이고 이곳의 데이타는 7f ff 80...1 이고 tSnan2의 번지는 보라색 박스친 11400ah 번지이고 그곳의 데이타는 7f ff 80..2 이다. 그리고 결과값이 저장된 빨간박스친 tS의 번지는 114014h 이고 그곳의 데이타는 7f ff c0...2 다. 예상대로 가수가 큰수인 tSnan2가 Qnan으로 변해서 나타났다.

7) Qnan, Snan
Snan이 오퍼랜드로 쓰였기때문에 IE가 발생했고 Qnan의 값이 반환되었다.


NaN은 이쯤에서 끝내고 IE를 마저 보자.

유효하지 않는 산술오퍼랜드에 의한 IE

산술( arithmetic ) 은 덧셈,뺄셈,나눗셈,곱셈,삼각함수,로그같은 계산을 말하며 오퍼랜드는 그 계산의 대상을 말한다. 

y = f(x) 

에서 함수의 인자인 x를 산술오퍼랜드라고 할 수가 있는데 함수 f 에 따라서 x의 범위 또는 조건이 정해져 있는것들이 있다. x 가 그것을 벗어났을때 IE가 발생하고 이때 y 에 담기는 값이 QNan FP 이다. log 함수를 예로 들면 좋겠다.

y = logaX ( a > 0 ,a ≠ 1, X > 0 )
이렇게 로그함수는 a와 X 에 제한이 가해져 있는 함수이다. 그래서log2(-1) 이렇게 인자로 음수를 넣으면 IE가 발생되고 결과값으로 QNan FP가 반환되며 문자로 찍힐때는 #IND ( Indefinte)로 표시된다.

ff c0 00 00 이 QNan FP 의 값이다.

이제 IE 를 일으키는 모든 경우를 다 살펴보자.

01) 지원하지 하는 형식 ( unsupported format )
  4바이트 싱글, 8바이트 더블에서는 찾아볼 수가 없고 10바이트 더블확장에서 그 예를 찾을 수 있다. 최소값보다 더 작은 수(denormal)를 나타낼때 지수부분을 0으로 채우고, 가수부분의 정수비트(b63)를 0 으로 만든다음 나머지부분은 계산의 결과를 담는데 싱글과 더블에서는 정수비트가 없기때문에 해당사항이 없다. 정수비트가 0 일때 지수부분이 0 이 아닌수가 될때 이 숫자는 IE를 일으킨다.
02) 산술연산의 오퍼랜드가 SNan 일때 : 앞에서 살펴보았다.
03) 순서있는 비교테스트명령의 오퍼랜드로 QNan이 될때 
04) ∞ + (-∞), ∞ - ∞,
05) 0 ÷ 0, ∞ ÷ ∞
06) fprem1 에서 0으로 나누거나, ∞ 를 나눌때
07) 삼각함수(fcos, fsin, fptan, fsincos)의 오퍼랜드가 ∞ 일때
08) fsqrt(음수) : 예외) 인자가 -0 일때는 -0 을 반환
09) fyl2x y*log2(x) 에서 x의 값이 음수일때 : 예외) x가 -0 이면 -∞ 이 반환
10) fyl2xp1 :  이 명령어는 log함수임에도 진수가 음수가 되어도 IE가 발생하지 않는다. 그냥 겉모습만 보면  9번 함수와 다른점은 x 가 x+1 로 바뀐것밖에 없어보여도 내부적으로는 많이 다를듯. 9번 함수는 x 의 값에 제한이 없지만 (로그함수 고유의 제한은 있다)이 함수는 x의 값이 약 -0.29에서 +0.29 사이로 딱 정해져있다. 범위를 벗어난 값을 넣어서 얻어지는 반환값은 정의되어있지 않다고 매뉴얼에 적혀는 있지만 그렇지도 않아보인다. y에 1을 넣고 x에 3을 넣어보았는데 값이 정상적으로 2가 나온다. 그렇지만 PE가 발생했다. 보고싶은 IE는 발생하지 않았다. x에 넉넉하게 -100을 집어넣어도 PE만 발생한다. 아마 명령어 내부적으로 IE 발생을 막아놓은듯하다.
  fst [메모리번지]는 스택의 내용을 해당번지로 복사하는 명령으로 따로 많은 변환은 하지 않고 크기조정만 한다. 스택의 크기가 10바이트라서 4바이트나 8바이트로 바꿀때 나름 의미를 살려서 복사한다. 스택에서 Nan이라면 4바이트짜리 또는 8바이트짜리 Nan을 만들고 정상적인 숫자라면 지수부는 지수부로 , 소수부는 소수부로 복사를 한다. Nan이나 ±∞를 대상으로 하더라도 IE를 발생시키지 않는다. 그러나 b나 i 가 붙은것은 사정이 다르다. b 가 붙으면 10바이트 팩된 bcd로 변환을 해야하고 i 가 붙으면 소수점 아래를 버려 정수를 만드는 변환을 하는데 오퍼랜드가 Nan, ±∞, 지원하지하는 형식의 숫자라면 IE를 발생시킨다.
12) fxch : 해당 레지스터스택이 비어있을때 QNan FP값을 사용한다.

직접 눈으로 확인해보자.

01) 지원하지 않는 형식
가수부를 0이 아닌수로 정수비트를 0으로 만들어서 덧셈을 해보았다.
IE 발생, 결과값이 1#IND 인데 그 값을 읽어보니 QNan FP 이다.
03) 순서있는 비교와 테스트의 오퍼랜드가 QNan 일때
적당한 QNan을 만들어 파이값과 비교했다. 
st0에 QNan이 있고
st1에 파이값이 있다.
IE가 발생했고 상태워드의 값이 7501 이다.
code flag가 unordered 값을 가질것이고 위치는 b14, b10, b8 이다.

0111 0101 0000 0001
똑같은 조건 unordered 비교를 해보자. QNan을 만나더라도 IE를 발생시키지 않는다고 배웠는데 마저 확인해보자


IE가 발생하지 않았다. 그러나 code flag의 값은 unordered를 가리키고 있다.
7500
0111 0101 0000 0000

ftst 명령이다.
IE가 발생했고 code flags값이 unorderd를 가리키고 있다.
7d01
0111 1101 0000 0001
04) ∞ + (-∞) 산술연산이니까 #IND 가 스택에 들어있고 IE발생
 ∞ - ∞ 산술연산이니까 #IND 가 스택에 들어있고 IE발생

05) 0 ÷ 0 
  ∞ ÷ ∞
06) fprem1 에서 0으로 나눈다
fprem1 에서  ∞ 를 나눌때

07) 삼각함수(fcosfsinfptanfsincos)의 오퍼랜드가 ∞ 일때
fsincos 하나만 테스트

08) fsqrt(음수) : 예외) 인자가 -0 일때는 -0 을 반환
09) fyl2x : y*log2(x) 에서 x의 값이 음수일때 : 예외) x가 -0 이면 -∞ 이 반환

반환값이 ff800000 이다. #INF 무한대인데 부호비트가 1이니까 -무한대를 반환했다.

10) fyl2xp1 :  
상태워드 3820
0011 1000 0010 0000 ( PE 발생)

역시 PE 만 발생했고 결과값은 -100.0 이 그대로 나타났다. 스택의 상황은 정상이다.

11) fbstp

IE가 발생했고 st7을 보면 무한대가 전에 로드되었다는 사실을 알수 있다. 화면에는 없지만 tBcd값은 ff ff 00 00 00 00 00 00 00 이었다. 이값은 10바이트짜리 QNan FP 이다.

fist를 살펴본다.
이번에는 pop을 하지 않는 명령어다. 그래서 st0에 #inf 무한대가 남아있다. IE발생했다. 그러나 bcd 와는 다르게 정수는 반환값이 -무한대 (80 00 00 00 )로 나타난다.

12) fxch : 해당 레지스터스택이 비어있을때 반환값으로 QNan FP값을 사용한다.
비어있는 스택에다 대고 교환명령을 내리니까 st0과 st1에 #IND가 나타닜을뿐만 아니라 스택폴트(SP)까지 나타나있다. 물론 IE 발생. . 상태워드 41
0100 0001 이 SP 비트이다.
결과값으로 예상대로 QNan FP 가 나타났다.

덧글

댓글 입력 영역