MBR

Этой статьей я хочу помочь всем, кто начинает разбираться с созданием операционных систем и около того, а именно с написанием MBR. В этой статье я постараюсь рассказать о создании MBR.bin на практике, и вообще описать все это как можно более подробно и интересно. Ну и писать мы, конечно же, будем на FASM'е.

О загрузчиках загрузчиков

Загрузка системы начинается с того, что BIOS после успешного окончания процедуры POST считывает первый физический сектор жесткого диска, размещает его в памяти по адресу 0000:7С00h и передает сюда управление.

Этот сектор называется Главной Загрузочной Записью (Master Boot Record - сокращенно MBR). В начале MBR расположен машинный код загрузчика, за ним идет Таблица Разделов (Partition Table), описывающая схему разбиения логических дисков. В конце загрузочного сектора находится сигнатура 55AAh, говорящая BIOS'у о том, что это действительно MBR, а не что-то еще.

Загрузчик должен проанализировать Таблицу Разделов, найти предпочтительный логический диск, считать его первый сектор (он называется загрузочным - boot) и передать ему управление. Можно сказать, что он загружает загрузчик ОС.

Смещение Размер Назначение
000h 1BEh Код загрузчика
1BEh 040h Таблица разделов
1FEh 002h Сигнатура 55AAh

Структура MBR

Рассмотрим таблицу разделов. Она представляет собой 64 байтную структуру, в свою очередь разделенную на 4 по 16 байт каждая. Каждая из этих структур описывает отдельный раздел. Но это конечно не означает что на диске может быть до 4 разделов. Каждый раздел может указывать на целую цепочку разделов. Такие разделы называются расширенными.

Смещение Размер Назначение
1BEh 10h Раздел 1
1CEh 10h Раздел 2
1DEh 10h Раздел 3
1EEh 10h Раздел 4

Структура Partition Table

Раздел диска может загрузочным или не загрузочным. Флаг загрузочности раздела, находящийся в самом начале структуры описывающей раздел может иметь одно из 2 значений, это либо 00h, либо 80h. 80h обозначает возможность загрузки ОС с данного раздела, 00h - ее отсутсутвие.

Далее идут координаты начала раздела в формате CHS (Цилиндр, головка, сектор). Их мы рассматривать не будем, так как формат CHS устарел и может адресовать только 8 ГБайт дискового пространства. CHS адрес занимает три байта или 24 бита, что при длине сектора в 512 байт дает 512 * 224 = 8 ГБайт. Вместо этого мы будем использовать LBA адресацию, последовательно нумерующую все сектора. LBA адрес занимает 32 бита, что дает нам 512 * 232 = 2 Тбайт. Чтобы использовалась LBA адресация нужно установить начало раздела CHS - FFFFFFh и конец раздела CHS - FFFFFFh.

В поле код типа раздела нужно положить значение указывающее ОС какая файловая система используется. Не должно быть равно 00h, так как BIOS не станет загружать в память такой MBR.

Смещение Размер Назначение
Раздел 1 Раздел 2 Раздел 3 Раздел 4
1BEh 1CEh 1DEh 1EEh 01h Флаг загрузочности раздела
1BFh 1CFh 1DFh 1EFh 01h Начало раздела — головка
1C0h 1D0h 1E0h 1F0h 02h Начало раздела — сектор (биты 0-5), цилиндр (биты 6-15)
1C2h 1D2h 1E2h 1F2h 01h Код типа раздела
1B3h 1C3h 1D3h 1E3h 01h Конец раздела — головка
1C4h 1D4h 1E4h 1F4h 02h Конец раздела — сектор (биты 0-5), цилиндр (биты 6-15)
1C6h 1D6h 1E6h 1F6h 04h Смещение первого сектора
1CAh 1DAh 1EAh 1FAh 04h Количество секторов раздела

Структура для описания раздела

Начнем писать...

Со структурой MBR разобрались, теперь приступим к формированию 512 байтного бинарника. Первое, что нам нужно сделать - написать код загрузчика. Потом мы должны составить таблицу разделов диска и добавить сигнатуру 55AAh. Сейчас мы попробуем написать простой загрузчик, выводящий сообщения об этапах своей работы.

Итак, вот наш MBR:

