2010년 1월 25일 월요일

12장 - 부호 확장과 마스킹 & 기타 잡동사니들

12장 - 부호 확장과 마스킹 & 기타 잡동사니들


알다시피 컴퓨터는 무한정의 데이터를 저장할 수 없다. 한상 한정된 양 (magnitude)으로 연산을 수행하는 기계이다. 주요 연산인 산술 연산을 어떤
데이터에 수행하면 자리 올림/내림 (carry) 문제가 발생할 수 밖에 없다. 십진수로 2 자릿수 (digit) + 2 자릿수 덧셈을 하다보면 그 결과는
4 ~ 5 유효 자릿수 (significant digit)가 될 수 있다. 반면 뺄셈을 하다보면 2 자릿수 - 2 자릿수 = -1 유효 자릿수가 될 수 있다. 곱셈은
2 자릿수 * 3 자릿수이면 두 자릿수를 더해서 유효 자릿수 (5)를 계산하고, 나눗셈은 큰 자릿수에서 작은 자릿수를 빼서 유효 자릿수를 구한다.

컴퓨터는 또한 음수/양수를 모두 저장할 수 있으며, 8비트 양수 (unsigned)일 경우 0, 1, 2, ...255를, 8비트 음/양 모두 포함수 (signed)일 경우
-128, -127,...-1, 0, 1,...126, 127 까지를 표현할 수 있다. 양수의 경우 255를 넘는 순간 캐리 플랙 (CF)을 토글하고, 음수일 경우 -1 (FFh)을
넘어갈 경우 CF를 토글한다. CF를 토글하면 자리 올림수는 CF에 저장되고 무시되며, 담을 수 있는 수만 담는다. 255 + 1 = 0, -1 + 1 = 0

음수/양수를 모두 다루는 수 (signed)는 중간에 7Fh (양수 127)와 80h (음수 -128)로 2진 레벨 1이 증가할 때 OF를 토글하여 부호가 바뀌었는지를
체크하고, 양수/음수 간의 전환 (wrap)이 일어난다. 즉, 127 + 1 = -128 = 80h. 하지만, 16진수 80h는 사인 비트 (MSb, 최상위/최좌측 비트)가
1이므로 엄밀히 음수이다. 음수의 크기 (magnitude)를 구하려면 절대치로 구하며 절대치는 2의 보수로 계산된다. 그러므로 80h의 크기를 알고
싶으면 1000 0000 (2진수로 사인 비트가 on이므로 음수다) -> 0111 1111 (뒤집어서) -> 1000 0000 (1을 더한다). 즉, 80h 는 2의 보수로도 80h라서
크기를 알수 없는 수라고 할 수 있다. 이 수를 십진수로 -128 (가장 큰 음수)라고 부르기로 했다.

음수이건 양수이건 간에 1 바이트 두수를 더한다고 2바이트가 되지는 않지만, 곱셈을 하면 그 결과물의 유효 자릿수가 배 (double)가 될 수 있으며,
나눗셈을 하면 그 절반 (half) 자릿수가 될 수 있다. 이런 이유로 부호 확장이 필요하다. 덧셈/뺄셈의 경우 캐리에 자리 올림/내림을 반영하면
되므로 크게 문제되지 않지만, 현대 컴퓨팅에서 단순히 덧셈/뺄셈만 하는 경우가 드물기에 또한 부호 확장을 고려해야한다.

n- 바이트 데이터에서 최상위 비트는 사인 비트로 사용되며, +/- 의 부호를 결정한다. unsigned는 암시적인 +가 붙은 데이터라고 할 수 있고 이는
0으로 상위 비트를 채워 부호를 확장한다. 예로, 8비트 80h: 1000 0000 -> 16비트 0080h: 0000 0000 1000 0000 처럼 앞에다 0을 붙여 부호를 확장
하며 이를 제로 확장 (0 확장, zero-extension)이라고 한다. 반면, 음수는 8비트 -80h: 1000 0000  -> 16비트 FF80h: 1111 1111 10000 0000 처럼
앞에 -1을 의미하는 1을 붙여 확장한다. 부호 확장은 원래의 데이터에 있는 최상위 비트를 확장될 상위 데이터 영역에 가득히 복사해주는 것이다.

부호 확장의 반댓말이 부호 축소(sign contraction)인데 위처럼 앞에 가상의 00 또는 FF가 붙은 데이터 만이 음/양을 결정할 수 있으므로, 0CAD,
F001 같은 데이터는 unsigned 레벨에서는 부호 축소를 할 수 없고, signed일 경우 축소할 경우 오버플로 (여기서는 언더플로)에 이르므로 역시
부호 축소할 수 없다. 우리는 부호 축소는 무시할 것이다.

