Итак, после некоторого перерыва мне снова хочется поделиться прочитанным.
Загрузчик для FAT*
На этот раз речь пойдет о загрузчике, работающем с файловой системой. Мало написать код, который переключает процессор в защищенный режим и выполняет какие-то действия. Его еще нужно загрузить в память и передать ему управление. Для этого можно использовать эмулятор, например, Bochs, или VMWare. Первый хорош возможностями отладки (немного кривой), второй -- стабильностью. Однако, это все же эмуляторы, а тестировать программу желательно и на реальном железе. Вот тут в игру вступает загрузчик.
Для начала приведу краткое описание ограничений и возможностей моего загрузчика.
Загрузчик ищет в корневом каталоге файловых систем FAT* файл с заданным именем, загружает его по адресу сегмент:смещение и передает ему управление, по адресу так же указанному в форме сегмент:смещение. Загрузчик написан на FASM.
Ограничения:
- Макисмальный размер файла -- 64КБ.
- Сегмент, используемый для загрузки файла, возможно, будет использован полностью в процессе поиска.
- В случае, если корневая директория имеет размер более 64К файл, файл возможно не будет найден (только для FAT12/16).
Имя файла задается константой 'ldr_name'. Сегмент для загрузки -- 'LDR_BASE', Смещение для загрузки -- 'LDR_OFFS', смещение точки входа -- 'LDR_ENTRY_POINT'. Все константы можно менять по своему желанию.
Как я писал
здесь, код MBR, после нахождения активного раздела загружает первый сектор активного раздела по адресу 0x0:0x7C00 и передает ему управление. Именно в первом секторе активного раздела должен располагаться загрузчик (или начальная его часть), который умеет читать файловую систему раздела, загружать файл (например, ядро) в память и передавать ему управление. Назовем этот загрузчик "загрузчиком файловой системы" (File System Boot Sector -- FSBS), т.к. наболее вероятно, что его код будет зависеть от файловой системы, установленной на раздел. Конечно, для разных ОС действия FSBS различаются, например, FSBS WinNT* ищет в корневом каталоге файловой системы файл NTLDR, загружает его и передает ему управление. А уже NTLDR в свою очередь производит подготовку и загрузку ядра. Или же, FSBS может занимать более одного сектора -- в этом случае первыми его действиями будут загрузка и исполнение остальной части FSBS. Важно понимать одно -- загрузчик MBR просто читает первый сектор активного раздела, загружает его по адресу 0x0:0x7C00 и передает ему управление.
Итак, значит в задачу входит поиск некоторого файла в некоторой файловой системе, загрузка оного в память и передача ему упраления. Т.к. наиболее простой файловой системой с моей т.з. является FAT*, то я написал загрузочный код для FAT*. Не буду вдаваться в подробности организации FAT* -- это все неплохо описано
здесь и
здесь. Первый документ -- общее описание, позволяет понять что такое FAT, ее основную идею и основные структуры, второй является спецификацией и содержит более детальное описание, с примерами кода.
Надо заметить, что рамки, налагаемые на размер кода довольно узкие. Размер сектора -- 512 байт (надеюсь, меньше не бывает), за вычетом двух байт на сигнатуру, шестидесятидвух байт на параметры файловой системы (в случае FAT12/16) и девяноста байт в случае FAT32. В итоге на все про все -- 420 байт для FAT32 и 448 байт для FAT12/16. Несмотря на это, мне очень хотелось не превращать код в месиво инструкций, а сохранить некоторую структуру кода при минимальном его объеме. Т.е. иметь хотя бы несколько ф-ий. Сохранить структуру, думаю, отчасти, у меня получилось, а вот с размером я потерпел поражение: FAT12/32 занимают весь FSBS. Так что если вы найдете способ выкрасть хотя бы несколько байт, не меняя общей структуры -- сообщите пожалуйста, буду благодарен.
Сам загрузчик можно взять здесь:
FAT12FAT16FAT32В случае, если ссылки сдохнут -- напишите в комментариях -- обновлю.
Теперь, после длинного предисловия можно приступать к описанию загрузчика.
Для чтения секторов с винчестера загрузчик использует три функции: две из них подготавливают параметры LBA и CHS -вызовов (prepare_LBA_call/prepare_CHS_call), третья (read_sectors) косвенно, по адресу в переменной 'PREPARE_ROUTINE_OFFS', вызывает одну из этих двух функций перед выполнением прерывания int 0x13. При получении управления загрузчик проверяет возможонсти BIOS выполнять LBA-вызов и в зависимости от результата сохраняет в 'PREPARE_ROUTINE_OFFS' либо адрес prepare_LBA_call либо prepare_CHS_call. Далее действия загрузчика для FAT12/16 и FAT32 несколько различаются. Дело в том, что организация корневого каталога в FAT12/16 и FAT32 имеет значительные отличия -- в FAT12/16 корневой каталог занимает жестко заданное количество секторов, следующие сразу за таблицами FAT, и является "системной" частью файловой системы, а в FAT32 корневой каталог разжалован до статуса обычного файла, номер его начального сектора хранится в блоке параметров файловой системы. Т.е. если раздел, с установленной FAT12/16 можно условно разделить на четрые части:
- Параметры файловой системы.
- Таблицы FAT.
- Корневой каталог.
- Данные.
то для FAT32 пункт три будет отсутствовать. Соответственно, загрузчик FAT12/16 для чтения корневого каталога:
- Высчитывает количество зарезервированных секторов + размер таблиц FAT, определяя тем самым его физическое расположение.
- Высчитывает размер корневого каталога и загружает его целиком в память.
Вот здесь мне пришлось пойти на небольшую сделку с совестью -- загрузчик не способен загружать данные размером более одного сегмента, т.е. 64К. Теоретически, размер корневого каталога ничем не ограничен. Т.е. возможна ситуация, когда чтение слишком большого корневого каталога приведет перезаписи уже считанной его части. К счастью, размер корневого каталога обычно не превышает шестнадцати килобайт, так что по этому поводу можно особо не беспокоиться.
В FAT32 корневой каталог представлен в виде обычного файла. Загрузчик читает его по кластерам с помощью функций read_cluster и read_next_clst_num, (подробнее о них дальше) и выполняет поиск файла 'LDR_NAME', в каждом считанном кластере. Это удобно тем, что максимальный размер кластера равен 64К, т.е. проблемы слишком большого корневого каталога в FAT32 не будет.
После чтения корневого каталога загрузчик выполняет в нем поиск файла с именем, заданным константой 'LDR_NAME'. Если файл с именем 'LDR_NAME' не найден загрузчик выводит сообщение 'LDR lost' и останавливается.
Когда искомый файл найден, загрузчик приступает к чтению его цепочки кластеров. К счастью, больших отличий между FAT12/16/32 на этой стадии нет. Для чтения используются две функции: 'read_cluster' и 'read_next_clst_num'. Как можно догадаться из названий, функция 'read_cluster' загружает кластер по переданному ей номеру, а функция 'read_next_clst_num' -- возвращает следующий номер кластера. Т.к. загрузчик не умеет оперировать сегментными регистрами (на это не хватило места), то чтение секторов, принадлежащих кластерам файла и сектора, содержащего следующий номер кластера происходит в одном сегменте. Алгоритм чтения цепочки кластеров выглядит так:
- Считать номер следующего кластера не увеличивая указатель буфера чтения. Сохранить номер следующего кластера в регистре.
- Считать текущий кластер, увеличить указатель буфера чтения на размер кластера.
- Если номер следующего кластера содержит признак конца цепочки перейти к пункту 4, иначе -- к пункту 1.
- Передать управление прочитанному файлу.
При написании read_next_clst_num для FAT12 вылезла небольшая проблема -- запись в таблице FAT для FAT12 может пересекать границу двух секторов. Спецификация предлагает просто загружать два подряд идущих сектора, что в моем случае не приемлемо. Если размер кластера равен одному сектору и размер сектора равен 512 байт, то в случае, когда считано уже 63,5КБ чтение двух секторов подряд приведет к перезаписи первых 512 байт считанного файла -- произойдет "зацикливание" адреса при переполнении. Чтобы этого избежать нужно проверять, не пересекает ли запись FAT границу секторов, и, в случае если пересекает, "дочитывать" следующий сектор не увеличивая указатель буфера чтения. Ничего особо страшного в этом нет, но ННое количество байт этот код занимает.
После того, как файл считан загрузчик передает ему управление по адресу 'LDR_BASE:LDR_ENTRY_POINT'.
Немного наблюдений:
- Есть относительно простой (но совсем ненадежный!) способ спрятать файл без хуков, фильтров и т.д. -- для этого нужно поменять сигнатуру записи удаленного файла в каталоге с 0xE5 на 0x0. Все записи, содержащиеся в этом каталоге после записи с сигнатурой 0x0 не будут отображены. Сигнатура 0x0 означает удаленную запись и служит признаком того, что после нее записей в каталоге больше нет. Способ не надежен по той причине, что такая запись может быть перезаписана при создании в каталоге нового файла. Помимо этого chkdsk разоблачает такие штучки за раз, и, считая это ошибкой файловой системы восстанавливает "спрятанные" файлы в файлы *.CHK. В общем, 'just for fun'.
- Остается открытым вопрос, когда драйвер помечает свободными кластеры, принадлежащие каталогу и содержащие только записи удаленных файлов. Интереса ради я создал 65533 пустых файла в каталоге, потом удалил. Драйвер даже в ус не подул. А вместе с тем, 2МБ на винчестере эти записи занимают.
- Драйвер FAT от BSD 4.5 при создании файлов не старается разместить их кластера подряд (в отличие от драйвера от MS), а напротив задает кластерам какие-то фантастические номера. Сделано ли это от большого ума или наоборот я не разбирался. Для тестирования, впрочем, весьма удобно. Помимо этого драйвер FAT32 от BSD 4.5 преподнес мне неприятный сюрприз: он считает, что 2 байта перед сигнатурой 0xAA55 в FSBS также являются сигнатурами и должны быть обнулены. Об этом ни слова нет в спецификации. Пришлось срочно выискивать возможность уменьшить объем кода для FAT32 на 2 байта.
- Драйвер FAT от MS не производит никаких действий над секторами принадлежащими последнему кластеру файла, но лежащими "за пределом" размера файла. Т.е. если файл занимает 10 байт и размер кластера 8 секторов, то 7 последних секторов кластера останутся нетронутыми до тех пор, пока файл не "подрастет". А до того их можно свободно использовать. Помимо этого, если раздел занимает количесто секторов, не кратное размеру кластера, то в конце раздела останется незанятое место.