Отправляет email-рассылки с помощью сервиса Sendsay
  Все выпуски  

Пишем свою операционную систему. Простейший загрузчик


Приветствую всех своих подписчиков!

Как вы уже знаете, BIOS загружает первые 512 байт диска и передаёт коду, который там расположен, управление. Разумеется, этого слишком мало, чтобы уместить туда код операционной системы, поэтому основной задачей этого загрузчика является подгрузка остальных частей операционной системы в память. У многих операционных систем даже сам полноценный загрузчик не умещается в один сектор и тогда используется многоэтапная загрузка - boot sector загружает вторичный загрузчик, который уже загружает всё остальное. Я постараюсь писать более-менее универсальный загрузчик, когда же он перестанет помещаться в 1 сектор буду догружать второй, а уж в целый килобайт, думаю, уместимся...

 Функции BIOS для дискового ввода-вывода

Драйвер для какого-либо устройства хранения данных слишком сложная программа, чтобы уместиться в начальный загрузчик, поэтому BIOS предоставляет набор сервисов для работы с дисками. Нас будет интересовать только чтение, ибо начальному загрузчику нет нужды изменять данные. За все дисковые операции отвечает прерывание под номером 0x13. Как обычно, код вызываемой функции указывается в регистре AH. Большинство функций этого прерывания принимают в DL идентификатор диска. Узнать его не составит проблем, ведь предыдущая версия нашего загрузчика сохраняет номер, переданный BIOS, в переменную disk_id. Пока нас интересуют три функции дискового сервиса:

 Номер функции (значение для AH)Описание и параметры 
0x00Сброс дисковой подсистемы. Некоторые носители, такие как дискеты, могут даже в случае исправной работы выдавать ошибки чтения. В этом случае следует пытаться читать несколько раз, прежде чем считать это настоящей ошибкой. Между попытками чтения следует сбрасывать статус с помощью этой функции. 
0x02

Чтение сектора. AL содержит количество секторов для чтения, ES:BX - адрес буфера куда следует поместить прочитанные данные. CH:DH:CL - адрес сектора в формате ЦИЛИНДР:ГОЛОВКА:СЕКТОР. В случае ошибки чтения после вызова этого прерывания будет установлен флаг переноса, который можно проверить командами условного перехода JC и JNC. 

0x08

Определение параметров диска. Рекомендуется обнулить ES:DI, для обхода багов некоторых BIOS. После вызова DL содержит количество дисков в системе, DH - максимальный номер головки, CX - максимальный номер цилиндра и сектора (данные о количестве секторов находятся в 6 младших битах), BL - тип устройства, ES:DI - указатель на таблицу параметров устройства. В случае ошибки, также устанавливается флаг переноса.

Ввиду исторических обстоятельств, сервисы BIOS требуют указания номера сектора в такой неудобной форме (вдобавок адресация ограничена 8 ГБ), нам остаётся только преобразовывать нормальный, линейный номер (называется LBA) сектора в адресацию CHS, используя данные от последней функции.

Реализация подпрограммы чтения сектора 

Начнём с описания дополнительных переменных:

; Данные начального загрузчика
align 4
label sector_per_track word at $$ ; Количество секторов на одной дорожке
label head_count byte at $$ + 2 ; Количество головок у дисковода
label disk_id byte at $$ + 3
 ...

Прежде, чем мы сможем что-либо загружать следует заполнить эти переменные параметрами, полученными от BIOS.

; Запомним номер загрузочного диска
mov [disk_id], dl
; Определим параметры загрузочного диска
mov ah, 0x08 ; Номер функции
xor di, di ; ES и так равен нулю
push es ; Указатель на блок параметров BIOS нам не нужен, а вот нулевой ES - нужен
int 0x13
pop es
jc load_sector.error ; Обработка дисковых ошибок находится в функции, которую мы напишем позднее
inc dh ; Отсчёт номера головки начинается от нуля - чтобы получить количество необходимо прибавить единицу
mov [head_count], dh ; Запоминаем количество головок
and cx, 111111b ; Выделяем 6 младших бит CX
mov [sector_per_track], cx ; Запоминаем количество секторов на дорожке

Теперь у нас достаточно данных для преобразования LBA to CHS. Напишем функцию загрузки одного сектора. Для передачи номера сектора используем пару регистров DX:AX (DX - старшая часть, AX - младшая часть), потому что адрес может быть больше 16 бит. Прочитать сектор будем пытаться 3 раза, если все три раза неудачны, скорее всего, имеет место аппаратная проблема - выводим сообщение об ошибке и ожидаем перезагрузки.

