Глава 5. Управление файлами
Файл - важнейшая образующая практически любой программы. С появлением скоростных дисков больших объемов значение файлов сильно возросло. Использование API-функций управления файлами может сделать вашу программу более эффективной и производительной. Большинство программ данной главы являются консольными, потому что консольная программа как никакая другая подходит для демонстрации файловой обработки.
I
32-битная FAT. Характеристики файлов
Давая описание характеристикам файлов, я буду основываться на характеристиках, которыми манипулируют функции API. О типах и структуре файловых систем речь пойдет далее.
Атрибут файла. Размер - DWORD.
FILE_ATTRIBUTE_READONLY equ 1h
Атрибут - "только чтение". Приложения могут лишь читать данный файл.
Соответственно, попытка открыть файл для записи вызовет ошибку.
FILE_ATTRIBUTE_HIDDEN equ 2h
Атрибут - "скрытый файл". "Невиден" при обычном просмотре каталога
(см. ниже, поиск файлов).
FILE_ATTRIBUTE_SYSTEM equ 4h
Атрибут - "системный файл". Говорит о том, что данный файл принадлежит
операционной системе.
FILE_ATTRIBUTE_DIRECTORY equ 10h
Атрибут - "директорий", С файлами с таким атрибутом операционная система
обращается особым образом, считая его каталогом, т.е. считая его списком файлов, состоящим
из записей по 32 байта.
FILE_ATTRIBUTE_ARCHIVE equ 20h
Со времен MS DOS таким атрибутом отмечались файлы, над которыми не произведена операция
BACKUP или XCOPY. Для целей программирования данный атрибут эквивалентен нулевому значению
атрибута.
FILE_ATTRIBUTE_NORMAL equ 80h
Данный атрибут означает, что у файла не установлены другие атрибуты.
FILE_ATTRIBUTE_TEMPORARY equ 100h
Атрибут означает, что данный файл предназначен для временного хранения. После закрытия
файла система должна его удалить.
FILE_ATTRIBUTE_COMPRESSED equ 800h
Для файла это означает, что он сжат
системой; для директория - что вновь создаваемый файл по умолчанию должен быть сжат.
FILE_ATTRIBUTE_OFFLINE equ 1000h
Данный атрибут означает, что данные файла не доступны в данный момент.
Смену атрибута можно осуществить функцией SetFileAttributes, получить значение атрибута функцией GetFileAttributes. Следует заметить, что если операционная система не накладывает никаких ограничений на возможности изменения атрибутов файлов, то фактически обесценивается смысл самих атрибутов - всегда можно снять атрибут "только чтение" и делать с файлом, что заблагорассудится.
Файл имеет три временные характеристики: время создания, время последней модификации, время последнего доступа. Время отсчитывается в наносекундных интервалах начиная с 12.00 по полудни 1 января 1600 года и хранится в двух 32-битных величинах. Надо сказать, что время хранится в так называемых универсальных координатах и должно еще быть преобразовано в локальное время (функция FileTimeToLocalFileTime). Получить значение всех трех времен можно функцией GetFileTime.
Длина файла в байтах хранится обычно в двух 32-битных величинах либо в одной 64-битной величине. Если 32-битные величины обозначить как l1 (младшая часть) и l2 (старшая часть), то 64-битная величина выразится формулой l2*0FFFFH+l1. Paзмер файла можно получить функцией GetFileSize.
Кроме указанных характеристик, файл, разумеется, имеет имя. При этом мы будем различать длинное и короткое имя. Точно также будем различать полный путь (со всеми длинными именами) и укороченный путь (все длинные имена заменены укороченными). Необходимость использования укороченного имени и пути диктуется, прежде всего, тем, что некоторые программы получают путь или имя на стандартный вход и трактуют пробелы как разделители для параметров. Преобразование длинного имени в короткое можно осуществить функцией GetShortPathName, которая работает и для имени, и для пути. Обратное преобразование можно осуществить функиией GetFullPathName.
В данной книге мы не рассматриваем вопроса о прямом доступе к диску. Но вопрос о структуре записей каталога может у читателя все же возникнуть. Это и понятно, ведь с переходом к FAT3233, во-первых, появилась возможность хранения файлов с длинным именем, во-вторых, у файла, кроме времени и даты модификации, появились еще время и дата создания и доступа. Где же все это хранится?
Для того чтобы ответить на поставленный вопрос, вспомним, что каталог в файловых системах FAT34 делится на записи длиной 32 байта. Ниже приводится структура записи для FAT32. Пустыми записями считаются записи, содержащие нулевые байты, либо начинающиеся с кода E5H (для удаленных записей). На файл с обычным именем (восемь байт на имя и 3-на расширение) отводится 32 байта. В байте со смещением +11 содержится атрибут файла. Если атрибут файла равен 0FH, то система считает, что здесь содержится длинное имя. Длинное имя кодируется в UNICODE и записывается в обратном порядке. За одной или несколькими записями с длинным именем должна следовать запись с обычным именем, содержащим знак "~" - тильда. Здесь содержится также остальная информация о файле. Как видите, алгоритм просмотра каталога с выявлением информации о файле весьма прост. Обратимся теперь к структуре записи каталога. В старой операционной системе MS DOS байты с 12 по 21 никак не использовались системой (см. [1]). Новой системе они пригодились. Ниже в таблице дана новая структура записи каталога.
Смещение | Размер | Содержимое |
---|---|---|
(+0) | 8 | Имя файла или каталога, выровненное на левую границу и дополненное пробелами. |
(+8) | 3 | Расширение имени файла, выровненное на левую границу и дополненное пробелами. |
(+11) | 1 | Атрибут файла. |
(+12) | 2 | Время доступа. |
(+14) | 2 | Время создания. |
(+16) | 2 | Дата создания. |
(+18) | 2 | Дата доступа. |
(+20) | 2 | Два старших байта номера первого кластера файла. |
(+22) | 2 | Время модификации файла. |
(+24) | 2 | Дата модификации файла. |
(+26) | 2 | Два младших байта номера первого кластера файла. |
(+28) | 4 | Размер файла в байтах. |
Как видите, все байты 32-байтной записи каталога теперь заняты. Лишний раз убеждаешься в первоначальной непродуманности файловой системы MS DOS, Это касается, в частности, длины файла. Как можно заметить, на длину файла отводится всего 4 байта. А как найти длину файла, если на нее требуется более 4 байт? Разумеется, в этом случае следует считать, что в каталоге хранятся младшие байты длины, а полную длину легко определить, обратившись к таблице размещения файлов. Но, согласитесь, что это уже явная недоработка. Странно также выглядит функция GetFileSize, которая возвращает четыре младших байта длины файла, старшие же байты возвращаются во втором параметре функции.
Иное дело в файловой системе NTFS, поддерживаемой Windows NT, изначально планируемой для работы с файлами больших размеров. Здесь для индексации кластеров используются 64-битные поля.
33 В начале Windows 95 работала с 16-битной FAT, но длинные имена уже поддерживала.
34 FAT (File Allocation Table) - один из элементов, на котором базируются файловые системы MS DOS и Windows 9х. По этой причине часто такие файловые системы называют FAT системами.
II
Поиск файлов. Для поиска файлов в Windows существуют две функции FindFirstFile и FindNextFile, очень похожие на аналогичные функции MS DOS и, как и там, работающие в паре. При успешном поиске первая функция возвращает некое число или идентификатор, который затем используется второй функцией для продолжения поиска.
Первым параметром функции FindFirstFile является указатель на строку для поиска файлов, второй параметр - указатель на структуру, которая получает информацию о найденных файлах. Функция FindNextFile первым своим параметром имеет идентификатор, полученный первой функцией, а вторым параметром - указатель на структуру. Эту структуру можно изучить по программе на Рис. 2.5.1.
Основным отличием этих функций от соответствующих функций MS DOS является то, что поиск ограничивается только маской поиска (*.*, *.ЕХЕ и т.п.). Если файл найден, то тогда по возвращаемой структуре, где содержится вся информация о нем, Вы уже можете решать, подходит файл или нет.
На Рис. 2.5.1 представлена программа, осуществляющая поиск файлов в указанном каталоге. Программа может иметь один или два параметра, или не иметь их вовсе. Если имеются два параметра, то первый параметр трактуется как каталог для поиска, причем программа учитывает, есть ли на конце косая черта или нет (допустимо c:, c:\, c:\windows\, c:\windows\system и т.п.). Второй параметр (в программе он третий, так как первым считается командная строка), если он есть, представляет собой маску поиска. Если его нет, то маска поиска берется в виде "*.*". Наконец, если параметров нет вообще, то поиск осуществляется в текущем каталоге по маске "*.*". Эту программу легко развить и сделать из нее полезную утилиту. Предоставляю это Вам, дорогой читатель. Ниже будет дан комментарий к означенной программе.
; файл FILES.ASM .386P ; плоская модель .MODEL FLAT, stdcall ; константы STD_OUTPUT_HANDLE equ -11 STD_INPUT_HANDLE equ -10 ; прототипы внешних процедур EXTERN wsprintfA:NEAR EXTERN CharToOemA@8:NEAR EXTERN GetStdHandle@4:NEAR EXTERN WriteConsoleA@20:NEAR EXTERN ReadConsoleA@20:NEAR EXTERN ExitProcess@4:NEAR EXTERN GetCommandLineA@0:NEAR EXTERN lstrcatA@8:NEAR EXTERN FindFirstFileA@8:NEAR EXTERN FindNextFileA@8:NEAR EXTERN FindClose@4:NEAR ;----------------------------- ; структура, используемая для поиска файла ; при помощи функций FindFirstFile и FindNextFile _FIND STRUC ; атрибут файла ATR DWORD ? ; время создания файла CRTIME DWORD ? DWORD ? ; время доступа к файлу ACTIME DWORD ? DWORD ? ; время модификации файла WRTIME DWORD ? DWORD ? ; размер файла SIZEH DWORD ? SIZEL DWORD ? ; резерв DWORD ? DWORD ? ; длинное имя файла NAM DB 260 DUP (0) ; короткое имя файла ANAM DB 14 DUP(0) _FIND ENDS ;------------------------------------------------- ; директивы компоновщику для подключения библиотек includelib c:\masm32\lib\user32.lib includelib c:\masm32\lib\kernel32.lib ;------------------------------------------------- ; сегмент данных _DATA SEGMENT DWORD PUBLIC USE32 'DATA' BUF DB 0 DB 100 dup(0) LENS DWORD ? ; количество выведенных символов HANDL DWORD ? HANDL1 DWORD ? MASKA DB "*.*",0 АР DB "\",0 FIN _FIND <0> TEXT DB "Для продолжения нажмите клавишу ENTER",13,10,0 BUFIN DB 10 DUP(0) FINDH DWORD ? NUM DB 0 NUMF DWORD 0 ; счетчик файлов NUMD DWORD 0 ; счетчик каталогов FORM DB "Число найденных файлов: %lu",0 FORM1 DB "Число найденных каталогов: %lu",0 BUFER DB 100 DUP (?) DIR DB "<DIR>",0 PAR DB 0 ; количество параметров _DATA ENDS ; сегмент кода _TEXT SEGMENT DWORD PUBLIC USE32 'CODE' START: ; получить HANDLE вывода PUSH STD_OUTPUT_HANDLE CALL GetStdHandle@4 MOV HANDL,EAX ; получить HANDL1 ввода PUSH STD_INPUT_HANDLE CALL GetStdHandle@4 MOV HANDL1,EAX ; преобразовать строки для вывода PUSH OFFSET TEXT PUSH OFFSET TEXT CALL CharToOemA@8 PUSH OFFSET FORM PUSH OFFSET FORM CALL CharToOemA@8 PUSH OFFSET FORM1 PUSH OFFSET FORM1 CALL CharToOemA@8 ; получить количество параметров CALL NUMPAR MOV PAR,EAX ; если параметр один, то искать в текущем каталоге CMP EAX, 1 JE NO_PAR ;------------------------------------------------- ; получить параметр номером EDI MOV EDI, 2 LEA EBX,BUF CALL GETPAR PUSH OFFSET BUF CALL LENSTR ; если в конце нет "\" - добавим CMP BYTE PTR [BUF+EBX-1],"\" JE NO_PAR PUSH OFFSET AP PUSH OFFSET BUF CALL lstrcatA@8 ; нет ли еще параметра, где задана маска поиска CMP PAR,3 JB NO_PAR ; получить параметр - маску поиска MOV EDI,3 LEA EBX,MASKA CALL GETPAR NO_PAR: ;------------------------------------------------- CALL FIND ; вывести количество файлов PUSH NUMF PUSH OFFSET FORM PUSH OFFSET BUFER CALL wsprintfA LEA EAX, BUFER MOV EDX,1 CALL WRITE ; вывести количество каталогов PUSH NUMD PUSH OFFSET FORM1 PUSH OFFSET BUFER CALL wsprintfA LEA EAX, BUFER MOV EDX, 1 CALL WRITE _END: PUSH 0 CALL ExitProcess@4 ;************************ ; область процедур ;************************ ; вывести строку (в конце перевод строки) ; EAX - на начало строки ; EDX - с переводом строки или без WRITE PROC ; получить длину параметра PUSH EAX CALL LENSTR MOV ESI,EAX CMP EDX,1 JNE NO_ENT ; в конце - перевод строки MOV BYTE PTR [EBX+ESI],13 MOV BYTE PTR [EBX+ESI+1],10 MOV BYTE PTR [EBX+ESI+2],0 ADD EBX,2 NO_ENT: ; вывод строки PUSH 0 PUSH OFFSET LENS PUSH EBX PUSH EAX PUSH HANDL CALL WriteConsoleA@20 RET WRITE ENDP ; процедура определения длины строки ; строка - [EBP+08Н] ; длина в EBX LENSTR PROC PUSH EBP MOV EBP,ESP PUSH EAX ;---------------------- CLD MOV EDI, DWORD PTR [EBP+08Н] MOV EBX, EDI MOV ECX,100 ; ограничить длину строки XOR AL,AL REPNE SCASB ; найти символ 0 SUB EDI, EBX ;-длина строки, включая 0 MOV EBX, EDI DEC EBX ;---------------------- POP EAX POP EBP RET 4 LENSTR ENDP ; процедура определения количества параметров в строке ; определить количество параметров (->EAX) NUMPAR PROC CALL GetCommandLineA@0 MOV ESI,EAX ; указатель на строку XOR ECX,ECX ; счетчик MOV EDX,1 ; признак L1: CMP BYTE PTR [ESI],0 JE L4 CMP BYTE PTR [ESI],32 JE L3 ADD ECX,EDX ; номер параметра MOV EDX, 0 JMP L2 L3: OR EDX,1 L2: INC ESI JMP L1 L4: MOV EAX,ECX RET NUMPAR ENDP ; получить параметр из командной строки ; EBX - указывает на буфер, куда будет помещен параметр ; в буфер помещается строка с нулем на конце ; EDI - номер параметра GETPAR PROC CALL GetCommandLineA@0 MOV ESI, EAX ; указатель на строку XOR ECX, ECX ; счетчик MOV EDX, 1 ; признак L1: CMP BYTE PTR [ESI], 0 JE L4 CMP BYTE PTR [ESI], 32 JE L3 ADD ECX,EDX ; номер параметра MOV EDX,0 JMP L2 L3: OR EDX,1 L2: CMP ECX,EDI JNE L5 MOV AL,BYTE PTR [ESI] MOV BYTE PTR [EBX],AL INC EBX L5: INC ESI JMP L1 L4: MOV BYTE PTR [EBX], 0 RET GETPAR ENDP ; поиск в каталоге файлов и их вывод ; имя каталога в BUF FIND PROC ; путь с маской PUSH OFFSET MASKA PUSH OFFSET BUF CALL lstrcatA@8 ; здесь начало поиска PUSH OFFSET FIN PUSH OFFSET BUF CALL FindFirstFileA@8 CMP EAX,-1 JE _ERR ; сохранить дескриптор поиска MOV FINDH,EAX LF: ; исключить "файлы" "." и ".." CMP BYTE PTR FIN.NAM,"." JE _NO ; не каталог ли? TEST BYTE PTR FIN.ATR,10H JE NO_DIR PUSH OFFSET DIR PUSH OFFSET FIN.NAM CALL lstrcatA@8 INC NUMD DEC NUMF NO_DIR: ; преобразовать строку PUSH OFFSET FIN.NAM PUSH OFFSET FIN.NAM CALL CharToOemA@8 ; здесь вывод результата LEA EAX, FIN.NAM MOV EDX,1 CALL WRITE ; увеличить счетчики INC NUMF INC NUM ; конец страницы? CMP NUM, 22 JNE _NO MOV NUM, 0 ; ждать ввод строки MOV EDX,0 LEA EAX, TEXT CALL WRITE PUSH 0 PUSH OFFSET LENS PUSH 10 PUSH OFFSET BUFIN PUSH HANDL1 CALL ReadConsoleA@20 _NO: ; продолжение поиска PUSH OFFSET FIN PUSH FINDH CALL FindNextFileA@8 CMP EAX,0 JNE LF ; закрыть поиск PUSH FINDH CALL FindClose@4 _ERR: RET FIND ENDP _TEXT ENDS END START
Программа на Рис. 2.5.1 довольно проста. Из нового здесь Вы обнаружите лишь то, как обращаться с функциями FindFirstFile и FindNextFile. Процедуры, которые используются для работы с параметрами командной строки, Вы уже встречали ранее. Вывод информации осуществляется в текущую консоль, с чем Вы тоже знакомы. Для получения дескриптора консоли используется функция GetStdHandle. Процедура WRITE позволила несколько упростить те участки программы, которые отвечают за вывод информации на экран. Ранее я обещал, что мы не обойдем вниманием строковые API-функции. В данной программе это обещание выполнено, и наряду со строковыми процедурами "собственного изготовления" используется строковая функция lstrcat, которая осуществляет сложение (конкатенацию) строк. По поводу параметра в командной строке замечу, что при наличии в имени каталога пробела Вам придется задавать имя в укороченном виде. Так, например, вместо C:\Program Files придется написать C:\Progra~1. Это должно быть понятно - пробелы отделяют параметры. Чтобы корректно решать проблему, необходимо ввести специальный разделитель для параметров, например "-" или "/".
Данная программа осуществляет поиск в указанном или текущем каталоге. Если бы программа была написана на языке высокого уровня, например Си, ее легко можно было бы видоизменить так, чтобы она осуществляла поиск по дереву каталогов. Собственно, небольшая модификация потребовалась бы только для процедуры FIND, которая должна была бы вызываться рекурсивно. Можно видеть, что эта легкость произрастает из наличия в языках высокого уровня такого элемента, как локальная переменная. Попробуем осуществить это, основываясь на материале Главы 1.2. А можно осуществить это без использования локальных переменных?
Программа на Рис. 2.5.2 немного похожа на предыдущую программу. Но поиск она осуществляет по дереву каталогов, начиная с заданного каталога. Эта программа - одна из самых сложных в книге, поэтому советую читателю скрупулезно в ней разобраться. Может быть, Вам удастся ее усовершенствовать. Я могу дать и направление, в котором возможно такое усовершенствование. Дело в том, что вторым параметром командной строки можно указать маску поиска. Если, например, указать маску "*.ЕХЕ", по этой маске будет осуществляться поиск не только файлов, но и каталогов. Этот недостаток и следовало бы устранить в первую очередь.
Поиск по дереву каталогов оптимально производить рекурсивным образом, однако для этого необходимы локальные переменные35. Смысл использования локальной переменной в рекурсивном алгоритме заключается в том, что часть данных должна сохраняться при возврате из процедуры.
В данной программе я, ради простоты, отказался от процедуры LENSTR и использую функцию API lstrlen. Кроме того, я усовершенствовал вывод так, чтобы на экран выводилось полное имя файла.
35 Конечно, можно обойтись и без них, храня данные, например, в глобальном массиве, обращаясь к той или иной области массива в зависимости от уровня рекурсии.
; файл FILES.ASM .386P ; плоская модель .MODEL FLAT, stdcall ; константы STD_OUTPUT_HANDLE equ -11 STD_INPUT_HANDLE equ -10 ; прототипы внешних процедур EXTERN wsprintfA:NEAR EXTERN CharToOemA@8:NEAR EXTERN GetStdHandle@4:NEAR EXTERN WriteConsoleA@20:NEAR EXTERN ReadConsoleA@20:NEAR EXTERN ExitProcess@4:NEAR EXTERN GetCommandLineA@0:NEAR EXTERN lstrcatA@8:NEAR EXTERN lstrcpyA@8:NEAR EXTERN lstrlenA@4:NEAR EXTERN FindFirstFileA@8:NEAR EXTERN FindNextFileA@8:NEAR EXTERN FindClose@4:NEAR ;---------------------------- ; структура, используемая для поиска файла ; при помощи функций FindFirstFile и FindNextFile _FIND STRUC ; атрибут файла ATR DWORD ? ; время создания файла CRTIME DWORD ? DWORD ? ; время доступа к файлу ACTIME DWORD ? DWORD ? ; время модификации файла WRTIME DWORD ? DWORD ? ; размер файла SIZEH DWORD ? SIZEL DWORD ? ; резерв DWORD ? DWORD ? ; длинное имя файла NAM DB 260 DUP (0) ; короткое имя файла ANAM DB 14 DUP (0) _FIND ENDS ;------------------------------------------------- ; директивы компоновщику для подключения библиотек includelib c:\masm32\lib\user32.lib includelib c:\masm32\lib\kernel32.lib ;------------------------------------------------- ; сегмент данных _DATA SEGMENT DWORD PUBLIC USE32 'DATA' BUF DB 0 DB 100 dup (0) LENS DWORD ? ; количество выведенных символов HANDL DWORD ? HANDL1 DWORD ? MASKA DB "*.*" DB 50 DUP (0) AP DB "\",0 FIN _FIND <0> TEXT DB "Нажмите клавишу ENTER",13, 10, 0 BUFIN DB 10 DUP (0) ; буфер ввода NUM DB 0 NUMF DWORD 0 ; счетчик файлов NUMD DWORD 0 ; счетчик каталогов FORM DB "Число найденных файлов: %lu",0 FORM1 DB "Число найденных каталогов: %lu",0 DIRN DB " <DIR>",0 PAR DWORD 0 PRIZN DB 0 _DATA ENDS ; сегмент кода _TEXT SEGMENT DWORD PUBLIC USE32 'CODE' START: ; получить HANDLE вывода PUSH STD_OUTPUT_HANDLE CALL GetStdHandle@4 MOV HANDL,EAX ; получить HANDL1 ввода PUSH STD_INPUT_HANDLE CALL GetStdHandle@4 MOV HANDL1,EAX ; преобразовать строки для вывода PUSH OFFSET TEXT PUSH OFFSET TEXT CALL CharToOemA@8 PUSH OFFSET FORM PUSH OFFSET FORM CALL CharToOemA@8 PUSH OFFSET FORM1 PUSH OFFSET FORM1 CALL CharToOemA@8 ; получить количество параметров CALL NUMPAR MOV PAR,EAX ; если параметр один, то искать в текущем каталоге CMP EAX, 1 JE NO_PAR ;---------------------------------- ; получить параметр номером EDI MOV EDI,2 LEA EBX,BUF CALL GETPAR CMP PAR,3 JB NO_PAR ; получить параметр - маску поиска MOV EDI,3 LEA EBX, MASKA CALL GETPAR NO_PAR: ;---------------------------------- PUSH OFFSET BUF CALL FIND ; вывести количество файлов PUSH NUMF PUSH OFFSET FORM PUSH OFFSET BUF CALL wsprintfA LEA EAX, BUF MOV EDX,1 CALL WRITE ;+++++++++++++++++ ; вывести количество каталогов PUSH NUMD PUSH OFFSET FORM1 PUSH OFFSET BUF CALL wsprintfA LEA EAX, BUF MOV EDX, 1 CALL WRITE _END: PUSH 0 CALL ExitProcess@4 ; область процедур ;************************************* ; вывести строку (в конце перевод строки) ; EAX - на начало строки ; EDX - с переводом строки или без WRITE PROC ; получить длину параметра PUSH EAX PUSH EAX CALL lstrlenA@4 MOV ESI,EAX POP EBX CMP EDX, 1 JNE NO_ENT ; в конце - перевод строки MOV BYTE PTR [EBX+ESI],13 MOV BYTE PTR [EBX+ESI+1],10 MOV BYTE PTR [EBX+ESI+2],0 ADD EAX,2 NO_ENT: ; вывод строки PUSH 0 PUSH OFFSET LENS PUSH EAX PUSH EBX PUSH HANDL CALL WriteConsoleA@20 RET WRITE ENDP ; процедура определения количества параметров в строке ; определить количество параметров (->EAX) NUMPAR PROC CALL GetCommandLineA@0 MOV ESI,EAX ; указатель на строку XOR ECX,ECX ; счетчик MOV EDX,1 ; признак L1: CMP BYTE PTR [ESI],0 JE L4 CMP BYTE PTR [ESI],32 JE L3 ADD ECX,EDX ; номер параметра MOV EDX,0 JMP L2 L3: OR EDX, 1 L2: INC ESI JMP L1 L4: MOV EAX,ECX RET NUMPAR ENDP ; получить параметр из командной строки ; EBX - указывает на буфер, куда будет помещен параметр ; в буфер помещается строка с нулем на конце ; EDI - номер параметра GETPAR PROC CALL GetCommandLineA@0 MOV ESI,EAX ; указатель на строку XOR ECX,ECX ; счетчик MOV EDX,1 ; признак L1: CMP BYTE PTR [ESI],0 JE L4 CMP BYTE PTR [ESI],32 JE L3 ADD ECX,EDX ; номер параметра MOV EDX,0 JMP L2 L3: OR EDX,1 L2: CMP ECX,EDI JNE L5 MOV AL, BYTE PTR [ESI] MOV BYTE PTR [EBX], AL INC EBX L5: INC ESI JMP L1 L4: MOV BYTE PTR [EBX], 0 RET GETPAR ENDP ;----------------------------------- ; поиск в каталоге файлов и их вывод ; локальные переменные FINDH EQU [EBP-4] ; дескриптор поиска DIRS EQU [EBP-304] ; полное имя файла DIRSS EQU [EBP-604] ; для хранения каталога DIRV EQU [EBP-904] ; для временного хранения DIR EQU [EBP+8] ; параметр - имя каталога FIND PROC PUSH EBP MOV EBP,ESP SUB ESP,904 ; инициализация локальных переменных MOV ECX,300 MOV AL,0 MOV EDI,0 CLR: MOV BYTE PTR DIRS+[EDI],AL MOV BYTE PTR DIRSS+[EDI],AL MOV BYTE PTR DIRV+[EDI],AL INC EDI LOOP CLR ; определить длину пути PUSH DIR CALL lstrlenA@4 MOV EBX,EAX MOV EDI, DIR CMP BYTE PTR [EDI],0 JE _OK ;если в конце нет "\" - добавим CMP BYTE PTR [EDI+EBX-1],"\" JE _OK PUSH OFFSET AP PUSH DWORD PTR DIR CALL lstrcatA@8 _OK: ; запомним каталог PUSH DWORD PTR DIR LEA EAX,DIRSS PUSH EAX CALL lstrcpyA@8 ; путь с маской PUSH OFFSET MASKA PUSH DWORD PTR DIR CALL lstrcatA@8 ; здесь начало поиска PUSH OFFSET FIN PUSH DWORD PTR DIR CALL FindFirstFileA@8 CMP EAX,-1 JE _ERR ; сохранить дескриптор поиска MOV FINDH,EAX LF: ; исключить "файлы" "." и ".." CMP BYTE PTR FIN.NAM,"." JE _FF ;--------------------- LEA EAX,DIRSS PUSH EAX LEA EAX,DIRS PUSH EAX CALL lstrcpyA@8 ;--------------------- PUSH OFFSET FIN.NAM LEA EAX, DIRS PUSH EAX CALL lstrcatA@8 ; не каталог ли? TEST BYTE PTR FIN.ATR, 10H JE NO_DIR ; добавить в строку <DIR> PUSH OFFSET DIRN LEA EAX, DIRS PUSH EAX CALL lstrcatA@8 ; увеличим счетчики INC NUMD DEC NUMF ; установим признак каталога MOV PRIZN,1 ; вывести имя каталога LEA EAX, DIRS PUSH EAX CALL OUTF JMP _NO NO_DIR: ; вывести имя файла LEA EAX, DIRS PUSH EAX CALL OUTF ; признак файла (не каталога) MOV PRIZN,0 _NO: CMP PRIZN,0 JZ _F ; каталог, готовимся в рекурсивному вызову LEA EAX,DIRSS PUSH EAX LEA EAX, DIRV PUSH EAX CALL lstrcpyA@8 PUSH OFFSET FIN.NAM LEA EAX,DIRV PUSH EAX CALL lstrcatA@8 ; осуществляем вызов LEA EAX, DIRV PUSH EAX CALL FIND ; продолжение поиска _F: INC NUMF _FF: PUSH OFFSET FIN PUSH DWORD PTR FINDH CALL FindNextFileA@8 CMP EAX,0 JNE LF ; закрыть дескриптор поиска PUSH DWORD PTR FINDH CALL FindClose@4 _ERR: MOV ESP, EBP POP EBP RET 4 FIND ENDP ;---------------------------------- ; страничный вывод имен найденных файлов STRN EQU [EBP+8] OUTF PROC PUSH EBP MOV EBP,ESP ; преобразовать строку PUSH DWORD PTR STRN PUSH DWORD PTR STRN CALL CharToOemA@8 ; здесь вывод результата MOV EAX, STRN MOV EDX, 1 CALL WRITE INC NUM ; конец страницы? CMP NUM, 22 JNE NO MOV NUM, 0 ; ждать ввод строки MOV EDX, 0 LEA EAX, TEXT CALL WRITE PUSH 0 PUSH OFFSET LENS PUSH 10 PUSH OFFSET BUFIN PUSH HANDL1 CALL ReadConsoleA@20 NO: POP EBP RET 4 OUTF ENDP _TEXT ENDS END START
Рис. 2.5.2. Пример программы, которая осуществляет рекурсивный поиск по дереву каталогов.
Разберем ту роль, которую играют локальные переменные в процедуре FIND. Переменная FINDH - здесь хранится дескриптор поиска в данном каталоге. Рекурсивный вызов процедуры FIND может происходить и тогда, когда поиск в текущем каталоге еще не закончился. Следовательно, после возврата из рекурсии поиск должен быть продолжен. Это можно обеспечить только старым значением дескриптора. Локальная переменная обеспечивает такую возможность, поскольку она разрушается только при переходе на более низкий уровень (к родительскому каталогу).
Аналогичную роль играет переменная DIRSS. В ней хранится текущий каталог. Это важно, т.к. с помощью этой переменной формируется полное имя файла.
Переменные DIRS и DIRV играют вспомогательную роль. В принципе, вместо них можно было бы использовать и глобальные переменные. Хотя, с точки зрения эффективности рекурсивных алгоритмов, чем меньше объем локальных переменных - тем лучше.
Еще один вопрос я хочу здесь обсудить. Для передачи имени каталога при вызове процедуры используется переменная DIRV. Почему же для этой цели нельзя использовать переменную DIRSS? Причина вот в чем. В процедуру передается не само значение, а указатель (адрес). Следовательно, любые изменения с параметром DIR приведет к аналогичным изменениям с переменной DIRSS на нижнем уровне рекурсии, В чем мы, разумеется, не заинтересованы.
Трансляция программы в TASM. Основная проблема при трансляции программ на Рис. 2.5.1 и Рис. 2.5.2 возникнет с локальными метками. Локальная метка - это метка, которая действует в пределах некоторого блока программы. В нашем случае таким блоком программы является процедура. Транслятор MASM автоматически различает метки, находящиеся в пределах процедуры, и считает их локальными. Поэтому не возникает проблемы, когда в разных процедурах встречаются метки с одинаковым именем. В TASM несколько иной подход: по умолчанию метки считаются глобальными. Локальные метки должны иметь перед именем обозначение "@@". Кроме того, в начале программы следует поставить директиву LOCALS. Сделав нужные метки локальными и поставив директиву LOCALS, Вы без труда, уже известными действиями, приведете программу к виду, приемлемому для TASM. Не забудьте о преобразовании wsprintfA -> _wsprintfA.
III
Приемы работы с двоичными файлами*. Манипуляция внешними файлами36 основывается на нескольких функциях API, главной и наиболее сложной из которых является функция CreateFile.
В связи с ограниченностью объема книги, мы не можем подробно остановиться на свойствах функции CreateFile. Однако замечу, что с помощью этой функции можно не только создавать или открывать файл, но и такие объекты как каналы (PIPE), консоли, устройства жесткого диска (disk device), коммуникационный ресурс и др. Функция различает устройство по структуре имени. К примеру, "C:\config.sys" определяет файл, a "CONOUT$" - буфер вывода текущей консоли.
Сейчас я представлю две простые, но весьма важные программы (Рис. 2.5.3(1) и Рис. 2.5.3(2)). Обе программы выводят содержимое текстового файла37, имя которого указано в командной строке, в текущую консоль. В первом случае мы получаем дескриптор текущей консоли стандартным способом. Во втором случае - открываем консоль как файл и, соответственно, выводим туда информацию, как в файл. Хочу обратить Ваше внимание на роль буфера, в который читается содержимое файла. Поэкспериментируйте с размером буфера, взяв для пробы большой текстовый файл. Интересно, что в указанных программах никак не учитывается структура текстового файла. Для такого ввода-вывода это ни к чему. Ниже мы поговорим и о структуре текстового файла.
36 Имеется в виду файлами, расположенными на внешнем устройстве.
37 Точнее любого файла, но смысл выводить файл на консоль именно таким образом имеется только для текстового файла.
* Кажется, автор имеет ввиду текстовые файлы, а не бинарные, как это следует из дальнейшего повествования :)
;файл FILES1.ASM .386P ; плоская модель .MODEL FLAT, stdcall ; константы STD_OUTPUT_HANDLE equ -11 GENERIC_READ equ 80000000h GENERIC_WRITE equ 40000000h GEN = GENERIC_READ or GENERIC_WRITE SHARE = 0 OPEN_EXISTING equ 3 ; прототипы внешних процедур EXTERN GetStdHandle@4:NEAR EXTERN WriteConsoleA@20:NEAR EXTERN ExitProcess@4:NEAR EXTERN GetCommandLineA@0:NEAR EXTERN CreateFileA@28:NEAR EXTERN CloseHandle@4:NEAR EXTERN ReadFile@20:NEAR ;------------------------------------------------- ; директивы компоновщику для подключения библиотек includelib c:\masm32\lib\user32.lib includelib c:\masm32\lib\kernel32.lib ;------------------------------------------------- ; сегмент данных _DATA SEGMENT DWORD PUBLIC USE32 'DATA' HANDL DWORD ? HFILE DWORD ? BUF DB 100 DUP (0) BUFER DB 300 DUP (0) NUMB DWORD ? NUMW DWORD ? _DATA ENDS ; сегмент кода _TEXT SEGMENT DWORD PUBLIC USE32 'CODE' START: ; получить HANDLE вывода PUSH STD_OUTPUT_HANDLE CALL GetStdHandle@4 MOV HANDL,EAX ; получить количество параметров CALL NUMPAR CMP EAX,1 JE NO_PAR ;------------------------------ ; получить параметр номером EDI MOV EDI,2 LEA EBX, BUF CALL GETPAR ; открыть файл PUSH 0 ; должен быть равен 0 PUSH 0 ; атрибут файла (если создаем) PUSH OPEN_EXISTING ; как открывать PUSH 0 ; указатель на security attr PUSH 0 ; режим общего доступа PUSH GEN ; режим доступа PUSH OFFSET BUF ; имя файла CALL CreateFileA@28 CMP EAX,-1 JE NO_PAR MOV HFILE, EAX L00: ; прочесть в буфер PUSH 0 PUSH OFFSET NUMB PUSH 300 PUSH OFFSET BUFER PUSH HFILE CALL ReadFile@20 ; вывести содержимое буфера на консоль PUSH 0 PUSH OFFSET NUMW PUSH NUMB PUSH OFFSET BUFER PUSH HANDL CALL WriteConsoleA@20 ; проверить, не последние ли байты прочитаны CMP NUMB, 300 JE L00 ; закрыть файл PUSH HFILE CALL CloseHandle@4 ; конец работы программы NO_PAR: PUSH 0 CALL ExitProcess@4 ; область процедур ; процедура определения количества параметров в строке ; определить количество параметров (->ЕАX) NUMPAR PROC CALL GetCommandLineA@0 MOV ESI,EAX ; указатель на строку XOR ECX,ECX ; счетчик MOV EDX,1 ; признак L1: CMP BYTE PTR [ESI],0 JE L4 CMP BYTE PTR [ESI],32 JE L3 ADD ECX.EDX ; номер параметра MOV EDX,0 JMP L2 L3: OR EDX,1 L2: INC ESI JMP L1 L4: MOV EAX, ECX RET NUMPAR ENDP ; получить параметр из командной строки ; EBX - указывает на буфер, куда будет помещен параметр ; в буфер помещается строка с нулем на конце ; EDI - номер параметра GETPAR PROC CALL GetCommandLineA@0 MOV ESI,EAX ; указатель на строку XOR ECX,ECX ; счетчик MOV EDX,1 ; признак L1: CMP BYTE PTR [ESI], 0 JE L4 CMP BYTE PTR [ESI],32 JE L3 ADD ECX,EDX ; номер параметра MOV EDX,0 JMP L2 L3: OR EDX,1 L2: CMP ECX,EDI JNE L5 MOV AL, BYTE PTR [ESI] MOV BYTE PTR [EBX], AL INC EBX L5: INC ESI JMP L1 L4: MOV BYTE PTR [EBX], 0 RET GETPAR ENDP _TEXT ENDS END START
Рис. 2.5.3(1). Вывод на консоль содержимого текстового файла. Первый способ.
; файл FILES2.ASM .386P ; плоская модель .MODEL FLAT, stdcall ; константы STD_OUTPUT_HANDLE equ -11 GENERIC_READ equ 80000000h GENERIC_WRITE equ 40000000h GEN = GENERIC_READ or GENERIC_WRITE SHARE = 0 OPEN_EXISTING equ 3 ; прототипы внешних процедур EXTERN ExitProcess@4:NEAR EXTERN GetCommandLineA@0:NEAR EXTERN CreateFileA@28:NEAR EXTERN CloseHandle@4:NEAR EXTERN ReadFile@20:NEAR EXTERN WriteFile@20:NEAR ;------------------------------------------------- ; директивы компоновщику для подключения библиотек ; includelib c:\masm32\lib\user32.lib includelib c:\masm32\lib\kernel32.lib ;------------------------------------------------- ; сегмент данных _DATA SEGMENT DWORD PUBLIC USE32 'DATA' HANDL DWORD ? HFILE DWORD ? BUF DB 100 DUP (0) BUFER DB 300 DUP (0) NUMB DWORD ? NUMW DWORD ? NAMEOUT DB "CONOUT$" _DATA ENDS ; сегмент кода _TEXT SEGMENT DWORD PUBLIC USE32 'CODE' START: ; получить HANDLE вывода (консоли) как файла PUSH 0 PUSH 0 PUSH OPEN_EXISTING PUSH 0 PUSH 0 PUSH GEN PUSH OFFSET NAMEOUT CALL CreateFileA@28 MOV HANDL,EAX ; получить количество параметров CALL NUMPAR CMP EAX, 1 JE NO_PAR ;--------------------------------------------- ; получить параметр номером EDI MOV EDI,2 LEA EBX,BUF CALL GETPAR ; открыть файл PUSH 0 PUSH 0 PUSH OPEN_EXISTING PUSH 0 PUSH 0 PUSH GEN PUSH OFFSET BUF CALL CreateFileA@28 CMP EAX,-1 JE NO_PAR MOV HFILE,EAX L00: ; прочесть в буфер PUSH 0 PUSH OFFSET NUMB PUSH 300 PUSH OFFSET BUFER PUSH HFILE CALL ReadFile@20 ; вывести на консоль как в файл PUSH 0 PUSH OFFSET NUMW PUSH NUMB PUSH OFFSET BUFER PUSH HANDL CALL WriteFile@20 CMP NUMB, 300 JE L00 ; закрыть файл PUSH HFILE CALL CloseHandle@4 ; конец работы программы NO_PAR: PUSH 0 CALL ExitProcess@4 ; область процедур ; процедура определения количества параметров в строке ; определить количество параметров (->EAX) NUMPAR PROC CALL GetCommandLineA@0 MOV ESI,EAX ; указатель на строку XOR ECX,ECX ; счетчик MOV EDX,1 ; признак L1: CMP BYTE PTR [ESI],0 JE L4 CMP BYTE PTR [ESI],32 JE L3 ADD ECX,EDX ; номер параметра MOV EDX,0 JMP L2 L3: OR EDX,1 L2: INC ESI JMP L1 L4: MOV EAX,ECX RET NUMPAR ENDP ; получить параметр из командной строки ; EBX - указывает на буфер, куда будет помещен параметр ; в буфер помещается строка с нулем на конце ; EDI - номер параметра GETPAR PROC CALL GetCommandLineA@0 MOV ESI,EAX ; указатель на строку XOR ECX,ECX ; счетчик MOV EDX,1 ; признак L1: CMP BYTE PTR [ESI],0 JE L4 CMP BYTE PTR [ESI],32 JE L3 ADD ECX,EDX ; номер параметра MOV EDX,0 JMP L2 L3: OR EDX,1 L2: CMP ECX,EDI JNE L5 MOV AL,BYTE PTR [ESI] MOV BYTE PTR [EBX], AL INC EBX L5: INC ESI JMP L1 L4: MOV BYTE PTR [EBX], 0 RET GETPAR ENDP _TEXT ENDS END START
Рис. 2.5.3(2). Вывод на консоль содержимого текстового фаша. Второй способ.
Сейчас мы поговорим более подробно о структуре текстового файла. При работе с языками высокого уровня теряются определенные алгоритмические навыки. Это касается, в частности, и работы с текстовыми файлами. Ассемблер не дает расслабиться. Рассмотрим возможные варианты работы с текстовыми файлами.
Основным признаком текстового файла является то, что он состоит из строк разной длины. Строки отделены друг от друга разделителями. Чаще всего это последовательность двух кодов - 13 и 10. Возможны и другие варианты, например, некоторые DOS-редакторы отделяли строки только одним кодом 13.
Построчное чтение текстового файла можно осуществить четырьмя наиболее очевидными способами.
- Побайтное чтение из файла. Как только достигаем символа-разделителя, производим действие над считанной строкой и переходим к чтению следующей строки. При этом, разумеется, следует учесть, что на конце файла может не быть символа-разделителя. Если кто-то решит, что это слишком медленный способ, то замечу, что Windows неплохо кэширует диск, так что все выглядит не так уж плохо.
- Чтение в небольшой буфер, но так чтобы туда входила, по крайней мере, одна строка. Прочитав, находим в буфере конец строки и производим над ней какое-либо действие. Далее следует обратиться к файлу и передвинуть указатель так, чтобы он был в файле на начале следующей строки и, разумеется, повторить действие.
- Чтение в произвольный буфер. После чтения производится поиск всех строк, попавших в буфер, и совершение над ними действий. При этом с большой вероятностью должна возникнуть ситуация, когда одна строка неполностью умещается в буфере. Мы обязаны учесть такую возможность.
- Чтение в буфер, в который помещается весь файл. Это частный случай третьего подхода, и наиболее простой с точки зрения программирования.
В программе на Рис. 2.5.4 реализуется третий подход.
; файл FILES2.ASM .386P ; плоская модель .MODEL FLAT, stdcall ; константы STD_OUTPUT_HANDLE equ -11 GENERIC_READ equ 80000000h GENERIC_WRITE equ 40000000h GEN = GENERIC_READ or GENERIC_WRITE SHARE = 0 OPEN_EXISTING equ 3 ; прототипы внешних процедур EXTERN ExitProcess@4:NEAR EXTERN GetCommandLineA@0:NEAR EXTERN CreateFileA@28:NEAR EXTERN CloseHandle@4:NEAR EXTERN ReadFile@20:NEAR EXTERN WriteFile@20:NEAR EXTERN CharToOemA@8:NEAR ;------------------------------------------------- ; директивы компоновщику для подключения библиотек includelib c:\masm32\lib\user32.lib includelib c:\masm32\lib\kernel32.lib ;------------------------------------------------- ; сегмент данных _DATA SEGMENT DWORD PUBLIC USE32 'DATA' HANDL DWORD ? ; дескриптор консоли HFILE DWORD ? ; дескриптор файла BUF DB 100 DUP (0) ; буфер для параметров BUFER DB 1000 DUP (0) ; буфер для файла NAMEOUT DB "CONOUT$" INDS DD 0 ; номер символа в строке INDB DD 0 ; номер символа в буфере NUMB DD ? NUMC DD ? PRIZN DD 0 STROKA DB 300 DUP (0) _DATA ENDS ; сегмент кода _TEXT SEGMENT DWORD PUBLIC USE32 'CODE' START: ; получить HANDLE вывода (консоли) как файла PUSH 0 PUSH 0 PUSH OPEN_EXISTING PUSH 0 PUSH 0 PUSH GEN PUSH OFFSET NAMEOUT CALL CreateFileA@28 MOV HANDL,EAX ; получить количество параметров CALL NUMPAR CMP EAX, 1 JE NO_PAR ;--------------------------------------------------- ; получить параметр номером EDI MOV EDI,2 LEA EBX,BUF CALL GET PAR ; открыть файл PUSH 0 PUSH 0 PUSH OPEN_EXISTING PUSH 0 PUSH 0 PUSH GEN PUSH OFFSET BUF CALL CreateFileA@28 CMP EAX,-1 JE NO_PAR MOV HFILE, EAX ;++++++++++++++++++++++++++++ L00: ; читать 1000 байт PUSH 0 PUSH OFFSET NUMB PUSH 1000 PUSH OFFSET BUFER PUSH HFILE CALL ReadFile@20 MOV INDB, 0 ; проверим, есть ли в буфере байты CMP NUMB, 0 JZ _CLOSE ; заполняем строку L001: MOV EDI,INDS MOV ESI,INDB MOV AL,BYTE PTR BUFER[ESI] CMP AL,13 ; проверка на конец строки JE _ENDSTR MOV BYTE PTR STROKA[EDI],AL INC ESI INC EDI MOV INDS,EDI MOV INDB,ESI CMP NUMB, ESI ; проверка на конец буфера JNBE L001 ; закончился буфер MOV INDS,EDI MOV INDB,ESI JMP L00 _ENDSTR: ; делаем что-то со строкой CALL OUTST ; обнулить строку MOV INDS,0 ; перейти к следующей строке в буфере ADD INDB,2 ; не закончился ли буфер? MOV ESI,INDB CMP NUMB, ESI JAE L001 JMP L00 ;++++++++++++++++++++++++++++++ _CLOSE: ; проверим, не пустая ли строка CMP INDS,0 JZ CONT ; делаем что-то со строкой CALL OUTST CONT: ; закрыть файлы PUSH HFILE CALL CloseHandle@4 ; конец работы программы NO_PAR: PUSH 0 CALL ExitProcess@4 ; область процедур ; процедура определения количества параметров в строке ; определить количество параметров (->EAX) NUMPAR PROC CALL GetCommandLineA@0 MOV ESI,EAX ; указатель на строку XOR ECX,ECX ; счетчик MOV EDX,1 ; признак L1: CMP BYTE PTR [ESI],0 JE L4 CMP BYTE PTR [ESI],32 JE L3 ADD ECX,EDX ; номер параметра MOV EDX,0 JMP L2 L3: OR EDX,1 L2: INC ESI JMP L1 L4: MOV EAX,ECX RET NUMPAR ENDP ; получить параметр из командной строки ; EBX - указывает на буфер, куда будет помещен параметр ; в буфер помещается строка с нулем на конце ; EDI - номер параметра GETPAR PROC CALL GetCommandLineA@0 MOV ESI,EAX ; указатель на строку XOR ECX,ECX ; счетчик MOV EDX,1 ; признак L1: CMP BYTE PTR [ESI],0 JE L4 CMP BYTE PTR [ESI],32 JE L3 ADD ECX,EDX ; номер параметра MOV EDX,0 JMP L2 L3: OR EDX,1 L2: CMP ECX,EDI JNE L5 MOV AL,BYTE PTR [ESI] MOV BYTE PTR [EBX],AL INC EBX L5: INC ESI JMP L1 L4: MOV BYTE PTR [EBX],0 RET GETPAR ENDP ; вывести строку в консоль с разделителем OUTST PROC MOV EBX,INDS MOV BYTE PTR STROKA[EBX],0 PUSH OFFSET STROKA PUSH OFFSET STROKA CALL CharToOemA@8 ; в конце строки - разделитель MOV BYTE PTR STROKA[EBX],6 INC INDS ; вывести строку PUSH 0 PUSH OFFSET NUMC PUSH INDS PUSH OFFSET STROKA PUSH HANDL CALL WriteFile@20 RET OUTST ENDP _TEXT ENDS END START
Рис. 2.5.4. Пример обработки текстового файла.
Программа на Рис. 2.5.4 демонстрирует один из возможных алгоритмов обработки текстового файла - построчное чтение текстового файла. Часть программы, занимающаяся чтением и анализом текстового файла, сосредоточена между метками L00 и CONT. Детально разберитесь в алгоритме и проникнитесь тем, что язык высокого уровня никогда не будет стимулировать написание таких алгоритмов, а значит, язык ассемблера делает нас интеллектуально богаче.