; эти 2 директивы не занимают места в программе.
; первая указывает компилятору прибавлять ко всем меткам 7C00h (BIOS загружает MBR по этому адресу)
; вторая заставляет компилятор генерировать 16 битный код
org 7C00h
use16
 ; определяем стек на 768 байт впереди начала кода (стек растет вниз, размер MBR 512 байт)
 mov ax, cs
 mov ds, ax
 mov ss, ax
 mov sp, 7F00h
 ; устанавливаем видеорежим BIOS'ом (00h int 10h)
 ;  вход:
 ;   al - режим (00h - текстовый черно - белый 16 цветов 40x25)
 mov al, 00h
 mov ah, 00h
 int 10h
 ; выводим строку (Functions.ink)
 push MBR_Loaded
 push 000Ah
 push 0103h
 call BIOSprintstring
 ; ждем нажатия Enter (код 0Dh) (Functions.ink)
 push 000Dh
 call BIOSreadkey
 ; читаем сектора в память (LBA) BIOS'ом (42h int 13h)
 ;  вход:
 ;   dl - номер диска
 ;   si - смещение структуры для LBA чтения
 mov dl, 80h
 lea si, [lba]
 mov ah, 42h
 int 13h
 ; если ошибка чтения
 jc error
 ; в si считанное с диска по смещению 7FEh
 mov si, word [ds:8DFEh]
 ; если это 66FFh (байты в обратном порядке) то выводим сообщение, что нашли
 cmp si, 0FF66h
 jz found
 ; иначе - выводим сообщение об ошибке
 jmp error
found:
 ; выводим строку (Functions.ink)
 push Boot_Found
 push 0011h
 push 0203h
 call BIOSprintstring
 ; переходим на выполнение загрузчика
 jmp 0000:8600h
error:
 ; выводим строку (Functions.ink)
 push Error
 push 0005h
 push 0203h
 call BIOSprintstring
 ; бесконечный цикл (jmp на эту же строку)
 jmp $
; здесь описаны используемые функции
include 'BIOSFunctions.ink'
; выводимый текст
MBR_Loaded db 'MBR loaded'
Boot_Found db 'Boot sector found'
Error db 'Error'
; структура для LBA чтения
lba:
; +----------------------------------------------------------------------------------+
; | Смещение   Тип                         Назначение                                |
; |                                                                                  |
; |   00h     byte                      размер структуры                             |
; |   01h     byte                       зарезервировано                             |
; |   02h     word                   сколько секторов читать                         |
; |   04h     dword        адрес буфера в формате сегмент(word):смещение(word)       |
; |   08h     qword             стартовый номер сектора для чтения                   |
; |   10h     qword  хвост 64 битного адреса (используется при 32х битном FFFF:FFFF) |
; +----------------------------------------------------------------------------------+
db 10h
db 00h
dw 0004h
dd 00008600h
dq 0000000000000001h
dq 0000000000000000h
; эта директива заставляет компилятор заполнить оставшееся (до 446-и байт) место нулями
times 1BEh-($-$$) db 00h
; таблица разделов
; +----------------------------------------------------------------------------------+
; | Смещение   Тип                         Назначение                                |
; |                                                                                  |
; |   00h     byte    признак активного раздела (00h - неактивный, 80h - активный)   |
; |   01h     byte                 стартовая головка раздела                         |
; |   02h     word            стартовые номера цилиндра и сектора                    |
; |   04h     byte                      код типа раздела                             |
; |   05h     byte                 конечная головка раздела                          |
; |   06h     word             конечные номера цилиндра и сектора                    |
; |   08h     dword     абсолютный номер начального сектора раздела (CHS=FFFFFFh)    |
; |   0Ch     dword             число секторов в разделе (CHS=FFFFFFh)               |
; +----------------------------------------------------------------------------------+
db 80h
db 0FFh
db 0FFh
db 0FFh
db 01h
db 0FFh
db 0FFh
db 0FFh
dd 00000001h
dd 00100000h
; остальные 3 раздела заполняем нулями
times 48 db 00h
; эти 2 байта нужны чтобы BIOS определил что это действительно MBR
db 055h, 0AAh

Рассмотрим теперь этот код.

Так как BIOS загружает MBR по адресу 0000:7C00h то необходимо поставить org 7C00h, чтобы компилятор правильно выставлял смещение при использовании меток и символов $ (текущее смещение кода) и $$ (адрес в памяти с которого начинается код). Загрузчик выполняется в реальном (16-разрядном) режиме, значит ставим use16, чтобы генерировать 16-разрядный код.

Сначала нужно настроить сегменты кода, данных и стека. Данные и стек у нас будут располагаться в одном сегменте с кодом. Установим стек на 256 байт, нам этого вполне хватит. Для этого установим sp на 768 (300h) байт после начала кода (на 256 от конца), так как стек растет вниз.

Далее мы функцией 00h прерывания int 10h устанавливаем текстовый режим. В al должен находится номер режима. У нас это самый первый текстовый режим 00h.

