2010년 2월 3일 수요일

15장 - BCD 32비트 버전


BCD 32비트

우리는 4장에서 BCD 수를 16진수 레벨에서 살펴봤다. 이번 장은 4장의 연장선이라고 할 수 있다. 이미 다룬 것을 다시 다루는 이유는 조금있다가 나올
부동 소수 연산에 나오게 될 지 모르니 미리 복습하기 위함이다. 4장과의 차이라고 하면, 이번 장은 단지 32비트 코드를 쓰는 점 뿐이라고 할 수 있다.
부동 소수 연산에서 BCD는 자주 나오게 되니 미리 알아두면 진행에 무리가 없을 것이다.

32비트 콘솔 모드 뼈대 코드를 먼저 준비하자.

.486
.model flat, stdcall
option casemap:none

include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\masm32.inc

includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\masm32.lib

.data
     msgBuffer db "Hello, World!", 0Dh, 0Ah, 0
     msgLen dd ?
.code
start:

 invoke StdOut, ADDR msgBuffer
 invoke StdIn, ADDR msgBuffer, msgLen
 invoke ExitProcess, NULL

end start

보다시피 별 것 없다. 먼저 .486 지시어는 앞으로 자주 쓰게 될 것이니 간략히 설명하면, 이 프로세서 지시어가 들어가면 FPU 명령을 쓸 수 있음을
의미하며 32비트 코드임을 의미한다. 그러므로 debug.exe로 트레이싱하면 "도스에서 실행안됨" 메시지를 접하게 될 것이다. 32비트 코드니 우리는
올리 디버거 (http://www.ollydbg.de/)로 이를 트레이싱하면 된다. windows.inc/kernel32.inc는 32비트 표준 인터페이스가 정의된 인클루드 파일
이며, masm32.inc는 기본적인 입출력 관련 함수 (StdOut, StdIn)가 정의된 인클루드 파일이다. masm32.inc 파일은 C:\masm32\m32lib 에 정의되어
있으며, StdOut/StdIn은 다음처럼 정의된 콘솔 입출력 함수이다.

StdOut       PROTO :DWORD
StdIn        PROTO :DWORD,:DWORD

첫번째 인자는 버퍼 주소이고, 두번째 인자는 버퍼 길이이며, 이 버퍼 주소는 DWORD로 정해주므로 ADDR로 정해주었고, 그 주소는 실제로는
바이트 배열로 (ASCIIZ, Zero-terminated ASCII string) 선언해주었다. StdIn의 두번째 인자는 DWORD이며 이는 입력 버퍼의 길이 dword이다.
option casemap:none으로 대/소문자를 따로 인정하게 했다. 컴파일과 링크 스위치는 다음과 같다:

 ml /c /coff test.asm
 link /subsystem:console /libpath:c:\masm32\lib test.obj

간단한 뼈대 코드이며, 버퍼에 있는 내용을 StdOut으로 화면에 덤프하고 지연을 두기 위해 StdIn을 사용했다.


우리는 이미 BCD가 무엇인지 알고 있으므로 여기서는 간단히 32비트 데이터를 (un)packed BCD로 출력하는 방법만 다루면 된다. 32비트 코드에선 ds/es
를 따로 정해주지 않아도 된다. 여기서는 32비트 데이터를 하나 선언해주고 이를 32 바이트 (넉넉 잡고) 버퍼에 복사해주되 BCD 포맷으로 복사해보자.

먼저, unpakced BCD는 myBCD db 1, 2, 3, 4 로 선언하면 01 02 03 04 형식으로 한 바이트의 반토막만 숫자를 표시함을 기억해보자. 그리고 이번엔
mov al, [si]의 스트링 명령 버전이라고 할 수 있는 lodsb도 써보자. 이 명령은 나중에 스트링 연산을 다룰 때 다시 언급하겠지만, 당분간은
CPU 매뉴얼을 참고하자. CPU 매뉴얼에 간략히 Src 스트링을 Accumulator 레지스터로 옵셋 단위로 로드한다고 나와있을 것이다.

;================== 32-bits unpacked BCD dump ============

.486
.model flat, stdcall
option casemap:none

include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\masm32.inc

includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\masm32.lib

.data
     msgBuffer db "12345678", 0
     bcdBuffer db 32 dup (?)
     digitcnt dd ?    
     msgLen dd ?
.code
start:

  mov esi, offset msgBuffer                     ; src
  mov edi, offset bcdBuffer                     ; dest
  xor ecx, ecx                                  ; loop counter
 
@@:
  lodsb                                         ; al <- [esi]
  or al, al                                     ; 널 문자면 뒤의 레이블로 점프
  jz @F                                         ; 뒤로 분기 (forward branch)
  and al, 0Fh                                   ; 널 문자가 아니면, 상위 니블 마스킹
  mov [edi], al                                 ; 저장
  inc edi                                       ; 루프 증가
  inc ecx                                       ; 카운터 증가
  jmp @B                                        ; 앞으로 분기 (backward branch)
 
@@:
  mov digitcnt, ecx                             ; 카운터 백업
 
  invoke StdOut, ADDR bcdBuffer                 ; 출력
  invoke StdIn, ADDR msgBuffer, msgLen          ; 지연
  invoke ExitProcess, NULL                      ; 종료

end start

보다시피 단순히 샘플 스트링을 마스킹해서 출력해주었으며, 삐(아스키 벨)소리가 나면 성공이다. 자세한 내용은 주석을 참고하면 될 것이다.


BCD를 FPU로 로드하고 저장하는 FBLD/FBSTP 명령은 18자리 packed BCD (9바이트)를 다루는 명령이므로, packed BCD 포맷이 FPU와 호환이 잘 되는
포맷이라고 할 수 있다. 또한 8비트를 4비트씩 나눠서 아껴쓰자는 취지에도 더 맞다고 할 수 있다. 그러므로 우리는 될 수 있으면 packed BCD를
써야할 것이다. packed BCD에서 myBCD db 12h, 34h, 56h, 78h 식으로 선언하면, 메모리 상에는 12 34 56 78 형식으로 저장되어야 할 것이다.
경우에 따라서는 이 갯수를 바이트 갯수 (위의 예는 4바이트)로 알아둘 필요가 있다. 그러므로 여기서는 바이트 카운트까지 설정해주자.

;================== 32-bits packed BCD dump ============

.486
.model flat, stdcall
option casemap:none

include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\masm32.inc

includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\masm32.lib

.data
     myBCD db "12345678", 0                     ; ASCIIZ input string
     BCDBuffer db 32 dup (?)                    ; 넉넉하게
     digitcnt dd sizeof myBCD-1                 ; 유효 문자 갯수 (null은 무시)
     bytecnt dd ?
    
.code
start:

  lea esi, myBCD                                ; 소스
  lea edi, BCDBuffer                            ; 대상
  mov ecx, digitcnt                             ; 유효 문자 갯수
  add esi, ecx                                  ; 뒷자리부터
  add edi, ecx
 
  shr ecx, 1                                    ; 한 번에 두 바이트씩 4번 루프
  mov bytecnt, 0                                ; 메모리 카운터 = 0
 
@@:
  dec esi                                       ; 최하위 니블부터
  mov al, [esi]
  and al, 0Fh                                   ; 마스킹
  ror ax, 4                                     ; ah 상위 니블로 저장
 
  dec esi                                       ; 한칸 전진
  mov al, [esi]
  and al, 0Fh
  rol ax, 4                                     ; 저장된 니블 복원
 
  mov [edi], al                                 ; 대상 버퍼에 저장
  dec edi
 
  dec ecx
  jz @F
  inc bytecnt
  jmp @B
 
@@:
 
  invoke StdOut, ADDR BCDBuffer                 ; 출력
  invoke StdIn, ADDR myBCD, digitcnt            ; 지연
  invoke ExitProcess, NULL                      ; 종료

end start

이미 한번씩 다뤘던 내용이므로 별 어려움 없으리라 믿는다. 간단히 설명하면, esi -> edi로 압축해서 복사한다고 할 수 있다. 물론 압축된 BCD
이므로 깨진 문자로 출력되어야 정상이다. esi는 두 바이트씩 로드하고 이를 압축하여 edi로 한 바이트로 저장하므로 루프내에서 esi는 두번
감소 edi는 한 번 감소하는 형식이다.


언팩 BCD 숫자는 AAA/AAS/AAM/AAD 명령으로 사칙 연산후 변환할 수 있다. 먼저 16비트 코드로 4자리 십진 BCD 덧셈을 해보자.

;==============================
;
; 컴파일 스위치: ml test.asm
; 링크 스위치: link16 test.obj
;==============================
.model small
.stack 100h
.data
;==============================
src1 db "1911"                ; unpacked BCD 4-digits
src2 db "4321"
dest db 5 dup ('0')           ; sum of 2 src
size1 dw sizeof src1          ; size
size2 dw sizeof src2
;==============================

.code
main proc
  mov ax, @data
  mov ds, ax

;==============================

  lea si, src1                ; si = src1 포인터
  lea di, src2                ; di = src2 포인터
  lea bx, dest                ; bx = dest 포인터
 
  mov cx, size1               ; cx = size of src1
  mov dx, size2               ; dx = size of src2
   
  add si, cx                  ; 뒷 자리 (마지막 옵셋)부터 연산
  add di, dx
  add bx, sizeof dest
 
  dec si                      ; -1
  dec di
  dec bx

  xor ax, ax
  clc                         ; 덧셈 캐리 미리 초기화
L1:

  mov al, [si]                ; 로드
  adc al, [di]                ; 덧셈 (+ 캐리)
  aaa                         ; 변환

L2:
  mov [bx], al                ; 저장
 
  dec si                      ; 포인터 전진
  dec di
  dec bx
  dec cx                      ; 카운터 감소
  jnz L1                      ; 루프
  jnc L3                      ; 제일 앞자리 캐리만 따로 처리
  mov al, 1                   ; 1로 강제 설정후 저장
  mov [bx], al
 
L3: 
 
@@:
  mov ah, 01h                 ; 지연
  int 21h

  or al, al                        
  jz Last

Last:
;==============================

  mov ax, 4C00h
  int 21h
main endp
end main
;==============================

간략히 설명하면, src1 (ASCII decimal) + src2 = dest (unpacked BCD)가 되는 방식이다. 덧셈/뺄셈은 캐리를 항상 염두에 둬야하므로, adc로
더해줬다. aaa 명령은 캐리를 일으킬 수 있으므로 jnz/jnc 를 조합해서 루프를 돌렸다. 즉, 같은 자릿수일 경우 adc로 전부 더해서 추가로 캐리도
더해줬고, 만약 4번의 덧셈후 캐리가 일어나지 않으면 jnc에 의해 점프하고 그렇지 않으면 "mov al, 1", "mov [bx], al"로 강제로 설정해줬다.
그러므로 캐리를 감안하여 n자릿수 덧셈을 한다면, 그 결과를 담을 버퍼는 n+1자릿수가 되어야 한다.


하지만 보통 숫자 배열이 같은 크기인 경우는 드물어 이는 단지 연습에 불과하다. 그러므로 우리는 두 스트링의 길이가 다를 경우도 고려해야한다.
"9999", "123456" 두 아스키 십진 스트링을 더할 경우, 첫번째 소스는 4자리에 두번째 소스는 6자리이므로 뒷 4자리는 덧셈을 하되, 남은 두자리는
캐리를 감안하여 이를 0으로 더해주면 될 것이다. 이 아이디어로 위의 코드를 수정한 복정밀 BCD 연산 코드는 이렇다:

;==============================
;
; 컴파일 스위치: ml test.asm
; 링크 스위치: link16 test.obj
;==============================
.model small
.stack 100h
.data
;==============================
src1 db "1911"                ; unpacked BCD 4-digits
src2 db "129999"
dest db 20 dup ('0')          ; 넉넉히 잡아주고
size1 dw sizeof src1          ; 크기도 구해두고
size2 dw sizeof src2
;==============================

.code
main proc
  mov ax, @data
  mov ds, ax

;==============================

  lea si, src1                ; si = src1 포인터
  lea di, src2                ; di = src2 포인터
  lea bx, dest                ; bx = dest 포인터
 
  mov cx, size1               ; cx = size of src1
  mov dx, size2               ; dx = size of src2
   
  add si, cx                  ; 뒷 자리 (마지막 옵셋)부터 연산
  add di, dx
  add bx, sizeof dest
 
  dec si                      ; -1
  dec di
  dec bx

  xor ax, ax
  clc                         ; 덧셈 캐리 미리 초기화
L1:

  mov al, [si]                ; 로드
  adc al, [di]                ; 덧셈 (+ 캐리)
  aaa                         ; 변환

L2:
  mov [bx], al                ; 저장
 
  dec si                      ; 포인터 전진
  dec di
  dec bx
  dec cx                      ; 카운터 감소
  jnz L1                      ; 루프
  jnc L3                      ; 제일 앞자리 캐리만 따로 처리
  mov al, 1                   ; 1로 강제 설정후 저장
  mov [bx], al
 
L3:
  mov cx, size1
  sub dx, cx                  ; 남은 자릿수를 계산하여

L4:
  mov al, [di]                ; src2 포인터 값을 구하고
  adc al, [bx]                ; 원래 저장된 값을 더하여
  aaa
  mov [bx], al                ; 변환후 갱신
 
  dec di
  dec bx
  dec dx                      ; 차감된 포인터만큼 루프
  jnz L4
  jnc L5
  mov al, 1
  mov [bx], al
 
L5:
 
@@:
  mov ah, 01h                 ; 지연
  int 21h

  or al, al                        
  jz Last

Last:
;==============================

  mov ax, 4C00h
  int 21h
main endp
end main
;==============================

첫번째 L1 루프는 첫 4자리를 si + CF + di = bx 형식으로 더해주고, 중간에 낀 L3는 남은 자릿수가 몇개인지를 계산하여 dx를 카운터로 사용하여,
L4 루프에서 루프 카운터로 만들어주었고, di + CF + bx = bx 로 갱신해주었다. 하지만 남은 자릿수도 만약 "99"처럼 캐리를 일으킬지 모르므로
루프끝에 캐리를 판단하여 만약 캐리가 없으면 건너띄고 캐리가 있으면 캐리를 다시 한번 더 세팅해주었다.


하지만 이걸로도 부족하다. 왜냐하면 스트링의 크기가 src1 < src2 라는 가정에 시작했기 때문이며, 우리는 어느 것이 크냐에 상관없이 양쪽을 모두
더할 수 있게끔 개선해야 한다. 하지만 이는 다소 쉽게 해결할 수 있는데 이미 두 스트링의 크기를 구해뒀기 때문이다. 물론 저장 버퍼 또한 동적
메모리 할당으로 해결할 수 있지만, 여기서는 일단 건너띄기로하고 스트링의 크기에 따라 조건적으로 컴파일하는 분기처리를 해주자.

이 분기 처리는 cmp/jcc/jmp 조합으로 할 수 있지만, 여기서는 masm에서 지원하는 고급 언어 (HLL)식 .if/else/endif 매크로로 해결해보자. 순수
어셈블리식으로 할 때와 고급 언어식으로 할때의 가장 큰 차이가 "참"일때 분기하냐 "거짓"일때 분기하냐의 차이라고 할 수 있다. 물론 대부분의
디버거는 .if/else/endif 매크로를 인식하지 못하므로 만약 리버싱을 고려한다면 순수한 cmp/jxx/jmp 조합을 쓰는 것이 좋을 것이다. 사실 나는
조건적 매크로를 거의 쓰지 않는다. 여기서는 단지 32비트 코드에 미리 길들일 필요가 있으므로 다루는 것이다. 둘간의 차이를 비교해보자.

if:
  cmp X, Y
  jcc else
  ;< do something false >
  jmp endif
 
else:
  ;< do something true >

endif:

보다시피 순수 어셈블리 코딩일 경우 X, Y에 따라 위처럼 흉내낼 수 있다. .if/else/endif 블럭이 이와 유사하게 작동하며, 예로 다음을 고려해보자.

  mov ax, 01
.if ax == 01
  mov bx, 0
.else
  mov bx, 1
.endif

둘 간의 차이는 혼동의 여지가 있다. 왜냐하면 보통 어셈블리에서 jcc 명령 다음은 "거짓"일 경우 실행되는 반면, 고급 언어식 구문일 경우 .if
다음에 이어서 오는 문장이 "참"일 경우 실행되기 때문이다. 위의 코드를 역어셈해보면 다음처럼 보일 것이다.

  mov ax, 01
if:
  cmp ax, 01
  jnz else
  mov bx, 0
  jmp endif
 
else:
  mov bx, 1
 
endif:

이처럼 고급 언어식 코드를 쓰면 무수히 많은 cmp/jcc/jmp의 일부를 대체할 수 있음을 알 수 있고, 특히 코드가 길어진다면, 고급 언어식 표현이
가독성을 높이는 중요한 역할을 할 수 있다. 또한, if 뒤에 비교 연산자 (<, >, =, <= or, and 등)는 대부분의 고급 언어 스타일로 흉내낼 수 있는
장점이 있다.

이제 두 스트링을 바꿔서 조건 매크로와 함께 코딩해보자. 조건에 따라 바꿔줘야할 것은 스트링 포인터와 스트링 크기 계산 부분이다. src1/src2가
크기가 서로 다를 경우, 둘 간의 차이만큼 처음은 루프를 돌리고, 루프가 끝나면 둘 스트링의 길이를 다시 비교하여, 차이를 새로 구해서 차이만큼
루프를 다시 더 돌려주면 된다.

;==============================
;
;==============================
.model small
.stack 100h
.data
;==============================
src2 db "9999"                ; di:dx = 큰 스트링 (크기)
src1 db "99"                  ; si:cx = 작은 스트링 (크기)

dest db 20 dup ('0')          ; 넉넉히 잡아주고
size1 dw sizeof src1          ; 크기도 구해두고
size2 dw sizeof src2
;==============================

.code
main proc
  mov ax, @data
  mov ds, ax

;==============================

  lea bx, dest                ; bx = dest 포인터
 
  mov ax, size1
.if ax <= size2               ; size1 <= size2 ?
  lea si, src1                ; si = src1
  lea di, src2                ; di = src2
  mov cx, size1               ; cx = size1
  mov dx, size2               ; dx = size2

.else
  lea si, src2                ; si = src2
  lea di, src1                ; di = src1
  mov cx, size2               ; cx = size1
  mov dx, size1               ; dx = size2
 
.endif
   
  add si, cx                  ; 99 + (2)
  add di, dx                  ; 9999 + (4)
  add bx, sizeof dest
 
  dec si
  dec di
  dec bx
 
  xor ax, ax
  clc
L1:

  mov al, [si]
  adc al, [di]
  aaa

L2:
  mov [bx], al
 
  dec si
  dec di
  dec bx
  dec cx                      ; 작은 스트링만큼 루프
  jnz L1
  jnc L3
  mov al, 1
  mov [bx], al
 
L3: 
  mov cx, size1               ; 같지 않은 자릿수는 차이를 구해서 루프
  cmp cx, dx
  jne @F
  mov cx, size2
   
@@:
  sub dx, cx                  ; size2 - size1

L4:
  mov al, [di]
  adc al, [bx]
  aaa
  mov [bx], al
 
  dec di
  dec bx
  dec dx                      ; 두 스트링의 크기 차이에 기반한 루프
  jnz L4
  jnc L5
  mov al, 1
  mov [bx], al
 
L5:
 
@@:
  mov ah, 01h                 ; 지연
  int 21h

  or al, al                        
  jz Last

Last:
;==============================

  mov ax, 4C00h
  int 21h
main endp
end main
;==============================

이미 위에서 다 설명했으므로 주의를 요하는 부분만 살펴보면 L3라고 할 수 있다. 이는 큰 스트링에서 작은 스트링만큼 크기를 빼주는 부분인데,
여기서는 .if 블럭보다는 cmp/jcc가 수월하므로 이를 사용했고, 조건에 따라 카운터를 바꾸던가 유지하던가하게끔 해줬다. 이제 크기에 상관없이
BCD 덧셈을 수행할 것이다.


위의 아이디어 그대로 이제 32비트 코드로 포팅해보자. 별것없다. 단지, 확장 레지스터로 바꿔주기만 하면된다.

;================== 32-bits unpacked BCD addition ============
; compile : ml /c /coff abc.asm
; link : link /subsystem:console /libpath:c:\masm32\lib abc.obj
;
.486
.model flat, stdcall
option casemap:none

include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\masm32.inc

includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\masm32.lib

.data
;==============================
src1 db "9999999999999999999999", 0
src2 db "9999", 0
size1 dd sizeof src1 - 1
size2 dd sizeof src2 - 1
dest db 32 dup (0)
;==============================
.code
start:
;==============================
 
  mov eax, size1
.if eax <= size2              ; size1 <= size2 ?
  lea esi, src1
  lea edi, src2
  mov ecx, size1
  mov edx, size2

.else
  lea esi, src2
  lea edi, src1
  mov ecx, size2
  mov edx, size1
 
.endif
  add esi, ecx
  add edi, edx

  lea ebx, dest  
  add ebx, sizeof dest
 
  dec esi
  dec edi
  dec ebx

  xor eax, eax
  clc
L1:
  mov al, [esi]                   ; 서로 중복된 자릿수만 캐리로 덧셈해서 저장
  adc al, [edi]
  aaa
 
L2:
  mov [ebx], al
 
  dec esi
  dec edi
  dec ebx
  dec ecx
  jnz L1
  jnc L3
  mov al, 1
  mov [ebx], al
 
L3:
  mov ecx, size1                    ; 만약 더 남았으면, 자릿수 뺄셈
  cmp ecx, edx
  jne @F
  mov ecx, size2
@@:
  sub edx, ecx
 
L4:
  mov al, [edi]                     ; 남은 자리만 캐리로 덧셈해서 저장
  adc al, [ebx]
  aaa
  mov [ebx], al
 
  dec edi
  dec ebx
  dec edx
  jnz L4
  jnc L5
  mov al, 1
  mov [ebx], al
 
L5:
  lea esi, dest                       ; 샘플 출력을 위해 아스키 십진수로 변환
  mov ecx, sizeof dest
 
@@:
  mov al, [esi]
  add al, 30h
  mov [esi], al
  inc esi
  dec ecx
  jnz @B
 
L6:
  invoke StdOut, addr dest              ; 출력
  invoke StdIn, addr src1, size1        ; 지연
  invoke ExitProcess, NULL              ; 종료
 
end start
;==============================

위에서 이미 설명한 코드에 추가로 아스키 변환 루틴만 들어간 셈이다. 다양한 응용이 가능하니 직접 연습해보는 것이 중요하다고 할 수 있다.
한가지 기억할 것은 결과를 담을 버퍼는 덧셈에 사용된 큰 자릿수보다 최소한 한자리는 커야할 것이다.


 



  





 

댓글 없음:

댓글 쓰기

블로그 보관함