2010년 1월 3일 일요일

4장 - 수 체계와 진수 변환 3


4장 - 수 체계와 진수 변환 3

우리는 지난 시간에 2진수와 16진수로 변환하는 방법을 알아봤다. 이번 장은 BCD라는 특별한 수를 다뤄보자. BCD (Binary Coded Decimal) 숫자는
비트를 아껴쓰자는 구호를 외쳐대는 혁명가 쯤에 해당한다. 지금은 CPU 처리량이 32비트에서 64비트로 넘어가는 과도기적 단계에 있다고 할 수 있으며,
BCD는 데이터 처리 용량이 늘어날 때마다 어김없이 화두로 등장할 수 있다.

앞장에서 우리는 1바이트가 상/하위 두 니블로 구성되는 것을 알았다. 이 니블은 0~15까지의 십진수 (또는 0~Fh)를 각각 담을 수 있다. 그러므로
1바이트라면 15 * 15 = 255 까지의 십진수 (또는 00~FFh) 까지의 영역을 담을 수 있다. 예로 3Fh를 바이트 레지스터에 담는다면, 비트맵은 0011 1111b
이다. 여기서 우리는 십진수는 0~9까지의 영역이므로 1니블 (4비트)만 가지고도 충분히 의미를 전달할 수 있음을 알 수 있다. 즉, 십진수를 표현하기
위해 1바이트를 통째로 사용하는 종래의 방식은 낭비라는 소리를 모면할 수 없다. 그런 이유로 BCD가 존재하는 것이다. 1니블이면 충분한 것을 1바이트로
표현하려면 두배의 비트가 낭비된다고 할 수 있고, BCD로 표현하면 1바이트에 두자리 숫자를 담을 수 있기 때문이다.

니블에 담는 BCD를 unpacked BCD 라고 하며, 바이트에 남는 BCD를 packed BCD라고 한다. MASM은 10바이트 데이터를 dt로 선언할 수 있다. dt로
선언된 데이터에 BCD를 담아두면 나중에 80비트 FPU 값으로 쉽게 변환될 수 있다. 그런 의미에서 FPU/SIMD 연산은 BCD와 친숙하다고 할 수 있다.

bin       hex   ASCII Dec   BCD

0000 0000  0     30h         0
0000 0001  1     31h         1
0000 0010  2     32h         2
0000 0011  3     33h         3
0000 0100  4     34h         4
0000 0101  5     35h         5
0000 0110  6     36h         6
0000 0111  7     37h         7
0000 1000  8     38h         8
0000 1001  9     39h         9
0000 1010  A     3Ah         x
0000 1011  B     3Bh         x
0000 1100  C     3Ch         x
0000 1101  D     3Dh         x
0000 1110  E     3Eh         x
0000 1111  F     3Fh         x

BCD 연산 결과는 AF (auxiliary carry flag)에 반영된다. 이 플랙에 영향을 주는 명령은 add/adc/sub/sbb/mul/imul/div/idiv 등이다. 만약, BCD
연산 결과 L.O. 니블에서 오버플로가 발생하면 AF 플랙이 토글된다. BCD 연산에 특화된 명령은 AAA/AAS/AAM/AAD/DAA/DAS 등이다. 보통 특화된 명령은
위의 기본 연산 명령 뒤를 이어서 나온다. 예로,

add     sub     mul     div
aaa     aas     aam     aad

이론은 누구라도 나열할 수 있으며, 이론만 나열하면 다소 추상적일지 모르니 기본적인 BCD 연산을 하여 십진수로 출력해보자. 여기서는 한자리
unpacked BCD 두개를 mul로 곱하기로 하자. 알다시피 곱셋/나눗셈은 연산후 자릿수가 두배 또는 1/2배 증감하기 때문에 테스트로 쓰기 좋다.


;============ BCD multiplication ==================

.model small
.stack 100h
.data
myBCD db 09h, 08h               ; unpacked BCD 1자리 십진수 2개
dump db "du"                    ; 결과 저장

.code
main proc
  mov ax, @data
  mov ds, ax
 
  lea si, dump                  ; si = dump의 시작 옵셋
 
  xor ax, ax
  xor bx, bx
 
  mov al, myBCD                 ; 첫 BCD
  mov bl, myBCD+1               ; 두번째 BCD
 
  mul bl                        ; ax = al * bl
  aam                           ; 십진 결과로 보정
 
  add ax, 3030h                 ; 아스키로 변환
  mov [si], ah                  ; 메모리에 저장
  mov [si+1], al
 
 
L52:
  mov ah, 40h                   ; write to file handle
  mov bx, 0                     ; bx = 파일 핸들 (1 = 모니터)
  mov cx, sizeof dump           ; cx = 길이
  lea dx, dump                  ; dx = 버퍼 주소
  int 21h
 
L53:
  mov ah, 0
  int 16h                       ; 키보드 인터럽트 호출하여 잠시 지연 (wait for key-press)
 
  mov ax, 4C00h
  int 21h

main endp
end main


이제 분석해보자. 눈치가 있는 독자라면 쉬프팅과 마스킹을 안해준 것을 알 것이다. aam 명령이 우리를 대신해서 쉬프팅과 마스킹을 해준 것이라 생각할
수 있다. 이 명령의 작동 유사 코드를 살펴보면 이렇다.

임시 값 <- AL 복사
AH <- 임시 값 / Ah
AL <- 임시 값 Mod Ah

즉, 이 명령 결과 AH는 A로 나눈 몫으로 리셋하고 AL은 A로 나눈 나머지로 리셋한다. 그렇다면 우리는 16진수 나눗셈을 알아야한다. 하지만 나는 솔직히
16진수 나눗셈을 할 지 모른다. 아니 알아야할 필요를 못 느낀다. 왜냐하면 16 * 16단 곱셈 테이블을 외우지 못하며, 십진수로 변환해서 생각해도
충분하기 때문이다. 어셈블리 관련 참고 서적에 보면 가로 * 세로 16단 곱셈 테이블이 나오는 경우가 많으니 심심하면 외워보라. 여튼 AH는 16진수
나눗셈을 하여 그 몫을 저장하므로 자동으로 0~9 사이의 값을 가진다. 왜냐하면 우리가 정해준 BCD 한자리 곱은 최대 값이 9 * 9 이므로 이 결과를
10으로 나눠봤자 A보다 못하기 때문에 자동으로 0~8 사이의 값으로 마스킹이 되어 나온 값을 가지게 된다. AL은 A로 나눈 나머지가 되므로 자동으로
0~9 사이의 값을 가지므로 이 또한 마스킹을 해줄 필요가 없는 것이다. AAM 명령의 설명을 인용하면 이렇다.

Adjusts the result of the multiplication of two unpacked BCD values to create a pair of unpacked
(base 10) BCD values.

두 언팩 BCD 값의 곱한 결과를 조정하여 한쌍의 십진 BCD 값을 생성한다.

그렇다면 이제 곱셈의 역연산인 나눗셈은 어떨지 바로 코드를 보자.


;============ BCD division ==================

.model small
.stack 100h
.data
myBCD db 07, 02h, 08h           ; unpacked BCD 2개 (dividend)와 1자리 BCD 1개(divisor)
dump db "du"                    ; 결과 저장

.code
main proc
  mov ax, @data
  mov ds, ax
 
  lea si, dump                  ; si = dump의 시작 옵셋
 
  xor ax, ax
  xor bx, bx
 
  mov ah, myBCD                 ; 상위 BCD
  mov al, myBCD+1               ; 하위 BCD (ax = 0702h)
  mov bl, myBCD+2               ; 나누려는 수
 
  aad                           ; ax = 0048h
  div bl
 
  add ax, 3030h                 ; 아스키로 변환
  mov [si], ah                  ; 메모리에 저장
  mov [si+1], al
 
 
