Загрузочная запись раздела PBR Windows 7

Метки:  , , , , ,

В предыдущей статье я рассказывал о логике работы загрузочного сектора MBR Windows 7. Код MBR, стартующий сразу после отработки кода BIOS, загружает код загрузочного сектора раздела (PBR) в память по адресу 0000:7C00 и передает управление по данному адресу. В продолжении цикла статей о загрузке Windows, в данной публикации мы будем рассматривать следующий этап загрузки ОС и рассмотрим логику работы загрузочного сектора раздела PBR Windows 7.

PBR (Partition Boot Record) - это загрузочная запись раздела (партиции), которая является вторым условным этапом запуска операционной системы Windows 7 и выполняет действия по нахождению и загрузке менеджера загрузки (BOOTMGR).

Часто можно встретить альтернативное название загрузочной записи раздела - Volume Boot Record (VBR, загрузочная запись тома) или Partition Boot Sector (PBS, загрузочный сектор раздела). Физически PBR (VBR) размещается в области первого (первых) сектора раздела (партиции). Теоретически, в системах с несколькими разделами записей PBR может быть несколько - по одной записи на каждый раздел (партицию), но практически PBR располагается только на активном (загрузочном) разделе. В случае с операционной системой Windows 7 PBR занимает 9 физических секторов по 512 байт каждый. Некоторые источники утверждают, что сам PBR Windows 7 занимает один сектор, а все остальные 8 секторов относятся уже к коду загрузки BOOTMGR. Это разночтение для нас не столь уж принципиально и мы условимся, что для PBR у раздела резервируются первые последовательные 9 секторов.

Как и ранее, для начала мы получим дамп сектора PBR ОС Windows 7 с помощью специализированной утилиты DMDE, и посмотрим, что же он из себя представляет:

Обратим более пристальное внимание на детали дампа. Первые 2 байта (EB 52) занимает инструкция перехода jmp на 82 байта вперед по коду. Третий байт (90) представляет из себя инструкцию nop (No Op, нет операции), которая ничего не делает и является пустым заполнителем. Спрашивается, зачем нам переход в самом начале сектора? Ответ на этот вопрос заключается в том, что в начале сектора располагаются служебные данные. За командой nop идет сигнатура (OEM ID) NTFS раздела, которая занимает 8 байт, поскольку PBR Windows 7 располагается на разделе с файловой системой NTFS. За сигнатурой располагается BIOS Parameter Block (BPB) для раздела NTFS.

BIOS Parameter Block (BPB) - это специализированная структура данных, располагающаяся в загрузочном секторе раздела, которая описывает схему размещения данных на разделе

Расширенный блок параметров BIOS (BPB NTFS) занимает 73 байта и располагается в области со смещением 000B-0053. Рассмотрим подробнее его структуру:

Смещение Размер Значение Описание
000B 2 байта 00 02 (512) Размер сектора в байтах. 512. Не рекомендуется использоваться другие значения, поскольку в мире уже огромное количество кода использует именно это значение.
000D байт 08 Секторов на кластер. Размер кластера = 512*8 = 4Кб.
000E 2 байта 00 00 Зарезервированные сектора.
0010 1 байт 00 Для NTFS разделов всегда = 0. Количество таблиц FAT для FAT-раздела.
0011 2 байта 00 00 Для NTFS разделов всегда = 0. Максимальное количество записей в корневой директории для FAT12/16.
0013 2 байта 00 00 Для NTFS разделов всегда = 0, поскольку параметр на используется в NTFS. Общее количество секторов раздела.
0015 байт F8 Дескриптор носителя. F8 = Fixed Disk.
0016 2 байта 00 00 Для NTFS разделов всегда = 0. FAT: секторов на таблицу FAT.
0018 2 байта 3F 00 Геометрия диска. Секторов на дорожку.
001A 2 байта FF 00 Геометрия диска. Количество головок.
001C 4 байта 00 08 00 00 (2048) Количество "скрытых секторов". Количество скрытых секторов ОС Windows7/Vista было увеличено до значения 2048.
0020 4 байта 00 00 00 00 в NTFS не используется. Всегда = 0. FAT - старшая часть слова количества секторов.
0024 4 байта 80 00 80 00 в NTFS не используется. Всегда = 80008000. FAT: первый байт - номер диска (80h), второй байт - флаги, третий байт - сигнатура, четвертый байт - зарезервирован.
0028 8 байт FF 1F 03 00 00 00 00 00 Всего секторов в разделе.
0030 8 байт 55 21 00 00 00 00 00 00 Номер стартового кластера для $MFT файла раздела.
0038 8 байт 02 00 00 00 00 00 00 00 Номер стартового кластера для $MFTMirror файла раздела.
0040 знаковое двойное слово F6 00 00 00 Кластеров на каждую файловую запись. Значение знаковое. Поэтому, если старший бит первого байта равен 1, то длина записи равна степени двойки отрицательного значения. Например, если байт = F6, то длина записи равна 210 (потому как 10==-F6). Последние 3 байта всегда 0 для NTFS, не используются.
0044 двойное слово 01 00 00 00 Кластеров на каждый блок индекса. Алгоритм идентичен описанному выше. Последние 3 байта всегда 0 для NTFS, не используются.
0048 8 байт 1A AA 3B C8 C2 3B C8 CC Серийный номер раздела. На самом деле, по команде DIR мы видим не все байты номера.
0050 4 байта 00 00 00 00 Контрольная сумма. Почему всегда 0?

