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

이제는 명령어를 살펴본다.

인텔 명령어표를 보면 꽤 복잡해보인다. 명령어도 눈에 익은게 있는 반면 생소한 것도 엄청많다. 명령어의 사용법도 가지가지다. 한눈에 이해가 가는 것도 있지만 어떤건 배경지식없이는 당최 모를 것들도 많다.

(앞으로 쓰이는 모든 숫자는 16진 숫자이며 16진수임을 나타내는 h는 빼고 표시한다)

00 = add Eb, Gb 
ff =  inc/dec Grp5-1A 

0f 50 =  vmovmskps Gy,Ups

0f 38 00 =  pshufb Pq,Qp 

1바이트 짜리 명령어에 두개의 오퍼랜드를 가지는것에서 부터 2바이트 또는 3바이트 명령어에 두개 또는 세개의 오퍼랜드를 가지는 것도 있다. 인텔에서 사용하는 Eb 나 Qp같은 약어도 뭔지를 알아야 그 명령어를 사용할 수 있고 기계어를 만들어낼수있다.

먼저 부동소수점 명령부터 살펴본다.

FPU 부터 SSE, AVX로 이어지는 역사적인 순서대로 FPU(Floating Point Unit)부터 시작한다.

첫번째 바이트가 d8 부터 df 까지의 영역이 fpu관련 명령이 있는곳이다. 두번째 바이트가 modR/m 이며 이 바이트의 의미에 따라 크게 두가지로 명령어가 갈린다. 첫번째는 이 바이트의 숫자가 00 부터 bf까지일때이고 두번째는 그외 나머지 영역 c0 부터 ff까지 일때이다. 

1번) 00 부터 bf : 주소지정방식
2번) c0 부터 ff : 레지스터 방식

아래의 기계어를 한번 보자.

d8 05 12 34 56 78 

두번째 바이트가 05 이니까 1번 방식의 명령어가 된다.

fadd real8 ptr [78563412] 

이렇게 번역된다.

78563412번지에 있는 single-real의 값과 st(0)의 값을 더해서 st(0)에 넣어라는 뜻이다. 

st(0) += [78563412]

d8 e1 

이 기계어는 두번째 바이트인 modR/m이 e1 이니까 2번 방식이다.

fsub st(0), st(1)  

이렇게 번역되고 의미는 st(0) 레지스터에 들어있는 값에서 st(1) 레지스터에 있는 값을 빼서 st(0) 레지스터에 집어넣어라는 뜻이다.

st(0) -= st(1)

fpu에서 사용하는 레지스터는 앞에서 살펴본 ax나 rax같은 레지스터가 아니다. 전용 레지스터가 따로 있다. 방금 사용한 st(0)가 그 전용레지스터중에 하나다.

아래의 표를 살펴보자


첫번째 바이트가 d8 이고 두번째 바이트가 1번방식인 것과 2번 방식인것으로 나누어져 있다.

이해하기 쉬운 2번방식부터 보자. 테이블 A-8 이다.

왼쪽에 16진수 C,D,E,F (영문자처럼 보이지만 사실은 16진수다) 가 있고 위쪽에 0 부터 7까지의 숫자가 있다. 그 밑의 표에는 왼쪽의 숫자는 같은데 위쪽의 숫자만 8부터 F까지로 다르다. 원래는 밑에 있는 표를 위쪽의 오른쪽에 붙어있던건데 공간이 부족하여 두개로 나눈것이다. 이어서 붙여서 보면 왼쪽에 C,D,E,F가 있고 위쪽에 0부터 F까지의 숫자가 있는 표가 된다.

읽는 방식은 왼쪽의 번호를 먼저 읽고 두번째는 위쪽의 숫자를 읽으면 된다.

c0 는 fadd st(0), st(0)
c1 은 fadd st(0), st(1)
...
c7 은 fadd st(0), st(7)
c8 은 fmul st(0), st(0)
...
ce 는 fmul st(0), st(6)
cf 는 fmul st(0), st(7)
...
ff 는 fdivr st(0), st(7)

이런식으로 읽으면 된다.
결코 어렵지 않을 뿐더러 규칙이 딱 정해져있어서 외우기도 좋다. 무료하면 외워도 좋을듯하다.

한눈에 들어오지 않는것은 1번 방식이다. 

두번째 바이트인 modR/m 이 00 - bf 까지는 방식이 다르다.

modR/m의 b3,b4,b5가 reg/op 부분인데 이것의 값에 따라 명령어의 종류가 달라지고 mm이나 r/m에 따라 오퍼랜드의 모양이 달라진다. 