L52:
  mov ah, 40h                   ; write to file handle
  mov bx, 0                     ; bx = 파일 핸들 (1 = 모니터)
  mov cx, sizeof dump           ; cx = 길이
  lea dx, dump                  ; dx = 버퍼 주소
  int 21h
 
L53:
  mov ah, 0
  int 16h                       ; 키보드 인터럽트 호출하여 잠시 지연 (wait for key-press)
 
  mov ax, 4C00h
  int 21h

main endp
end main


보다시피 위와 별반 차이가 없다. 다만 div으로 나눗셈하기 전에 aad를 먼저 써줬으며, 데이터 섹션에 각 unpacked BCD 선언문이 조금 다를 뿐이다.
AAD는 CPU 매뉴얼이 상당히 어렵게 설명되어 있으므로 debug로 트레이싱하면서 살펴보자. CPU 매뉴얼에 보면 AAD는 다음처럼 설명되어있다.






The AAD instruction sets the value in the AL register to (AL + (10 * AH)), and then clears the
AH register to 00H.

- AAD 명령은 AH에 10을 곱한 값을 AL의 원래 값에 더해 AL을 갱신하고 AH를 0으로 지운다.

이를 유사 코드로 살펴보면 이렇다.

AL 복사본 <- AL
AH 복사본 <- AH
AL <- AL 복사본 + (AH 복사본 * 10) and FFh
AH <- 0

보다시피 AL값이 먼저 갱신되고 AH가 나중에 0으로 지워진다. AL이 FFh로 마스킹이 되는데, 이 마스킹은 플랙을 세팅하는 역할외엔 하는일이 없다.
디버그로 트레이싱한 것을 보면, AAD 명령 후 AX 가 0702h에서 0048h로 변경됨을 알 수 있다 (48은 72의 16진수이다).

이왕 언급한김에 BCD 덧셈도 살펴보자.


;============ BCD addition ==================

.model small
.stack 100h
.data
myBCD db 07h, 08h               ; unpacked BCD 2개
dump db "du"                    ; 결과 저장

.code
main proc
  mov ax, @data
  mov ds, ax
 
  lea si, dump                  ; si = dump의 시작 옵셋
 
  xor ax, ax
  xor bx, bx

  mov al, myBCD
  mov bl, myBCD+1
  
  add al, bl
  aaa
 
  add ax, 3030h                 ; 아스키로 변환
  mov [si], ah                  ; 메모리에 저장
  mov [si+1], al
 
 
L52:
  mov ah, 40h                   ; write to file handle
  mov bx, 0                     ; bx = 파일 핸들 (1 = 모니터)
  mov cx, sizeof dump           ; cx = 길이
  lea dx, dump                  ; dx = 버퍼 주소
  int 21h
 
L53:
  mov ah, 0
  int 16h                       ; 키보드 인터럽트 호출하여 잠시 지연 (wait for key-press)
 
  mov ax, 4C00h
  int 21h

main endp
end main

이를 디버그로 트레이싱한 그림은 이렇다.




이 명령의 작동 유사 코드는 이렇다.

IF ((AL and 0Fh) > 9) or (AF = 1)
then
  AL <- (AL + 6)
  AH <- AH + 1
  AF <- 1
  CF <- 1
else
  AF <- 0
  CF <- 0
END IF
AL <- AL and 0Fh

AL의 원래 값에서 상위 니블을 마스킹한 값이 9보다 크거나 AF = 1로 세팅되어있으면, AL은 6을 더하고, AH는 1을 더하며, AF/CF를 세팅하고, 그렇지
않으면 (즉, AL 마스킹한 값이 8이하이거나 AF가 0이라면), 플랙만 세팅하고, 두 경우 모두 AL은 0Fh로 마스킹한다.

예로, AL이 A라고 하자. 그러면 9보다 크므로 6을 더하여 10이 되지만, 0F로 마스킹되어 0이 되고, 결국 AH만 1더해줘 AX = 0100h이 된다. aas는 따로
코드를 들 필요없이 위의 코드에서 aaa만 aas로 바꾸고 트레이싱한 그림과 연산 코드는 이렇다.




IF ((AL and 0Fh) > 9) or (AF = 1)
then
  AL <- AL - 6
  AH <- AH - 1
  AF <- 1
  CF <- 1
else
  CF <- 0
  AF <- 0
FI
AL <- AL and 0Fh

보다시피 덧셈을 뺄셈으로 바꿔준 것을 제외하고는 위의 유사 코드랑 별반 차이가 없다. 십진 바로우 (deciaml borrow)가 일어난 증거는 CF/AF 두 플랙을
살펴보면 알 수 있다. 디버그에서 트레이싱한 결과 7에서 8을 빼주므로 십진 바로우가 일어나 AF/CF가 동시에 AC/CY로 세팅되었다. 그 결과 음수인
FF를 가지므로 이 코드 그대로 하면 이상하게 출력될 것이다. 하지만 지난 시간에 음수를 어떻게 처리할 것인지는 다뤘으므로 크게 문제되진 않을 것이다.

여기서 유사 코드를 몇번 언급했는데 이유가 뭐라고 생각하는가 ? 우리가 지금까지 다룬 내용이 64비트에서는 지원되지 않을 수도 있기 때문이다. IA-64
인텔 CPU 명령어 레퍼런스를 살펴보면 다음처럼 나와있다.



빨간색 블럭으로 표시한 invalid의 의미는 이렇다. 비록 BCD 데이터가 효율적이긴하나, 이미 사람들의 배가 64비트로 커졌는데, 이제와서 허리띠를
졸라매자면 무슨 소용이 있겠는가 ? 64비트 환경에 맞는 SSE 명령을 쓰거나 아니면 직접 유사 코드를 에뮬레이션해서 쓰던지하면 되지 않은가 ?
그렇게 aaa/aad/aam/aas 4대천왕은 비운의 스토리를 간직한채 역사속으로 사라질뻔했으나 새로 등장한 SSE 라는 막강한 명령들이 그 모토를 따르고
있다. SSE는 나중에 다시 다루던지하자.

우리는 이제 dt라는 지시어로 데이터를 선언해보자.

;============ dt (define ten bytes) ==================

.model small
.stack 100h
.data
dump dt 0123456789, '$', "0123456789", '$'    ; 10 * 4 = 40 바이트 배열

.code
main proc
  mov ax, @data
  mov ds, ax
 
  lea si, dump
 
L52:
  mov ah, 40h                   ; write to file handle
  mov bx, 0                     ; bx = 파일 핸들 (1 = 모니터)
  mov cx, sizeof dump           ; cx = 길이
  lea dx, dump                  ; dx = 버퍼 주소
  int 21h
 
L53:
  mov ah, 0
  int 16h                       ; 키보드 인터럽트 호출하여 잠시 지연 (wait for key-press)
 
  mov ax, 4C00h
  int 21h

main endp
end main


이를 디버그로 덤프한 내용은 이렇다.




보다시피 40바이트 메모리가 할당되긴하나 엉뚱한 내용으로 저장됨을 알 수 있다. 즉, 우리가 원하는 것은 masm이 정해준 변수가 아니라 직접 우리가 정한
변수이다. 하물며 이렇게 변환되어 저장된다면 다른 변수는 어떻게 선언해서 쓰라는 소린가 ? 그런 의미에서 dt는 우리에겐 전혀 쓸모없다고 할 수 있다.
그렇다면 db로 10바이트 unpacked BCD를 선언해서 이를 십진수로 변환해보자.