Структуру BPB приведена не спроста, для наглядности, поскольку будет удобнее ссылаться и пояснять кое-какие записи.
Теперь нам необходимо получить исходный код. Я воспользуюсь привычным способом и дизассемблирую полученный нами ранее дамп сектора в виде .bin-файла с помощью дизассемблера IDA. После получения исходного кода самое время приступить к его изучению.
В самом начале сектора PBR Windows 7, как мы уже и говорили, имеется переход по смещению 0000:7C54, это реализовано с целью "перепрыгнуть" BPB NTFS.

  1. переход по адресу 0000:7C54
  2. пустой операнд (ничего не делаем)

Сразу за этим командами следует блок данных BPB NTFS. Его мы уже приводили и увидеть его можно на общем дампе сектора PBR Windows 7, размещенном выше в статье.
Следующий фрагмент кода сектора PBR демонстрирует хорошо уже нам знакомую по предыдущей статье технику инициализации сегментов реального режима.

  1. Запрещаем асинхронные аппаратные прерывания (все прерывания кроме немаскируемых);
  2. Обнуляем регистр ax;
  3. Обнуляем ss при помощи ax;
  4. Указатель стека sp инициализируем в значение 7С00h;
  5. Разрешаем асинхронные аппаратные прерывания.

Затем мы видим блок кода, который при помощи команды возврата из процедуры retf, выполняет переход по адресу 07C0:0066 (он же 0000:7C66). Поэтому, дальнейшая адресация, после данного блока, пойдет у нас уже относительно новой пары сегмента:смещения.

  1. Занесем в стек значение 07C0h;
  2. Восстановим его в ds. то есть ds=07C0h;
  3. Сохраним ds (07C0h);
  4. Занесем в стек значение 0066h;
  5. Команда возврата, извлекающая из стека значения для перехода. Прыгаем на 07C0:0066.

Итак, ранее мы уже условились, что в начиная с текущего блока, код PBR Windows 7 будет адресоваться относительно сегмента и смещения 07C0:0066.
Нижеприведенная часть кода проверяет наличие расширений прерывания int 13h. Почему то это делается повторно, поскольку подобная логика уже работала в коде MBR. Из этого можно заключить, что использования общих блоков кода у PBR и MBR не наблюдается, и можно предположить, что в некоторых ситуациях код PBR выполняется без предварительного исполнения кода MBR?

  1. Запишем значение регистра dl в память по смещению 000Eh в сегменте данных. В dl у нас содержится номер диска. Остался от MBR, либо содержится в dl после передачи управления от BIOS;
  2. Проверяем двойное слово по смещению 0003 на значение "NTFS". Действительно ли это загрузочная запись NTFS?
  3. Если нет - то уходим на вывод ошибки A disk read error occured;
  4. Запишем в регистр ah значение 41h. Это номер функции;
  5. Запишем в регистр bx значение 55AAh. Требуемое условие для функции 41h;
  6. Вызываем функцию прерывания. Определяем наличие расширений.

