Компьютерная история

Ниже изложена крайне увлекательная история моих мытарств ради решения одной любопытной проблемы. Ещё там есть куча полезных ссылок. Под конец будет вопрос ко зрителям.
Если вы не знаете, что такое Windows API, то можете сэкономить время, не читая этой записи. Вот вам пересказ её вкратце: я горжусь собой, поскольку я крутой и бесстрашный ;)

А теперь постановка задачи

постановка задачи. Есть производственная USB-флешка размером 4 гига, якобы сверхбыстрая и сверхнадёжная. Несколько сотен таких флешек. Есть образ диска с операционной системой, собранный, кстати, мной. Образ побайтовый, с копированием пустого места, в общем, копия диска один в один. При подготовке POS-терминала к выпуску этот образ прошивается на флешку, после чего флешку вставляют в компьютер и загружаются. Терминал готов.

Примерно каждый восьмой раз записанная от корки до корки флешка не запускается. Винда вылетает в чёрный экран при загрузке с надписью
File is missing or corrupt – C:\WINDOWS\SYSTEM32\CONFIG\SYSTEM
Это, к сведению, файл с ветвью HKEY_LOCAL_MACHINE системного реестра. Ошибка повторяется при перезагрузках. Указанный файл на диске есть, более того – он читается, и его можно подгрузить в реестр на любой рабочей машине (“File -> Load hive” в regedit, если кто не знает). Теперь сюрприз: чтобы исправить ошибку, достаточно перенести этот файл на другой диск, а затем перенести его обратно. После этого винда с флешки загрузится.

Почему операционная система на другом компьютере читает файл целиком и нормально, а та же операционная система при загрузке не может его прочесть?
Может быть, файл читается не с первой попытки, а на этапе загрузки глупая система не догадывается попробовать снова? Заряжаю procmon – это утилита, перехватывающая обращения к диску и реестру. Нет: в работающей операционной системе все запросы на чтение выполняются без ошибок. Но это мало чего даёт, драйвер может сам бороться с ошибками, не сообщая об этом наружу.

Пишу простое приложение, читающее диск по секторам. Замеряю время отклика: одинаковое для всех секторов. Вряд ли где-то есть повторное чтение. Попутно выясняется ещё одна любопытная вещь: если со сломанного диска снять образ, а потом записать его обратно, то он начнёт грузиться!

Ну что тут поделаешь? Под работающей системой всё безупречно. При загрузке стабильно ошибка. Логики никакой. Будь это пользовательское приложение, я бы его дизассемблировал и отладил, но тут ошибка в режиме ядра. При загрузке системы. Там ещё и графики нет.
Сдаваться?
Чёрта с два.

Приобретаю нуль-модемный кабель – это COM-COM, замкнутый крест-накрест, что-то вроде кроссовера для Ethernet. Соединяю тестовый компьютер с рабочим, на рабочем запускаю WinDbg, а на тестовом – вы уже догадались, к чему дело идёт? – пишу в boot.ini “/DEBUG /PORT=COM1 /BAUDRATE=115200”.
После часа мытарств оно подключается, соединяется, срабатывает брейкпоинт и я уже замахиваюсь руками над клавиатурой – сейчас я буду отлаживать ядро операционки! – , но… фиг вам. Не судьба. Ошибка происходит до инициализации дебаггера. Ошибка происходит вообще до загрузки NTOSKRNL. Ошибка в NTLDR.

Краткий экскурс в сторону. Как загружаются все Windows NT, за исключением Висты и Server 2008.
Вначале работает BIOS. Сделав свои дела, он передаёт управление коду в загрузочном секторе активного раздела диска. Этот код очень маленький, и делает он только одно: находит на диске файл NTLDR, и передаёт управление ему. NTLDR – это загрузчик системы. В него встроены драйвера FAT и NTFS, при помощи которых он читает с диска boot.ini, и предлагает пользователю выбрать систему. Если у вас несколько операционок, вы наверняка видели это меню. Если операционка одна, меню на экране не появляется, но вы всё равно можете в него попасть, долбя по клавише F8 при загрузке.
Если выбранная система – DOS или 9x, NTLDR просто передаёт управление сохранённому в файле загрузочному сектору DOS. Иначе он сам начинает загружать NT: подгружает HAL и пару драйверов, затем читает реестр в память, находит в нём список boot-time драйверов (это первичные драйверы, самые необходимые для загрузки – драйверы жёстких дисков и файловых систем, чипсета, и тому подобного). К слову, в это время драйверы NTFS ещё не загружены! Вообще ничего ещё не загружено! В NTLDR намертво вкомплирован свой собственный драйвер NTFS, которым он и пользуется на первых порах, пока не загрузит boot-time драйверы.
И наконец, когда вся эта пляска закончена, управление передаётся NTOSKRNL.EXE, который и загружает далее операционную систему. Где-то в нём запускается отладчик, позволяющий контролировать загрузку через COM-port.

Лирическое отступление было нужно для того, чтобы вы полностью осознали: в момент, когда грузится реестр, нет ничего. Вообще НИЧЕГО. Это даже не режим ядра, это вообще чистый ассемблерный код, пользующийся только собственными сервисами и биосом.
Сдаёмся?
Ну конечно же, нет.

