Pull to refresh

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

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

Вступление


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


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


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


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

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


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


Чтение файла


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

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

Articles