Не так давно решил чуть получше изучить архитектуру IA-32. А что лучше всего для запоминания? Конечно же практика. Но программируя в ОС мы врядли получим самый низкий уровень доступ к железу без помех. Поэтому для этих целей будем писать собственное подобие операционной системы. То есть проще говоря будем выполнять свой код, сразу после загрузки BIOS'а.
Первой проблемой с которой столкнется желающий программировать на низком уровне — как же загрузить свой код?
Обычно в BIOS'е есть список устройств с которых он пытается загрузиться, перебирая по очереди. Этот список как правило состоит из дисковода, CD-привода, жесткого диска. Загрузка с дискеты и CD-диска почти не отличается — для совместимости в загрузочную область диска, помещается образ дискеты, который потом копируется в память и выступает в роли виртуального привода. А т.к. загрузка с дискеты является самой простой, ее и будем использовать.
После того как BIOS обнаружит все устройства, и выполнит все необходимое для себя, он грузит первый сектор дискеты в память по адресу 0000:7C00 и передает туда управление. Вот тут мы и встречаемся с первой проблемой — размер сектор на дискеты размером всего 512 байт, и мы должны уложиться в эти рамки, чтоб загрузить весь остальной код. Если и это вам еще кажется много, скажу что для совместимости из них еще ~60 уходит на сервисные цели. Можно конечно и выкинуть их, но тогда дискета может не видеться в системах, и копировать на нее файлы будет затруднительно.
Для упрощения добавим эти данные позже. Сразу оговорюсь, что весь приведенный код будет приводиться в синтаксисе FASM.
Итак начнем с самого простого, получения управления и вывода текста.
Если Вы думаете что на этом мы закончим, как в сотнях других примерах, то я Вас разочарую, а может и обрадую — наш загрузчик будет искать на диске файл и загружать его.
Для начала расскажу про разметку дискеты под FAT12.
Первый сектор отводится под сервисные данные — блок параметров BIOS (BPB), а так же под загрузочный код.
BPB в нашем случае будет выглядет так:
Для дальнейшей работы с дискетой и файлами напишем несколько вспомогательных процедур.
Первая из них — чтение одного сектора по абсолютному адресу, т.е. сектор будет задаваться не набором параметров головка, кластер, сектор, а одним порядковым числом.
Для тех кто не знаком с FASM поясню про метки для прыжков.
@@ — универсальная метка, может встречаться сколь угодно раз в коде;
@b — прыжок на первую метку @@ вверх по коду (back);
@f — прыжок на первую метку @@ дальше по коду (forward);
Следующая процедура — надстройка над этой, и будет считывать сразу несколько подряд секторов. Можете высказать недовольство, а где же проверки? Проверок на корректность номеров секторов я не делаю для экономии места.
Как видите ничего сложного пока нет, а функционала прибавляется.
Теперь собственно расскажу, что нам понадобится для чтения файла. Для начала считаем в память всю таблицу FAT и корневой каталог.
Вся таблица FAT12 состоит из записей по 12 бит(!), которые обьединены в цепочки. Числа указывают абсолютный адрес сектора. Читаем до тех пор пока числом не окажется 0xFFF — это конец цепочки.
Т.е. если файл занимает 513 байт, то под него выделится 2 сектора, хоть второй и будет занят одним байтом только.
Теперь что касается таблицы главного каталога — она состоит из 32-байтовых записей, в которой содержатся все данные о файле.
Вот её формат:
Одна особенность — при удалении файлов сами записи не удаляются, а всего лишь первый байт имени заменяется на символ 0xE5.
Файлов с нулевой длиной быть не может — т.к. таким образом обозначаются папки, а по смещению +1Ah записывается номер первой записи вложенных в каталог файл.
Первые две из которых — . и .., которые соответственно указывают на первую запись текущего каталога, и родительского.
Напишем еще две совсем маленькие процедуры — которы будут читать обе таблицы в память
Теперь для чтения в память файла, осталось собрать все что мы написали, т.е. найти запись о нем в корневом каталоге, и по номерам секторов из FAT считать в память.
Собственно эта операция заняла больше всего времени и кода.
Первой проблемой с которой столкнется желающий программировать на низком уровне — как же загрузить свой код?
Вступление
Обычно в BIOS'е есть список устройств с которых он пытается загрузиться, перебирая по очереди. Этот список как правило состоит из дисковода, CD-привода, жесткого диска. Загрузка с дискеты и CD-диска почти не отличается — для совместимости в загрузочную область диска, помещается образ дискеты, который потом копируется в память и выступает в роли виртуального привода. А т.к. загрузка с дискеты является самой простой, ее и будем использовать.
После того как BIOS обнаружит все устройства, и выполнит все необходимое для себя, он грузит первый сектор дискеты в память по адресу 0000:7C00 и передает туда управление. Вот тут мы и встречаемся с первой проблемой — размер сектор на дискеты размером всего 512 байт, и мы должны уложиться в эти рамки, чтоб загрузить весь остальной код. Если и это вам еще кажется много, скажу что для совместимости из них еще ~60 уходит на сервисные цели. Можно конечно и выкинуть их, но тогда дискета может не видеться в системах, и копировать на нее файлы будет затруднительно.
Для упрощения добавим эти данные позже. Сразу оговорюсь, что весь приведенный код будет приводиться в синтаксисе FASM.
Итак начнем с самого простого, получения управления и вывода текста.
-
- Use16
- org 0x7C00
- start:
- cli ; Запрещаем прерывания
- mov ax, cs ; Инициализируем сегментные регистры
- mov ds, ax
- mov es, ax
- mov ss, ax
- mov sp, 0x7C00 ; Т.к. стек растет в обратную сторону, то код не затрется
-
- mov ax, 0xB800
- mov gs, ax ; Использовал для вывода текста прямой доступ к видеопамяти
-
- mov si, msg
- call k_puts
-
- hlt ; Останавливаем процессор
-
- jmp $ ; И уходим в бесконечный цикл
-
- k_puts:
- lodsb
- test al, al
- jz .end_str
- mov ah, 0x0E
- mov bl, 0x07 ; Серый на черном
- int 0x10
-
- jmp k_puts
-
- .end_str
- ret
-
- msg db 'Hello world', 0x0d, 0x0a, 0
-
- times 510-($-$$) db 0
- 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
Для дальнейшей работы с дискетой и файлами напишем несколько вспомогательных процедур.
Первая из них — чтение одного сектора по абсолютному адресу, т.е. сектор будет задаваться не набором параметров головка, кластер, сектор, а одним порядковым числом.
-
- ;
- ; Процедура чтения сектора дискеты по абсолютному номеру
- ;
- ; Вход:
- ; dx - абсолютный номер сектора
- ; si - адрес буффера
- ;
- k_read_sector:
- ;S = N mod 18 + 1
- ;T = N / 18
- ;H = T mod 2
- ;C = T / 2
-
- pusha
-
- mov ax, dx
- mov cx, [BPB_SecPerTrk]
- mov bx, si
-
- xor dx, dx ; Начиная отсюда делаем пересчет по формулам выше
- div cx
- mov ch, al
- shr ch, 1
- mov cl, dl
- inc cx
- mov dh, al
- and dh, 1
- mov ax, 0x0201
- xor dl, dl
- int 0x13
- jnc @f ; Если флаг C выставлен, то произошла ошибка
- mov si, msgErrorRead
- call k_puts ; Сообщим об этом
- @@:
-
- popa
- ret
-
Для тех кто не знаком с FASM поясню про метки для прыжков.
@@ — универсальная метка, может встречаться сколь угодно раз в коде;
@b — прыжок на первую метку @@ вверх по коду (back);
@f — прыжок на первую метку @@ дальше по коду (forward);
Следующая процедура — надстройка над этой, и будет считывать сразу несколько подряд секторов. Можете высказать недовольство, а где же проверки? Проверок на корректность номеров секторов я не делаю для экономии места.
-
- ;
- ; Процедура последовательного чтения нескольких секторов
- ;
- ; Вход:
- ; dx - начальный сектор
- ; cx - сколько секторов подряд читать
- ; si - адрес памяти, куда читать
- ;
- k_read_sectors:
- push dx
- push cx
- @@:
- call k_read_sector
-
- inc dx
- add si, [BPB_BytsPerSec]
-
- dec cx
- jnz @b ; Читаем пока не 0
-
- pop cx
- pop dx
- 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 записывается номер первой записи вложенных в каталог файл.
Первые две из которых — . и .., которые соответственно указывают на первую запись текущего каталога, и родительского.
Напишем еще две совсем маленькие процедуры — которы будут читать обе таблицы в память
-
- ;
- ; Процедура читает таблицу FAT в память
- ;
- k_read_fat:
- mov dx, 1 ; Размещается сразу за бут-сектором
- mov cx, [BPB_FATSz16] ; 9 секторов
- mov si, FAT
- call k_read_sectors
- ret
-
- ;
- ; Процедура читает корневой каталог в память
- ;
- k_read_root_dir:
- mov dx, 19 ; 1 + 9*2
- mov cx, 15
- mov si, ROOT
- call k_read_sectors
- ret
-
Чтение файла
Теперь для чтения в память файла, осталось собрать все что мы написали, т.е. найти запись о нем в корневом каталоге, и по номерам секторов из FAT считать в память.
Собственно эта операция заняла больше всего времени и кода.
-
- ;
- ; Процедура читает файл с дискеты в память
- ;
- ; Вход:
- ; di - адрес буффера
- ; si - имя файла строго в формате NNNNNNNNEEE
- ; Выход:
- ; ax - 0 если файл не найден, 1 - найден
- ;
- k_read_file:
- push di
- mov di, ROOT
- mov cx, 0xE0 ;BPB_RootEntCnt
- .next_item:
- mov al, byte [di]
- cmp al, 0xE5 ;Метка удаленного файла
- je .space_item
- cmp al, 0 ;Пустая запись
- je .space_item
- push di
- push si
- push cx
- mov cx, 11 ;8+3
- repe cmpsb ;Сравниваем имя файла с искомым
- cmp cx, 0
- pop cx
- pop si
- pop di
-
- je .read_file ;break
- .space_item:
- add di, 32 ;Длина записи
- loop .next_item
-
- xor ax, ax
- ;jmp .end_of_file
- ret
-
- .read_file:
- pop si
- mov bp, word [di+0x1A] ;Номер начальной ячейки FAT
- mov bx, word [di+0x1C] ;Размер файла
-
- .read_next_claster:
- pusha
- mov dx, bp
- sub dx, 3
- add dx, 0x22
- call k_read_sector
- popa
-
- cmp di, 0xFFF
- je .end_of_file
-
- mov di, bp
- mov ax, bp ; сохраняем для проверки на четность
- mov bx, bp ; сохраняем на случай если будет 0xFFF
- imul di</f