Номер режима Тип Формат Цвет
00h Текст 40x25 Черно-белый 16 цветов
01h Текст 40x25 Цветной 16 цветов
02h Текст 80x25 Черно-белый 16 цветов
03h Текст 80x25 Цветной 16 цветов
04h Графика 320x200 Цветной 4 цвета
05h Графика 320x200 Черно-белый 4 цвета
06h Графика 640x200 Черно-белый 2 цвета
07h Текст 80x25 Черно-белый 2 цвета

Режимы int 10h

Далее мы используем две своих функции, которые разберем позднее - вывод строки и ожидание нажатия клавиши.

Выводим строку мы функцией BIOSprintstring. Просто передаем ей смещение строки (word), длину строки (word), X строки в символах (byte) и Y строки в символах (byte). Так как класть в стек byte нельзя, а X и Y - это byte, то обьединяем их в word. Для нашей функции удобно в стек положить word, где старший байт - Y, а младший - X.

После вывода строки подождем пока пользователь нажмет Enter. Для этого используем функцию BIOSreadkey. В нее мы передаем ASCII код Enter'a - 0Dh.

Теперь, как загрузчик мы должны считать с диска в память boot sector. Делаем это функцией 42h прерывания int 13h. В dl должен находится номер диска для чтения 80h - первый диск, 81h - второй и так далее. В si мы должны положить смещение структуры для LBA чтения. Ее мы составим позднее.

Смещение Размер Назначение
00h byte Размер структуры
01h byte Зарезервировано
02h word Сколько секторов читать
04h dword Адрес буфера в формате сегмент(word):смещение(word)
08h qword Стартовый номер сектора для чтения
10h qword Хвост 64 битного адреса (используется при 32х битном FFFF:FFFF)

Структура для LBA чтения

Если чтение по какой то причине не удалось BIOS установит флаг переноса. В этом случае прыгаем на error, которая выведет сообщение об ошибке на 2й строке (на 1й - 'MBR loaded') и повесит машину.

Если чтение успешно - проверяем загружен ли boot sector в память. Для этого мы как и BIOS проверим последние 2 байта считанного. Пусть это будет 66FFh. Считываем два байта (а считывать мы будем в память по адресу 8600h) в si по смещению 7FEh (пусть boot sector занимает 2 килобайта) от начала boot sector'а и сравниваем их с FF66h (байты считываются в обратном порядке). Если байты совпадут, то выводим сообщение, что boot sector загружен на 2й строке (на 1й - 'MBR loaded') и переходим на его выпонение командой jmp. Если нет, то прыгаем на error, которая выводит сообщение об ошибке и уходит в бесконечный цикл.

Далее идет код функций, которые выводят сообщения о нахождении boot sector'а и об ошибке. Впринципе их работа уже описана выше, нужно только сказать, что они тоже работают через BIOSprintstring.

На этом основной исполняемый код MBR заканчивается. Но это еще не все. Мы же еще не рассмотрели функции BIOSprintstring и BIOSreadkey, а также структуру для LBA чтения. Следующая строка по коду - include 'BIOSFunctions.ink', значит пора рассмотреть BIOSreadkey и BIOSprintstring которые, как видно из названия используют прерывания BIOS.

Чтобы сэкономить место (512 байт не резиновые) вынесем часто используемые прерывания в функции, которые в свою очередь поместим в .ink файл, который подключим в конце нашего MBR.

Вот как у нас будет выглядеть файл BIOSFunctions.ink:

; ждем нажатия определенной клавиши
;  в стеке:
;   sp - точка возврата
;   sp+2 - код нужной клавиши
BIOSreadkey:
 ; читаем код нажатой клавиши BIOS' ом (00h int 16h)
 ;  выход:
 ;   al - код нажатой клавиши
 mov ah, 00h
 int 16h
 ; в регистр bp заносим смещение стека для кода клавиши
 mov bp, sp
 add bp, 02h
 ; если та клавиша, то возвращаемся
 cmp al, byte [ss:bp]
 ; иначе снова читаем
 jnz BIOSreadkey
 ; освобождаем стек на 2 байта
 retn 02h
; выводим строку
;  в стеке:
;   sp - точка воврата
;   sp+01h - y
;   sp+02h - x
;   sp+04h - длина строки
;   sp+06h - смещение строки (в сегменте кода)
BIOSprintstring:
 ; выводим строку BIOS' ом (13h int 10h)
 ;  вход:
 ;   es:bp - что выводить
 ;   cx - длина строки
 ;   dx - куда выводить (dl - x, dh - y)
 ;   bx - настройки отображения (bh - видеостраница, bl - аттрибут)
 ;   al - режим (01h - перевести курсор в конец строки)
 pop si
 pop dx
 pop cx
 mov ax, cs
 mov es, ax
 pop bp
 ; не передаваемые в функцию параметры
 mov bh, 00h
 mov bl, 0Fh
 mov al, 01h
 mov ah, 13h
 int 10h
 ; восстановим точку возврата
 push si
 retn