Код, приведенный далее, проверяет возвращенные функцией 41h прерывания 13h значения. Он отвечает на вопрос: есть ли расширения прерывания работы с диском (int 13h) в системе? Для этого проверяется три условия.

  1. Если carry flag (флаг переноса) не сброшен, то переходим на вывод ошибки A disk read error occured;
  2. Сравним значение в регистре bx с AA55h. Сменилось ли значение с 55AA на AA55? Это один из признаков успешного выполнения функции;
  3. Если не сменилось, то опять уходим на вывод ошибки A disk read error occured;
  4. Проверим нулевой бит регистра cx. Это второе условие наличия расширений;
  5. Если нулевой бит регистра cx не 0 (не сброшен), то продолжаем выполняться и переходим по адресу 07C0:008D. Фактически проверяется условие cx = 1?
  6. Если же 0, то произошла ошибка и снова переходим на процедуру вывода ошибок по адресу 07C0:016A.

Что мы видим в идущем далее фрагменте? Управление на этот участок кода переходит только в случае обнаружения расширений прерывания 13h. Далее, код получает у нас расширенные параметры текущего накопителя для использования этих данных в дальнейшем. Делается это с помощью функции 48h прерывания 13h. Итогом является блок параметров или результирующий буфер (Result Buffer), занимающий 30 байт и содержащий искомые параметры накопителя. Обратите внимание, что в этом фрагменте кода проводятся хитроумные манипуляции со стеком. Для лучшего понимания логики работы этого блока стоит обратить внимание на так называемые "внутренние переменные", которые активно используются кодом на данном этапе. Эти переменные адресуются непосредственно через сегментный регистр ds и в нашем коде имеют вид ds:????:

  • ds:000E - номер диска.
  • ds:000B - размер сектора в байтах.
  • ds:000F - размер сектора в байтах, деленный на 16. Используется для приращения сегментного регистра es.
  • ds:0011 - содержит абсолютный номер сектора для чтения через функцию 42h.
  • ds:0016 - счетчик количества циклов внутри процедуры sub_011D.
  • ds:001C - количество скрытых секторов.

Для начала, я приведу сам блок параметров:

Блок параметров
Смещение Размер Описание
00h..01h 2 байта Размер буфера = 30 = 1Eh
02h..03h 2 байта Информационные флаги
04h..07h 4 байта Количество цилиндров диска (нумерация начинается с 0, поэтому реальных на 1 больше)
08h..0Bh 4 байта Количество головок диска (нумерация начинается с 0, поэтому реальных на 1 больше)
0Ch..0Fh 4 байта Секторов на дорожке (нумерация начинается с 1)
10h..17h 8 байт Общее количество секторов на диске (нумерация начинается с 0, поэтому реальных на 1 больше)
18h..19h 2 байта Байт на сектор
1Ah..1Dh 4 байта (Опционально) Указатель на параметры расширенной конфигурацию диска (Enchanced Disk Drive, EDD)
  1. Сохраним ds в стек;
  2. Вычтем из текущего значения регистра sp значение 18h. Тем самым мы подготовим регистр sp как базу для задания смещения блока в регистре si;
  3. Запишем в стек значение 1Ah. Тем самым мы сместили указатель стека sp еще на 2 байта в минус;
  4. Зададим номер функции (48h) получения расширенного блока параметров диска;
  5. dl = индекс диска. Ранее мы его сохраняли во внутренней переменной ds:000Eh. Теперь восстанавливаем для использования;
  6. si = sp. То есть si = 7BE4. si задает смещение буфера, куда будет помещен блок параметров диска. Таким образом блок размером в 30 байт у нас будет лежать по адресу ds:7BE4;
  7. Сохраним ss в стек;
  8. Восстановим ds из стека. Таким способом мы сравняли ds = ss;
  9. Вызываем функцию прерывания. Считываем блок параметров;
  10. Загружаем младший байт регистра флагов в ah;
  11. Добавляем к sp значение 18h. Тем самым мы выставили значение sp = 7BFC, и не спроста. По данному адресу в стеке у нас лежит параметр "байт на сектор";
  12. Загружаем содержимое ah в младший байт регистра флагов;
  13. Восстановим ax. Теперь в ax у нас получается слово "байт на сектор";
  14. Восстановим ds;
  15. Если carry flag (CF) после вызова прерывания int 13h не сброшен, то произошла ошибка и мы переходим на вывод ошибки A disk read error occured";
  16. Сравним значение ax (байт на сектор, которое вернула функция 48h) со значением слова по смещению ds:000Bh (07C0:000B, или 0000:7C0B), то есть байт на сектор в BPB. Параметры диска совпадают?
  17. Если не равно, то переход на код вывода ошибки A disk read error occured;
  18. Сохраним содержимое регистра ax по адресу 07С0:000Fh (0000:7C0F). То есть мы записываем младший байт параметра "Reserved Sectors" и параметр "Количество таблиц FAT для раздела FAT". Видимо, просто трюк, поскольку последний параметр не нужен и всегда равен нулю для NTFS, его можно подпортить?
  19. Логический сдвиг вправо слова по адресу ds:000Fh на 4 бита. Тем самым мы делим значение на 16. Зачем? Для того, чтобы далее по коду использовать в приращении значения сегментного регистра es, который у нас отвечает за сегментную часть адреса считывания сектора в память;
  20. Сохраним значение сегментного регистра ds в стеке;
  21. Восстановим это значение в регистр dx. dx=ds;
  22. Обнулим регистр bx;
  23. присвоим cx значение 2000h (десятичное 8192). Вероятно, cx у нас будет использоваться как счетчик считываемых байтов. Будем читать сразу 16 секторов. Больше не меньше, нам то нужно всего 8?
  24. Вычтем из cx значение ax. Это сделано для корректирования размера считываемого блока;
  25. Увеличим слово по адресу ds:0011h на единицу. Это незначащее поле BPB для NTFS, у нас используется как абсолютный номер сектора для чтения через функцию 42h. Поскольку начальное значение было нулевым, теперь там 1, тем самым указывая на второй от начала сектор раздела. Ведь первым является сам PBR, поэтому начинать читать мы будем со следующего после него;
  26. Прибавляем к значению регистра dx значение слова по адресу ds:000Fh;
  27. es = dx. Зачем нам в цикле каждый раз менять значение сегмента es? Дело в том, что внутри процедуры чтения сектора sub_011D мы используем сегмент es для задания сегментной части целевого адреса чтения сектора;
  28. Увеличим на единицу слово по адресу ds:0016h. Это незначащее поле BPB для NTFS, вероятно используется как счетчик циклов внутри процедуры sub_011D. После увеличения приобретает значение 1, поскольку изначально поле было нулевым;
  29. Вызываем подпрограмму sub_011D, которая у нас отвечает за чтение секторов;
  30. Вычтем из cx (начальное значение =8192), значение ax (=512). Таким образом каждую итерацию цикла уменьшаем счетчик;
  31. Переход на cs:00C5, если cx > ax. Тем самым образуется цикл чтения секторов по условию.

