Уроки ОСдева №3: блок параметров BIOS

В прошлый раз мы разобрались с физической геометрией дискеты и расположением данных. Кроме того, мы узнали, как записать программу-загрузчик в нулевой сектор носителя. Если кто-то из вас действительно пытался повторить мои действия и потом использовать дискету как загрузочную, то наверняка обнаружил два неприятных момента:

1. При попытке открыть образ дискеты Wndows говорит, что она не отформатирована.
2. BIOS не воспринимает дискету как загрузочную и пишет что-нибудь вроде "no bootable device found".
Давайте разбираться. Если помните, в конце прошлого поста наша будущая программа-загрузчик выглядела так:
.386p
CSEG segment use16
ASSUME cs:CSEG, ds:CSEG, es:CSEG, fs:CSEG, gs:CSEG, ss:CSEG
begin:                    cli
                             hlt
CSEG ends
end begin

По сути это просто заглушка, которая при запуске должна останавливать процессор. Причина ругани Виндоус в том, что в нулевом секторе на отформатированном носителе хранится важная структура данных - блок параметров BIOS (BPB). Записав туда же нашу программу, мы его пот+ёрли. Для того, чтобы этого избежать, нам придётся воссоздать BPB в тексте программы. Для этого нужно знать геометрию носителя. К счастью, в наше время сохранился только один тип дискет.

Непроверенный метод: возможно, если с помощью утилиты debug записать программу не в начало сектора, а со смещением, достаточным, чтобы пропустить оригинальный BPB, то он не пострадает, но я не проверял. По-моему, debug всё равно забьёт остатки сектора каким-то мусором.

Во-первых, измените программу вот так:

.386p
CSEG segment use16
ASSUME cs:CSEG, ds:CSEG, es:CSEG, fs:CSEG, gs:CSEG, ss:CSEG
begin:                     jmp short execute
                             nop
execute:                 cli
                             hlt
CSEG ends
end begin

У нас появились две новые инструкции: jmp short и nop. Последняя - просто пустой оператор. Процессор пропускает его, не выполняя никаких действий. Занимает 1 байт. jmp - инструкция перехода. jmp short - переход в пределах 127 байт от текущего положения. Исполняется гораздо быстрее jmp, так что везде где возможно - используйте его. Занимает 2 байта. execute - название метки, на которую указывает инструкция jmp short.

Зачем всё это и зачем nop? BPB должен располагаться строго в трёх байтах от начала нулевого сектора. Эти три байта и занимают инструкции jmp short execute и nop. Таким образом, когда программа начнёт исполняться, первой инструкцией, которую выполнит процессор, будет пропустить BPB. В противном случае он бы попытался исполнить его как код, что привело бы к катастрофе.
Теперь давайте вставим сам блок параметров BIOS между nop и меткой execute.

.386p
CSEG segment use16
ASSUME cs:CSEG, ds:CSEG, es:CSEG, fs:CSEG, gs:CSEG, ss:CSEG
begin:                     jmp short execute
                             nop

                BPB_OEMname          db    'BOOTDISK'
                BPB_bytespersec       dw    512
                BPB_secperclust        db     1
                BPB_reserved            dw    1
                BPB_numFATs           db     2
                BPB_RDentries          dw    224
                BPB_sectotal             dw    2880
                BPB_mediatype         db     0F0h
                BPB_FATsize             dw     9
                BPB_secpertrack        dw     18
                BPB_numheads         dw     2
                BPB_hiddensec          dd     0
                BPB_sectotal32          dd     0
                EBPB_drivenum         db     0
                EBPB_NTflags            db     0
                EBPB_extsign            db     29h
                EBPB_volID               dd     0
                EBPB_vollabel            db     'BOOTLOADER '
                EBPB_filesys              db     'FAT12   '

execute:                 cli
                             hlt

CSEG ends
end begin

BPB - это блок данных, и здесь мы впервые объявляем переменные. В TASM это выглядит так: BPB_OEMname (имя) -пробел- db, dw, dd или dq -пробел- 'BOOTDISK' (значение). Имени может и не быть, но тогда к переменной нужно будет обращаться по смещению, это не очень удобно. DB, DW, DD и DQ - сокращение от define byte (word, double word или quad word) - обозначают размер переменной. Соответственно, 1, 2, 4 или 8 байт. Инстркция этого типа позволяют объявлять целые серии значений через запятую: myvalue dw 2, 5, 165, 776. С помощью инструкции db можно объявлять строки: mytext db 'Allo, Yoba!' Обратите внимание, что в плоском бинарнике переменные при компиляции не выносятся в какую-то специальную область данных. В исполняемом файле они будут именно там, где вы их объявили в тексте программы. Ещё важный момент: имена переменных только для вашего личного пользования, в исполняемый файл они не попадут, так что вы не обязаны копировать названия у меня. Теперь давайте посмотрим, что за информация хранится в BPB.

