Обновить

Чуть больше о загрузке самодельных ОС — пишем bootloader

Системное программирование
Не так давно решил чуть получше изучить архитектуру IA-32. А что лучше всего для запоминания? Конечно же практика. Но программируя в ОС мы врядли получим самый низкий уровень доступ к железу без помех. Поэтому для этих целей будем писать собственное подобие операционной системы. То есть проще говоря будем выполнять свой код, сразу после загрузки BIOS'а.
Первой проблемой с которой столкнется желающий программировать на низком уровне — как же загрузить свой код?

Вступление


Обычно в BIOS'е есть список устройств с которых он пытается загрузиться, перебирая по очереди. Этот список как правило состоит из дисковода, CD-привода, жесткого диска. Загрузка с дискеты и CD-диска почти не отличается — для совместимости в загрузочную область диска, помещается образ дискеты, который потом копируется в память и выступает в роли виртуального привода. А т.к. загрузка с дискеты является самой простой, ее и будем использовать.
После того как BIOS обнаружит все устройства, и выполнит все необходимое для себя, он грузит первый сектор дискеты в память по адресу 0000:7C00 и передает туда управление. Вот тут мы и встречаемся с первой проблемой — размер сектор на дискеты размером всего 512 байт, и мы должны уложиться в эти рамки, чтоб загрузить весь остальной код. Если и это вам еще кажется много, скажу что для совместимости из них еще ~60 уходит на сервисные цели. Можно конечно и выкинуть их, но тогда дискета может не видеться в системах, и копировать на нее файлы будет затруднительно.
Для упрощения добавим эти данные позже. Сразу оговорюсь, что весь приведенный код будет приводиться в синтаксисе FASM.
Итак начнем с самого простого, получения управления и вывода текста.
  1. Use16
  2. org 0x7C00
  3. start:
  4. cli ; Запрещаем прерывания
  5. mov ax, cs ; Инициализируем сегментные регистры
  6. mov ds, ax
  7. mov es, ax
  8. mov ss, ax
  9. mov sp, 0x7C00 ; Т.к. стек растет в обратную сторону, то код не затрется
  10. mov ax, 0xB800
  11. mov gs, ax ; Использовал для вывода текста прямой доступ к видеопамяти
  12. mov si, msg
  13. call k_puts
  14. hlt ; Останавливаем процессор
  15. jmp $ ; И уходим в бесконечный цикл
  16. k_puts:
  17. lodsb
  18. test al, al
  19. jz .end_str
  20. mov ah, 0x0E
  21. mov bl, 0x07 ; Серый на черном
  22. int 0x10
  23. jmp k_puts
  24. .end_str
  25. ret
  26. msg db 'Hello world', 0x0d, 0x0a, 0
  27. times 510-($-$$) db 0
  28. db 0x55, 0xaa


Если Вы думаете что на этом мы закончим, как в сотнях других примерах, то я Вас разочарую, а может и обрадую — наш загрузчик будет искать на диске файл и загружать его.
Для начала расскажу про разметку дискеты под FAT12.
Первый сектор отводится под сервисные данные — блок параметров BIOS (BPB), а так же под загрузочный код.

Блок параметров BIOS


BPB в нашем случае будет выглядет так:
jmp	start							; прыжок на наш код
	db 0
	BS_OEMName	db 'MicLib  '				; любой текст
	BPB_BytsPerSec	dw 0x200				; байт в секторе
	BPB_SecPerClus	db 1					; секторов в кластере
	BPB_RsvdSecCnt	dw 1					; число зарезервированных секторов
	BPB_NumFATs	db 2					; число таблиц FAT
	BPB_RootEntCnt	dw 0x00E0				; число записей в корневом дереве
	BPB_TotSec16	dw 0x0B40
	BPB_Media	db 0xF0
	BPB_FATSz16	dw 9					; размер FAT в секторах
	BPB_SecPerTrk	dw 0x12					; секторов на дорожке
	BPB_NumHeads	dw 2					; число читающих головок
	BPB_HiddSec	dd 0
	BPB_TotSec32	dd 0


Для дальнейшей работы с дискетой и файлами напишем несколько вспомогательных процедур.
Первая из них — чтение одного сектора по абсолютному адресу, т.е. сектор будет задаваться не набором параметров головка, кластер, сектор, а одним порядковым числом.

  1. ;
  2. ; Процедура чтения сектора дискеты по абсолютному номеру
  3. ;
  4. ; Вход:
  5. ; dx - абсолютный номер сектора
  6. ; si - адрес буффера
  7. ;
  8. k_read_sector:
  9. ;S = N mod 18 + 1
  10. ;T = N / 18
  11. ;H = T mod 2
  12. ;C = T / 2
  13. pusha
  14. mov ax, dx
  15. mov cx, [BPB_SecPerTrk]
  16. mov bx, si
  17. xor dx, dx ; Начиная отсюда делаем пересчет по формулам выше
  18. div cx
  19. mov ch, al
  20. shr ch, 1
  21. mov cl, dl
  22. inc cx
  23. mov dh, al
  24. and dh, 1
  25. mov ax, 0x0201
  26. xor dl, dl
  27. int 0x13
  28. jnc @f ; Если флаг C выставлен, то произошла ошибка
  29. mov si, msgErrorRead
  30. call k_puts ; Сообщим об этом
  31. @@:
  32. popa
  33. ret


Для тех кто не знаком с FASM поясню про метки для прыжков.
@@ — универсальная метка, может встречаться сколь угодно раз в коде;
@b — прыжок на первую метку @@ вверх по коду (back);
@f — прыжок на первую метку @@ дальше по коду (forward);