; Загрузка
сектора DX:AX в буфер ES:DI
load_sector:
	push ax bx cx dx si ; Сохраняем все используемые регистры
	div [sector_per_track] ; Делим DX:AX на количество секторов на дорожке (частное от деления будет записано в AX, остаток в DX)
	mov cl, dl ; Остатком является номер сектора - запоминаем в CL
	inc cl ; Сектора в отличии от всего остального отсчитываются от единицы
	div [head_count] ; Делим частное на количество головок у дисковода (в этот раз мы делим на byte, а не word)
	mov dh, ah ; Остатком (который в AH) является номер головки - сохраняем в DH
	mov ch, al ; Частным (которое в AL) является номер дорожки - сохраняем в CH
	mov dl, [disk_id] ; Загружаем в DL сохранённый номер диска
	mov bx, di ; Помещаем в BX смещение буфера. Его сегментная часть и так в ES
	mov al, 1 ; Помещаем в AL количество секторов для чтения - 1.
	mov si, 3 ; Количество попыток чтения
 @@: ; Цикл чтения
	mov ah, 2 ; Номер функции чтения 
	int 0x13 ; Пробуем прочитать
	jnc @f ; Если всё хорошо, выходим из подпрограммы
	xor ah, ah ; Номер функции сброса
	int 0x13 ; Сбрасываем дисковую подсистему
	dec si ; Уменьшаем счётчик попыток
	jnz @b ; Если попытки ещё есть, переходим к началу цикла
 .error: ; Попыток больше не осталось - ошибка чтения
	call error ; Вызываем аварийное завершение загрузчика
	db "DISK ERROR",13,10,0 ; Сообщение об ошибке
 @@:
	pop si dx cx bx ax ; Восстанавливаем регистры
	ret ; Выходим из подпрограммы

Вот мы и написали подпрограмму для удобного чтения секторов диска. Эта процедура применима как к дискетам, так и к жёстким дискам. Возможно, она может прочитать и CD (только будет прочитано соответственно 2 КБ вместо 512 байт), но в этом не уверен, потому что не проверял. Мы не используем возможность чтения нескольких секторов подряд, потому что это приведёт к излишнему усложнению кода начального загрузчика (последовательное чтение возможно лишь в пределах одной дорожки), в котором размер гораздо важнее производительности (грузить в любом случае нам надо не много, разница заметна не будет).

Расширенный дисковый сервис BIOS

Код выше обладает одним значительным недостатком - он умеет загружать только сектора в пределах первых 8 ГБ диска. Для дискет это ограничение не актуально, но жёсткие диски, да и многие флешки давно перевалили за этот порог. Если мы будем работать с файловой системой, то нам необходим доступ ко всему объёму диска. Спустя какое-то время производители BIOS дополнили набор функций прерывания 0x13 так называемым "расширенным дисковым сервисом". Он позволяет указывать при чтении 64-битный линейный адрес. Однако полностью перейти на его использование нельзя - он не поддерживает работу с дискетами, поэтому мы реализуем оба способа чтения, а загрузчик будет сам выбирать подходящий.

Для начала дополним код определения параметров загрузочного диска. Чтобы определить поддержку сервиса существует функция 0x41 прерывания 0x13. Если она устанавливает флаг переноса, то это означает отсутствие поддержки для указанного диска.

        ; Определим параметры загрузочного диска
	mov ah, 0x41 ; Номер функции
	mov bx, 0x55AA ; Сигнатура для вызова
	int 0x13 ; Вызов дискового сервиса
	jc @f ; Если сервис недоступен, то переходим к старому определению параметров
	mov byte[sector_per_track], 0xFF ; Помещаем в sector_per_track невозможное значение
	jmp .disk_detected ; Пропускаем старое определение параметров
 @@:
	mov ah, 0x08
	xor di, di
	push es
	int 0x13
	pop es
	jc load_sector.error
	inc dh
	mov [head_count], dh
	and cx, 111111b
	mov [sector_per_track], cx
 .disk_detected: 

Новый код в случае поддержки расширенного дискового сервиса присваивает переменной sector_per_track значение 0xFF. Такого значения при обычной работе там быть не может - это и будет признаком для load_sector, что надо использовать другой метод для чтения.

За чтение секторов отвечает функция 0x42 прерывания 0x13. Помимо номера диска в DL эта функция принимает в DS:SI указатель на "пакет дискового адреса", который имеет следующую структуру:

СмещениеРазмерОписание
00hbyte Размер структуры (должен быть не меньше 16) 
01hbyteНе используется и должно быть равно нулю 
02hwordКоличество секторов для чтения 
04hdwordАдрес буфера для чтения. Указывается в формате СЕГМЕНТ:СМЕЩЕНИЕ (смещение записывается перед сегментом) 
08hqword64-битный номер сектора  

Модифицируем код load_sector для поддержки расширенного дискового сервиса BIOS:

; Загрузка сектора DX:AX в буфер ES:DI
load_sector:
	cmp byte[sector_per_track], 0xFF ; Проверяем признак использования расширенного чтения
	je .use_EDD ; Переходим в другую часть подпрограммы
	push ax bx cx dx si
	...
	pop si dx cx bx ax
	ret
 .use_EDD:
	push ax dx si ; Сохранение используемых регистров
	mov byte[0x600], 0x10 ; Разместим структуру после области данных BIOS - по адресу 0000:0600
	mov byte[0x601], 0
	mov word[0x602], 1 ; Читаем всё так же 1 сектор
	mov [0x604], di ; Смещение
	push es
	pop word[0x606] ; Сегмент
	mov [0x608], ax ; Младшая часть номера сектора
	mov [0x60A], dx ; Старшая часть номера сектора
	mov word[0x60C], 0 ; Верхние 32 бита
	mov word[0x60E], 0 ; мы не используем
	mov ah, 0x42 ; Код функции
	mov dl, [disk_id] ; Номер диска
	mov si, 0x600 ; Начальный адрес структуры
	int 0x13 ; Обращение к BIOS
	jc .error ; Для жёстких дисков и флешек нет необходимости производить многократные попытки чтения
	pop si dx ax ; Восстанавливаем регистры
	ret ; Возврат из подпрограммы

 Итак, теперь у нас есть универсальная подпрограмма для чтения секторов с любых носителей с возможностью адресации до 2 ТБ данных. Её мы сможем использовать, когда заходим загружать остальные части системы. Например, вот так можно загрузить дополнительные 512 байт загрузчика, если они следуют в следующем секторе.

xor dx, dx
mov ax, 1
mov di, 0x7E00
call load_sector

Полный код загрузчика и заключение

; Начальный загрузчик ядра для архитектуры x86
format Binary as "bin"
org 0x7C00
	jmp boot
; Данные начального загрузчика
align 4
label sector_per_track word at $$
label head_count byte at $$ + 2
label disk_id byte at $$ + 3
boot_msg db "MyOS boot loader. Version 0.04",13,10,0
reboot_msg db "Press any key...",13,10,0
; Вывод строки DS:SI на экран
write_str:
	push ax si
	mov ah, 0x0E
 @@:
	lodsb
	test al, al
	jz @f
	int 0x10
	jmp @b
 @@:
	pop si ax
	ret
; Критическая ошибка
error:
	pop si
	call write_str
; Перезагрузка
reboot:
	mov si, reboot_msg
	call write_str
	xor ah, ah
	int 0x16
	jmp 0xFFFF:0
; Загрузка сектора DX:AX в буфер ES:DI
load_sector:
	cmp byte[sector_per_track], 0xFF
	je .use_EDD
	push ax bx cx dx si
	div [sector_per_track]
	mov cl, dl
	inc cl
	div [head_count]
	mov dh, ah
	mov ch, al
	mov dl, [disk_id]
	mov bx, di
	mov al, 1
	mov si, 3
 @@:
	mov ah, 2
	int 0x13
	jnc @f
	xor ah, ah
	int 0x13
	dec si
	jnz @b
 .error:
	call error
	db "DISK ERROR",13,10,0
 @@:
	pop si dx cx bx ax
	ret
 .use_EDD:
	push ax dx si
	mov byte[0x600], 0x10
	mov byte[0x601], 0
	mov word[0x602], 1
	mov [0x604], di
	push es
	pop word[0x606]
	mov [0x608], ax
	mov [0x60A], dx
	mov word[0x60C], 0
	mov word[0x60E], 0
	mov ah, 0x42
	mov dl, [disk_id]
	mov si, 0x600
	int 0x13
	jc .error
	pop si dx ax
	ret
; Точка входа в начальный загрузчик
boot:
	; Настроим сегментные регистры
	jmp 0:@f
 @@:
	mov ax, cs
	mov ds, ax
	mov es, ax
	; Настроим стек
	mov ss, ax
	mov sp, $$
	; Разрешим прерывания
	sti
	; Выводим приветственное сообщение
	mov si, boot_msg
	call write_str
	; Запомним номер загрузочного диска
	mov [disk_id], dl
	; Определим параметры загрузочного диска
	mov ah, 0x41
	mov bx, 0x55AA
	int 0x13
	jc @f
	mov byte[sector_per_track], 0xFF
	jmp .disk_detected
 @@:
	mov ah, 0x08
	xor di, di
	push es
	int 0x13
	pop es
	jc load_sector.error
	inc dh
	mov [head_count], dh
	and cx, 111111b
	mov [sector_per_track], cx
 .disk_detected:
	; Завершение
	jmp reboot
; Пустое пространство и сигнатура
rb 510 - ($ - $$)
db 0x55,0xAA 

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

В следующем выпуске нам предстоит разобраться с файловыми системами. Все вопросы и предложения, как обычно, можно направлять на мой личный адрес. До встречи! 


В избранное