BPB_OEMname - 8 байт: по идее здесь должно быть название производителя, но по факту вы можете писать что угодно, никто на это значение не смотрит.
BPB_bytespersec - 2 байта: размер сектора в байтах, для дискет как правило 512.
BPB_secperclust - 1 байт: число секторов в кластере. Про кластеры мы поговорим позже, но в случае с дискетами секторы и кластеры соответствуют друг другу.
BPB_reserved - 2 байта: число зарезервированных секторов, недоступных файловой системе. В нашем случае такой один, это наш загрузочный сектор.
BPB_numFATs - 1 байт: количество FAT (file allocation table), таблиц распределения файлов. Так как носители информации (особенно дискеты) подвержены порче, а FAT - очень важная часть файловой системы, для неё часто делается резервная копия.
BPB_RDentries - 2 байта: количество записей в корневой директории (Root Directory). Про корневую директорию тоже будем говорить в другой раз, но пока можете представить её как список файлов с указанием их физического расположения на носителе.
BPB_sectotal - 2 байта: число секторов на диске, если их не больше 65535. Если больше, здесь должен быть 0.
BPB_mediatype - 1 байт: тип носителя. F0 - код для 3,5-дюймовой дискеты с 18 секторами в дорожке.
BPB_FATsize - 2 байта: размер одной FAT в секторах.
BPB_secpertrack - 2 байта: число секторов в дорожке.
BPB_numheads - 2 байта: число головок.
BPB_hiddensec - 4 байта: количество скрытых секторов перед загрузочным, в нашем случае 0.
BPB_sectotal32 - 4 байта: число секторов, если их больше 65535. Если меньше, здесь должен быть 0.

Здесь стандартный BIOS Parameter Block заканчивается и начинается расширенный, который появился в поздних версиях DOS.

EBPB_drivenum - 1 байт: бесполезная переменная, хранящая номер привода, в который был вставлен носитель при форматировании.
EBPB_NTflags - 1 байт: флаги Вин НТ. Если установлен бит 0, необходимо проверить носитель на битые секторы. Значения других флагов не знаю.
EBPB_extsign - 1 байт: признак расширенного BPB. Для нашей версии должно быть 29h.
EBPB_volID - 4 байта: случайный номер, который присваивается при форматировании. В общем бесполезен.
EBPB_vollabel - 11 байт: имя носителя.
EBPB_filesys - 8 байт: имя файловой системы.

Если вы теперь заново скомпилируете программу и запишите на дискету, то она отлично откроется в Windows. Первая проблема решена, но осталась вторая: дискета всё ещё не опознаётся как загрузочная. Вспоминаем: для этого последние 2 байта загрузочного сектора должны иметь значения AAh и 55h. Добавим ещё две строчки в нашу программу:

.386p
CSEG segment use16
ASSUME cs:CSEG, ds:CSEG, es:CSEG, fs:CSEG, gs:CSEG, ss:CSEG
begin:                     jmp short execute
                             nop

                BPB_OEMname          db    'BOOTDISK'
                BPB_bytespersec       dw    512
                BPB_secperclust        db     1
                BPB_reserved            dw    1
                BPB_numFATs           db     2
                BPB_RDentries          dw    224
                BPB_sectotal             dw    2880
                BPB_mediatype         db     0F0h
                BPB_FATsize             dw     9
                BPB_secpertrack        dw     18
                BPB_numheads         dw     2
                BPB_hiddensec          dd     0
                BPB_sectotal32          dd     0

                EBPB_drivenum         db     0
                EBPB_NTflags            db     0
                EBPB_extsign            db     29h
                EBPB_volID               dd     0
                EBPB_vollabel            db     'BOOTLOADER '
                EBPB_filesys              db     'FAT12   '

execute:                 cli
                             hlt

               org 510
                dw 0AA55h

CSEG ends
end begin

Команда org 510 заполнит нулями место от текущей позиции до 510 байта, а в последние два мы поместили метку загрузочного сектора. Вуаля, проблема 2 решена.

Выражалось мнение, что всё это ебучее легаси и современные пацаны предпочитают UEFI, но UEFI не даст вам того интимного, я бы сказал, понимания железа, на котором работает ваша ось, так что основная серия будет продолжена по старинке.