(32비트모드라고 가정한다.)

d8 05 12 34 56 78

modR/m이 05 이다.

05 = 0000 0101 = 00 000 101 => 모드 0, reg 0, r/m 5 => [disp32] 

이건 [기계어분석 #1] 에서 살펴본대로다.

reg 0 는 위 표에 의하면 fadd single-real 이니까 

fadd real4 ptr [78563412] 로 된다.

78563412번지에 있는 4바이트의 실수데이타와 st(0)의 값을 더해서 st(0)에 넣는 명령이다. 
명령어의 자세한 의미는 뒤에서 따로 다룬다. 여기서는 기계어를 번역하는 과정에 집중하자.

d8 84 1e 12 34 56 78 는 어떻게 될까?

84 = 1000 0100 = 10 000 100 => 모드 2, reg 0, r/m 4 => sib 필요
1e = 0001 1110 = 00 011 110 => scale 0, index 3 (edx), base 6 (esi)

reg 0 는 위와 같다 fadd single-real.

fadd real4 ptr[esi + edx + 78563412] 가 된다.

d8 7c f3 12 => ?

7c = 0111 1100 = 01 111 100 =>모드1, reg 7, r/m 4 =>sib필요

f3 = 1111 0011 = 11 110 011 => scale 3, index 6(esi), base 3(ebx)

fdivr real4 ptr[ebx + esi*8 + 12] 

나머지 d8부터 df까지도 다 이런식으로 기계어를 만들면 된다.


이 매뉴얼상에 표시된 기호의 의미는 다음과 같다.

single-real
m32fp
real44바이트 실수
double-real
m64fp
real88바이트 실수
extended-real
m80fp
real1010바이트 실수
word-integer
m16int
word2바이트 정수
dword-integer
m32int
dword4바이트 정수
qword intege
m64int
qword8바이트 정수
packed-bcd
m80bcd
tbyte10바이트
packed bcd

packed bcd의 의미가 조금 애매한데 풀어 설명하면 다음과 같다
십진수 1234 는 16진수로는 4d2 이고 이 숫자가 메모리에 저장된다면 d2 04 
이렇게 되는게 일반적인 경우인데 pack bcd 방식은 

34 12 

가 된다. 따로 16진수로 변환되지않고 한 바이트에 십진수 두개의 숫자가 저장된다. 이걸 pack 즉 압축했다고 하고 만약 한바이트에 숫자하나를 저장할때를 즉 04 03 02 01 이런식으로 저장하면 unpack-bcd, 비압축 bcd 라고 한다.

.data
num    real8 123456.2
bcd    tbyte     ?

.code
fld     real8 ptr [num]    
; floating point load 
fbstp  bcd
;floating point bcd store and pop

이렇게 하면 

bcd 번지에 123456 이 저장된다. 
소수점 아래자리는 버린다.

음수는 어떻게 될까?
 
.data
num    real8  -123456.2
bcd    tbyte     ?

.code
fld     real8 ptr [num]
fbstp  bcd 

56 34 12 00 00 00 00 00 00 80

이렇게 10바이트(80비트)의 공간을 차지한다. 그래서 m80bcd 이다.

제일 오른쪽, 10바이트째에는 음수일때는 80, 양수일때는 00이 저장된다.

그래서 다시 적으면 

56 34 12 00 00 00 00 00 00 00 (양수일때)
56 34 12 00 00 00 00 00 00 80 (음수일때)

부호 한바이트를 빼면 9바이트로 18자리의 십진수를 저장할 수 있다.


*참고* 
masm 에서는 [bcd]를 그냥 bcd 라고 적어도 됨. 마찬가지로 [num]을 num으로 적어도 됨
모든 변수이름은 주소를 의미한다. 
bcd + 10 은 bcd 번지 + 10 이지 bcd 번지에 있는 값 + 10 이 아니다.
레지스터는 대괄호 본래의 의미가 살아난다. 즉 eax 와 [eax]는 같지않다.

이제 부동소수점 연산에 대해서 알아본다.
 
FPU의 구조

모든명령은 f 로 시작한다. 

add => fadd
mul => fmul

오퍼랜드가 정수이면 i 가 붙고 bcd이면 b가 붙고 실수이면 아무것도 붙지 않는다

fld   single-real
fild   dword-integer
fbld  packed-bcd

fpu 는 일반 레지스터를 사용하지 않는다. ax 나 bx 같은거. (에외가 두개있을뿐이다)
따로 전용으로 사용하는 80비트(10바이트)크기의 레지스터가 있다. 
4바이트크기의 single 또는 8바이트크기의 double을 전용레지스터로 읽어올때 전용레지스터의 크기에 맞게 10바이트 extended double 로 변환해서 읽어온다.

갯수는 8개.
그걸 스택처럼 사용한다고 해서 레지스터 스택이라고 부르며 기호는 다음과 같다.

st(0) st(1) st(2) st(3) st(4) st(5) st(6) st(7)

st를 스택약자라고 말해도 되겠다.

st(0) 는 레지스터 스택의 top을 가리킨다.



왼쪽에 표시된 R0 부터 R7 까지의 레지스터 번호를 직접 사용할수없고 오른쪽의 st(i)를 사용한다. top pointer의 값이 3이니까 r3이 top이고 그 지점이 st(0)가 된다. 그 레지스터에 1.0 이 저장되어 있다. 이때 숫자 2.0 을 push (load) 한다고 하면 이렇게 변한다.

top pointer의 값이 1 줄어들어 2가 되고 r2 에 2.0 이 저장되며 이제 그곳이 st(0) 가 된다.
계속 push가 일어나서 r0가 top이 된 상태에서 push가 되면 r7이 top이 된다.

가장 간단한 명령 7개가 d9 에 나온다.

d9 e8fld1load 1.0
+1.0000000000000000e+0000 
d9 e9fldl2tload log210
+3.0102999566398119e-0001
d9 eafldl2eload log2e
+1.4426950408889634e+0000
d9 ebfldpiload π
+3.1415926535897932e+0000
d9 ec fldlg2load log102
+3.0102999566398119e-0001
d9 edfldln2load loge2
+6.9314718055994530e-0001
d9 eefldz load 0.0
+0.0000000000000000e+0000

많이 사용하는 상수 7개를 레지스터 스택에 넣는 명령이다.  st(0)에 해당 값이 보관된다.

b0 부터 b5 까지의 6개가 예외플래그이다.
플래그 각각의 이름처럼 그런 에러가 발생하면 해당 비트가 1로 바뀐다
fpu control word에도 해당되는 자리에 그런 에러 플래그가 6개 있는데 그것을 exception mask bit 예외마스크비트라고 부른다
마스크 비트가 1이면 mask 되었다고 말하고 0이면 unmask 되었다고 말하는데 마스크가 되면 해당 예외처리를 하지 않는다. 
만약 0.0 으로 나누면 상태워드의 b2(zero divide)가 1로 변하는데 이때 제어워드의 b2도 1이라면 예외처리를 하지않고 그냥 지나가지만 제어워드의 b2가 0이라면 예외처리를 한다. 

b6의 Stack Fault 는 레지스터스택이 꽉 차 있을때  더 넣거나 텅 비어있을때 빼내면 1로 변한다. 
이 레지스터 스택은 돌고 도는 링방식이다.

push 인 경우 : r7 -> r6 ->.. -> r0 -> r7 (한바퀴 돌았다)
pop인 경우   : r0 -> r1 -> ..->r7 -> r0 (한바퀴 돌았다)

경계를 넘어갈때 stack fault가 발생하는게 아니다. 
각각의 레지스터가 비어 있는지 값이 있는지를 나타내는 뭔가가 있어서 그 값을 읽어서 해당 레지스터의 상태를 판단한다. 그 뭔가가 태그 레지스터다. 레지스터 하나당 두개의 비트를 사용하니까 8x2 = 16비트의 크기를 갖는다.

b15
r7 r7r6 r6r5 r5r4 r4r3 r3r2 r2r1 r1r0 r0

00 : 유효한 값이 들어있다.
01 : 값이 0 이다
10 : 유효하지 않다.에러가 발생했다
11 : 비어있다

이 태그레지스터의 초기값은 ffff이다. 다 1로 채워진다. 즉 빈 레지스터가 초기값이다.
이때 top의 값이 3 이고 마침 pop명령을 만났다면 
top이 가리키고 있는 r3의 태그값을 읽어 그 값이 11 즉 비어있는 값이니까 stack fault 에러가 발생한다.

b7(ES)의 error summary status
예외가 발생했고 마스크비트가 0이라면 이 b7비트와 b15(busy)가 1로 변한다. 지금 예외처리중임을 나타낸다.

code flags c3 c2 c1 c0
명령어의 연산결과를 나타내는 상태비트들인데 cpu의 캐리나 제로플래그와 유사하다
직접 읽을수는 없고 cpu의 eflags 로 fpu의 상태워드를 복사한뒤 읽어온다
비교연산을 다루면서 다시 설명한다.


마스크비트의 초기값은 다 1이다. 즉 전부 마스크 되어있어서 예외처리가 되지않는다.

정밀도 PC(Precision Control) 

00single
precision
   4바이트 실수
24bit 정밀도
가수는23bit
01예약
10double
 precision
8바이트 실수
53bit 정밀도
가수는 52bit
11double extended
precision
10바이트 실수
64bit 정밀도
가수는 63bit
디폴트값


RC(Rounding Control)
반올림, 올림,버림의 문제.

PC 가 11일때 가수의 자리수가 63비트인데 연산결과가 63비트보다 크다면 반올림을 하든 버림을 하든지 해야된다. 그것에 관한 문제이다.

00round
nearest(even)
가장 가까운 짝수
디폴트값
01round
down(toward -∞)
작은쪽으로 선택
10round
up(toward +∞)
큰쪽으로 선택
11round
toward zero
(truncate)
0에서 가까운쪽을 선택


작은수에서 큰수 순으로 차례대로 숫자를 몇개 적어보았다.

1번-0.110
2번-0.101
3번-0.100
4번-0.011
5번-0.010
6번..0..
7번0.010
8번0.011
9번0.100
10번0.101
11번0.110


소수점 3자리 숫자인데 소수점 2자리로 만들어야 한다고 하자.
끝자리가 0으로 끝나는 숫자는 (1,3,5,7,9,11번) 2자리 숫자이므로 해당사항 없다.

양수,홀수의 판별은 끝자리가 1이면 홀수, 0이면 짝수다.
자리수에 따라 다르다. 두자리로 보느냐, 세자리로 보느냐에 따라 다르다.

1번,5번,7번,11번은 세자리로 보면 0 으로 끝났으니까 짝수이지만 두자리로 보면 1 로 끝났으니 홀수
3번,9번은 두자리숫자로 보았을때 0으로 끝났으니까 짝수이다.

8번의 숫자를 선택해서 각각의 경우에 어떤 숫자가 선택되는지 살펴보자

하나 작은숫자는 7번 숫자. 0.01
하나 큰 숫자는 9번 숫자 0.10
0에서 가까운 숫자는 7번 숫자 0.01
가까운 짝수는 7번과 9번중 짝수를 택하면 된다. 9번 숫자가 짝수이다. 0.10

이런식으로 10번과 2번,4번 숫자의 경우 각각의 모드에 따라 선택되는것을
차이점을 명확하게 하기 위해 표를 만들어보았다. 

모드8번10번2번4번
00 ,가까운 짝수9933
01, 작은쪽7913
10, 큰쪽91135
11, 0에서 가까운쪽7935


b12(infinity control)은 쓰이지 않는다. 


  • 상태,제어 등 레지스터 관련 명령어 
db e2fnclexclear exception flags
no check exception
현재 예외처리를 하고 있는지 관계없이 
fpu 상태워드의 예외비트와 busy비트를 지움
b[0:7] = 0 ,b[15] = 0
9b db e2fclexclear exception flags
9b + db e2 = fwait + fnclex
두개의 명령으로 나누어진다
9bfwait(wait)check exception
예외처리를 하고 있다면 그것이
끝날때까지 기다려라
d9 f6fdecstpdec stack top pointer
상태워드의 top가 0이면 7 로,
0이 아니면 1감소시킴
d9 f7fincstpinc stack top pointer
상태워드의 top 이 7이면 0 으로,
7이 아니면 1증가시킴
db e3fninitinitialize fpu unit
no check exception
제어워드: 037f
상태워드: 0
태그워드:ffff
레지스터스택값: 변화없음
last instruction point : 0
last data point : 0
9b db e3finitinitialize fpu unit
9b + db e3 = fwait + fninit
두개의 명령으로 나누어진다
dd /7fnstsw m2bytestore state word
 at m2byte
no check exception
상태워드의 값, 2바이트를
주소지정방식에 따른 
메모리의 특정지점에 저장
예외체크를 하지 않음
df e0fnstsw axstore state word in ax
no check exception
상태워드의 값을 ax에 저장
예외체크를 하지 않음
9b dd/7fstsw m2bytestore state word
at m2byte
9b + dd/7 = fwait + fnstsw
9b df e0fstsw axstore state word in ax
9b + df e0 = fwait + fnstsw
d9/7fnstcw m2bytestore control word
at m2byte
no check exception
제어워드의 값, 2바이트를 
주소지정방식에 따른 
메모리의 특정지점에 저장
예외체크를 하지 않음
9b d9/7fstcw m2bytestore control word
at m2byte
9b + d9/7 = fwait + fnstcw
d9/5fldcw m2byteload control word
from 2byte
2바이트를 읽어
제어워드에 저장한다
dd/6fnsave m94/108bytesave fpu state
no check exception
st(0) - st(7)
control word register
status word register
tag register
last instruction pointer
last data pointer
opcode 를 
주소지정방식에 따른
메모리의 특정지점에 저장
예외체크를 하지 않음
9b dd/6fsave m94/108bytesave fpu state
9b + dd/6 = fwait + fnsave
d9/6fnstenv m14/28bytestore fpu environment
no check exception
control word register
status word register
tag register
last instruction pointer
last data pointer
opcode 를 
주소지정방식에 따른 
메모리의 특정지점에 저장
예외체크를 하지 않음
예외마스크를 1로 만듬
9b d9/6fstenv m14/28bytestore fpu environment 
9b + d9/6 = fwait + fnstenv
d9/4fldenv m14/28byteload fpu environment
from m14/28byte
fnstenv로 저장한 데이타를 
읽어들여 fpu 상태를 복원한다
dd/4frstor m94/108byterestore fpu state
fnsave로 저장한 데이타를 
읽어들여 fpu 상태를 복원한다
dd c0+iffree st(i)free tag 
태그레지스터의 st(i)의 값을
11로 바꿈(empty를 의미)
st(i)와 top pointer의 값은 
바꾸지 않는다.


  • 단항함수
d9 f0f2xm12x minus 1
-1 ≤ x ≤1 (소수)
-0.5 ≤ y ≤1
d9 e1fabsabsolute value
st(0) = |st(0)|
양수로 만듬
d9 e0fchschange sign
st(0)의 부호를 반대로 함
d9 fffcoscosine
가까운 cosine값을 구함
-263<= st(0) 라디안값< +263
d9 d0fnopno operation 
 아무짓도 안하고 쉼
d9 f3fpatanarctangent and pop
st(1) = arctan(st(1)/st(0)) 
결과를 st(1)에 둔뒤 pop한다
d9 f8fprempartial remainder
st(0) %= st(1)
st(0)를 st(1)로 나누어
그 나머지를 st(0)로.
부호는 소스 st(0)를 따른다
ieee754표준에 맞지않는다
사용하지말것
d9 f5fprem1partial remainder
with IEEE
st(0) %= st(1)
st(0)를 st(1)로 나누어 
그 나머지를 st(0)로.
부호는 소스 st(0)를 따른다
ieee754표준에 맞는다
d9 f2fptanpartial tangent
라디언단위인 st(0)의
가장 가까운 탄젠트값을
st(0)에 저장한뒤 
push 1.0 한다
st(1) = tan( st(0) )
st(0) = 1.0
d9 fcfrndintround to integer
st(0) = int(st(0))
가장 가까운정수로 바꾼다
d9 fdfscalescale
st(1)을 0에서 가까운정수로 만든뒤
2의 승수하여  st(0)에 곱한다
d9 fefsinsin
st(0) = sin( st(0) )
라디언단위인 st(0)의
가장 가까운 sin값을 
st(0)에 저장한다
d9 fbfsincossin cos
temp = cos(st(0))
st(0) = sin(st(0))
push temp
d9 fafsqrtsqrt
st(0) = sqrt(st(0))
d9 e4ftsttest
st(0)의 값이 0.0 인가?

st(0) > 0.0 c3,c2,c0 = 000
st(0) < 0.0 c3,c2,c0 = 001
st(0) = 0.0 c3,c2,c0 = 100
unordered  c3,c2,c0 = 111
d9 e5fxamexamine
st(0)의 값을 검사한다
c3 c2 c0 = 000 지원안함
c3 c2 c0 = 001 NaN
c3 c2 c0 = 010 Normal
c3 c2 c0 = 011 Infinity
c3 c2 c0 = 100 Zero 
c3 c2 c0 = 101 Empty 
c3 c2 c0 = 110 Denormal 
d9 f4fxtractextract 
st(0)의 지수부와 가수부를 분리
st(0) = 지수부(bias되지 않은 본래값)
push 가수부
d9 f1fyl2xy*log2X
pop
d9 f9fyl2xp1y*log2(X+1)
x값에 제한이 있다
x값이 아주 작을때 쓸것
-(1-√2/2) ≤ x ≤ (1-√2/2)
-0.292983..≤ x ≤ 0.292893..
pop


첨자표현이 스마트폰에서 나타나지 않아 부득불 이렇게..
까만바탕에 보라색글자는 파이썬 대화형에서 출력해본것이다. 두개의 데이타가 일치한다.

핑백

덧글

댓글 입력 영역