Следующая процедура — надстройка над этой, и будет считывать сразу несколько подряд секторов. Можете высказать недовольство, а где же проверки? Проверок на корректность номеров секторов я не делаю для экономии места.

  1. ;
  2. ; Процедура последовательного чтения нескольких секторов
  3. ;
  4. ; Вход:
  5. ; dx - начальный сектор
  6. ; cx - сколько секторов подряд читать
  7. ; si - адрес памяти, куда читать
  8. ;
  9. k_read_sectors:
  10. push dx
  11. push cx
  12. @@:
  13. call k_read_sector
  14. inc dx
  15. add si, [BPB_BytsPerSec]
  16. dec cx
  17. jnz @b ; Читаем пока не 0
  18. pop cx
  19. pop dx
  20. ret


Как видите ничего сложного пока нет, а функционала прибавляется.
Теперь собственно расскажу, что нам понадобится для чтения файла. Для начала считаем в память всю таблицу FAT и корневой каталог.

Таблица FAT и корневой каталог


Вся таблица FAT12 состоит из записей по 12 бит(!), которые обьединены в цепочки. Числа указывают абсолютный адрес сектора. Читаем до тех пор пока числом не окажется 0xFFF — это конец цепочки.
Т.е. если файл занимает 513 байт, то под него выделится 2 сектора, хоть второй и будет занят одним байтом только.
Теперь что касается таблицы главного каталога — она состоит из 32-байтовых записей, в которой содержатся все данные о файле.
Вот её формат:

+0	11	Имя файла в формате 'ИИИИИИИИРРР'
		Имя файла длиной 8 символов, если короче - заполняется пробелами. Точки-разделителя нет.
		Расширение в 3байт
+0Bh	1	Атрибуты файла: 
			01h – Только чтение 
			02h – Скрытый 
			04h – Системный 
			08h – Метка тома 
			10h – Директория 
			20h – Архив 
+0Ch	10	Зарезервировано 
+16h	2	Время создания или модификации в формате filetime 
+18h	2	Дата создания или модификации в формате filetime 
+1Ah	2	Номер первой записи цепочки в FAT
+1Ch	4	Размер

Одна особенность — при удалении файлов сами записи не удаляются, а всего лишь первый байт имени заменяется на символ 0xE5.
Файлов с нулевой длиной быть не может — т.к. таким образом обозначаются папки, а по смещению +1Ah записывается номер первой записи вложенных в каталог файл.
Первые две из которых — . и .., которые соответственно указывают на первую запись текущего каталога, и родительского.

Напишем еще две совсем маленькие процедуры — которы будут читать обе таблицы в память

  1. ;
  2. ; Процедура читает таблицу FAT в память
  3. ;
  4. k_read_fat:
  5. mov dx, 1 ; Размещается сразу за бут-сектором
  6. mov cx, [BPB_FATSz16] ; 9 секторов
  7. mov si, FAT
  8. call k_read_sectors
  9. ret
  10. ;
  11. ; Процедура читает корневой каталог в память
  12. ;
  13. k_read_root_dir:
  14. mov dx, 19 ; 1 + 9*2
  15. mov cx, 15
  16. mov si, ROOT
  17. call k_read_sectors
  18. ret


Чтение файла


Теперь для чтения в память файла, осталось собрать все что мы написали, т.е. найти запись о нем в корневом каталоге, и по номерам секторов из FAT считать в память.
Собственно эта операция заняла больше всего времени и кода.

  1. ;
  2. ; Процедура читает файл с дискеты в память
  3. ;
  4. ; Вход:
  5. ; di - адрес буффера
  6. ; si - имя файла строго в формате NNNNNNNNEEE
  7. ; Выход:
  8. ; ax - 0 если файл не найден, 1 - найден
  9. ;
  10. k_read_file:
  11. push di
  12. mov di, ROOT
  13. mov cx, 0xE0 ;BPB_RootEntCnt
  14. .next_item:
  15. mov al, byte [di]
  16. cmp al, 0xE5 ;Метка удаленного файла
  17. je .space_item
  18. cmp al, 0 ;Пустая запись
  19. je .space_item
  20. push di
  21. push si
  22. push cx
  23. mov cx, 11 ;8+3
  24. repe cmpsb ;Сравниваем имя файла с искомым
  25. cmp cx, 0
  26. pop cx
  27. pop si
  28. pop di
  29. je .read_file ;break
  30. .space_item:
  31. add di, 32 ;Длина записи
  32. loop .next_item
  33. xor ax, ax
  34. ;jmp .end_of_file
  35. ret
  36. .read_file:
  37. pop si
  38. mov bp, word [di+0x1A] ;Номер начальной ячейки FAT
  39. mov bx, word [di+0x1C] ;Размер файла
  40. .read_next_claster:
  41. pusha
  42. mov dx, bp
  43. sub dx, 3
  44. add dx, 0x22
  45. call k_read_sector
  46. popa
  47. cmp di, 0xFFF
  48. je .end_of_file
  49. mov di, bp
  50. mov ax, bp ; сохраняем для проверки на четность
  51. mov bx, bp ; сохраняем на случай если будет 0xFFF
  52. imul di</f
Теги:bootloaderx86assembler
Хабы: Системное программирование
Рейтинг +109
Количество просмотров 12,3k Добавить в закладки 173
Комментарии
Комментарии 49

Похожие публикации

Лучшие публикации за сутки