어셈블리 코딩시 부호 확장은 명령으로 해결할 수 있으며, CBW/CWD/CWDE/CDQ 등의 명령이 우리 대신 부호 확장을 해주는 명령이다. 이는 데이터의
사인 비트를 체크하여 자동으로 사인 비트를 상위 데이터에 복사해넣는다. 하지만, CBW는 바이트 레지스터인 AL의 사인 비트 (bit 7)를 취해 워드
레지스터인 AX의 상위 바이트 (AH)를 가득 채워 워드 데이터로 변환한다. 즉, 1000 0000 -> 1111 1111 1000 0000. CWD/CWDE는 AX의 사인 비트
(bit 15)를 취해 각각 DX/EAX에 상위 워드를 채워 결국 더블워드 데이터로 만든다. CDQ는 EAX의 사인 비트 (bit 31)을 취해 EDX를 가득 채워 결국
쿼드워드로 둔갑시킨다.

부호 확장 자체가 그렇지만, 이들 명령은 덧셈/뺄셈에서는 별 효력이 없으나, 나눗셈에서 만약 이를 빠뜨리고 div/idiv으로 연산할 경우 #DE에 이를
수 있으므로 나눗셈에서는 선택이 아니라 필수다. 곱셈도 마찬가지로 연산하기 전에 먼저 부호를 확장해줘야 한다. (선 확장 후 연산)

이들 명령은 Accumulator를 암시적 레지스터로 사용하는 단점이 있다. 이 단점을 개선하여 나온 스트링 명령이 MOVSX/MOVZX이며 각각 0이나 1로
특정 연산자 (보통 16비트 이상의 레지스터나 8비트 이상의 메모리 주소)에 채운다. 하지만 이 명령은 masm에서 .386 이상의 프로세서 지시어를
써줘야 사용할 수 있는 단점이 있다. 이 명령의 64비트 버전이 MOVSX/MOVSXD 이며, MOVSX는 32비트에서도 사용된다.

부호 확장 명령이 각각 장단점이 있으므로 이를 우리는 에뮬레이트 할 수 있다. CBW를 에뮬레이트하면 대략 이렇다:

  test al, 10000000b
  <양수일 경우 연산>
  jz L1
  jmp Last
 
L1:
  mov ah, -1
  <음수일 경우 연산>
 
Last:

이 경우 L1 부분에 "mov ah, -1" 대신 "mov bl, -1" 식으로 다른 아무 레지스터라도 부호 확장이 가능하다. 다른 부호확장 명령도 이와 유사한 유사
코드로 흉내낼 수 있다.

또한 산술 연산을 하다보면 캐리가 발생할 경우 adc/sbb로 확장할 수 있다.

  <산술 연산>
  jc L1
  <캐리가 없을 경우 연산>
  jmp Last
 
L1:
  adc/sbb 확장할 레지스터, 0/1
 
Last:

논리 연산일 경우도 adc/sbb로 확장할 수 있으며 rcl/rcr로 캐리가 발생하면 확장할 수 있다.

  <논리 연산>
  jc L1
  <캐리가 없을 경우 연산>
  jmp Last
 
L1:
  adc/sbb 확장할 레지스터, 0/1
  rcl/rcr 확장할 레지스터, 연산자 크기
  loop L1

Last:

물론 논리 연산시 "mov X, -1"로 해줘도 무관하다. 이 방법은 특정 레지스터에 구속되지 않고 원하는 레지스터에 확장할 수 있는 장점이 있다.



위에서 다룬 명령과 수동 방식을 혼합하여 다양한 방법으로 부호 확장할 수 있다. 상수 0이나 -1로 채울 경우:

  mov al, 10110000b               =     mov al, 10110000b
  movsx ax, al                          cbw
 
  xor ax, ax          ; 양수로          mov ax, 0FFFFh          ; 음수로 = mov ax, -1
  mov al, 01010000b                     mov al, 10110000b

비트 복사해서 쉬프트:

  mov al, 양수        ; 양수로          mov al, 음수      ; 음수로
  mov ah, al          ; 복사            mov ah, al        ; 복사
  shr ax, 8           ; 논리 쉬프트     sar ax, 8         ; 산술 쉬프트

비트 복사해서 마스킹 값 쉬프트:

  mov al, 양수/음수 무관
  mov ah, al                            ; 복사
  and ah, 80h                           ; 마스킹 (and x, 1000 0000)
  sar ah, 8                             ; 쉬프트