;============ unpacked BCD to ASCII decimal ==================

.model small
.stack 100h
.data
myBCD db 00h, 01h, 02h, 03h, 04h, 05h, 06h, 07h, 08h, 09h   ; unpacked BCD 10개
dump db 10 dup (20h)                                        ; 아스키 십진 변환 결과를 저장할 버퍼

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

  lea si, myBCD                 ; 소스 버퍼 시작 주소
  lea di, dump                  ; 저장 버퍼 시작 주소
  mov cl, 10                    ; 루프 카운터 (총 변환할 갯수)
 
L1:
  mov al, [si]                  ; BCD 값을 얻어서
  add al, 30h                   ; 30h 더해 아스키로 변환후
  mov [di], al                  ; 저장
  inc si                        ; 포인터 이동
  inc di
  loop L1
 
L52:
  mov ah, 40h                   ; write to file handle
  mov bx, 0                     ; bx = 파일 핸들 (1 = 모니터)
  mov cx, sizeof dump           ; cx = 길이
  lea dx, dump                  ; dx = 버퍼 주소
  int 21h

L53:
  mov ah, 0
  int 16h                       ; 키보드 인터럽트 호출하여 잠시 지연 (wait for key-press)

  mov ax, 4C00h
  int 21h

main endp
end main


보다시피 db로 선언해주면 쓰기 쉬워짐을 알 수 있다. 그렇다면 이제 반대로 아스키 십진수를 unpacked BCD로 변환해보자.


;============ ASCII decimal to unpacked BCD ==================

.model small
.stack 100h
.data
myASCII db 30h, 31h, 32h, 33h, 34h, 35h, 36h, 37h, 38h, 39h   ; 아스키 십진수 10개
dump db 10 dup (20h)                                          ; unpacked BCD로 변환 결과를 저장할 버퍼

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

  lea si, myASCII               ; 소스 버퍼 시작 주소
  lea di, dump                  ; 저장 버퍼 시작 주소
  mov cl, 10                    ; 총 변환할 갯수
 
L1:
  mov al, [si]                  ; ASCII 값을 얻어서
  sub al, 30h                   ; 30h를 빼 BCD로 변환 후
  mov [di], al                  ; 저장
  inc si                        ; 포인터 이동
  inc di
  loop L1
 
L52:
  mov ah, 40h                   ; write to file handle
  mov bx, 0                     ; bx = 파일 핸들 (1 = 모니터)
  mov cx, sizeof dump           ; cx = 길이
  lea dx, dump                  ; dx = 버퍼 주소
  int 21h

L53:
  mov ah, 0
  int 16h                       ; 키보드 인터럽트 호출하여 잠시 지연 (wait for key-press)

  mov ax, 4C00h
  int 21h

main endp
end main


보다시피 데이터만 서로 바꿔주고 add 30h를 sub 30h로 바꾼거 외엔 별다른 특이사항이 없다. 이런 변환이 가능한 이유는 unpacked BCD가 ASCII처럼
1바이트를 쓰기 때문이다. 즉, 압축을 하지 않고 그냥 아스키 행세를 하는 BCD라고 할 수 있다. 물론 BCD는 숫자로 의미를 가지기에 이를 출력해봤자
아스키 해당 코드로 출력해줄 뿐이다. 진정한 BCD라고 하면 packed BCD인 셈이다. packed BCD는 1니블당 숫자 하나를 담으므로 데이터 저장 공간이
절반으로 줄어든다. 이는 만약 아스키 변환을 한다면 두배의 더 큰 저장 메모리가 필요하다는 뜻이다. 코드를 보자.


;============ packed BCD to ASCII decimal ==================

.model small
.stack 100h
.data
myBCD db 12h, 34h, 56h, 78h, 90h            ; packed BCD 5개
dump db sizeof myBCD * 2 dup (20h)          ; 버퍼 크기 = 소스 * 2

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

  lea si, myBCD                 ; 소스 버퍼 시작 주소
  lea di, dump                  ; 저장 버퍼 시작 주소
  mov dl, sizeof myBCD          ; 총 변환할 갯수 / 2 (두 자리씩 변환)
  mov cl, 4                     ; 쉬프트 카운터
  xor ax, ax
 
 
L1:
  mov ah, [si]                  ; BCD 값을 얻어
  mov al, ah                    ; 복사하여
 
L2:
  shr ah, cl                    ; 상위 니블은 4비트 쉬프팅
  and al, 0Fh                   ; 하위 니블은 4비트 마스킹
 
  or ax, 3030h                  ; 십진 변환
 
  mov [di], ah                  ; 저장
  mov [di+1], al
 
  inc si                        ; 소스 포인터는 1증가
  add di, 2                     ; 대상 포인터는 2증가
 
  dec dl
  cmp dl, 0
  jnz L1
 
L52:
  mov ah, 40h                   ; write to file handle
  mov bx, 0                     ; bx = 파일 핸들 (1 = 모니터)
  mov cx, sizeof dump           ; cx = 길이
  lea dx, dump                  ; dx = 버퍼 주소
  int 21h

L53:
  mov ah, 0
  int 16h                       ; 키보드 인터럽트 호출하여 잠시 지연 (wait for key-press)

  mov ax, 4C00h
  int 21h

main endp
end main


데이터 섹션의 선언 부분을 보면 다음처럼 버퍼를 두배 크기로 잡아줬다.

dump db sizeof myBCD * 2 dup (20h)          ; 버퍼 크기 = 소스 * 2

그리고 반복으로 쉬프트를 해야하는데, 쉬프트 연산이 cl을 쉬프트 카운터로 사용하므로 dl을 루프 카운터로 사용했다. 우리는 소스 데이터를 두 니블씩
연산할 것이므로 루프 카운터는 소스 데이터 크기와 같게 해줬다.

  mov dl, sizeof myBCD          ; 총 변환할 갯수 / 2 (두 자리씩 변환)
 
두 니블씩 변환하려 ah = al로 만들어줬다.

  mov ah, [si]                  ; BCD 값을 얻어
  mov al, ah                    ; 복사하여
 
그렇다면 이제 해야할 일은 상위 니블은 쉬프팅, 하위 니블은 마스킹으로 동시에 4비트씩 처리해줬다.

  shr ah, cl                    ; 상위 니블은 4비트 쉬프팅
  and al, 0Fh                   ; 하위 니블은 4비트 마스킹
 
이후 상하위 니블에 30h씩 더하여 십진수로 변환하고, 각각 저장 포인터에 저장했다.

  or ax, 3030h                  ; 십진 변환
 
  mov [di], ah                  ; 저장
  mov [di+1], al

이제 소스 포인터는 1씩 증가시켜 한 바이트씩 얻어온 반면, 저장 포인터는 2씩 증가시켜 두배씩 자라나게 해주었다.

  inc si                        ; 소스 포인터는 1증가
  add di, 2                     ; 대상 포인터는 2증가
 
그리고 카운터가 0이 될때까지 루프를 돌았다.

  dec dl
  cmp dl, 0
  jnz L1



 이제 우리는 반대로 ASCII 십진수를 입력받아 이를 packed BCD인척 변환해서 화면에 에코해보자. 에코에 사용할 인터럽트는 int 16h/ah=00h 롬
바이오스 키보드 인터럽트로 두번 입력 받고, 출력할 땐 Int10h/ah=0Eh 롬바이오스 비디오 인터럽트로 임시로 출력하자. 또한 BCD와 상관없는
문자는 변환 문자 (여기서는 20h - 아스키 공백)로 만들어 출력해주고, 십진수는 BCD로 변환해서 출력하자. 그러므로 다음과 같은 인터럽트 정보가
필요할 것이다.