Здесь описаны функции, используемые в нашем загрузчике.

Начнем с BIOSprintstring. Она извлекает переданный код клавиши из стека, сравнивает его со считанным с клавиатуры и если они совпадут возвращает управление вызвавшему коду, если нет - повторяет эти действия снова.

Итак, функция BIOSreadkey ждет нажатия клавиши, переданной параметром функции или, проще говоря, через стек. Но так как передавать через стек byte нельзя, передаем word, а потом читаем байт по адресу sp+02h.

Думаю стоит обьяснить почему именно sp+02h. В первых двух байтах (так как код 16-и разрядный) у нас точка возврата из функции. Так как байты кладутся в память (а стек - это память) в обратном порядке, то переданный байт, который мы кладем в стек последним (передаем код клавиши как 00XXh) окажется спереди.

Читаем символ с клавиатуры мы функцией 00h прерывания int 16h. После вызова прерывания в al будет помещен код нажатой клавиши.

Теперь вывод строки - функция BIOSprintstring. Она получает смещение строки, длину строки, строку и столбец начала строки, выводит строку и возвращает управление вызвавшему коду.

Здесь мы получаем данные из стека инструкцией pop. Поэтому сохраняем точку возврата. Сохраняем мы ее в регистр si, так как регистр bp занят для прерывания в функции.

Строку выводим функцией 13h прерывания int 10h. В es:bp должен находится адрес выводимой строки. В cx кладем длину выводимой строки. В регистре dx должна быть позиция строки на экране. В dl - позиция по X, а в dh - по Y. В bx настроки отображения строки на экране. В bh - видеостраница, в bl - аттрибут строки. В al у нас код подфункции.

Так как мы передаем только смещение, длину, X и Y строки, остальное заполняет сама функция. Здесь установлена нулевая видеостраница, белый цвет строки, перемещение курсора в конец строки.

Al Назначение
00h Использовать аттрибут в bl, не трогать курсор
01h Использовать аттрибут в bl, перевести курсор в конец строки
02h Использовать формат строки символ,аттрибут; символ,аттрибут; ..., не трогать курсор
03h Использовать формат строки символ,аттрибут; символ,аттрибут; ..., перевести курсор в конец строки

Подфункции функции 13h прерывания int 10h

Биты Назначение
0 - 2 Цвет символа
3 Яркость
4 - 6 Цвет фона
7 Мерцание

Аттрибут символа

Номер Цвет RGB Образец
00h (000b) Черный #000000 Hello World!
01h (001b) Синий #0000AA Hello World!
02h (010b) Зеленый #00AA00 Hello World!
03h (011b) Cине - Зелёный #00AAAA Hello World!
04h (100b) Красный #AA0000 Hello World!
05h (101b) Пурпурный #AA00AA Hello World!
06h (110b) Коричневый #AA5500 Hello World!
07h (111b) Светло - Серый #AAAAAA Hello World!

Цвета текста (еще 8 цветов можно получить установив бит яркости)

Функции рассмотрели - на этом код заканчивается. Остались только структуры данных.

Сначала здесь идут выводимые MBR строки, здесь мы видим строки на английском языке, так как русских символов у нас нет следующего содержания: 'MBR загружен', Boot sector найден' и 'Ошибка'.

Теперь рассмотрим нашу LBA структуру. Здесь указан размер структуры 10h байт, читать 4 сектора, то есть наш boot sector будет иметь размер 2 килобайта, считанное поместить в память по адресу 8600h и стартовый сектор для чтения - первый (MBR - нулевой).

Далее идет директива times, которая указывает компилятору заполнить оставшееся (до 1BEh байт) место нулями. Количество нулевых байтов здесь равно 1BEh (то есть все место, отводимое по код загрузчика) минус разница в стартовом адресе программы в памяти и текущего смещения кода (то есть место, занятое кодом).

Код загрузчика закончился официально. Заполняем таблицу разделов. Указываем, что данный раздел загрузочный, CHS адресацию мы использовать не будем - заполняем все ее поля FFh, код типа раздела устанавливаем 01h - важно, чтобы здесь был не ноль. И выставляем раздел от первого до произвольного сектора. Остальные 3 раздела заполним нулями директивой times.

Все! Последние 2 байта указывают BIOS'у, что это действительно MBR.