Код, следующий далее, предназначен для определения наличия TPM версии 1.2. с целью последующего использования. По аналогии с MBR, код PBR Windows 7 активно использует функционал модуля TPM. Это покажется странным, но практически аналогичную проверку мы проводили в коде MBR, однако разработчики решили определить наличие TPM заново? Похоже требуется автономность. Как и ранее, возникает предположение, что это сделано для того, что бы PBR мог грузиться независимо от MBR.

  1. Загружаем в регистр ax номер функции BB00h;
  2. Вызываем прерывание 1Ah. TCG_StatusCheck;
  3. Проверим eax на ноль. Одно из условий;
  4. Если не ноль, то BIOS не поддерживаем спецификацию TCG, выходим из кода TCG и переходим по адресу 010D;
  5. Иначе, если поддержка есть - проверим регистр ebx на значение 41504354h (TCPA), значения расположены в обратном порядке;
  6. Если значение не совпадает, то выход из кода TCG, переход по адресу 010D;
  7. Сравним значение регистра cx с 102h. Версия 1.02?
  8. Если нет, то выход из кода TCG - переход по смещению 010D.

Идущий далее блок кода PBR Windows 7 у нас выполняется в случае поддержки TPM оборудованием и в случае наличия необходимой разработчикам версии TPM - 1.02. Вызывается функция "TCG_CompactHashLogExtendEvent". Более подробно аналогичный участок кода описан в предыдущей статье, посвященной MBR.