KEYBOARD - GET KEYSTROKE

AH = 00h

Return:
AH = BIOS scan code AL = ASCII character

이 인터럽트를 호출하면 AL에 아스키 코드가 반환된다.

VIDEO - TELETYPE OUTPUT

AH = 0Eh
AL = character to write
BH = page number
BL = foreground color (graphics modes only)

Return:
Nothing

이 인터럽트를 호출하면 AL의 아스키 코드가 화면에 출력된다. 일단 여기까지 코딩하여 한 문자라도 에코해보자.

;============ echo with BIOS service ==================

.model small
.stack 100h
.data

.code
main proc
  mov ax, @data
  mov ds, ax
 
L1:
  mov ah, 0                     ; 키보드 인터럽트로 키를 얻어서 (ah = 스캔 코드, al = 아스키 코드)
  int 16h
  mov dl, al                    ; 변환을 위해 아스키 문자 백업
 
  mov ah, 0Eh                   ; 비디오 인터럽트로 출력 (Teletype output)
  xor bx, bx                    ; BH/BL = 0
  int 10h                       ; al을 출력

L53:
  mov ah, 0
  int 16h                       ; 키보드 인터럽트 호출하여 잠시 지연 (wait for key-press)

  mov ax, 4C00h
  int 21h

main endp
end main

보다시피 int 16h/ah =00h 인터럽트를 두번 호출하여 처음은 키를 얻어 변환을 위해 dl에 저장하고, 두번째는 프로그램을 지연시키는 역할을 한다.
이제 xlat으로 변환 필터 테이블을 만들어주자. 입력과 출력 사이에 변환을 해야하므로 이 명령은 jz 명령 뒤에 다음처럼 붙여주면 된다.

  jz L1
...
  xlat
 
그리고 20h로 변환하는 테이블을 만들고, 이 테이블 주소를 bx에 정해준다.

table db 20h
...
  lea bx, table
 
여기까지 코딩하면 이렇다.

;============ echo with BIOS service ==================

.model small
.stack 100h
.data

table db 20h
.code
main proc
  mov ax, @data
  mov ds, ax
  lea bx, table
 
L1:
  mov ah, 0                     ; 키보드 인터럽트로 키를 얻어서 (ah = 스캔 코드, al = 아스키 코드)
  int 16h
  mov dl, al                    ; 변환을 위해 아스키 문자 백업
  xlat
 
  mov ah, 0Eh                   ; 비디오 인터럽트로 출력 (Teletype output)
  xor bx, bx                    ; BH/BL = 0
  int 10h                       ; al을 출력

L53:
  mov ah, 0
  int 16h                       ; 키보드 인터럽트 호출하여 잠시 지연 (wait for key-press)

  mov ax, 4C00h
  int 21h

main endp
end main

보다시피 아직은 별다른 변환 루틴이 없으므로 xlat을 거친 모든 문자는 [bx] = 0 (30h) = 테이블의 시작 옵셋주소로 변환된다. 이제 테이블을 아스키
256 문자로 재정의 해주자. 즉 자신의 코드 테이블을 만드는 것이다. 이 원리를 이해하면 xlat으로 암호 문자로 인코딩 또한 가능하다는 것을 알게 될
것이다. 또한 EBCDIC등의 다른 문자 셋을 자신만의 문자셋으로 만들어낼 수 있지만 시간 관계상 여기서는 다루지 않겠다.

아스키 256문자를 어떻게 재정의할 것인지는 코딩하는 사람 맘이다. 나는 label 지시어 (masm 레퍼런스를 참조하라.)로 256 문자를 각각 다음처럼 정의했다.

table label byte
  db 47 dup (20h)
  db 10 dup (30h)
  db 199 dup (20h)
 
보다시피 label 지시어로 각 원소를 바이트 단위 3 그룹으로 구성했다. 0~47 (2Fh) 까지는 모두 공백 (20h)으로, 이어지는 십진수 10개는 0(30h) 으로,
나머지 아스키 코드 199개는 모두 20h로 정의했다. 이제 입력한 모든 문자는 공백 아니면 0으로 출력된다. 이때 0이 처음으로 나오는 옵셋은 십진 순서상
48번째이다. 이는 0부터 셌을때 얘기므로, 우리는 db 47을 db 48로 1을 더해줘야 제대로 변환된다. 언젠가 말썽을 피울줄 알았다. 컴퓨터는 0부터 센다는
것을 순식간 까먹으면 이런 골치아픈 버그에 직면하게 된다. 새로 만든 테이블은 이렇다.

table label byte
  db 48 dup (20h)
  db 10 dup (30h)
  db 197 dup (20h)

이제 이를 데이터 섹션에 넣어주고 여기까지 코딩하면 이렇다.

;============ echo with BIOS service ==================

.model small
.stack 100h
.data

table label byte
  db 48 dup (20h)
  db 10 dup (30h)
  db 197 dup (20h)
 
.code
main proc
  mov ax, @data
  mov ds, ax
  lea bx, table
 
L1:
  mov ah, 0                     ; 키보드 인터럽트로 키를 얻어서 (ah = 스캔 코드, al = 아스키 코드)
  int 16h
  mov dl, al                    ; 변환을 위해 아스키 문자 백업
  xlat

  mov ah, 0Eh                   ; 비디오 인터럽트로 출력 (Teletype output)
  xor bx, bx                    ; BH/BL = 0
  int 10h                       ; al을 출력

L53:
  mov ah, 0
  int 16h                       ; 키보드 인터럽트 호출하여 잠시 지연 (wait for key-press)

  mov ax, 4C00h
  int 21h

main endp
end main

이제 숫자는 모두 0으로 기타 아스키 문자는 모두 공백으로 변환된다. 위의 테이블을 서로 바꿔가면서 / (아스키 2Fh)나 9를 테스트로 차이를 비교해보라.
우리는 지금까지 숫자가 아닌 문자는 공백으로 처리했지만, 사실 공백은 무슨 일이 일어나는지 유저 입장에서 알 수가 없으므로 우리는 이를 0 (30h)로
바꿔주자. 또한 10개의 숫자를 각각 해당 아스키 문자로 다음처럼 재구성한다.

table label byte
  db 48 dup (30h)
  db "0123456789"
  db 197 dup (30h)
 
우리는 인터럽트를 호출하여 xlat으로 변환하기 전에 이미 dl에 입력 키를 백업해두었다. 즉, 변환하기 전에 먼저 백업을 해두고 두자리 packed BCD를
입력 받을 것이다. 하나는 dl에 다른 하나는 dh에 백업해두고 이제 출력 인터럽트도 두문자 이상 출력이 가능한 파일 핸들 출력으로 바꿔주자. 변환된
결과 00h packed BCD 형식으로 출력할 것이므로 출력 변수를 데이터 섹션에 dump db "00h" 처럼 선언해주고 이를 변환하자. 결과는 다음과 같다.

;============ echo with packed BCD translation ==================

.model small
.stack 100h
.data

table label byte
  db 48 dup (30h)
  db "0123456789"
  db 197 dup (30h)
 
dump db "00h"
 
.code
main proc
  mov ax, @data
  mov ds, ax
  lea bx, table
  lea si, dump
 
