середу, 17 квітня 2013 р.

Аналіз TIME.COM з PC-DOS 1.00

Написавши про DATE.COM, задумався, яка частина описаних "нюансів" коду була характерна для нього, а яка для коду Microsoft взагалі. Для контролю дизасемблював ще одну дуже схожу крихітну утиліту, яка теж не пережила виходу наступної версії: TIME.COM.

Загальні твердження та посилання в попередньому пості, "Аналіз DATE.COM з PC-DOS 1.00 ", тут -- тільки про TIME.COM, який теж є звичайною COM програмою.  Нагадаю хіба, що такі програми побайтово вантажаться у пам'ять, починаючи із зміщення 100h від початку сегменту (вище неї, в цих 0FFh байт, знаходиться PSP). Коли програмі передається керування, гарантується, що:
  • CS=DS=ES=SS, всі вказують на той же сегмент,
  • SP=0FFFEh -- стек у кінці сегмента, росте вниз,
  • IP=0100h -- починаємо зразу після PSP.
  • AL = 00h, якщо перший FCB в PSP має правильну літеру диску, 0FFh, якщо ні.
  • AH -- аналогічно для другого FCB з PSP.
Код, сумісний з NASM:


; This file is generated by The Interactive Disassembler (IDA)     
;  Copyright (c) 2010 by Hex-Rays SA, <support@hex-rays.com>     
;  Licensed to: Freeware version       
;  
; Modified to compile by nasm and commented by Indrekis, indrekis2.blogspot.com
;
;  Input MD5 : 66E912F7DCD34AABEE47E0637D3B470C
;  File Name : <>\DOS1\images\IBM PC-DOS 1.0 (5.25)\files\TIME.COM
;  Format    : MS-DOS COM-file
;  Base Address: 0h Range: 100h-1FAh Loaded length: FAh

; stack_top is set to allow for variables, which are located above programm code.
; This "self-willed" memory management was widely used by early DOS commands
; By the way, there were no memory management means in DOS 1.XX 
; DATE.COM do not use any such variables, though.
stack_top EQU  286h  ; New Stack top

  CPU 8086
  use16
  org 100h

start:
  mov sp, stack_top ; 286h -- two bytes less than for DATE.COM
  mov dx, aCurrentTimeIs ; "Current time is $"
  mov ah, 9
  int 21h  ; DOS - PRINT STRING
     ; DS:DX -> string terminated by "$"
  mov ah, 2Ch
  int 21h  ; DOS - GET CURRENT TIME
     ; Return: CH = hours, CL = minutes, DH = seconds
     ; DL = hundredths of seconds
  mov al, ch  ; Print hours
  call PrnDecFromAL
  mov al, ':'         ; Print separator
  call PrnASCIIFromAL ; AL -- symbol to print
  mov al, cl  ; Print minutes
  call PrnDecFromAL
  mov al, ':'
  call PrnASCIIFromAL ; AL -- symbol to print
  mov al, dh  ; Print seconds
  call PrnDecFromAL
  mov al, '.'         ; Print decimal point
  call PrnASCIIFromAL ; AL -- symbol to print
  mov al, dl  ; Print hundredths of seconds
  call PrnDecFromAL
; By this point we have printed previous date,
; so asking for new.