32비트 부호 확장:

  xor eax, eax                          mov al, mem
  mov al, mem/imm8                      cwd/cwde
 
  mov edx, eax
  shr edx, 31

메모리 데이터 확장:

  movsx cx, Byte          ; Byte -> Word
  movzx Word, Byte        ; Byte -> Word
  movsx ebx, Word         ; Word -> Dword
  movzx Dword, cx         ; Word -> Dword

메모리 -> 메모리 확장: rep movs

간혹 서로 크기가 다른 연산자로 연산을 할 경우가 생긴다. 이럴 경우는 작은 연산자를 큰 연산자로 부호 확장하여 연산한다. 작은 연산자를 큰
연산자로 부호 확장할 때, signed 연산인 경우 부호 확장, unsigned 연산인 경우 제로 확장을 해준다.

  mov al, Unsigned        ; unsigned 덧셈           mov al, Signed        ; signed 덧셈
  mov ah, 0                                         cbw
  add ax, Extended                                  add ax, Extended

어쩔땐 부호를 먼저 확장해야할 지 아니면 연산을 먼저 해야할 지 결정하기 힘들 때도 있다. 이럴 경우 연산을 수행후 (후) 확장을 해준다.

 
  add al, myVar
  adc ah, 0
 
만약 특정 레지스터가 어떤 값을 지녀야 할 경우 보조 레지스터를 사용하여 부호 확장한다.

  mov bx, ax            ; 복사
  mov al, myVar
  cbw
  add ax, bx
 
만약 보조 레지스터마저 여의치 않으면, 직접 확인하는 수 밖에 없다.

  add al, myVar           ; 로드
 
  cmp myVar, 0            ; 0 ?
  jge ZeroExtend          ; no
 
  adc ah, 0FFh            ; yes
  jmp Next
 
ZeroExtend:
Next:

물론 임시로 스택에 확장할 보조 레지스터를 저장해도 좋다.

  push bx                   ; 백업
  mov bx, ax                ; 복사
  mov al, myVar             ; 로드
  cbw                       ; 확장
  add ax, bx                ; 연산
  pop ax                    ; 복원
 
물론 메모리에 임시로 저장해두고 확장해도 된다. 이 방법은 주로 고급 언어에서 사용하는 방법이다.

  mov mem, ax               ; 백업
  mov al, myVar
  cbw
  add ax, mem               ; 로드해서 연산

만약 signed 데이터일 경우 변수를 두개 선언하고 필요에 따라 확장한다.

  myByte db ?
  myWord dw ?
 
  mov al, myByte
  cbw                         ; byte -> word
  cwd                         ; word -> Dword
  add ax, word ptr [myWord]
  adc dx, word ptr [myWord+2]
 
물론 386 이상이라면 다음 코드를 고려해보자.

  movsx eax, Var1
  add eax, Var2
 
386 이상에서 쿼드워드에 연산한다면 다음을 고려해보자.

  myByte  db -1
  myQWord dq 1
 
  movsx eax, myByte
  cdq                             ; Dword -> Qword
  add eax, dowrd ptr myQword
  adc edx, dword ptr myQword+4
 
그래도 안된다면 쉬프트와 로테이트로 연산하여 스택에 저장해가면서 연산하자. 이외에도 다양한 방법이 있으며 몇가지 흔히 볼 수 있는 것만 예로
들었을 뿐이다. 특정 연산과 결합하는 경우를 살펴보자. 먼저 산술 덧셈 연산의 예,

L1:
  movsx ecx, byte ptr [edx]             ; edx의 바이트 데이터를 ecx로 확장
  add ecx, eax                          ; (s) ecx + eax
  dec edx                               ; next
  loop L1

뺄셈:

  movsx ecx, bl                          movzx eax, byte ptr [eax]      ; (z) eax
  sub ecx, 10h                           movzx edx, byte ptr [edx]      ; (z) edx
                                         sub eax, edx                   ; (z) (eax - edx)
                                        
곱셈:

  movsx edx, dl         ; (s) edx <- dl
  mov ebx, ecx          ; multiplier
  imul ebx, edx         ; (s) ebx (high) * (s)edx (low)
 
나눗셈:

  movzx eax, ax         ; (z) eax (dividend low)
  mov ecx, 10h          ; divisor
  xor edx, edx          ; (z) edx (high)
  div ecx               ; edx (R): eax (Q)
  mov ebx, edx          ; 나머지 (R)만 저장
 
스택 액세스:

  movzx cx, 63h
  push ecx               ; (z) ecx, (z) eax
  movzx eax, ax
  push eax
 