Поисками в торрентах и по интернетам была найдена отладочная версия ntldr. Эта дикая редкость поставлялась вместе с DDK, причём c XP_DDK, который больше не распространяется, поскольку его заменил 2003_DDK, содержащий всё то же самое, кроме одного. Этой самой отладочной версии ntldr. Можете себе представить, что она нужна максимум паре тысяч человек на планете, и у большинства из них она давно есть.
Был скачан пакет символов для checked-версии XP Rtm. Ни один пакет символов на сайте не подходит к ntldr из XP_DDK, и в самом DDK символов для неё не было (о чём они вообще думали?!), но checked-версия чистой XP подходит более-менее (начала функций прыгают на пару десятков инструкций в стороны, но это уже мелочи).

Дебаггер запустился, подключился и успешно застыл ещё до загрузки HAL. Я ликовал. Но ликование моё было преждевременным.
Убив почти сутки на тупой разбор ассемблерного кода, исписав кипу бумаги адресами и изрисовав блок-схемами, я вычленил виноватых. Во-первых, файл реестра открывался. Во-вторых, он читался до определённой точки. В третьих, очередной кусок файла не читался, причём ошибку возвращала даже не функция Ntfs, а вызываемая ей функция низкоуровневого чтения с диска. То есть, тупо не читается какой-то сектор. Но почему?
Я влез в функцию чтения. Функция чтения была короткой.

push 058h
push 021Ch
retf

Неплохо, да? Учитывая, что ассемблер я учил на ходу, я развеселился. (Вообще, я веселился всё время, пока разбирал код загрузчика. Иногда я веселился так, что мне хотелось биться головой об стол от смеха). Какого чёрта?
А всё просто. До сих пор адресное пространство было flat – и режим уже был защищённым, это постарался загрузчик ещё до того, как запустил дебаггер. А этой функции, чтобы сделать запрос к int 13h, потребовался segemnted-режим. Она запихала в стек оффсет и адрес, и сделала retf, который вытащил оффсет с адресом и перешёл по ним. В следующее мгновение я уже был в сегментной адресации. И там меня ждал сюрприз, плохой. Дебаггер не работал. Единственное, что он мог – это исполнить первую команду. На второй или любой из последующих отлаживаемый компьютер перезагружался.

А мне нужно было отлаживать именно эту функцию. Во всяком случае, я хотел узнать, что передают в int13h, и что она возвращает. Что делать? Ну ясно, что. Я сделал дамп памяти функций, исполняющихся в сегментированном режиме, и стал их разбирать.

Оказалось, в этой несчастной функции вызова int13 всё устроено примерно так:
1. Создать новый стек по фиксированному адресу.
2. Скопировать старый стек в новый.
3. Вызвать функцию, делающуют НЕИЗВЕСТНО_ЧТО в одну сторону (НЕИЗВЕСТНО_ЧТО включало в себя перезапись таблицы прерываний и глобальной таблицы дескрипторов, я подозреваю, какие-то трюки с режимом исполнения. Скорее всего, управляемый real режим под protected).
4. С кучей мытарств подгрузить из стека параметры и вызвать int13.
5. Вызывать функцию, делающую НЕИЗВЕСТНО_ЧТО в обратную сторону.
6. Выгрузить новый стек, восстановить старый.
7. Ну и запихать в стек flat-адрес и сделать нормальный ret.

Теперь-то мне всё кажется простым. Теперь у меня есть карта, с точностью до байта, куда какие параметры попадают, и как с ними обращаются. Тогда это был тихий кошмар – и не забывайте, что всё это делалось на бумажке и в текстовых файлах, а проверить свои предположения я никак не мог, поскольку дебаггер в этой функции не работал вообще.
Ну да ладно.

Я разобрался, какие параметры передаются, и что приходит в ответ. На нормальный набор параметров (и да, я знаю, что блоки памяти должны не пересекать границу сегмента) биос возвращает 8012h. 80 в AH (то есть, это код возврата – “Drive not ready, timeout”), 12 в AL (по документации это число прочитанных секторов, но экспериментально я убедился, что он возвращает 8012 и тогда, когда просто выдёргиваешь флешку ещё до выполнения int13).

Вот теперь я почти в тупике. Ну да, я дизассемблировал всё на свете до самого источника проблемы. Ну да, я знаю, что на нормальный запрос биос возвращает ошибку. Я только не знаю, почему.
Есть какие-нибудь идеи, что делать дальше? Я так подумываю дампнуть биос, лол. Где наша не пропадала! Ещё есть идея загрузиться из под доса, и повторить int13, приводящий к ошибке, вручную – но только что мне это даст, каким бы не был результат?

Ну да, ещё можно вставить флешку в другой компьютер. Тоже мысль. Надо было сразу попробовать. Но хотелось бы не просто найти виноватых (уже ясно, кто они: пара флешка-биос), а ещё и придумать что-нибудь, что спасло бы сотни стоящих в терминалах флешек от апгрейда. Ну например, если я пойму причину ошибки, я смогу переписать прошивальщик так, чтобы он шил только в “безопасные” сектора – те, что биос читает.

Напишите комментарий:

Если хотите, можно залогиниться.

*