L1:
  mov ah, 0                     ; 키보드 인터럽트로 키를 얻어서 (ah = 스캔 코드, al = 아스키 코드)
  int 16h
  xor ah, ah                    ; 스캔 코드는 버리고

  xlat                          ; 하위 바이트는 바로 변환후 저장
  mov dl, al                    ; 변환된 아스키 문자 백업
 
  int 16h                       ; 입력 인터럽트 한번 더 호출 (ah는 무시)
 
  xlat
  mov dh, al                    ; 상위 바이트 백업

  mov [si], dl                  ; 먼저 입력 받은 하위 바이트는 앞으로
  mov [si+1], dh                ; 나중에 입력 받은 상위 바이트는 뒤로 저장

  mov ah, 40h                   ; 파일 핸들 출력
  mov bx, 1                     ; bx = 핸들 (1 = 모니터)
  mov cx, sizeof dump
  lea dx, dump
  int 21h

L53:
  mov ah, 0
  int 16h                       ; 키보드 인터럽트 호출하여 잠시 지연 (wait for key-press)

  mov ax, 4C00h
  int 21h

main endp
end main

우리는 화면에 출력하기 위해 packed BCD를 db "0123456789"로 선언해주고 아스키 십진수로 출력했다. 보다시피 xlat을 사용하면 쉬프팅 & 마스킹이라는
번거로운 절차를 건너띌 수 있다. 이제는 변환된 packed BCD를 파일로 저장해보자. 화면에 덤프하는 것은 그냥 30h를 더해서 출력해주면 되는데, 파일로
덤프할 땐 packed BCD 데이터로써 의미를 가져야하므로 특정한 포맷을 미리 정할 필요가 있다. 우리가 테이블에 정한 db "0123456789"는 출력을 위해서일
뿐이지 파일 저장을 위한 것은 아니었다. 그렇다면 새로운 룩업 테이블을 만들어야 하지 않겠는가 ? 이는 조금있다 다시 생각해보자.

파일로 저장할 땐 롬 바이오스 디스크 인터럽트 int 13h로 해줄수 있지만, 헤더/섹터/실린더 등의 개념이 필요하므로 여기서는 간단한 int 21h 도스
인터럽트로 해결하자. 도스 파일관련 인터럽트도 여러 가지가 있지만, 대략적인 방식은 "열기-처리-닫기"의 단순한 방식으로 요약할 수 있다. 열고
닫는 행위의 대상이 파일 핸들이고, 파일 내의 특정 위치를 파일 포인터라고 한다. 즉, 열어서 핸들을 얻고 포인터를 옮겨가면서 기록하고 핸들을
닫는 방식이다. 열린 핸들은 반드시 닫아줘야 메모리 누수가 일어나지 않는다고 한다. 나중에 파일 처리 관련 부분에서 다시 다루던지 하겠다.

우리의 목표는 화면에 찍히는 내용을 파일로 저장하는 것이다. 화면이 우선인가 파일이 우선인가 ? 이는 코더의 선택 사양이며 나는 화면을 우선시
하기로 했다. 즉, 화면에 글자가 찍히는 동안 외부에서 인터럽트가 발생하면 파일에 반영되지 않을 수도 있다는 뜻이다. 여기서는 외부 인터럽트를
당분간은 무시하고 진행해나가자. 그만큼 파일 처리시 오류가 일어날 확률이 많다는 뜻이고, 대부분의 OS 오류가 파일 관련 오류라고 할 수 있다.

파일 관련 int 21h중 지금 우리에게 필요한 것은 파일을 만들고 (ah = 3Ch), 파일 포인터를 이동하고 (ah = 42h), 파일에 기록하고 (40h), 열린 파일을
닫는 (3Eh) 것이다. 이 과정이 순차적으로 실행되며, int21h/ah=40h 인터럽트는 여러번 사용해봐서 익숙할 것이다. 파일 핸들 관련 인터럽트를 간략히
나열하면 이렇다.

int 21h/ah=3Ch (파일 생성, Create or Truncate File) - cx: 파일 속성,  ds:dx - ASCIIZ 파일명 (ax에 파일 핸들 리턴)
int 21h/ah=3Dh (파일 열기, Open Existing File) - al: 공유 모드, ds:dx - ASCIIZ 파일명 (ax에 파일 핸들 리턴)
int 21h/ah=3Eh (파일 닫기, Close File) - bx: 파일 핸들
int 21h/ah=3Fh (파일 읽기, Read from file or device) - bx : 파일 핸들, cx: 읽어들일 바이트 수, ds:dx : 버퍼
int 21h/ah=40h (파일 쓰기, Write tof file or device) - bx: 파일 핸들, cx: 쓰려는 바이트 수, ds:dx: 버퍼
int 21h/ah=41h (파일 삭제, Delete File) - ds:dx - ASCIIZ 파일명
int 21h/ah=42h (파일 탐색, Set Current File Position) - al: 파일 포인터 이동 원점, bx: 파일 핸들 cx:dx - 새 파일 포인터 옵셋
int 21h/ax=4300h (파일 속성 얻기, Get File Attribute) - ds:dx: ASCIIZ 파일명
int 21h/ax=4301h (파일 속성 설정, Set File Attribute) - cx: 새 파일 속성, ds:dx - ASCIIZ 파일명

더 자세한 내용은 랄프 브라운의 인터럽트 리스트를 참고하라. 이 중 우리에게 필요한 것은 3Ch/3Eh/40h/42h 뿐이다.

이제 "Hello, world!"를 화면과 파일에 동시에 덤프하는 코드를 만들자.

;============ file create with handle ==================

.model small
.stack 100h
.data

hello db "Hello, world!"
fname db "hello.txt", 00h             ; 파일명은 널 (00h) 종료
fhandle dw ?                          ; 파일 핸들은 16비트
.code
main proc
  mov ax, @data
  mov ds, ax
 
  mov ah, 40h                   ; 파일 핸들 출력
  mov bx, 1                     ; bx = 핸들 (1 = 모니터)
  mov cx, sizeof hello
  lea dx, hello
  int 21h
 
  mov ah, 3Ch                   ; create file
  mov cx, 00100000b             ; cx = 속성 (비트 5 = archive bit)
  lea dx, fname                 ; dx = 널-종료 파일명 스트링 주소
  int 21h                       ; 호출 후 ax에 파일 핸들 리턴
 
  mov fhandle, ax               ; 리턴된 파일 핸들 백업
 
  mov ah, 40h                   ; write to file or device
  mov bx, fhandle               ; bx = 파일 핸들
  mov cx, sizeof hello          ; cx = 기록할 바이트 크기
  lea dx, hello
  int 21h

  mov ah, 3Eh                   ; close file
  mov bx, fhandle
  int 21h

L53:
  mov ah, 0
  int 16h                       ; 키보드 인터럽트 호출하여 잠시 지연 (wait for key-press)

  mov ax, 4C00h
  int 21h

main endp
end main
 
보다시피 일렬로 순차처리되는 단순한 코드이다. 몇가지만 짚어보면 이렇다.

fname db "hello.txt", 00h             ; 파일명은 널 (00h) 종료
fhandle dw ?                          ; 파일 핸들은 16비트

여기서 fname이라는 변수에 00h (아스키 NULL)로 종료되는 파일명 (ASCIIZ 라고한다)을 정해주고, fhandle을 워드 단위 (도스 시절 파일 핸들은 워드
단위였다.) 변수로 정해주었다.