덧셈 & 나눗셈:

  movzx eax, byte ptr [edx+eax]     ; (z) eax (low)
  movzx edx, byte ptr [edx]         ; (z) edx (high)
  add eax, edx                      ; (z) eax + edx
  mov ecx, 0Ah                      ; divisor
  cdq                               ; reset edx
  idiv ecx                          ; R:Q = edx:eax
  add edx, 30h                      ; R + 30h
  mov eax, edi                      ; next dword
 
곱셈 & 나눗셈

  mov ecx, 7                        ; divisor
  cdq                               ; -> edx:eax
  idiv ecx                          ; R:Q = edx:eax
  imul edi                          ; eax * edi = edx:eax
  add eax, 0FFFFFFFDFh              ; eax - 21
  mov edi, eax                      ; save to edi
  add eax, edi                      ; double edi
 
논리 연산의 예를 살펴보자.

  movzx ecx, cx                      movzx si, al                           mov eax, edx
  shl ecx, 08                        mov al, dl                             cdq
  or ebp, ecx                        and eax, 01                            sub eax, edx
                                     xor esi, eax                           mov edx, dword ptr [edi * 4]
                                                                            sar eax, 1
                                                                           
몇가지 역어셈 코드에서 인용했지만, 보다시피 부호 확장은 선택이 아니라 거의 필수다. 이외에도 지난 번에 다룬적 있는 니블 스와핑이나 bswap,
xchg, xchgadd 등으로도 해줄 수 있으며, 아예 룩업 테이블을 만들어두고 xlat으로 부호를 확장하거나 bt/btr/bts 등의 비트 세팅 명령을 부호
확장에 사용하기도 한다.  



* 마스킹

다른 값에 있는 비트를 이용하여 특정 값을 0이나 1로 강제로 만드는 것을 masking이라고 한다. 강제로 세팅할 비트를 제외하고는 건드리지 말아야
한다. 그러한 이유로 마스킹에 쓸 특정 비트를 꺼내서 (extract) 집어넣는 (insert) 과정이 필요하다. and 연산자는 마스킹 비트를 0으로 많이
산출하므로 마스킹 되기전의 소스가 1이라면 1로 마스킹하는 경우만 1을 유지한다. 그 결과 1이 아닌 비트는 0으로 변하며 변한 비트를 마스킹되어
떨어져 나간 비트 (masking out bits)라고 한다.

우리는 이전에 아스키 변환을 다뤘으므로 '0' ~ '9'가 ASCII 30~39h 에 각각 매칭되는 것을 알고 있다. 여기서 30h를 빼면 2진 값이 되므로 이를
0Fh로 and 마스킹하면 하위 니블만 얻어지고 그 값을 사용하면 된다. 물론 sub으로 30h를 빼줘도 되지만 사람들은 and를 더 선호한다. 특정 비트를
0으로 만들어 버리는 and와는 반대로 특정 비트를 1로 만들면 그 비트는 마스킹되어 세팅된 비트 (masking in bits)가 된다. 마찬가지로 0~9의
이진값을 30h로 or시키면 이 아스키 코드가 산출된다. 대문자/소문자 변환도 마찬가지 원리가 적용된다. 특정 데이터가 확연히 블럭으로 구분된다면
이 또한 마스킹을 적용할 수 있다. 그런 예로 압축 데이터가 해당한다.

이미 마스킹은 앞장에서 여러번 해봤으므로 따로 예를 들지 않아도 이해될 것이다.

* 복정밀 NOT 과 NEG

not 명령은 모든 비트를 뒤집는다. 이 명령은 아무 플랙에 영향주지 않으므로 조건 분기는 의미가 없다. 복정밀 not 연산은 간단하다. 변환하려는
대상 연산자를 모두 not 명령으로 뒤집어주면 된다.

  not ax                          not dx
  not dx                          not ax
 
not의 성격상 이 명령을 반복하면 다시 되돌리는 격이 된다. 이는 "xor X, 0FFh", "xor X, 0FFFFh" 등으로 해줄 수도 있다. not과 비슷하면서도
조금 복잡한 것이 음수로 만드는 복정밀 neg이다. 이는 neg + sbb 조합으로 해결할 수 있다. 0에서 특정 연산자를 빼주는 원리릴 이용한 것이다.
빼준다면 sub과 역할이 비슷하므로 다음처럼 복정밀 neg를 수행할 수 있다.

  neg dx                    ; 하위 워드를 음수로 변환해서
  neg ax
  sbb dx, 0                 ; 바로우가 일어나면 dx를 1 추가로 감소
 
