Написавши про 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.
; 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).
Про код
Код такий же плутаний і зневажливий до структурного програмування, як і в DATE.COM.
Декілька прикладів:
- CheckForDigit використовує "чужий" retn для виходу по помилці, свій, якщо все ОК.
- PrnDecFromAL розглядалася раніше, не повторюватиму.
- CheckNumberNSeparator, яка аналізує 1-2 цифри + сепаратор (який, ':', чи, для сотих секунди -- '.', передається їй в регістрі BL), якщо вирішує, що введене є некоректни, переходить до коду, який друкує повідомлення про помилку, встановлює стек на вершину по замовчуванню (автоматично анулюючи всі виклики підпрограм) і переходить на ще одну спробу ввести. Якщо все ОК, просто повертається у місце виклику за допомогою retn.
Повторюся -- не пишіть свій код так!
Дякую за увагу!
Немає коментарів:
Дописати коментар