코드를 보면 int 21h/ah=40h를 두번 호출해주어 한번은 모니터에 한번은 파일에 출력되게끔했다. 여기서 bx에 정해주는 값을 상수 1로 하면 stdout
(즉 모니터) 핸들로 정해지며, fhandle로 정해주면 변수로 정해준 파일 핸들에 내용이 기록된다. 3Ch로 열린 핸들을 40h로 되먹여 사용하고, 사용이
끝나면 이 핸들을 3Eh의 인자로 정해주고 파일을 닫는다. 이 인터럽트는 주의를 요하는데, ds:dx에 정해주는 값이 데이터 세그먼트의 메모리 주소라는
것이다. 그러므로 어떤 상수 하나라도 출력하려면, db 상수로 데이터 섹션에 먼저 정의해줘야하며 그만큼 공간을 점유한다. 즉, "mov x, 상수" 표현은
통하지 않고, "lea x, 상수" 형식을 써야한다. 만약 mov로 해주면, 다른 프로세서의 메모리를 가져오는 것이 되며, 도스 시절 버그의 주범중 하나였다.


우리의 코드는 엄연히 모니터에 먼저 출력하고 이어서 파일로 저장하는 것이지만, 워낙에 빠르게 진행되어 모니터와 파일에 동시에 출력되는 것처럼
보일 뿐이다. 물론 파일 생성 루틴이 그리 만족스럽지 못한 속도이지만 디스크 관련 인터럽트를 여기서 다뤄야 할 필요는 없다. 이제 이를 위의 BCD
변환 루틴과 혼합하면 된다. 하지만, 그전에 우리는 먼저 어떻게 입력하고 어떻게 출력할 것인지, 즉, 입출력 인터페이스를 정의해줘야한다.

정하기 나름이지만, 나는 입력을 무한 루프로 받게하고 이 루프를 나오려면 ESC를 누르게끔 해주고싶다. 또한 출력 내용을 12h, 34h, 56h,... 식으로
각 BCD를 컴마와 공백으로 구분하여 파일로 덤프하고 싶다. 이를 어떻게 할 것인가 ? 코드를 보자.

;============ dump packed BCD data to a file ==================

.model small
.stack 100h
.data

dump db "00h"                   ; 출력 변수
fname db "mybcd.txt", 00h       ; ASCIIZ (널-종료 아스키) 파일명
fhandle dw ?                    ; 16비트 파일 핸들

table label byte
  db 27 dup (30h)               ; 0~26 - 27개 문자는 0 (30h)으로 변환
  db 1Bh                        ; 27 - ESC 키 (27 = 1Bh)는 종료 핫키로 쓸 것이므로 변환에서 제외
  db 4 dup (30h)
  db 20h                        ; 공백 (space) 키 변환 제외
  db 11 dup (30h)
  db 2Ch                        ; 콤마 (comma) 키 변환 제외
  db 3 dup (30h)                ; 다른 문자는 0으로 변환
  db "0123456789"               ; 48~57 - 숫자 10개는 (48 = 30h, 57 = 39h) 변환
  db 198 dup (30h)              ; 나머지는 모두 0으로

space db 20h
comma db 2Ch

.code
main proc
  mov ax, @data
  mov ds, ax
 
  xor ax, ax
  xor bx, bx
 
  mov ah, 3Ch                   ; create file
  mov cx, 00100000b             ; cx = 속성 (비트 5 = archive bit)
  lea dx, fname                 ; dx = 널-종료 파일명 스트링 주소
  int 21h                       ; 호출 후 ax에 파일 핸들 리턴
 
  jc Error                      ; 캐리가 발생하면 파일 생성 오류 (샘플 에러 핸들러 분기문)
  mov fhandle, ax               ; 파일 핸들 미리 저장
  lea si, dump                  ; 출력 스트링 주소
 
L1:
  lea bx, table                 ; 테이블 로드
 
  mov ah, 0                     ; 키를 얻어 (ah = 스캔 코드, al = 아스키 코드)
  int 16h
  xor ah, ah                    ; 스캔 코드는 무시
 
  cmp al, 1Bh                   ; ESC면 변환하지 않고 강제 종료
  je Last
 
  cmp al, 2Ch                   ; Comma 필터
  je L4
 
  cmp al, 20h                   ; Space 필터
  je L5 
 
  mov dl, al                    ; ESC, Space, Comma가 아닐 경우 일단 백업
 
  mov ah, 0                     ; BCD 한자리 더 얻어
  int 16h
  xor ah, ah
 
  cmp al, 1Bh                   ; 필터 재설치
  je Last
  cmp al, 2Ch
  je L4 
  cmp al, 20h
  je L5

  mov dh, al                    ; 백업 (DX = DH:DL, 먼저 입력한 BCD : 나중에 입력한 BCD)
 
L2:
  mov al, dl                    ; 먼저 입력한 BCD 로드
  xlat
  mov [si], al                  ; 번역후 저장 (상위 메모리)
 
  mov al, dh                    ; 나중에 입력한 BCD 로드
  xlat
  mov [si+1], al                ; 번역후 저장 (다음 메모리 주소)
 
L3:
  mov ah, 40h                   ; 번역된 "00h" 포맷을 모니터에 출력
  mov bx, 1                     ; 파일 핸들 (1 = 모니터)
  mov cx, sizeof dump           ; 출력 변수 크기
  lea dx, dump                  ; 출력 변수 주소
  int 21h
 
  mov ah, 40h                   ; 번역된 포맷대로 파일로 출력
  mov bx, fhandle               ; 파일 핸들 = fhandle
  mov cx, sizeof dump
  lea dx, dump
  int 21h
 
L4:
  mov ah, 40h                    ; 모니터로 Comma 출력
  xor al, al
  mov bx, 1                      ; 핸들 = 모니터
  mov cx, 1                      ; 출력 문자 갯수
  lea dx, comma
  int 21h
 
  mov ah, 40h                    ; 모니터에도 Comma 출력
  xor al, al
  mov bx, fhandle
  int 21h
 
L5:
  mov ah, 40h                     ; 바로 이어서 모니터에 Space 출력
  xor al, al
  mov bx, 1
  mov cx, 1
  lea dx, space                   ; space가 저장된 주소
  int 21h
 
  mov ah, 40h                     ; 파일로 출력
  xor al, al
  mov bx, fhandle
  int 21h
 
  jmp L1                          ; 처음부터 다시 입력 (무한 루프)

Last:
  mov ah, 3Eh                     ; ESC일 경우 파일 핸들 반납 (파일 닫기)
  mov bx, fhandle
  int 21h
 
Error:
                                  ; 여기에 에러 핸들러 코드 작성 (생략)
Exit:
  mov ah, 0
  int 16h                       ; 키보드 인터럽트로 잠시 지연 (wait for key-press)

  mov ax, 4C00h
  int 21h

main endp
end main
 
보다시피 파일 처리 관련 코드 때문에 코드가 좀 길어졌다. 또한 2문자를 입력받아 하나의 변수로 결합하려다보니 부득이하게 반복 코드가 나왔다.
이를 최적화하는 것은 개인의 몫이다. 중요하다 싶은 것만 간략히 집어보자.

우리는 BCD 두개를 하나의 변수로 저장해야 한다. dump db "00h"이 변수가 그 역할을 한다. table label byte 부분은 각 아스키 문자를 해석하려
만든 번역 테이블 (lookup table)이다. 이 테이블은 우리의 프로그램 전반에 사용할 아스키 코드를 재정의한다. 이런 테이블을 만드는 과정은
지겨우면서도 프로그램의 가장 원초적인 (?) 방법이다. 룩업 테이블을 응용하는 곳은 많으며 함수 테이블과 함수 포인터, 특정 상수 테이블,
그래픽 컬러 테이블, 암호화 코드 테이블 등을 예로 들 수 있다. 테이블에서 중간 중간 특별한 역할을 하는 코드 (우리의 예에선, 아스키 ESC,
space, comma)등은 변환을 하지 않았다. 다른 나머지는 각자 역할에 맞게끔 변환했다.