문제는 자릿수가 많아져 반복 코드가 늘어날 때 오타가 자주 발생하는 점이 문제이다. AoA에서 인용한 386이상의 128비트 복정밀 neg코드는 이렇다.

myVar dd 0, 0, 0, 0         ; 128 비트 정수
 
  neg myVar+12              ; 최상위 dword
  neg myVar+8               ; 다음
  sbb myVar+12, 0           ; 상위 dword로 조절
 
  neg myVar+4               ; 2번째 dword
  sbb myVar+8, 0            ; 3번째 dword로 조절
  sbb myVar+12, 0           ; 최상위 워드에 바로우 뺄셈
 
  neg myVar                 ; 하위 워드
  sbb myVar+4, 0            ; 2번째 dword로 조절
  sbb myVar+8, 0            ; 3번째 dword로 조절
  sbb myVar+12, 0           ; 하위 워드로 바로우 뺄셈
 
이럴 경우 매번 neg 연산할 때마다 캐리를 전송해야 하므로 속도가 느려질 수 있으므로, 0에서 값을 빼는 방법으로 neg를 수행할 수 있다.

myVar dd 0, 0, 0, 0, 0      ; 160 비트 정수

  mov eax, 0
  sub eax, myVar            ; 0 - myVar 하위
  mov myVar, eax            ; 저장
 
  mov eax, 0
  sbb eax, myVar+4          ; 다음
  mov myVar+8, ax           ; ax
 
  mov eax, 0
  sbb eax, myVar+8
  mov myVar+8, ax
 
  mov eax, 0
  sbb eax, myVar+12
  mov myVar+12, ax
 
  mov eax, 0
  sbb eax, myVar+16
  mov myVar+16, ax

* xor로 부호 테스트

특정 값의 부호를 절대치로 구할 수 있으나 또한 사인 비트를 xor하여 음/양을 결정할 수 있다. 물론 xor이 절대치를 구해주진 않는다.

  mov al, byte ptr [op1+3]
  xor al, byte ptr [op2+3]
  mov cl, al                ; 부호 저장
 
  <기타 연산>
 
  cmp cl, 0                 ; 0 ?
  jns L1
 
  <음수 변환>
 
L1:
  <양수 처리>
 
* 플랙 테스팅

test 명령은 암시적 and 연산이며 결과를 버리는 점이 and와 다르다. 그러므로 and 연산으로 하던 것을 test 명령으로 미리 알아볼 수 있다. 그러므로
특정 데이터의 여러 비트를 여러번 테스트 할 수 있다. 

  test ax, 1
  jnz Bit0
  test ax, 2
  jnz Bit1
  test ax, 4
  jnz Bit3
  ...
 
하지만 test 명령이 결과를 갱신하지 않으므로 test/cmp를 조합해서 여러번 플랙을 테스트할 순 없다. 그런 이유로 비트 그룹에서 특정 한 비트를
측정하는 용도로 사용된다. 하지만, .386 이상에선 bt 명령으로 멀티비트를 개별적으로 테스트할 수 있다. test 명령은 제로 플랙을 테스트하는데
특히 유용하게 사용되며, "test X, X" 처럼 같은 연산자를 테스트해주면 된다. 이는 "cmp X, 0" 대체 명령이다. 물론 and/or 등의 논리 연산이랑
test를 조합해서 사용하면 의미가 없다. 486 이상의 파이프라인 실행이 가능한 프로세서에서는 결과를 반영하지 않으므로 위험을 미리 방지해 주는
역할도 수행한다.

복정밀 메모리 데이터라면 레지스터에 로드하여 test보다는 and로 테스트할 수 있다.

  mov ax, word ptr [var]
  and ax, word ptr [var+2]
  and ax, word ptr [var+4]
  ...
  cmp ax, word ptr [var+N]
  cmp ax, 0FFFFh
  je Is0FFFFh

and 명령은 2의 n승 -1 까지의 변수 카운터를 구현할 수 있다.

  inc myCounter
  and nBits
 
여기서 nBits는 우측에서 부터 비트를 누적하면 된다. 예로,

  inc myCounter
  and myCounter, 00001111b        ; 0~15까지 카운트
 
또한 and로 Mod 2^n 나머지를 구하는 것은 이전에 다뤘다.
 
* 복정밀 비교

애석하게 캐리를 감안한 비교 명령은 없다. 더구나 cmp, sub 두 명령이 같은 플랙으로 테스트를 하므로 sbb 명령과 혼합해 쓰기도 어렵다. 이 문제는
AoA를 직접 참고하기 바란다.

댓글 없음:

댓글 쓰기

블로그 보관함