enter_time_again:   
  mov dx, aEnterNewTime ; "\r\nEnter new time: $"
  mov ah, 9
  int 21h  ; DOS - PRINT STRING
     ; DS:DX -> string terminated by "$"
  mov ah, 0Ah
  mov dx, KbdBuffer
  int 21h  ; Reading from keyboard.
     ; DOS - BUFFERED KEYBOARD INPUT
     ; DS:DX -> buffer
     ; Buffer format (http://www.ctyme.com/intr/rb-2563.htm)
     ;  Size   Description   (Table 01344)
     ; 00h  BYTE  maximum characters buffer can hold
     ; 01h  BYTE  (call) number of chars from last input which may be recalled
     ; (ret) number of characters actually read, excluding CR
     ; 02h  N BYTEs  actual characters read, including the final carriage return
  mov si, KbdBuffer+2
  xor cx, cx
  xor dx, dx
  mov bl, ':'         ; Check and get hours.
  call CheckNumberNSeparator ; Check two digits and following separator (expected
     ; separator in BL).
     ; If digits are correct, saves corresponding number to AH.
     ; Orse jumps for one more attempt.
     ;
  jz short exit_prog ; Exit if just "Enter" is pressed.
  mov ch, ah  ; CH - hours
  call CheckNumberNSeparator ; Check two digits and following separator (expected
     ; separator in BL).
     ; If digits are correct, saves corresponding number to AH.
     ; Orse jumps for one more attempt.
     ;
  jz short call_set_time ; If enter is pressed instead of minutes, just set time,
     ; with minutes=0, seconds=0
  mov cl, ah  ; CL - minutes
  mov bl, '.'         ; Separator for parts of second
  call CheckNumberNSeparator ; Check two digits and following separator (expected
     ; separator in BL).
     ; If digits are correct, saves corresponding number to AH.
     ; Orse jumps for one more attempt.
     ;
  jz short call_set_time ; If enter is pressed instead of seconds, just set time,
     ; with seconds=0
  mov dh, ah  ; DH - seconds
  mov bl, 0Dh  ; "CR" serves as correct separator, if user tried to
     ; enter hundredth of second.
  call CheckNumberNSeparator ; Check two digits and following separator (expected
     ; separator in BL).
     ; If digits are correct, saves corresponding number to AH.
     ; Orse jumps for one more attempt.
     ;
  jz short call_set_time ; If enter is pressed instead of hundredth of seconds,
     ; just set time, with hundredth = 0
  mov dl, ah  ; DL -- hundredth of second

call_set_time:    
  mov ah, 2Dh
  int 21h  ; DOS - SET CURRENT TIME
     ; CH = hours, CL = minutes, DH = seconds, DL = hundredths of seconds
     ; Return: AL = 00h if no error / = FFh if bad value sent to routine
  or al, al
  jnz short InvalidTimeEntered

exit_prog:    
  int 20h  ; DOS - PROGRAM TERMINATION
     ; returns to DOS--identical to INT 21/AH=00h

; ============== almost subroutine ===========================================
InvalidTimeEntered:   
     
  mov dx, aInvalidTime-2 ; "Invalid time$" + CR/LB before it
  mov ah, 9
  int 21h  ; DOS - PRINT STRING
     ; DS:DX -> string terminated by "$"
  mov sp, stack_top
  jmp near enter_time_again ; Try once more
     ; (jmp is near, not short! -- original assembler pecularity)
; start  endp


; ============== subroutine ===========================================

; Check two digits and following separator (expected
; separator in BL).
; If digits are correct, saves corresponding number to AH.
; Orse jumps for one more attempt.
;
; Uses "or al,0FFh", where AL contains putative
; separator, to avoid spurious ZF=1.
; Can we have real symbol '\0' in buffer before
; 'CR'?


CheckNumberNSeparator:
  call CheckForDigit ; Checks for "Enter" code or char, which does not represents digit.
     ; Puts symbol in AL.
     ; If it is correct digit, advances SI to next symbol
     ; and clears CF and do not change SI.
     ; If "Enter" code found, sets ZF=1,
     ; If wrong char (not a digit) is found, sets CF=1
  jb short InvalidTimeEntered ; If met bad digit -- try once more.
  jz short BadSymbolMet ; If met "CR" -- exit, leaving ZF=1
  mov ah, al  ; Save digit to AH, check next digit.
  call CheckForDigit ; Checks for "Enter" code or char, which does not represents digit.
     ; Puts symbol in AL.
     ; If it is correct digit, advances SI to next symbol
     ; and clears CF and do not change SI.
     ; If "Enter" code found, sets ZF=1,
     ; If wrong char (not a digit) is found, sets CF=1
  jbe short DoCheckSeparator ; Checks if CF=1 or ZF=1 -- both signaling error
     ; from CheckForDigit
     ; This situation is not error here -- user can
     ; enter only one digit, not two.
  aad   ; If second digit is correct, call:
     ; aad -- "ASCII Adjust AX before Division",
     ; AL := AH * 10 + AL;
     ; AH := 0,
  mov ah, al  ; Move resulting number to AH (Two decimal digits
     ; always represent number, less than 100d, so
     ; 8-bit reg. is enough)

DoCheckSeparator:   
  cmp byte [si], 0Dh
  jz short LastSymbolAnalized ; Exit if have 'CR' instead of separator
  lodsb
  cmp al, bl  ; Compare next symbol with requested separator
  jnz short InvalidTimeEntered ; If separator is not correct

LastSymbolAnalized:   
  or al, 0FFh ; Set flags, do not change AL
     ; ZF will be 0 only if next byte in buffer
     ; was '\0'. Is it possible?

BadSymbolMet:    
     ; CheckForDigit+4 j ...
  retn
; CheckNumberNSeparator endp

; ============== subroutine ===========================================

; Checks for "Enter" code or char, which does not represents digit.
; Puts symbol in AL.
; If it is correct digit, advances SI to next symbol
; and clears CF and do not change SI.
; If "Enter" code found, sets ZF=1,
; If wrong char (not a digit) is found, sets CF=1

CheckForDigit:     
  mov al, [si]
  cmp al, 0Dh  ; Check for CR at SI position
  jz short BadSymbolMet ; Jump if ZF=1
  sub al, 30h  ; Check if char code is less then 30h -- code of '0'
  jb short BadSymbolMet ; Jump if CF=1
  cmp al, 0Ah  ; Check if 'Digit'-'0'>10 -- wrong digit met
  cmc   ; Invert CF -- for bad condition it will be reset, set
     ; for OK condition by last comand.
  jb short BadSymbolMet ; Jump if CF=1
  inc si
  retn
;CheckForDigit endp

; ============== subroutine ===========================================

PrnDecFromAL:
  aam   ; ASCII adjust AX after multiply
     ; AH := AL / 10;
     ; AL := AL MOD 10;
     ; Result:
     ;  AL -- lower digit
     ;  AH -- higher digit
  xchg al, ah  ; Printing higher digit
  or ax, 3030h ; '0'=30h, '1'='0'+1 and so on --
     ; converting digits to their ASCII codes
  call PrnASCIIFromAL ; AL -- symbol to print
  mov al, ah  ; Now prepare lower digit and fall through
     ; to char printing subroutine
;PrnDecFromAL endp   

; ============== subroutine ===========================================

; AL -- symbol to print

PrnASCIIFromAL:
  xchg ax, dx
  push ax
  mov ah, 2
  int 21h  ; DOS - DISPLAY OUTPUT
     ; DL = character to send to standard output
  pop ax
  xchg ax, dx
  retn
;PrnASCIIFromAL endp

; ============== subroutine ===========================================

CRLF_chars db  0Dh
  db  0Ah
aInvalidTime db 'Invalid time$'      ; DATA XREF: start:InvalidTimeEntered t
aCurrentTimeIs db 'Current time is $'  ; DATA XREF: start+3 o
aEnterNewTime db 0Dh,0Ah  ; DATA XREF: start:enter_time_again o
  db 'Enter new time: $'
KbdBuffer db  0Ch   ; DATA XREF: start+3A o start+3F t
     ; Format:
     ; 00h  BYTE  maximum characters buffer can hold
     ; 01h  BYTE
     ;      (call) number of chars from last input which may be recalled
     ;      (ret) number of characters actually read, excluding CR
     ; 02h  N BYTEs  actual characters read, including the final carriage return
     ; (http://www.ctyme.com/intr/rb-2563.htm)
  db    0
KbdBufferContent db    0

Цей код теж успішно рекомпілюється, і, за винятком опкодів-синонімів, (деталі див. попередній пост), тотожний вихідному. Скачати його, разом із лістингом, згенерованим IDA, скомпільованим файлом та, для порівняння, оригінальним TIME.COM, можна тут.

Алгоритм роботи

  • Програма встановлює стек на 286h, на 2 байти менше, ніж DATE.COM. Цікаво, як вони вибирали? :-)
  • Виводиться поточний час, години, хвилини, секунди і соті секунди: "11:05:33.11".  Для виводу використовуються функції, (названі мною в коді вище PrnDecFromAL і  PrnASCIIFromAL) тотожні тим, що були в DATE.COM. Можливо, використовувалася якась бібліотека... З іншого боку, CHKDSK.COM, виконуючи схожі задачі, здається, використовує зовсім інші функції.
  • Просить ввести новий час: "Enter new time".
  • Введене читає в буфер, (0Ah/int 21h), максимум 12 символів, (це визначається першим байтом структури KbdBuffer, 0Ch=12d).
  • Якщо користувач зразу натиснув "Enter", завершує роботу.
  • Якщо перших два символи задають коректне число, і наступний -- CR (користувач натиснув "Enter"), встановлює час як "<введені години>:00:00". Якщо ні, перевіряє, чи наступний символ є коректним сепаратором, ':'. Якщо ні -- повідомляє "Invalid time" і пропонує спробувати ще раз. 
  • Аналогічно до попереднього кроку, якщо два наступних символи є коректним числом і натиснуто "CR", встановлює час "<введені години>:<введені хвилини>:00". Якщо ні, перевіряє на правильний сепаратор, не зустрівши його, повідомляє про невірний час і пропонує спробувати ще раз.
  • Аналогічно із секундами, тільки тепер сепаратор (цілої і дробової частини секунд) -- '.'.
  • Отримані значення годин, хвилин, секунд і сотих секунди, ніяк не перевіряючи, передає функції встановлення часу, 2Dh/INT 21h, ("SET SYSTEM TIME"). Вважається, що всі перевірки коректності введених чисел робитиме ця функція. Якщо функція повернула помилку, повідомляє і пропонує спробувати ще, якщо завершилася успішно -- завершує програму. 

"Документація"


З аналізу коду бачимо, що:
  • Ніяких опцій команда не приймає, просто їх ігнорує.
  • Порядок введення: <години>:<хвилини>:<секунди>.<соті секунди>. Сепаратор єдиний, ':', для відділення дробової частини використовується десяткова крапка.
  • Всі числа можна вводити як однією, так і двома числами, допустимо писати як "1", так і "01".
  • Можна вводити лише частину величин, наприклад, тільки годину, всі решта тоді вважаються рівними нулю. (Нагадаю, що години відраховуються як 0-23, хвилини та секунди як 0-59).
Розмір цього щастя 250 байт (на два менше, ніж DATE.COM, можливо тому на стільки ж і стек нижчий? :)

Про код


Код такий же плутаний і зневажливий до структурного програмування, як і в DATE.COM.

Декілька прикладів:
  • CheckForDigit використовує "чужий" retn для виходу по помилці, свій, якщо все ОК.
  • PrnDecFromAL розглядалася раніше, не повторюватиму.
  • CheckNumberNSeparator, яка аналізує 1-2 цифри + сепаратор (який, ':', чи, для сотих секунди -- '.', передається їй в регістрі BL), якщо вирішує, що введене є некоректни, переходить до коду, який друкує повідомлення про помилку, встановлює стек на вершину по замовчуванню (автоматично анулюючи всі виклики підпрограм)  і переходить на ще одну спробу ввести. Якщо все ОК, просто повертається у місце виклику за допомогою retn.

Повторюся -- не пишіть свій код так!


Дякую за увагу!

Немає коментарів:

Дописати коментар