Нижележащий блок кода предназначен для заполнения нулями блока данных по адресу es:1028h.

  1. Обнулим регистр ax. Ноль будет использоваться как заполнитель;
  2. Инициализируем регистр di значением 1028h. Начиная с адреса es:di (es:1028h) у нас пойдет циклическая запись нулей;
  3. В cx запишем счетчик байтов - 0FD8h (4056);
  4. Сбросим флаг направления (DF), то есть направление - вперед, регистр di будет увеличиваться каждый раз на единицу;
  5. Выполнить команду инициализации строки байт. Область памяти будет заполнена нулями;
  6. Переход по адресу cs:027Ah. По этому адресу уже располагается дальнейший код PBR, который размещался на диске начиная со второго сектора раздела и был нами считан ранее. Всё, мы ушли!
  7. Нет операции. Простой заполнитель. Резерв.
  8. Нет операции. Простой заполнитель. Резерв.

В конце кода первого сектора PBR Windows 7 у нас размещена подпрограмма считывания секторов диска. Она выполняет "расширенное" чтение секторов с диска. Для чтения используется функция 42h прерывания 13h. Процедура всегда читает один сектор один раз. Заметьте, в отличие от кода MBR, код PBR выполняет чтение только с помощью "расширенной" функции чтения. В процедуре используется несколько внутренних переменных:

  • ds:0011h - содержит абсолютный номер сектора для чтения через функцию 42h. начальное значение =1, то есть начинаем со второго сектора раздела и далее.
  • ds:001Ch - количество скрытых секторов.
  • ds:000Eh - номер диска.
  • ds:000Fh - размер сектора в байтах, деленный на 16. Используется для увеличения сегментного регистра es.
  • ds:0016h - счетчик количества внутренних циклов. всегда равно 1 при входе в процедуру.

Финальная часть кода PBR Windows 7 представляет собой блок вывода сообщений об ошибках, которые могут возникать в процессе исполнения кода. Сам вывод происходит довольно распространенным методом, при помощи функции 0Eh прерывания 10h. После вывода сообщений происходит останов с помощью операнда hlt. "На всякий случай", за ним располагается инструкция перехода по адресу 0176, которая "зацикливает" hlt.

  1. Загружаем указатель на строку (8Ch) из ячейки памяти по адресу ds:01F8h в регистр al;
  2. Вызываем процедуру вывода;
  3. Загружаем указатель на строку (D6h) из ячейки памяти по адресу ds:01FBh в регистр al;
  4. Вызываем процедуру вывода;
  5. После вывода всех ошибок следует останов инструкцией hlt;
  6. На всякий случай, если hlt отработала некорректно (?)- выполняем прыжок по адресу 0176 (вечный цикл);
  7. Сама процедура вывода строки. Тут у нас вычисляется реальный адрес выводимой строки. Делается это следующим образом: заносим в ah значение 1, а в al у нас уже есть значение указателя. Тем самым пара ah, al (или, другими словами регистр ax) примут вид 1XXh, то есть либо 18Ch для первого и 1D6h для второго вызовов процедуры. А эти значения и есть реальные смещения сообщений об ошибках. Стоит заметить так же, что строки начинаются с преамбулы в два байта ODh, 0Ah, "возврат каретки" и "перевод строки" соответственно. Это распространенная техника перевода строки при использовании функций прерывания 10h;
  8. si = ax;
  9. Загрузим байт в al из ds:si;
  10. Сравним al с нулем, не конец ли строки? после каждой строки у нас располагает 0, знаменуя её окончание;
  11. Если достигнут конец строки - выход из процедуры;
  12. ah = 0Eh - функция вывода на экран в стиле телетайпа;
  13. bx = 7 - атрибут вывода;
  14. Вызов функции 0Eh прерывания 10h. Собственно вывод символа на экран;
  15. Цикл на 017D, пока не выведем всю строку по одному байту;
  16. Возврат из процедуры вывода символов на экран.

То есть, в коде, который представлен первым сектором PBR, мы видим вывод всего двух сообщений A disk read error occured и Press Ctrl+Alt+Del to restart. Все остальные сообщения, которые мы могли наблюдать на дампе сектора, вероятнее всего выводятся уже кодом из последующих секторов PBR, анализ которых мы проведем в будущих статьях.

Заключение

На этом анализ кода первого сектора PBR Windows 7 завершен. Стоит отметить, что логика работы не всех частей кода PBR была мною раскрыта должным образом, однако, я надеюсь исправить этот недостаток в будущем. По сути, что сделал у нас код первого сектора PBR Windows 7? Фактически, он загрузил код следующих за ним 8 секторов, которые все вместе составляют код PBR, с носителя в память и передал этому коду управление. На этапе PBR все операции выполняются в реальном, 16-битном режиме работы процессора.

  • Поделиться:

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *