CHKDSK. Скільки в цьому слові! ;-) (Для тих, хто ще застав відповідну епоху). Скільки надій на неї покладали, скільки розчарувань, чи навпаки -- несподіваних порятунків. Програма перевірки та виправлення помилок на дисках.
В DOS все починалося з FAT, тому поки говоритимемо саме про нього. Що таке FAT? Файлова система, спосіб організації файлів на диску. Детальніше див. File Allocation Table на wiki та FAT details на OSDev. А коротко --- все просто, структура диску наступна:
В DOS все починалося з FAT, тому поки говоритимемо саме про нього. Що таке FAT? Файлова система, спосіб організації файлів на диску. Детальніше див. File Allocation Table на wiki та FAT details на OSDev. А коротко --- все просто, структура диску наступна:
- Нульовий сектор диску (фізичної дискети, або логічного диску на HDD) --- boot-сектор. Він, окрім завантажувача операційної системи, містить інформацію про файлову систему, так званий BPB (BIOS Parameter Block).
- Згідно даних у BPB, або, у старіших версіях, просто в першому секторі, можна знайти початок FAT --- File Allocation Table, таблиці, яка дала ім'я і самій файловій системі. Вона містить записи для кожного кластера --- чи він порожній, зарезервованим ОС, пошкодженим, належить файлу. Якщо належить файлу, у записі зберігається номер наступного кластера в файлі, або код останнього кластера файлу. Розмір цього запису -- 12/16/32 біти і визначав, про яку FAT мова: FAT-12, FAT-16, FAT-32 (остання, насправді, використовує тільки 28 біт, але кожен запис 32-бітний). Кластер -- група апаратних секторів. Один сектор --- 512 байт (деякі дуже старі, епохи до IBM-PC, дискети, мали 128 байт, сучасні HDD потроху переходять на 4Кб сектори, однак все рівно емулюють 512-байтові, для зворотної сумісності). Кількість секторів теж можна взяти з BPB.
- Після FAT (яка присутня, зазвичай, у двох копіях, але бувають варіації, про які має писати у BPB), йде каталог. Каталог складається із 32-байтових записів, які містять інформацію про ім'я файлу, його розмір, дату та час модифікації/створення, атрибути, і --- номер першого кластера з даними. (Довгі імена файлів, довші за 8.3, зберігаються хитріше, див. посилання вище.). Таким чином, щоб знайти дані файлу, слід взяти з його запису в каталозі перший кластер, з запису FAT, що відповідає цьому кластеру, взяти номер наступного (або переконатися, що він зразу й останній), і так, поки не зустрінемо останній. Що цікаво, структура цього запису якраз співпадає із форматом FCB, тому ранні версії DOS всю інформацію про файл зберігали прямо в FCB, не виділяючи якісь внутрішні структури даних.
- Перший байт FAT --- media ID, використовувався ранніми системами для ідентифікації файлової системи.
DOS 1.00 не мав BPB, "зате" мав чітке уявлення про структуру дискети --- бут-сектор, дві копії FAT, розміром 1 сектор кожна, після них --- каталог на 4 сектора, розмір кластера --- 1 сектор.
Однак, з поправкою на ці, природні тоді, очікування щодо дискети, її CHKDSK виявився доволі просунутою програмою. Наприклад, він покладався на DOS, не роблячи власних припущень про розмір FAT, сектора чи їх кількість у кластері. Судячи з його розміру, я теж зразу подумав, що він всього лиш виводить вільну пам'ять, кількість файлів та місце на диску, але виявилося --- дааалеко не тільки. Як не смішно, так думав багато хто, про що свідчить стаття 1985 року, "CHKDSK, a Command That's Misunderstood".
Офіційна документація, DOS 1.00 -- First Edition Revised (January 1982), сторінки 3-19 -- 3-20, стверджує:
Крім того, в додатку пише про наступні можливі повідомлення:
- Allocation error for file filename -- CHKDSK. An invalid sector number was found in the
file allocation table. The file was truncated at the end of the last valid sector. - XXXXXXXXXX bytes disk space freed -- CHKDSK. Diskette space marked as allocated was not allocated. Therefore, the space was freed and made available.
- Directory error-file: filename -- CHKDSK. No valid sectors were allocated to the file. The filename is removed from the directory.
- Diskette not initialized -- CHKDSK. During its analysis of the diskette, CHKDSK could not recognize the directory or file allocation table. The diskette should be formatted again before further use (it may be possible first to copy files to another diskette in order to preserve as much data as possible).
- File size error for file filename -- CHKDSK. The file size shown in the directory is
different from the actual size allocated for the file. The size in the directory is adjusted, up or down, to show the actual size (rounded to a 512-'byte boundary). - Files cross-linked: filename and filename --- CHKDSK. The same data blockis allocated to both files. No corrective action is taken automatically, so you must correct the problem. For example, you can: Make copies of both files (use COpy command). Delete the original files (use ERASE command). Review the files for validity and edit as necessary.
- Invalid parameter --- CHKDSK, DISKCOMP, DISKCOPY, FORMAT, and SYS. The parameter entered for these commands was not a drive specifier. Be sure to enter a valid drive specifier, followed by a colon.
Подивимося на його організацію детальніше.
Розмір --- 1395 байт. Всього!
Скачати код, разом із лістингом, згенерованим IDA, скомпільованим файлом та, для порівняння, оригінальним CHKDSK.COM, можна тут. Скомпільований код, з точністю до опкодів-синонімів, тотожній оригінальному.
Як і інші ранні системні програми DOS, спочатку пересуває стек нижче, трішки вище власного коду, щоб вище нього розташувати свої дані. До менеджменту пам'яті система, фактично, ще не доросла.
Наступним кроком перевіряє, чи програмі передано коректний диск, якщо ні -- "Invalid drive specification" і вийти. Потім перевіряє, чи крім імені диску не передано ще щось. Якщо так --- "Invalid parameter" і вийти. Далі зберігає поточний диск, щоб відновити потім. (Як чесно зізнається офіційна документація, якщо програма перерветься передчасно, його не відновить.) Якщо він не є цільовим, робить цільовий диск поточним. Якщо не вдалося --- те ж повідомлення "Invalid drive specification".
За допомогою функції, умовно мною названої GetCurDrvInfo, цільовий диск ще раз вибирається (навіщо? хоча, ця функція потім використовується для переініціалізації і перечитування FAT після змін, може щоб "двічі не ходити"), читається інформація про нього, включаючи адресу байта media ID, кількість кластерів, кількість секторів у кластері, кількість байтів у секторі. Потім поточним знову робиться диск, який був поточним на момент запуску CHKDSK.
Зразу над новим стеком зануляється область розміром "(кількість кластерів)*2" --- по два байти на кластер. Змінні, що зберігають початкові значення загальної кількості кластерів у файлах (змінна, умовно, TotalClustersInFiles), кількість файлів (FoundFilesNumber), кількість пошкоджених файлів (FoundDamadgedFiles), встановлюються рівними нулю. Для кожного знайденого файла створюватиметься FCB, пам'ять для них розташовується зразу після області для опису секторів, описаної вище. Туди ж встановлюється DTA.
Наступним кроком, за допомогою FCB, збереженого у коді програми, шукаються будь-які файли. Якщо щось знайдено, переходиться до перевірки файлів, інакше зразу до всіх решта перевірок.
Якщо файл знайдено, виклик "FIND FIRST MATCHING FILE USING FCB", AH=11h/INT 21h, створює для нього в DTA не відкритий FCB того ж типу, що і вихідний FCB. Тоді, поки ще залишилися файли, DTA пересувається на розмір розширеного FCB , 2Ch байт, та здійснюється новий пошук, за допомогою "FIND NEXT MATCHING FILE USING FCB", AH=12h/INT 21h. По завершенню цієї процедури, ми маємо в пам'яті FCB для всіх файлів на диску (благо, DOS 1.00 не очікував побачити їх більше, ніж 64 штуки за раз). Повна їх кількість зберігається у змінній, умовно названій FoundFilesNumber.
Адреса цієї області знаходиться в регістрі BP. Після завершення пошуку вона зсувається на 22h. Звідки саме це значення? Нагадаємо, що розширений FCB -- це 7 байт розширення + 1 байт коду диску + копія запису з каталогу FAT. Точніше, частина полів залишку FCB після коду диску співпадають із відповідними полями запису каталогу. Решта вважаються зарезервованими системою. Однак, якщо глянути в код DOS 1.00 (кинув оком самостійно) чи DOS 1.10 (дизасембльований Don Jindra), видно що "FIND FIRST" просто копіює туди знайдений запис з каталогу. 22h - 7h - 1h = 1Ah. А за таким зміщенням в записі знаходиться початковий сектор файлу. Такої ж думки наша жертва, CHKDSK (див. код і коментарі нижче). Опис формату недокументованих полів FCB для DOS 1.00 від Ральфа Брауна стверджує інше, і, очевидно, є помилковим. Таким чином, після цієї процедури BP вказує на запис із номером першого кластеру файлу, що розглядається. Файл аналізується, потім до BP додається 2Ch --- переходиться до FCB наступного знайденого файлу, збільшується кількість опрацьованих файлів, якщо вона менша від визначеної раніше повної, переходиться до наступного.
Розглянемо аналіз одного файлу детальніше. Спочатку перевіряється, чи не є номер першого кластера більшим, ніж кількість кластерів + 1. (Чому саме так? Адже, якщо номер рівний кількості кластерів+1 --- він помилковий, але програма цього не побачить... Чи я щось впускаю?).
Якщо більший, в його FCB зануляються поля першого кластера файлу, файл за допомогою цього FCB видаляється, (адресу згаданого FCB код хитро так знаходить, замість BP-22h використати...). Після цього знову викликається GetCurDrvInfo (для переініціалізації диску?), відновлюється зіпсутий нею DS, CX, AX, перевіряється, чи не було помилки під час видалення. Якщо була --- виходить із дещо загадковим повідомленням "Diskette not initialized". Інакше збільшується кількість знайдених пошкоджених файлів, виводиться "Directory error-file :" з іменем пошкодженого файлу, (зверніть увагу на функцію PrintFileName -- така мила :), провалюється до коду, котрий підраховує зайняті кластери (хитрим чином йому передається нуль -- це ж перший кластер файлу зразу був помилковим) та зайняте місце з FCB (теж нуль) і т. д. Цей код розглянемо детальніше на прикладі нормальних, не пошкоджених, файлів.
Якщо ж номер першого кластера не є більшим за дозволений, в таблицю кластерів, виділену над стеком, поміщаємо порядковий номер файлу (в нашій табличці FCB), якому цей кластер належить, а те що там лежало, перевіряємо на рівність нулю. (Насправді, цьому довгому реченню відповідає одна команда, xchg di, [si+77Eh]). Якщо там не нуль --- кластер вже зайнятий. Тоді друкуємо "Files cross-linked:" та ім'я цього і попереднього файлів, які обидва посилаються на цей кластер. Після цього збільшується кількість зайнятих кластерів на ту кількість, яку було відскановано у файлі до того, як зустрівся "cross-link", та переходиться до наступного файлу. Якщо кластер був вільний, збільшуємо на 1 кількість зайнятих кластерів та переходимо до наступного кластеру файлу. (Для цього використовується підпрограма GetNextFileCluster, яка (справедливо) вважає, що FAT знаходиться за адресою ES:BX. Справді, в ранніх версіях DOS, після media ID, йшла копія FAT, потім цей недокументований бардак припинили). Якщо номер наступного кластера нуль --- повідомити "Allocation error for file" з іменем файлу, перед тим вказавши, що цей кластер останній (встановити його запис у FAT --- 0FFFh). Кількість зайнятих кластерів та зайнятого місця, відповідно, коректується. Кількість файлів із помилкою, здається, ні... Якщо номер наступного кластера не нуль, перевіряємо чи він не є більшим або рівним від 0FF8h. Якщо так --- це останній кластер файлу. Якщо не останній --- перевіряємо знову, чи він не завеликий, не більший за "максимальна кількість кластерів + 1". Якщо більший --- обробляємо відповідно, (див. вище опис для невірного першого кластера, тільки тепер додаємо до загального рахунку кластери файлу, оброблені до того, як зустрівся некоректний номер), якщо ж ні, аналізуємо наступний кластер.
Якщо успішно зустріли останній кластер, переходимо до своєрідного "підбиття підсумків". Воно вже згадувалося вище --- на нього ж потрапляємо, зустрівши файл із невірною кількістю кластерів. Додаємо кількість кластерів у файлі до повної кількості зайнятих, а їх розмір -- до розміру зайнятого простору. Далі, із FCB береться розмір файлу, записаний в каталог, переводиться у кількість зайнятих кластерів (відповідний код цікавий сам по собі -- як релікт деяких асемблерних стилів програмування). Якщо цей розмір співпадає із кількістю зайнятих кластерів, визначеною безпосередньо із обходу FAT, переходимо до наступного файлу. Якщо ж ні --- встановлюємо розмір, рівним кількості кластерів помноженій на розмір кластера. (В таких умовах точніше визначити розмір неможливо.) Зрозуміло, що, швидше за все, останній кластер міститиме трішки сміття, але цей розмір буде явно коректнішим, ніж той, що був у (пошкодженому) каталозі. Що цікаво, щоб зберегти цю зміну, адреса FCB цього разу береться оптимально, BP-22h, а не хитрується, як у видаленні некоректного файлу. Для збереження зміни розміру, новий розмір встановлюється в FCB, файл відкривається, записується в нього 0 байт, файл закривається, диск переініціалізовується викликом GetCurDrvInfo. Якщо відкрити файл не вдалося, повідомляється все те ж "Diskette not initialized" і виконання завершується. По завершенню коректування розміру, друкується "File size error for file" та імя файлу.
По завершенню аналізу файлів перевіряється, чи в FAT немає загублених кластерів --- таких, що не є порожніми, але на них не посилається жоден файл. Для цього використовується заповнена раніше табличка відповідності кластерів файлам. Якщо сектор належить файлу, пропускаємо. Якщо ні, дивимося що для нього записано у FAT. Якщо нуль --- не зайнятий, пропускаємо. Якщо не нуль --- він помічений зайнятим, але не належить жодному файлу. Помічаємо його як порожній, збільшуємо кількість звільнених кластерів, переходимо до наступного. Можливість відновлювати ланцюжки загублених кластерів явно з'явилася пізніше... Якщо звільнено щось, пишемо про це: " bytes disk space freed".
Що цікаво, FAT модифікується в пам'яті. Як він потрапляє на диск я так сходу не скажу (а лізти в "дизасембль" DOS --- лінь), можливо за це відповідають власне виклики "SELECT DEFAULT DRIVE", AH=0Eh/INT 21h, але швидше --- виклик "DISK RESET" (AH=0Dh/INT 21h) нижче.
Після завершення перевірок, поточним встановлюється той диск, що був таким на момент запису, друкується кількість файлів мінус кількість пошкоджених файлів, з підписом "disk files", потім кількість байт на диску (кількість кластерів, помножена на розмір кластера) і " bytes total disk space", кількість вільних байт та " bytes remain available", розмір пам'яті (береться з PSP!) + " bytes total memory", кількість вільних байт пам'яті, яка визначається як розмір з PSP мінус адреса початку CHKDSK в пам'яті та " bytes free".
Після цього диск "ресетиться" --- зберігається обробник помилок, INT 24h, замінюється на свій, про котрий нижче, робить "DISK RESET" (AH=0Dh/INT 21h), ймовірно записуючи при цьому модифікований FAT на диск, після чого відновлює обробник INT 24h.
"Наш" обробник INT 24h, "CRITICAL ERROR HANDLER", виконує хитрий аналіз (див. також відповідний запис в Interrupts Ральфа Брауна) --- якщо не встановлено код помилки в DI, якщо код помилки в AH=11b, якщо помилка стосується диску, який ми перевіряли, а FAT на ньому змінювався, то помилка ігнорується. Що природно --- сенсу реагувати на виправлені нами помилки, або результати цього виправлення, немає. Якщо ж помилка стосувалася чогось іншого --- викликаємо старий обробник INT 24h.
Виконавши все це, програма завершується.
УВАГА! Код відносно великий, дизасемблювання та написаний вище аналіз робилися шматками протягом дуже тривалого часу -- протягом 3-4 місяців (так як на такі дурні розваги його часто шкода :), тому вище можуть бути помилки, неточності, неуважності і т. д. Знайдете -- пишіть, буду вдячний!
Якщо в процесі роботи, наприклад під час видалення пошкодженого файлу, відбувається якась помилка, повідомляється, що дискета неформатована, "Diskette not initialized" і виконання завершується. При тому, не зважаючи на дуже дивне повідомлення, офіційна інструкція дає розумну рекомендацію --- перш ніж переформатувати, спробувати вручну врятувати вцілілі файли.
Взагалі, офіційна інструкція документує всі можливі повідомлення і не описує жодного зайвого.
Команда не може приймати жодних аргументів, хоча, якщо такі буде передано після імені диску, через пробіл, просто їх проігнорує.
На разі:
; 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 fasm and commented by Indrekis, indrekis2.blogspot.com ; ; Input MD5 : 9813BAE4E76B904552F58A3A826A0EF1 ; File Name : PC-DOS 1.00\CHKDSK.COM ; Format : MS-DOS COM-file ; Base Address: 0h Range: 100h-673h Loaded length: 573h FieldSize_forFoundFCBs = 2Ch OldInt24Hadnler = 673h FoundFilesNumber = 677h FoundFCBsAreaAddr = 679h TotalClustersInFiles = 67Bh TotalClustersPlus1 = 67Dh FoundDamadgedFiles = 67Fh InitialDefDriver = 681h stackTop = 782h ;.8086 use16 org 100h include "my_fcb_2b.inc" start: mov sp, stackTop ; Shrink stack, move it closer to our code, to ; use memory above for data. or al, al ; COM-format executables begin running with the following register values: ; AL = 00h if first FCB has valid drive letter, FFh if not ; AH = 00h if second FCB has valid drive letter, FFh if not ; CS,DS,ES,SS = PSP segment SP = offset of last word available in first 64K segment ; ( http://www.ctyme.com/intr/rb-2939.htm ) jnz short InvalidDriveSpec ; Here we know that user specified correct (existing at least) disk number cmp byte [ds:5Dh], 20h ; PSP: 5Ch-6Bh 16 bytes -- Unopened Standard FCB 1 ; So here will be any symbol, entered after [a-z][:][\] jnz short AnyArgIsInvalid ; User should give only disk, no filenames mov ah, 19h int 21h ; DOS - GET DEFAULT DISK NUMBER mov [ds:InitialDefDriver], al ; Save current default driver. ; Memory somewhere above or code mov dl, [ds:5Ch] ; Disk from PSP FCB1 or dl, dl ; Check if already current disk jz short DiskAlreadyCur mov ah, 0Eh dec dl ; disk numbering in FCB differs by one from others: ; 0 means default, 1 - A, and so on ; adjusting for INT 21h/AH=0Eh call int 21h ; DOS - SELECT DISK ; DL = new default drive number (0 = A, 1 = B, etc.) ; Return: AL = number of logical drives cmp al, dl ; Check if successfully changed current drive ja short WorkDriveIsCurrent ; If disk do not exists at all or cannot make it current: InvalidDriveSpec: mov dx, aInvalidDriveSp ; "Invalid drive specification$" ; Universal "print message and exit" code PrnMsgNExit: mov ah, 9 int 21h ; DOS - PRINT STRING ; DS:DX -> string terminated by "$" int 20h ; DOS - PROGRAM TERMINATION ; returns to DOS--identical to INT 21/AH=00h ; =========================================================================== AnyArgIsInvalid: mov dx, aInvalidParamet ; "Invalid parameter$" jmp short PrnMsgNExit ; =========================================================================== WrongMediaByte: mov ax, cs mov ds, ax mov ah, 0Eh mov dl, [ds:InitialDefDriver] int 21h ; DOS - SELECT DISK ; DL = new default drive number (0 = A, 1 = B, etc.) ; Return: AL = number of logical drives mov dx, aDisketteNotIni ; "Diskette not initialized $" jmp short PrnMsgNExit ; =========================================================================== DiskAlreadyCur: mov dl, al ; Now disk is current. Start checking. WorkDriveIsCurrent: mov [CurDriveNumber], dl inc dl mov [extFCB_1.Driver], dl ; Converted to FCB-style dec dl call GetCurDrvInfo ; Gets cur. drive info, sets as default the disk, ; which was default when CHKDSK started. ; On return AL -- sectors per cluster, DX -- total ; clusters number, CX -- bytes per sector, ; [DS:BX] --- media ID byte. Spoils DS! cmp byte [bx], 0FEh ; Check the media byte, only known by DOS 1.00 ; 0xFEh: 5.25-inch Single sided, 40 tracks per side, 8 sectors per track (160 KB) jnz short WrongMediaByte ; Error message would be "diskette not initialized"... push ds ; Save DS, pointing to media byte push es ; Restore DS to out segment pop ds mov [TotalClusters], dx ; total number of clusters mov [BytesPerSector], cx ; bytes per sector mov ah, 0 ; AL -- sectors per cluster mov [SectorsPerCluster], ax mov cx, dx mov di, stackTop xor ax, ax rep stosw ; Zeroing area, above of stack, equal in size to ; clusters*2 -- in fact, two bytes per cluster mov cx, ax ; cx=0 mov [ds:TotalClustersInFiles], ax mov [ds:FoundFilesNumber], ax mov [ds:FoundDamadgedFiles], ax mov bp, di ; BP poins to byte after the cleared area mov [ds:FoundFCBsAreaAddr], bp mov dx, di ; Set DTA above cleared area too mov ah, 1Ah int 21h ; DOS - SET DISK TRANSFER AREA ADDRESS ; DS:DX -> disk transfer buffer mov ah, 11h mov dx, extFCB_1 int 21h ; DOS - SEARCH FIRST USING FCB ; DS:DX -> FCB inc al ; Returns 0 or FF --> +1 will give 1 or 0, 0 means error pop es ; ES -- segment of media-byte (and, in fact, FAT copy in memory) jnz short FileFound jmp Finished_with_files ; =========================================================================== FileFound: inc cx ; count found file add di, FieldSize_forFoundFCBs ; Next file DTA -- 2Ch bytes above previous ; (which, in turn, is above stack top) mov dx, di mov ah, 1Ah int 21h ; DOS - SET DISK TRANSFER AREA ADDRESS ; DS:DX -> disk transfer buffer mov ah, 12h mov dx, extFCB_1 int 21h ; DOS - SEARCH NEXT USING FCB ; DS:DX -> FCB ; Return: AL = status inc al ; Returns 0 or FF --> +1 will give 1 or 0, 0 means error jnz short FileFound mov [ds:FoundFilesNumber], cx ; cx contains found files number mov cx, 1 mov dx, [TotalClusters] inc dx mov [ds:TotalClustersPlus1], dx add bp, 22h ; According to http://www.ctyme.com/intr/rb-2574.htm#Table1347 ; for DOS 1.0 (in non-extended FCB) ; 16h WORD location in directory (if high byte = FFh, low byte is device ID) ; 18h WORD number of first cluster in file ; 1Ah WORD current absolute cluster number on disk ; 1Ch WORD current relative cluster number within file ; (0 = first cluster of file, 1 = second cluster, etc.) ; 1Eh BYTE dirty flag (00h = not dirty) ; 1Fh BYTE unused ; ; 22h-7 = 1Bh --> looks strange... ; ; Anyway, both check belove (if that value, offset 22h in ; extended FCB, is less than max. clusters number), and ; article http://www.borrett.id.au/computing/art-1985-11-01.htm ; which states, that error: ; "Directory error-file: filename." ; Indicates that a directory entry was discovered ; for a nonexistent file. In other words, there ; were no allocation units assigned to this file. ; CHKDSK removes the phantom file from the directory. ; hint, that it is check for the first cluster of file. ; In DOS 1.1 FindFirst/FindNext just copies content of catalog entry ; to FCB, after the disk code. (See DOS 1.1 disasembly by Don Jindra) ; And in catalog we have 2-byte field at offset 1Ah with first cluster of file ; 1Ah + 1h (driver code) + 7h (extended FCB) = 22h ; ; So, it looks like Ralf Brown's Interrupt list ; has an error in FCB format description ( http://www.ctyme.com/intr/rb-2574.htm ), ; when states different layout for DOS 1.00. AnalNextFile: xor ax, ax mov si, [bp+0] ; First cluster in file cmp si, [ds:TotalClustersPlus1] mov di, aDirectoryError ; WTF? ja short TooLargeFirstClusterNumber ; Jump short if above (CF=0 and ZF=0) or si, si jz short ScanClusters ; If SI=0, will go to "jmp isLastCLuster" ; In following loop ; SI -- analized in current iteration cluster ; CX -- file number (in found files list) SeeNextCluster: mov di, cx ; cx -- number of file, we are working on (from the table FoundFCBsArea) shl si, 1 ; si=2*si ; Before it SI contains current (analyzed) cluster number xchg di, [si+77Eh] ; Stack top is: stackTop = 782h ; Lowest file cluster number is 2, so si*2>=4, ; 77Eh+4=782h ; So we are referencing memory above stack top, ; which is used as FAT map shr si, 1 ; si=si/2 or di, di jnz short FilesAreCrosslinked ; Before we encountered it, cluster should be free. inc ax ; AX contains number of analized clusters in file? call GetNextFileCluster ; Find next file cluster (FAT copy -- ES:BX) ; Input: SI -- current cluster number ; Output: DI -- next cluster number ; jz short ClusterNumberWrong cmp di, 0FF8h ; 0xFF8-0xFFF marker of last cluster in file for latter DOS ; ScanClusters: jnb short isLastCluster ; "Jump short if not below (CF=0)" cmp di, [ds:TotalClustersPlus1] ja short ClusterNumberWrong mov si, di ; Current cluster number to SI jmp short SeeNextCluster ; =========================================================================== TooLargeFirstClusterNumber: push ax ; ax should be 0? mov [bp+2], ax ; BP points to FCB+22h, presumably, first file cluster mov [bp+4], ax ; According to http://www.ctyme.com/intr/rb-2574.htm ; For DOS 1.1: ; 1Bh WORD current absolute cluster number on disk ; 1Dh WORD current relative cluster number within file ; Here we have DOS 1.0, for which that link has different structure. ; But, as also for first cluster, looks resonable, that table is wrong... ; ; mov ax, cx ; CX - found file number in FCB table? mov dx, FieldSize_forFoundFCBs dec ax mul dx ; DX:AX := AX * r/m16 (DX) mov dx, ax ; Result expected to be less than 64K add dx, [ds:FoundFCBsAreaAddr] ; DX -- pointer to FCB of bad file to delete. mov ah, 13h int 21h ; DOS - DELETE FILE via FCB ; DS:DX -> FCB with filename field filled with ; template for deletion ('?' wildcard allowed, but not '*') ; Return: AL = 00h file found, FFh file not found push ds push cx push ax ; Do DOS 1.0 change current disk on delete, so we need ; to set it again? call GetCurDrvInfo ; Gets cur. drive info, sets as default the disk, ; which was default when CHKDSK started. ; On return AL -- sectors per cluster, DX -- total ; clusters number, CX -- bytes per sector, ; [DS:BX] --- media ID byte. Spoils DS! pop ax pop cx pop ds or al, al ; Cheking error from FCB delete operation jz short FileWithBadClusterNumberDeleted jmp WrongMediaByte ; If file deletion failed, print "diskette not initialized"... ; Strange error for that case :-) ; =========================================================================== FileWithBadClusterNumberDeleted: inc word [ds:FoundDamadgedFiles] mov dx, aDirectoryError ; "Directory error-file : $" jmp short PrintErrorWithFilename ; =========================================================================== FilesAreCrosslinked: mov dx, aFilesCrossLink ; "Files cross-linked: \r\n $" push ax mov ah, 9 int 21h ; DOS - PRINT STRING ; DS:DX -> string terminated by "$" mov ax, di call PrintFileName ; ; Prints file name and extension for entry number CX from ; ; FoundFCBsArea. AX -- number of file FCB in that area. mov dx, aAnd ; " and $" mov ah, 9 int 21h ; DOS - PRINT STRING ; DS:DX -> string terminated by "$" mov ax, cx call PrintFileName ; ; Prints file name and extension for entry number CX from ; ; FoundFCBsArea. AX -- number of file FCB in that area. mov dx, aCRLF mov ah, 9 int 21h ; DOS - PRINT STRING ; DS:DX -> string terminated by "$" pop ax add [ds:TotalClustersInFiles], ax jmp DoNextFile ; =========================================================================== ClusterNumberWrong: mov dx, 0FFFh ; Set current cluster as last call SetFATEntry ; Set cluster entry in FAT (image in memory) to low 12 bits of DX ; DX -- new FAT entry value ; SI -- cluster we are working on mov dx, aAllocationErro ; "Allocation error for file $" push ax PrintErrorWithFilename: mov ah, 9 int 21h ; DOS - PRINT STRING ; DS:DX -> string terminated by "$" mov ax, cx call PrintFileName ; ; Prints file name and extension for entry number CX from ; ; FoundFCBsArea. AX -- number of file FCB in that area. mov dx, aCRLF mov ah, 9 int 21h ; DOS - PRINT STRING ; DS:DX -> string terminated by "$" pop ax ; If came here from FileWithBadClusterNumberDeleted, ; AX = 0 isLastCluster: add [ds:TotalClustersInFiles], ax mov si, ax mov ax, [bp+2] ; BP=FCB+22h, see comments above ; Because FCB+7+1 -- copy of dir entry ; BP+2/BP+4=FCB+24h=FCB+7h+1h+1Ch -- filesize. mov dx, [bp+4] push cx ; DX:AX -- file size from catalog mov cx, [SectorsPerCluster] dec cx add ax, cx adc dx, 0 inc cx ContDiv01: shr cx, 1 ; CX=CX/2 ; low-order bit of r/m; jb short FinishedDiv01 shr dx, 1 rcr ax, 1 ; Effectively div DX:AX by sectors per cluster jmp short ContDiv01 ; =========================================================================== FinishedDiv01: div [BytesPerSector] ; DX:AX div by Arg ; AX -- Quotient ; DX -- Remainder or dx, dx jz short FileSizeISMultipleOfCluster inc ax ; Rounding to next cluster FileSizeISMultipleOfCluster: cmp ax, si ; AX -- clusters in file from catalog ; SI -- clusters in file from FAT scanning ; jz short RestoreCXAndDoNextFile ; If not Z, have size mismatch ; Correcting file size to size, obtained from FAT scanning xor di, di mov cx, [SectorsPerCluster] call MulDI_SI_by_CX mov [bp+6], si ; BP points to extFCB+22, so bp+6 and bp+8 -- ; 4-byte random access record number. mov [bp+8], di mov dx, bp sub dx, 22h ; BP was FCB+22h :-) mov ah, 0Fh int 21h ; DOS - OPEN DISK FILE ; DS:DX -> FCB ; Return: AL = 00h file found, FFh file not found or al, al jz short TryEmptyWriteToFile jmp WrongMediaByte ; If failed to open file ; =========================================================================== TryEmptyWriteToFile: mov ax, [BytesPerSector] mov [bp-0Dh], ax ; BP points to extFCB+22, so BP-0Dh=0Eh+7h = ; = logical record size mov cx, 0 ; Empty write mov ah, 28h int 21h ; DOS - RANDOM BLOCK WRITE ; DS:DX -> FCB ; CX = number of records to be written ; if zero, truncate file to current random file position mov ah, 10h int 21h ; DOS - CLOSE DISK FILE ; DS:DX -> FCB ; Return: AL = 00h directory update successful ; FFh file not found in directory push ds call GetCurDrvInfo ; Gets cur. drive info, sets as default the disk, ; which was default when CHKDSK started. ; On return AL -- sectors per cluster, DX -- total ; clusters number, CX -- bytes per sector, ; [DS:BX] --- media ID byte. Spoils DS! pop ds mov dx, aFileSizeErrorF ; "File size error for file $" mov ah, 9 int 21h ; DOS - PRINT STRING ; DS:DX -> string terminated by "$" pop ax push ax call PrintFileName ; ; Prints file name and extension for entry number CX from ; ; FoundFCBsArea. AX -- number of file FCB in that area. mov dx, aCRLF mov ah, 9 int 21h ; DOS - PRINT STRING ; DS:DX -> string terminated by "$" RestoreCXAndDoNextFile: pop cx ; Restore file number in CX DoNextFile: add bp, 2Ch ; Next FCB in FoundFCBsArea inc cx ; Next file cmp cx, [ds:FoundFilesNumber] ja short Finished_with_files ; Was it last file? jmp AnalNextFile ; =========================================================================== Finished_with_files: xor ax, ax mov bp, stackTop ; table for FAT mov si, 2 ; First data cluster mov cx, [TotalClusters] OverAllClusters: test word [bp+0], 0FFFFh ; Test if at least single bit is set ; The field is equal to file number for ; which this cluster belongs. jnz short GoToNextCluster ; Test if cluster, not referenced by files, points to other cluster. ; If it does -- it is the lost cluster, mark as empty. call GetNextFileCluster ; Find next file cluster (FAT copy -- ES:BX) ; Input: SI -- current cluster number ; Output: DI -- next cluster number ; jz short GoToNextCluster xor dx, dx ; Mark cluster as free call SetFATEntry ; Set cluster entry in FAT (image in memory) to low 12 bits of DX ; DX -- new FAT entry value ; SI -- cluster we are working on inc ax ; Freed cluster number GoToNextCluster: inc bp inc bp ; Next record in our FAT table inc si ; Next cluster loop OverAllClusters or ax, ax jz short NoLostClusters mov bx, aBytesDiskSpace ; " bytes disk space freed\r\n\r\n$" call PrintClustersAsBytes ; AX -- clusters NoLostClusters: mov dl, [cs:InitialDefDriver] mov ah, 0Eh int 21h ; DOS - SELECT DISK ; DL = new default drive number (0 = A, 1 = B, etc.) ; Return: AL = number of logical drives mov si, [ds:FoundFilesNumber] sub si, [ds:FoundDamadgedFiles] xor di, di mov bx, aDiskFiles ; " disk files\r\n$" call PrintNumbers ; Prints DI:SI in decimal form, then string, pointed by BX mov ax, [TotalClusters] mov bx, aBytesTotalDisk ; " bytes total disk space\r\n$" call PrintClustersAsBytes ; AX -- clusters mov ax, [TotalClusters] sub ax, [ds:TotalClustersInFiles] mov bx, aBytesRemainAva ; " bytes remain available\r\n\r\n$" call PrintClustersAsBytes ; AX -- clusters mov ax, [ds:2] ; PSP: Memory size in paragraphs mov dx, 10h mul dx ; Mul AX by 10h to obtain memory in bytes mov si, ax mov di, dx mov bx, aBytesTotalMemo ; " bytes total memory\r\n$" call PrintNumbers ; Prints DI:SI in decimal form, then string, pointed by BX mov ax, [ds:2] ; PSP: Memory size in paragraphs mov dx, cs ; Current code segment is at the beginning of free memory, ; when chkdsk is not running. sub ax, dx mov dx, 10h mul dx ; Mul AX by 10h to obtain memory in bytes mov si, ax mov di, dx mov bx, aBytesFree ; " bytes free" call PrintNumbers ; Prints DI:SI in decimal form, then string, pointed by BX call SafeResetDisk mov dl, [cs:InitialDefDriver] mov ah, 0Eh ; Return to initial default driver int 21h ; DOS - SELECT DISK ; DL = new default drive number (0 = A, 1 = B, etc.) ; Return: AL = number of logical drives int 20h ; DOS - PROGRAM TERMINATION ;start endp ; returns to DOS--identical to INT 21/AH=00h ; ============== subroutine ================================================= SafeResetDisk: mov ax, ds push ds mov es, ax xor ax, ax mov ds, ax ; DS -- 0 mov si, 90h ; 90h/4=24h -- 24h interrupt vector ; CRITICAL ERROR HANDLER mov di, OldInt24Hadnler mov cx, 2 cld rep movsw mov al, 24h mov ah, 25h mov dx, int24Hadnler pop ds int 21h ; DOS - SET INTERRUPT VECTOR ; AL = interrupt number ; DS:DX = new vector to be used for specified interrupt mov ah, 0Dh int 21h ; DOS - DISK RESET push es xor ax, ax mov es, ax cld mov di, 90h mov si, OldInt24Hadnler mov cx, 2 rep movsw pop es retn ;SafeResetDisk endp ; =========================================================================== ; http://www.ctyme.com/intr/rb-4112.htm ; ; Values critical error handler is called with:. ; AH = type and processing flags (see #02544). ; AL = drive number if AH bit 7 clear. ; BP:SI -> device driver header (see #01646 at INT 21/AH=52h) ; (BP:[SI+4] bit 15 set if character device). ; DI low byte contains error code if AH bit 7 set (see #02545). ; ; Bitfields for critical error type and processing flags: ; ; Bit(s) Description (Table 02544) ; 7 class. ; =0 disk I/O error. ; =1 -- if block device, bad FAT image in memory ; -- if char device, error code in DI ; 6 unused ; 5 Ignore allowed (DOS 3.0+) ; 4 Retry allowed (DOS 3.0+) ; 3 Fail allowed (DOS 3.0+) ; 2-1 disk area of error. ; 00 = DOS area 01 = FAT. ; 10 = root dir 11 = data area ; 0 set if write, clear if read int24Hadnler: sti and di, 0FFh ; If have err code in low part of DI jnz short CallDefInt24Handler cmp ah, 11b ; If bit0 - write, and bit1 (low of area of error) is 1 ; (FAT/data area) jnz short CallDefInt24Handler cmp [cs:CurDriveNumber], al ; If for current drive jnz short CallDefInt24Handler cmp [cs:HaveChangedFAT], 1 ; If haven't changed FAT jz short CallDefInt24Handler xor al, al ; Return: "ignore error and continue processing request" iret ; =========================================================================== CallDefInt24Handler: db 2Eh ; CS segment override prefix mov si, ax ; push dx mov dl, [ds:InitialDefDriver] mov ah, 0Eh int 21h ; DOS - SELECT DISK ; DL = new default drive number (0 = A, 1 = B, etc.) ; Return: AL = number of logical drives pop dx mov ax, si jmp dword [cs:OldInt24Hadnler] ; ============== subroutine ================================================= ; Gets cur. drive info, sets as default the disk, ; which was default when CHKDSK started. ; On return AL -- sectors per cluster, DX -- total ; clusters number, CX -- bytes per sector, ; [DS:BX] --- media ID byte. Spoils DS! GetCurDrvInfo: mov dl, [cs:CurDriveNumber] mov ah, 0Eh ; Not sure, if it is neccessery to init it once more... int 21h ; DOS - SELECT DISK ; DL = new default drive number (0 = A, 1 = B, etc.) ; Return: AL = number of logical drives mov ah, 1Bh int 21h ; DOS - GET ALLOCATION TABLE INFORMATION FOR DEFAULT DRIVE ; (http://www.ctyme.com/intr/rb-2590.htm) ; AL = sectors per cluster (allocation unit) ; CX = bytes per sector ; DX = total number of clusters ; DS:BX -> media ID byte push dx push ax mov dl, [cs:InitialDefDriver] mov ah, 0Eh int 21h ; DOS - SELECT DISK ; DL = new default drive number (0 = A, 1 = B, etc.) ; Return: AL = number of logical drives pop ax pop dx retn ;GetCurDrvInfo endp ; ============== subroutine ================================================= ; Find next file cluster (FAT copy -- ES:BX) ; Input: SI -- current cluster number ; Output: DI -- next cluster number ; GetNextFileCluster: mov di, si shr di, 1 add di, si ; Now DI contains word number, which contains next cluster number ; (relatively to FAT beginning). ; If SI is even [BX+DI] will contain 12 bits in higher part of word mov di, [es:bx+di] test si, 1 ; Test if SI is odd jz short BitsAreInPlace shr di, 1 ; DI contains 12 bit of next cluster in higher bits. ; Shift them low shr di, 1 shr di, 1 shr di, 1 BitsAreInPlace: and di, 0FFFh ; mask to use 12 bits only retn ;GetNextFileCluster endp ; ============== subroutine ================================================= ; Set cluster entry in FAT (image in memory) to low 12 bits of DX ; DX -- new FAT entry value ; SI -- cluster we are working on SetFATEntry: mov [HaveChangedFAT], 1 push si mov di, si shr si, 1 add si, bx add si, di shr di, 1 ; Bit0 to CF mov di, [es:si] ; SI points to word with 12 bit of cluster number, ; se also previous function -- alog. is the same. jnb short WasEven02 ; Jump short if not below (CF=0) ; Means if DI (SI on func start) is odd shl dx, 1 ; Move DX bits to correct -- upper 12, position shl dx, 1 shl dx, 1 shl dx, 1 and di, word 0Fh ; Zero out upper 12 bits jmp short WasOdd02 ; =========================================================================== WasEven02: and di, 0F000h ; Zero lower 12 bits ; DI is prepeared, so to not change 4 bits of word, ; that corresponds to other cluster, not the one under consideration WasOdd02: or di, dx mov [es:si], di pop si locret_451: retn ;SetFATEntry endp ; ============== subroutine ================================================= MulDI_SI_by_CX: ; MulDI_SI_by_CX+8 j ... shr cx, 1 jb short locret_451 shl si, 1 rcl di, 1 jmp short MulDI_SI_by_CX ;MulDI_SI_by_CX endp ; ============== subroutine ================================================= ; AX -- clusters PrintClustersAsBytes: mul [BytesPerSector] mov cx, [SectorsPerCluster] mov si, ax mov di, dx ; Falls to PrintNumbers call MulDI_SI_by_CX ; After the call DI:SI contains bytes for given clusters ;PrintClustersAsBytes endp ; ============== subroutine ================================================= ; Prints DI:SI in decimal form, then string, pointed by BX PrintNumbers: push bx xor ax, ax mov bx, ax mov bp, ax mov cx, 20h ; I'm too lazy for detailed analysis of this code... loc_475: shl si, 1 rcl di, 1 xchg ax, bp call sub_4C8 xchg ax, bp xchg ax, bx call sub_4C8 xchg ax, bx adc al, 0 loop loc_475 mov cx, 1B10h call PrintUpperDecDigitFromAH ; Print decimal digit in upper 4 bits of DL ; Received AX is returned in DX mov ax, bx call PrintUpperDecDigitFromAH ; Print decimal digit in upper 4 bits of DL ; Received AX is returned in DX mov ax, bp call PrintUpperDecDigitFromAH ; Print decimal digit in upper 4 bits of DL ; Received AX is returned in DX pop dx mov ah, 9 int 21h ; DOS - PRINT STRING ; DS:DX -> string terminated by "$" retn ;PrintNumbers endp ; ============== subroutine ================================================= ; Print decimal digit in upper 4 bits of DL ; Received AX is returned in DX PrintUpperDecDigitFromAH: push ax mov dl, ah call PrintUpperDecDigitFromDL ; Print decimal digit in upper 4 bits of DL pop dx ;PrintUpperDecDigitFromAH endp ; ============== subroutine ================================================= ; Print decimal digit in upper 4 bits of DL PrintUpperDecDigitFromDL: mov dh, dl shr dl, 1 shr dl, 1 shr dl, 1 shr dl, 1 call sub_4B3 ; Print digit in dl, if zero -- fill, according to CX. ; The first call receives CX=1B10, which leads to printing zero ; May be, other CX can be used for some padding?... mov dl, dh ;PrintUpperDecDigitFromDL endp ; ============== subroutine ================================================= ; Print digit in dl, if zero -- fill, according to CX. ; The first call receives CX=1B10, which leads to printing zero ; May be, other CX can be used for some padding?... sub_4B3: and dl, 0Fh jz short loc_4BA mov cl, 0 loc_4BA: dec ch and cl, ch or dl, 30h sub dl, cl mov ah, 2 int 21h ; DOS - DISPLAY OUTPUT ; DL = character to send to standard output retn ;sub_4B3 endp ; ============== subroutine ================================================= sub_4C8: adc al, al daa xchg al, ah adc al, al daa xchg al, ah retn ;sub_4C8 endp ; ============== subroutine ================================================= ; ; Prints file name and extension for entry number CX from ; ; FoundFCBsArea. AX -- number of file FCB in that area. PrintFileName: push cx ; cx -- scanned file entry number dec ax mov cx, 2Ch ; Size of one file area in found FCB's mul cx mov si, ax add si, [ds:FoundFCBsAreaAddr] ; FCB field for file number from CX? add si, 8 ; FileName field in FCB mov cx, 8 mov ah, 2 call PrnNextChar ; Print file name mov dl, 20h int 21h ; DOS - mov cl, 3 call PrnNextChar ; Print file extension pop cx ; Restore CX retn ;PrintFileName endp ; ============== subroutine ================================================= ; At function start: ; AH=02 -- WRITE CHARACTER TO STANDARD OUTPUT ; CX -- chars to print PrnNextChar: lodsb ; al <= char from [si] mov dl, al int 21h ; AH=02h, WRITE CHARACTER TO STANDARD OUTPUT ; DL - char to print loop PrnNextChar ; At function start: ; AH=02 -- WRITE CHARACTER TO STANDARD OUTPUT ; CX -- chars to print retn ;PrnNextChar endp ; =========================================================================== extFCB_1 ExtFCB_t 0,"????????", "???", 0,6h,0 ;extFCB_1 db 0FFh ; db 5 dup(0) ; Reserved1 ; db 6 ; Attribute ; db 0 ; Driver ; db 8 dup('?') ; FileName ; db 3 dup('?') ; FileExt ; db 2 dup(0) ; CurBlock ; db 2 dup(0) ; RecordSize ; db 4 dup(0) ; FileSize ; db 2 dup(0) ; FileDate ; db 2 dup(0) ; FileTime ; db 8 dup(0) ; Reserved2 ; db 0 ; CurRecord ; db 4 dup(0) ; DirectRecord aAnd db ' and $' aInvalidDriveSp db 'Invalid drive specification$' aInvalidParamet db 'Invalid parameter$' aAllocationErro db 'Allocation error for file $' aDirectoryError db 'Directory error-file : $' aFilesCrossLink db 'Files cross-linked: ',0Dh,0Ah db ' $' aFileSizeErrorF db 'File size error for file $' aDisketteNotIni db 'Diskette not initialized $' aBytesDiskSpace db ' bytes disk space freed',0Dh,0Ah db 0Dh,0Ah,'$' aDiskFiles db ' disk files',0Dh,0Ah,'$' aBytesTotalDisk db ' bytes total disk space',0Dh,0Ah,'$' aBytesRemainAva db ' bytes remain available',0Dh,0Ah db 0Dh,0Ah,'$' aBytesTotalMemo db ' bytes total memory',0Dh,0Ah,'$' aBytesFree db ' bytes free' aCRLF db 0Dh db 0Ah db 24h ; $ TotalClusters dw 0 BytesPerSector dw 0 SectorsPerCluster dw 0 HaveChangedFAT db 0 CurDriveNumber db 0
Розмір --- 1395 байт. Всього!
Скачати код, разом із лістингом, згенерованим IDA, скомпільованим файлом та, для порівняння, оригінальним CHKDSK.COM, можна тут. Скомпільований код, з точністю до опкодів-синонімів, тотожній оригінальному.
Алгоритм роботи
Як і інші ранні системні програми DOS, спочатку пересуває стек нижче, трішки вище власного коду, щоб вище нього розташувати свої дані. До менеджменту пам'яті система, фактично, ще не доросла. Наступним кроком перевіряє, чи програмі передано коректний диск, якщо ні -- "Invalid drive specification" і вийти. Потім перевіряє, чи крім імені диску не передано ще щось. Якщо так --- "Invalid parameter" і вийти. Далі зберігає поточний диск, щоб відновити потім. (Як чесно зізнається офіційна документація, якщо програма перерветься передчасно, його не відновить.) Якщо він не є цільовим, робить цільовий диск поточним. Якщо не вдалося --- те ж повідомлення "Invalid drive specification".
За допомогою функції, умовно мною названої GetCurDrvInfo, цільовий диск ще раз вибирається (навіщо? хоча, ця функція потім використовується для переініціалізації і перечитування FAT після змін, може щоб "двічі не ходити"), читається інформація про нього, включаючи адресу байта media ID, кількість кластерів, кількість секторів у кластері, кількість байтів у секторі. Потім поточним знову робиться диск, який був поточним на момент запуску CHKDSK.
Зразу над новим стеком зануляється область розміром "(кількість кластерів)*2" --- по два байти на кластер. Змінні, що зберігають початкові значення загальної кількості кластерів у файлах (змінна, умовно, TotalClustersInFiles), кількість файлів (FoundFilesNumber), кількість пошкоджених файлів (FoundDamadgedFiles), встановлюються рівними нулю. Для кожного знайденого файла створюватиметься FCB, пам'ять для них розташовується зразу після області для опису секторів, описаної вище. Туди ж встановлюється DTA.
Наступним кроком, за допомогою FCB, збереженого у коді програми, шукаються будь-які файли. Якщо щось знайдено, переходиться до перевірки файлів, інакше зразу до всіх решта перевірок.
Якщо файл знайдено, виклик "FIND FIRST MATCHING FILE USING FCB", AH=11h/INT 21h, створює для нього в DTA не відкритий FCB того ж типу, що і вихідний FCB. Тоді, поки ще залишилися файли, DTA пересувається на розмір розширеного FCB , 2Ch байт, та здійснюється новий пошук, за допомогою "FIND NEXT MATCHING FILE USING FCB", AH=12h/INT 21h. По завершенню цієї процедури, ми маємо в пам'яті FCB для всіх файлів на диску (благо, DOS 1.00 не очікував побачити їх більше, ніж 64 штуки за раз). Повна їх кількість зберігається у змінній, умовно названій FoundFilesNumber.
Адреса цієї області знаходиться в регістрі BP. Після завершення пошуку вона зсувається на 22h. Звідки саме це значення? Нагадаємо, що розширений FCB -- це 7 байт розширення + 1 байт коду диску + копія запису з каталогу FAT. Точніше, частина полів залишку FCB після коду диску співпадають із відповідними полями запису каталогу. Решта вважаються зарезервованими системою. Однак, якщо глянути в код DOS 1.00 (кинув оком самостійно) чи DOS 1.10 (дизасембльований Don Jindra), видно що "FIND FIRST" просто копіює туди знайдений запис з каталогу. 22h - 7h - 1h = 1Ah. А за таким зміщенням в записі знаходиться початковий сектор файлу. Такої ж думки наша жертва, CHKDSK (див. код і коментарі нижче). Опис формату недокументованих полів FCB для DOS 1.00 від Ральфа Брауна стверджує інше, і, очевидно, є помилковим. Таким чином, після цієї процедури BP вказує на запис із номером першого кластеру файлу, що розглядається. Файл аналізується, потім до BP додається 2Ch --- переходиться до FCB наступного знайденого файлу, збільшується кількість опрацьованих файлів, якщо вона менша від визначеної раніше повної, переходиться до наступного.
Розглянемо аналіз одного файлу детальніше. Спочатку перевіряється, чи не є номер першого кластера більшим, ніж кількість кластерів + 1. (Чому саме так? Адже, якщо номер рівний кількості кластерів+1 --- він помилковий, але програма цього не побачить... Чи я щось впускаю?).
Якщо більший, в його FCB зануляються поля першого кластера файлу, файл за допомогою цього FCB видаляється, (адресу згаданого FCB код хитро так знаходить, замість BP-22h використати...). Після цього знову викликається GetCurDrvInfo (для переініціалізації диску?), відновлюється зіпсутий нею DS, CX, AX, перевіряється, чи не було помилки під час видалення. Якщо була --- виходить із дещо загадковим повідомленням "Diskette not initialized". Інакше збільшується кількість знайдених пошкоджених файлів, виводиться "Directory error-file :" з іменем пошкодженого файлу, (зверніть увагу на функцію PrintFileName -- така мила :), провалюється до коду, котрий підраховує зайняті кластери (хитрим чином йому передається нуль -- це ж перший кластер файлу зразу був помилковим) та зайняте місце з FCB (теж нуль) і т. д. Цей код розглянемо детальніше на прикладі нормальних, не пошкоджених, файлів.
Якщо ж номер першого кластера не є більшим за дозволений, в таблицю кластерів, виділену над стеком, поміщаємо порядковий номер файлу (в нашій табличці FCB), якому цей кластер належить, а те що там лежало, перевіряємо на рівність нулю. (Насправді, цьому довгому реченню відповідає одна команда, xchg di, [si+77Eh]). Якщо там не нуль --- кластер вже зайнятий. Тоді друкуємо "Files cross-linked:" та ім'я цього і попереднього файлів, які обидва посилаються на цей кластер. Після цього збільшується кількість зайнятих кластерів на ту кількість, яку було відскановано у файлі до того, як зустрівся "cross-link", та переходиться до наступного файлу. Якщо кластер був вільний, збільшуємо на 1 кількість зайнятих кластерів та переходимо до наступного кластеру файлу. (Для цього використовується підпрограма GetNextFileCluster, яка (справедливо) вважає, що FAT знаходиться за адресою ES:BX. Справді, в ранніх версіях DOS, після media ID, йшла копія FAT, потім цей недокументований бардак припинили). Якщо номер наступного кластера нуль --- повідомити "Allocation error for file" з іменем файлу, перед тим вказавши, що цей кластер останній (встановити його запис у FAT --- 0FFFh). Кількість зайнятих кластерів та зайнятого місця, відповідно, коректується. Кількість файлів із помилкою, здається, ні... Якщо номер наступного кластера не нуль, перевіряємо чи він не є більшим або рівним від 0FF8h. Якщо так --- це останній кластер файлу. Якщо не останній --- перевіряємо знову, чи він не завеликий, не більший за "максимальна кількість кластерів + 1". Якщо більший --- обробляємо відповідно, (див. вище опис для невірного першого кластера, тільки тепер додаємо до загального рахунку кластери файлу, оброблені до того, як зустрівся некоректний номер), якщо ж ні, аналізуємо наступний кластер.
Якщо успішно зустріли останній кластер, переходимо до своєрідного "підбиття підсумків". Воно вже згадувалося вище --- на нього ж потрапляємо, зустрівши файл із невірною кількістю кластерів. Додаємо кількість кластерів у файлі до повної кількості зайнятих, а їх розмір -- до розміру зайнятого простору. Далі, із FCB береться розмір файлу, записаний в каталог, переводиться у кількість зайнятих кластерів (відповідний код цікавий сам по собі -- як релікт деяких асемблерних стилів програмування). Якщо цей розмір співпадає із кількістю зайнятих кластерів, визначеною безпосередньо із обходу FAT, переходимо до наступного файлу. Якщо ж ні --- встановлюємо розмір, рівним кількості кластерів помноженій на розмір кластера. (В таких умовах точніше визначити розмір неможливо.) Зрозуміло, що, швидше за все, останній кластер міститиме трішки сміття, але цей розмір буде явно коректнішим, ніж той, що був у (пошкодженому) каталозі. Що цікаво, щоб зберегти цю зміну, адреса FCB цього разу береться оптимально, BP-22h, а не хитрується, як у видаленні некоректного файлу. Для збереження зміни розміру, новий розмір встановлюється в FCB, файл відкривається, записується в нього 0 байт, файл закривається, диск переініціалізовується викликом GetCurDrvInfo. Якщо відкрити файл не вдалося, повідомляється все те ж "Diskette not initialized" і виконання завершується. По завершенню коректування розміру, друкується "File size error for file" та імя файлу.
По завершенню аналізу файлів перевіряється, чи в FAT немає загублених кластерів --- таких, що не є порожніми, але на них не посилається жоден файл. Для цього використовується заповнена раніше табличка відповідності кластерів файлам. Якщо сектор належить файлу, пропускаємо. Якщо ні, дивимося що для нього записано у FAT. Якщо нуль --- не зайнятий, пропускаємо. Якщо не нуль --- він помічений зайнятим, але не належить жодному файлу. Помічаємо його як порожній, збільшуємо кількість звільнених кластерів, переходимо до наступного. Можливість відновлювати ланцюжки загублених кластерів явно з'явилася пізніше... Якщо звільнено щось, пишемо про це: " bytes disk space freed".
Що цікаво, FAT модифікується в пам'яті. Як він потрапляє на диск я так сходу не скажу (а лізти в "дизасембль" DOS --- лінь), можливо за це відповідають власне виклики "SELECT DEFAULT DRIVE", AH=0Eh/INT 21h, але швидше --- виклик "DISK RESET" (AH=0Dh/INT 21h) нижче.
Після завершення перевірок, поточним встановлюється той диск, що був таким на момент запису, друкується кількість файлів мінус кількість пошкоджених файлів, з підписом "disk files", потім кількість байт на диску (кількість кластерів, помножена на розмір кластера) і " bytes total disk space", кількість вільних байт та " bytes remain available", розмір пам'яті (береться з PSP!) + " bytes total memory", кількість вільних байт пам'яті, яка визначається як розмір з PSP мінус адреса початку CHKDSK в пам'яті та " bytes free".
Після цього диск "ресетиться" --- зберігається обробник помилок, INT 24h, замінюється на свій, про котрий нижче, робить "DISK RESET" (AH=0Dh/INT 21h), ймовірно записуючи при цьому модифікований FAT на диск, після чого відновлює обробник INT 24h.
"Наш" обробник INT 24h, "CRITICAL ERROR HANDLER", виконує хитрий аналіз (див. також відповідний запис в Interrupts Ральфа Брауна) --- якщо не встановлено код помилки в DI, якщо код помилки в AH=11b, якщо помилка стосується диску, який ми перевіряли, а FAT на ньому змінювався, то помилка ігнорується. Що природно --- сенсу реагувати на виправлені нами помилки, або результати цього виправлення, немає. Якщо ж помилка стосувалася чогось іншого --- викликаємо старий обробник INT 24h.
Виконавши все це, програма завершується.
УВАГА! Код відносно великий, дизасемблювання та написаний вище аналіз робилися шматками протягом дуже тривалого часу -- протягом 3-4 місяців (так як на такі дурні розваги його часто шкода :), тому вище можуть бути помилки, неточності, неуважності і т. д. Знайдете -- пишіть, буду вдячний!
"Документація"
Як бачимо, офіційна документація трохи занадто лаконічна. CHKDSK перевіряє, для кожного знайденого файлу, ланцюжки у FAT на коректність та чи в каталозі справді є правильний розмір. Кластери, що не належать жодному файлу, перевіряються на зайнятість. Якщо знайдено такі, загублені, кластери, вони помічаються як вільні. Аналогічно, виправляються некоректності ланцюжків файлів --- якщо перший номер кластера невірний, файл видаляється, якщо ж невірні наступні, файл урізається до того шматка, який коректний. Лише якщо два файли вказують на один і той же кластер --- про це повідомляється, але автоматичного виправлення не робиться. Що можна зробити -- описано в процитованому вище додатку.Якщо в процесі роботи, наприклад під час видалення пошкодженого файлу, відбувається якась помилка, повідомляється, що дискета неформатована, "Diskette not initialized" і виконання завершується. При тому, не зважаючи на дуже дивне повідомлення, офіційна інструкція дає розумну рекомендацію --- перш ніж переформатувати, спробувати вручну врятувати вцілілі файли.
Взагалі, офіційна інструкція документує всі можливі повідомлення і не описує жодного зайвого.
Команда не може приймати жодних аргументів, хоча, якщо такі буде передано після імені диску, через пробіл, просто їх проігнорує.
Про код
Ну, як і для інших проаналізованих програм DOS 1.00, код є жахливим спагеті. Місцями виглядає більш вилизаним, ніж код SYS.COM, але це може бути ілюзією... Місцями трапляються якісь дивні ускладнення -- навіщо так довго і плутано, якщо конкретно тут можна і простіше. Є декілька цікавих трюків. Підсумовуючи, читати --- одне задоволення, підтримувати таке --- не дуже весело. Хоча, я повторююся. :-)На разі:
Дякую за увагу!
Немає коментарів:
Дописати коментар