Разбор dos-подобной операционной системы. OzonOS
Встретил на Хабре очередную статью об написании «простой операционной системы с нуля» и решил поделится своими потугами на эту тему.
Немного предыстории
В далеком уже 2011 мне в руки каким-то чудом попала книга «Ассемблер. Экспресс курс» за авторством Александра Панова. После Паскаля , изучаемого в школе, ассемблер показался мне языком неограниченных возможностей. После того как я вдоволь наигрался со всякими огоньками и прочими бегущими символами мне захотелось создать что-то крутое. А что может быть круче чем собственная ОС? Так в новогодние каникулы с 2011–2012 гг появилась Ozon Os.
В этих статьях я хочу ознакомить вас с Ozon OS и фактически переписать её заново , исправив страшные косяки :)
И начнем конечно же с начального загрузчика.
OzonOS предполагается размещать на флоппи-диске (если кто-то еще помнит что это такое), потому что загрузка с флоппи наиболее проста — нам доступны функции BIOS для чтения секторов, а fat12 наиболее простая файловая система.
Здесь и далее будем использовать ассемблер fasm и эмулятор/отладчик bochs.
Процесс загрузки OzonOs выглядит так
Начальный загрузчик читает с дискеты таблицы ROOT и FAT, загружает с дискеты «дисковый драйвер» и ядро ос, устанавливает для 0×25 прерывания вектор «дискового драйвера» и передает управление ядру.
И первая сложность при работе с дискетой — режим адресации секторов:
Для прерывания BIOS нужно указывать адрес в виде CHS (Cylinder, Head, Sector — цилиндр, головка, сектор), А в таблице FAT адреса секторов указываются в формате LBA (Logical block addressing), поэтому для начала нам нужно написать процедуру чтения сектора по LBA которая будет пересчитывать LBA в CHS и читать сектор куда нам нужно.
;Читает сектор по LBA
;BX = номер сектора в LBA
;ES:DI буффер
readsect:
push es
push ds
pusha
mov ax,bx
mov cx, 18
mov bx, di
xor dx, dx div
cx ;Делим ax на 18
mov ch, al ;ch = ax div 18
shr ch, 1 ;сh = ch div 2
mov cl, dl
inc cx
mov dh, al
and dh, 1
mov ax, 0x201
mov dl, [ADrive]
int 0x13
popa
pop ds
pop es
ret
Процедура чтения секторов у нас есть , теперь можем прочитать таблицы FAT и ROOT.
На флоппи они идут сразу после boot сектора и занимают 34 сектора.
Читать будем по адресу (физическому) 0×500 или сегментный адрес 0050:0000 (смотрите метод адресации памяти в реальном режиме) — там как раз заканчивается область данных BIOS
;Читаем таблицы FAT и ROOT
push 0x050
pop es
xor di,di
mov bx,1
mov cx,18+15+1
@@:
call readsect
add di,0x200
dec cx
inc bx
cmp cx,0 jne @b
Хорошо — таблицы загружены.
Теперь нам нужно загрузить файлы ядра и «драйвера диска».
Для этого нужно:
В таблице ROOT найти по имени файла соответствующую запись и извлечь номер первого кластера
Прочитать соответствующую цепочку кластеров
Кластер — это группа секторов , для fat12 на флоппи справедливо 1 кластер=1 сектор. , только нумерация кластеров смещена на 0×1f относительно нумерации секторов
Структура записи корневого каталога выглядит так:
Процедура поиска и извлечения номера первого кластера в загрузчике OzonOs выглядит так:
;Поиск первого кластера файла
;ES:DI = указатель на имя файла
;Вывод - BX - первый кластер
Get_First_Claster:
push ds
pusha
push es
push 0x0050
pop ds
mov si,0x2400
@@: mov cx,11
push di
push si
repe cmpsb
pop si
pop di
add si,0x20
cmp si,0x5000
ja ErrorGFC ;Если корневой каталог кончился
test cx,cx
jnz @b
sub si,06
push word [ds:si]
pop ds
add si,2
push word [ds:si]
pop es
popa
mov bx,ds
mov cx,es
pop ds
pop es
ret
Для чтения цепочки нужна процедура извлечения номера следующего кластера. Сложность здесь в том что элемент FAT12 занимает 12 бит , или 1.5 байта, соответственно для определения смещения нужны некоторые вычисления.
;Выдает следующий за BX кластер
Get_next_claster:
push es
push ds
pusha
mov ax,bx ;копируем значение в ах
shr bx,1 ;если кластер не четный то в CF=1
sbb cx,cx ;Если CF=1 то CX=-1
add bx,ax ;Умножаем на три
push 0050h ;Сегмент FAT
pop es
xor si,si ;Смещение FAT
add si,bx
mov ax,word [es:si]
and cl,4
shr ax,cl
and ax,0FFFh
push ax
pop ds
popa
mov bx,ds
pop ds
pop es
ret
Теперь наконец то сможем прочитать файл «драйвера диска» и установить соответствующий вектор прерывания
;Ищем первый кластер
push cs
pop es
mov di,FileName_1
call Get_First_Claster
;Если bx=-1 значит не нашли
cmp bx,-1
je ErrorMesg ;Сообщение что не нашли
push bx
mov di,OkMesg
call print
pop bx
push 0x060 ;Дисковый драйвер
pop es ;по адресу
xor di,di ;0060h:0000h
;читаем файл
@@:
push bx
add bx,1fh ;LBA = CLASTER + 1Fh
push es
push di
call readsect ;читаем сектор
pop di
pop es
pop bx
add di,0x200
call Get_next_claster ;Ищем следующий кластер
cmp bx,0FFFh ;Если кластер не последний
jne @b ;То читаем его
;Установим обработчик прерывания 25h
cli ;Запретим прерывания
pushf ;Сохраним флаги
push 0
pop es
mov di,0x25*4
mov [es:di],word 0 ;Смещение 0000h
mov ax,0x060
mov di,0x25*4+2
mov [es:di],word ax ;Сегмент 0060h
popf ;Восстановим регистр флагов
sti ;Разрешим прерывания
И здесь кроется первый серьезный косяк — таблицы ROOT и FAT были загружены по адресу 0050:0000, а дисковый драйвер грузится по адресу 00060:0000.
То есть дисковый драйвер перекрывает системные таблицы , ведь они занимают 34 сектора, т.е. 17 Кб, а если перевести сегментные адреса в физические то разница между ними:
0×600 — 0×500 = 0×100 = 256 байт.
Что бы исправить это необходимо грузить дисковый драйвер по адресу
0×500 + 0×22*0×200 = 0×4900
что соответствует сегментному адресу 0490:0000, так что заменим в коде 0×060 на 0×0490.
Теперь можем прочитать ядро. В изначальном виде оно загружалось по статическому сегментному адресу 0070:0000, что опять же затирало системные таблицы (да и дисковый драйвер).
Для простоты сместим ядро на 4 кб выше дискового драйвера, т.е. на сегментный адрес 0590:0000.
Так как к этому моменту у нас уже загружен дисковый драйвер то можем воспользоваться его функциями :
mov ax,0x590
mov ds,ax
xor si,si
mov ax,cs
mov es,ax
mov di,kernelname
mov ah,08h ;Функция 0x8 - чтение файла
int 0x25
и далее остается только настроить регистры и передать управление ядра
;Настроим Регистры
mov ax,0x590
mov ds,ax
mov es,ax
mov ss,ax
mov sp,0FFFEh
call 0x590:0000
И получаем :
После компиляции исходника получается файл fddboot.img который отлично открывается программой ultraiso с помощью которой мы сохраняем на нашу виртуальную дискету необходимые для загрузки файлы.
Ссылка на архив с полным исходником и загрузочным образом