space db 20h
comma db 2Ch

이 녀석은 조금전에 설명한 int 21h/ah=40h 때문에 따로 선언해줬다. 이 인터럽트에 상수를 정해주는 것이 아니며, 변수가 선언된 주소를 lea 명령으로
정해줘야 하며 L4와 L5에 "lea dx, comma", lea dx, space가 그런 용도로 있다. 또한 L4/L5는 각각 모니터나 파일에 한번씩 교대로 출력해 주므로
이 인터럽트를 호출할 때마다 ax가 파괴되므로 다시 "mov ah,40h"로 정해주었다.

  cmp al, 1Bh
  je Last

이는 특정 문자를 필터링 한 예이다. 1Bh, 20h, 2Ch는 각각 필터링해서 변환하지 않게끔 해줬다. 대부분이 인터럽트 반복 호출이다보니 ax레지스터를
많이 사용하므로 dx에 안전하게 입력한 문자를 백업해두었다.

L2:
  mov al, dl                    ; 먼저 입력한 바이트 al로 로드
  xlat                          ; 번역 (al = BX + al)
  mov [si], dl                  ; 상위 메모리 주소에 저장
 
이는 xlat으로 변환을 수행하며, dx에 백업해둔 것에서 값을 새로 얻어왔다. 그리고 결과를 mov [si], dl ([]는 메모리의 내용 - content을 의미한다.)에
저장했으며, [si+1]은 다음 메모리 주소이다. 즉, 먼저 입력한 것을 상위 주소에 저장하고 나중에 입력한 것을 하위 메모리 주소에 저장하여 결국
00h가 packed BCD 포맷으로 된다. 이 프로그램은 숫자에 대해서는 잘 작동하는데 문자를 입력하면 어째 변환이 잘 되지 않아 버그를 고쳤다. 
하지만 정작 중요한 것은 packed BCD이므로 문자 변환은 별로 관심도 가지 않으며, 숫자만 잘 변환하면 된다. 이 버그를 각자 고쳐보던지하라. 







이는 xlat으로 변환을 수행하며, dx에 백업해둔 것에서 값을 새로 얻어왔다. 그리고 결과를 mov [si], dl ([]는 메모리의 내용 - content을 의미한다.)에
저장했으며, [si+1]은 다음 메모리 주소이다. 즉, 먼저 입력한 것을 상위 주소에 저장하고 나중에 입력한 것을 하위 메모리 주소에 저장하여 결국 00h가
packed BCD 포맷으로 된다. 이 프로그램은 명령 프롬프트로 실행하면 문제가 없는데, 더블클릭으로 실행시키면 변환이 잘 안되었다. 나는 왜 그런지
이유를 모르겠다. 하지만, 하지만 정작 중요한 것은 packed BCD이므로 근소한 버그는 별로 관심도 가지 않으며, 숫자만 잘 변환하면 된다. 이 버그를
각자 고쳐보던지하라. 참고로 comma/space를 둔 것은 테이블 정의 연습을 위한 용도이지 데이터에 반영되길 바라지는 않는다.

이제 이 프로그램으로 저장한 파일 (mybcd.txt)에서 로드해서 콤마로 구분된 packed BCD 값 (CSV, comma separated value)을 값으로 로드하여 연산해보자.
우리가 작성한 프로그램은 "00h," 포맷을 가진다. 예로, 12h, 34h, 56h, 78h, 90h, 00h, 물론 마지막을 마침표로 찍어준다든지 EOF (end of file) 기호를
붙여준다든지 하는 것은 옵션이다.

이제 이 값을 로드해서 연산에 이용하려면 어떻게 해야할까 ? 숫자만 걸러낼까 ? 아니면 "h, "를 무시할까 ? 이장 주제에 맞게 전자를 선택하는게 나을
듯 싶다. 즉 파일을 탐색해서 숫자를 찾으면 8비트 레지스터 두개를 이 값으로 채워주면 된다. 그러면 0은 ? 0은 무시해야할까 인정해야할까 ? 예에서
90h를 보면 답이 나온다. 즉, 무시할 수 없다.

그럼 어디에 채울까 ? dh에 앞자리를 dl에 뒷자리를 채우면 어떨까 ? 나는 그러기로 했다. 그럼 룩업 테이블은 어찌할까 ? 우리가 저장한 CSV 파일은
이미 BCD로 변환된 형태이므로 룩업 테이블을 또 작성할 필요는 없다. 마지막으로 comma/space/ESC는 어찌할까 ? 이 또한 이미 저장되었으므로 우리는
무시해도 된다. 이제 즐거운 코딩시간이다. 먼저 파일에서 2바이트만 메모리로 읽어들이는 간단한 뼈대 코드를 만들자.

;============ read packed BCD data from a file ==================

.model small
.stack 100h
.data

dump db "00h"                   ; 출력 변수
fname db "mybcd.txt", 00h       ; ASCIIZ (널-종료 아스키) 파일명
fhandle dw ?                    ; 16비트 파일 핸들
error db "File open failure, check your file!"

.code
main proc
  mov ax, @data
  mov ds, ax
 
  xor ax, ax
  xor bx, bx
 
  mov ax, 3D02h                 ; ah = open existing file, al = 02 (-rw)
  lea dx, fname                 ; 열려는 파일 ASCIIZ 주소
  int 21h
  jc E1
  mov fhandle, ax               ; 열린 파일 핸들 저장 (나중에 반납)
 
L1:
  mov ah, 3Fh                   ; read file
  mov bx, fhandle               ; bx = 핸들
  mov cx, 2                     ; cx = 읽을 바이트 수 (2)
  lea dx, dump
  int 21h
 
Last:
  mov ah, 3Eh                   ; 파일 핸들 반납 (파일 닫기)
  mov bx, fhandle
  int 21h
  jmp Exit
 
E1:
  mov ah, 40h                   ; 샘플 에러 핸들러
  mov bx, 1
  mov cx, sizeof error
  lea dx, error
  int 21h

Exit:
  mov ah, 0
  int 16h                       ; 키보드 인터럽트로 잠시 지연 (wait for key-press)

  mov ax, 4C00h
  int 21h

main endp
end main



보다시피 인터럽트 호출외엔 별로 눈에 띄는 것이 없다. 현재 우리가 읽어들인 바이트는 dump db "00h"에 저장된다. 파일 내용을 메모리로 읽어들이는
아주 흔한 코드이다. 이제 읽혀진 데이터로 무얼할까 ? 첫 2바이트 외에 다른 바이트를 모두 메모리로 읽으려면 어떻게 해야할까 ? 우리는 두 BCD를
읽어 단순히 두 값을 0으로 소거하는 연산을 수행하기로 하자. 그리고 두번째 문제에 대한 답은 버퍼를 크게 잡아주면 되지만 여기서는 생략한다.

아무래도 0으로 초기화하는 것이 나름 쓸모있는 (?) 연산인거 같아서 그러기로 했다. 변환된 0을 다시 dump에 저장해서 (초기값이 00h)이므로 굳이
생략해도 별 상관없지만, 그래도 정석(?)인것 같아서 저장하기로 했다. 변환된 dump를 화면에 출력하고 이를 그대로 파일에 덮어씌어보자. 즉, 패치를
수행해보자. 이 시점에서 더 많은 기능을 넣어야할 필요는 없다. 이 간단한 것만 수행하는 코드를 작성하자.


;============ read packed BCD data from a file & patch ==================

.model small
.stack 100h
.data

dump db "00h, "                 ; 출력 변수 (레코드 포맷 스트링)
fname db "mybcd.txt", 00h       ; ASCIIZ (널-종료 아스키) 파일명
fhandle dw ?                    ; 16비트 파일 핸들
error db "File open failure, check your file!"

.code
main proc
  mov ax, @data
  mov ds, ax
 
  xor ax, ax
  xor bx, bx
 
  mov ax, 3D02h                 ; ah = open existing file, al = 02 (-rw)
  lea dx, fname                 ; 열려는 ASCIIZ 파일명 주소
  int 21h
  jc E1
  mov fhandle, ax               ; 열린 파일 핸들 저장 (나중에 반납)
 
L1:
  mov ah, 3Fh                   ; read file
  mov bx, fhandle               ; bx = 핸들
  mov cx, 5                     ; cx = 읽을 바이트 수 (5)
  lea dx, dump                  ; dx = 버퍼 주소
  int 21h                       ; 읽혀진 바이트 수(5)만큼 파일 포인터를 이동한다 (ax = 읽은값, 0 = 더는 못 읽음)
 
  cmp ax, 0                     ; 더는 읽을 것이 없으면 종료
  je Last

  mov ah, 42h                   ; LSEEK (read file이 전진시킨 파일 포인터를 0으로 돌린다)
  mov al, 01h                   ; al = 원래 위치 (00 = start, 01 = current, 02 = end)
  mov bx, fhandle               ; bx = 파일 핸들
  mov cx, 0                     ; 파일 옵셋 (상위 = 0)
  mov dx, 0                     ; 파일 옵셋 (하위 = 0)
  int 21h                       ; 이동시킨 파일 포인터 옵셋을 ax에 리턴한다
 
  sub ax, 5                     ; 파일 포인터 옵셋 5칸 후퇴 매직 코드
  sbb dx, 0
  mov cx, dx
  mov dx, ax
  mov ah, 42h
  mov al, 0
  int 21h

L2:
  mov dh, [dump]                ; 2 바이트 로드
  mov dl, [dump+1]
 
  and dx, 3030h                 ; 마스킹해서 저장
 
  mov [dump], dh
  mov [dump+1], dl

L3:
  mov ah, 40h                   ; 화면에 덤프
  mov bx, 1
  mov cx, sizeof dump
  lea dx, dump
  int 21h
 
L4:
  mov ah, 40h                   ; 파일에 덤프 (CX에 정해준 크기만큼 파일 포인터를 이동한다)
  mov bx, fhandle
  mov cx, sizeof dump
  lea dx, dump
  int 21h
 
  jmp L1

Last:
  mov ah, 3Eh                   ; 파일 핸들 반납 (파일 닫기)
  mov bx, fhandle
  int 21h
  jmp Exit
 
E1:
  mov ah, 40h                   ; 샘플 에러 핸들러
  mov bx, 1
  mov cx, sizeof error
  lea dx, error
  int 21h

Exit:
  mov ah, 0
  int 16h                       ; 키보드 인터럽트로 잠시 지연 (wait for key-press)

  mov ax, 4C00h
  int 21h

main endp
end main


이론과는 다르게 또 코드가 길어졌는가 ? 분석해보자.

dump db "00h, "                 ; 출력 변수 (레코드 포맷 스트링)

출력 변수를 5자리 레코드 단위로 재정의했다. 5자리씩 블럭이 되므로 2~3개 문자만 따로 읽어봤자 계산만 복잡해지기 때문이다.

  mov ax, 3D02h                 ; ah = open existing file, al = 02 (-rw)
  
파일을 -rw모드 (읽기 또는 쓰기 모드)로 열었다.

  mov cx, 5                     ; cx = 읽을 바이트 수 (5)
 
L1은 파일을 읽되 cx에 바이트를 정해주었다. 즉 한 레코드 단위로 읽어들이게 해준 것이다. 이 인터럽트는 데이터를 읽어들이는 순간 자동으로
파일 포인터를 그 바이트만큼 이동하는 인터럽트이다. int21h/ah=40h도 마찬가지다. 그러므로 5바이트 레코드가 다음처럼 4개 있다고 하자.

rec1, rec2, rec3, rec4

3Fh 인터럽트로 rec1을 읽고 이를 40h 인터럽트로 수정하여 기록하면 rec2에 기록된다. 즉, 12h, 00h, 34h, 00h. 이 방식대로 읽고 쓴다면 우리의
목표와는 너무도 다른 형태가 되어버린다. 그러므로 이를 교정해야 하며 파일 포인터 (파일내 가상 커서)를 이동하는 int 21h/ah=42h가 이를 해준다.
이동량은 CX:DX에 상위:하위로 정해준다 (각각 16비트씩이므로 이론상 32비트 값이 되지만, 운영체제가 이를 제한하여 4GB는 커녕 그 절반이 2GB까지
밖에 액세스하지 못한다. 만약 이를 늘리려면 랄프 브라운의 인터럽트를 참조하라. 이 인터럽트는 CX:DX (상위:하위 파일 내 옵셋)에 signed 값을
정해줘야 한다. 즉, 음수를 정해주고 파일내 거꾸로 돌아갈 수 없다. 파일내 <- 백스페이스 키가 안 통한다는 소리다. 그래서 필요한 것이 마술
코드 (magic code)이다.

  sub ax, 5                     ; 파일 포인터 옵셋 5칸 후퇴 매직 코드
  sbb dx, 0
  mov cx, dx
  mov dx, ax
  mov ah, 42h
  mov al, 0
  int 21h
 
이 녀석이 바로 그 마술 코드이며 나 또한 뭔지 모르고 필요할 때 참조하는 코드이니 궁금하면 직접 연구해보라. 그럼 어떤 조건에 파일 탐색을
끝낼 것인가 ?

  cmp ax, 0                     ; 더는 읽을 것이 없으면 종료
  je Last
 
물론 다른 방법도 있겠지만, 가장 단순한 방법은 int21h/ah=3Fh의 반환 값이 0인지를 검사하는 것이다. 0이면 읽을 레코드가 없다는 뜻이다.

마스킹 코드는 이미 다 다뤘으므로 다시 언급할 필요는 없는 것 같다. 메모리 읽기 - 30h로 마스킹 - 메모리 저장.

화면과 파일에 동시에 덤프하는 것도 이전에 다뤘다. 다만, 파일에 덤프할 때 또한 파일 포인터를 데이터만큼 이동한다는 것을 주목할 필요가 있다.
이는 파읽 읽고 쓰는 두 인터럽트가 우리가 따로 정해주지 않아도 스스로 레코드 크기 * 2번씩 이동한다는 소리다. 그러므로 읽고 쓰는 그 중간에
브레이크를 한번 걸어야 한 레코드씩 이동하게 된다. 참고로 L1에서 분기 처리를 하지 않으면, 무한 루프가 되어 계속 되쓴다. 파일 포인터 이동
루틴에 음수를 넣어주면 바이러스 못지 않은 강력한 더미 레코드 생성기가 될 수 있다.

보다시피 팩된 BCD라고해서 별로 다를 건 없다. 이를 잘 활용하면 엄청난 (?) 데이터를 저장할 수 있음을 알았을 것이다. 이외에도 음수가 들어가는
경우나 중간 중간 특수 문자가 많이 들어간 경우, BCD로 특정 산술 연산을 취할 경우, 파일에 저장된 BCD 데이터를 로드할 경우 등을 고려해 볼 수
있으나, 이미 내용이 충분히 길어졌고, 이 주제에만 머물러 있을 수 없으므로 여기서는 다루지 않겠다. 각자 도전해보라.

댓글 없음:

댓글 쓰기

블로그 보관함