sfw
nsfw

OSDev

Подписчиков:
66
Постов:
10

ОСдев №10: основной загрузчик, часть 3. GDT.

Подготовительный этап закончен, можно приступать к интересному. Кроме собственно загрузки файлов наша программа должна подготовить для ОС рабочую среду. Это значит: переключить процессор в 32/64-битный режим, настроить таблицу прерываний и создать базовую GDT. Сегодня разберёмся с последним пунктом.
GDT - сокращение от Global Descriptor Table, глобальной таблицы дескрипторов. Что это такое? По сути - набор записей одинакового формата, описывающих области памяти и разрешения, которые они имеют. Упрощённо это выглядит так:
//область 0
адрес области
размер области
параметры доступа
флаги
//область 1
адрес области
размер области
параметры доступа
флаги
...
В 32/64-битном режиме дескрипторные таблицы используются вместо старой схемы адресации сегмент:смещение. Зачем? Сегмент:смещение - небезопасная технология, которая позволяет переписать любой участок памяти. Надо ли говорить, что при неосторожном обращении это легко может закончиться бедой? Дескрипторная таблица даёт возможность ограничить запись или выполнение кода в отдельных областях RAM. Дескрипторные таблицы появились как часть аппаратной защиты памяти вместе с 286 процессором.
Как это работает? После выхода из 16-битного режима процессор больше не принимает адреса в формате сегмент:смещение. Если попытаетесь так сделать - получите исключение GPT (General Protection Fault). Вместо этого в сегментном регистре процессор ожидает получить смещение дескриптора внутри ДТ. При этом для операций над этим сегментом будут действовать правила, указанные в дескрипторе. Например, попытка обратиться к памяти за пределами сегмента или запись в защищённый от записи сегмент будут вызывать исключения (кстати, про обработку исключений поговорим позже, пока давайте примем, что это фатальная ошибка, которая приведёт к остановке программы).
Кроме глобальной таблицы дескрипторов существуют ещё локальные (ЛДТ), TSS и таблицы дескрипторов прерываний (IDT). Для того, чтобы наша ОС могла начать работу, обязательно наличие только двух таблиц: GDT и IDT. Давайте теперь взглянем на GDT поподробнее. Скажу сразу, зрелище будет не очень приятное. Но начнём с лёгкого. Так как GDT - часть аппаратной схемы защиты памяти, у неё есть свой регистр: GDTR. Это 48-битный регистр, 4 байта которого предназначены для смещения GDT, а 2 - для её размера. Таким образом, GDT не может быть больше 65536 байтов в размере. Размер записи в GDT - 8 байтов, значит,< таблица может иметь максимум 8192 дескриптора. Зная всё это, хорошим тоном было бы сразу зарезервировать 64К под GDT, но в моей архитектуре ОС создаёт свои таблицы, так что сейчас я обойдусь минимумом. Минимум в данном случае - 3 дескриптора. Нулевой, сегмент кода и сегмент данных. Зачем отдельно выделять нулевой дескриптор? Дело в том, что обращение к нему в GDT приводит к, вы угадали, исключению. Это тоже своего рода мера предосторожности.
А теперь время взглянуть на структуру дескриптора. И тут, увы, наследие тяжёлого прошлого во всей красе. Ради обратной совместимости в кодом для старых процессоров дескриптор GDT превратили в кашу.
Первые два байта - это первые 16 битов границы сегмента.
Следующие три байта - первые 24 бита основания сегмента.
Следующий байт - параметры доступа. Рассмотрим ниже.
Следующий байт совмещает в себе биты 16-19 границы и флаги. Об этом тоже подробнее ниже.
Ну и последний байт - биты 24-31 основания.
Неудобно? Не то слово. Когда будем писать ядро - обязательно замутим процедуру для комфортной работы с этим месивом. К счастью, сейчас у нас статичная структура всего из трёх сегментов, так что заполнить можно и вручную. Создадим и подключим модуль GDT.inc. Как это сделать, мы рассматривали в прошлый раз. И добавим в него вот такую таблицу:

Это и есть наша GDT, ничего ужасного. Значения в нулевом дескрипторе для нас не важны, а вот остальные давайте рассмотрим подробнее. У нас есть два дескриптора: один - для кода, другой - для данных. Оба начинаются с 0 и занимают FFFFF*4Kib = 4Gib. Фактически это значит, что, начав работать, ОС сможет использовать всю память по своему усмотрению. Давайте теперь разберём параметры доступа и флаги.
Бит 1 - флаг чтения/записи. Его значение различается для сегментов кода и данных.
Для сегментов кода установленный флаг означает, что чтение разрешено.
Запись в сегменты кода запрещена всегда.
Для сегментов данных установленный флаг означает, что разрешена запись. Чтение из сегментов данных
Таким образом, значение параметров доступа 10010010b даёт нам вот что: это сегмент данных, запись в него разрешена, сегмент растёт вверх, уровень привилегий - ring0. А теперь флаги. Биты 0-3 здесь заняты границей сегмента, не обращаем на них внимания.
Бит 4 зарезервирован и должен быть равен 0.
Бит 5 указывает на 64-битный сегмент. Так как мы пока переходим в 32-битный, должен быть равен 0.
Бит 6 указывает на 32-битный сегмент. Наш выбор, устанавливаем в 1.
Бит 7 - гранулярность. Если равен 0, то значение границы сегмента используется как
Окей, теперь у нас есть GDT. Но как указать системе, что её нужно использовать? Процессор ведь не дурак, сам искать не станет. Всё просто, джентльмены из IBM в кои-то веки о нас позаботились. При помощи специальной ассемблерной команды lgdt (load GDT) мы можем передать в регистр GDTR линейный адрес таблицы и её размер. Для этого добавим перед GDT такую структуру:
60ТК_р1:г:
dы 00171п	;Размер таблицы - 1 (23 байта).
dd 00000000И	;Абсолютный адрес таблицы.,программирование,geek,Прикольные гаджеты. Научный, инженерный и  айтишный юмор,OSDev,Операционная система,ассемблер,разработка,длиннопост
Размер таблицы мы уже знаем, а вот адрес придётся посчитать, так что пока оставим 0 и напишем процедуру инициализации GDT:
init_GDT ргос
init_GDT endp
push eax pushfd
xor eax,eax
mov ax,offset GDT
add eax,00000500h
mov dword ptr [GDTR_ptr+2],eax
cli
lgdt pword ptr GDTR_ptr sti
popfd pop eax ret,программирование,geek,Прикольные гаджеты. Научный, инженерный и  айтишный юмор,OSDev,Операционная система,ассемблер
На случай, если тут не всё очевидно, поясню. Мы помещаем в EAX смещение GDT относительно сегмента, а потом добавляем адрес сегмента*16. Это и есть линейный адрес, сохраняем его в структуре. После этого отключаем прерывания, передаём структуру процессору командой lgdt и включаем прерывания обратно. По идее прерывания можно не трогать, так как в 16-битном режиме GDT не используется, но я перестраховщик.
Собственно, на этом всё. Добавьте вызов init_GDT в конец загрузчика перед cli и дело в шляпе. Сегодня без картинки, но вот вам котик.
Чистая дискета: https://drive.google.com/file/d/1Bold4ds8oEruHQ7fJZKHglVo7A2Vc5MR/view?
Предыдущие части:

ОСдев №9: основной загрузчик, часть 2. Работа с дисплеем при помощи функций BIOS.

Дисклеймер: эта серия постов не про UEFI. Это не значит, что я не знаю о существовании UEFI. Про UEFI будет отдельная серия постов. Почему я не пишу про UEFI прямо сейчас? Потому что UEFI - это уровень абстракции над железом, а мне интересно именно железо и работа с ним.
Продолжаем? Сейчас наш загрузчик второго уровня работает в "немом" режиме - без возможности подать сигнал об ошибке или выполнении операции. Это необходимо исправить. Самое очевидное решение - вывод информации на дисплей. Мы уже условились, что на нынешнем этапе для работы нашей ОС будет необходима VGA-совместимая карта и дисплей, так что вправе рассчитывать на их наличие.
Программирование VGA-контроллера - сложная штука. Однажды мы ею обязательно займёмся, но сейчас, раз уж мы всё ещё в Реальном режиме, есть вариант попроще: функции BIOS. Функции, связанные с работой дисплея, доступны через прерывание 10h. Мы уже пользовались им для вывода текста в первичном загрузчике, но так как теперь мы не ограничены в размере программы, функционал можно будет расширить.
Первое, что нам стоит сделать - установить нужный видеорежим на случай, если BIOS этого не сделала. Кроме того, понадобятся функции считывания положения курсора, прокрутки экрана и вывода строки. Весь код, связанный с вводом/выводом будет храниться в отдельном файле. У меня он называется io.inc. Мы ещё не использовали подключаемые файлы, но ничего сложного тут нет: в TASM они объявляются директивой include, после которой идёт путь и имя файла. Единственная тонкость тут в том, что подключенный файл не будет вынесен в какую-то изолированную область памяти, как это делается в языках высокого уровня, а окажется в исполняемом файле именно там, где был объявлен. Поэтому лучше объявлять подключаемые файлы где-нибудь в конце, за пределами основного кода.
VGA имеет набор стандартных режимов отображения, с которым можно ознакомиться тут:
Нас интересует режим номер 3 - 80х25 символов, 16 цветов. Для его включения создадим в файле io.inc процедуру set_vmode3. Её полный текст будет выглядеть так:
set_vmode3 proc
                                  push ax
                                  push bx
                                  pushf

                                  xor ax,ax
                                  mov ah,0Fh
                                  int 10h
                                  cmp al,03h
                                  je @@exit_good

                                  mov ax,0003h
                                  int 10h

                                  xor ax,ax
                                  mov ah,0Fh
                                  int 10h
                                  cmp al,03h
                                  jne @@exit_bad

@@exit_good:          mov byte ptr vmode,al
                                  mov byte ptr vcol,ah
                                  mov byte ptr vrow,19h
                                  mov byte ptr vpage,bh

                                  popf
                                  clc
                                  pop bx

                                  pop ax
                                  ret

@@exit_bad:            popf
                                  stc
                                  pop bx
                                  pop ax
                                  ret
set_vmode3 endp
Начинается процедура, как и почти всегда у меня, с сохранения состояния используемых регистров в стеке. Это нетипичный для ассемблера подход. Обычно, создавая низкоуровневый код, программист стремиться максимально оптимизировать использование инструкций, по возможности обходясь без обращений к памяти, использования стека и вызова процедур. К сожалению, при работе над большим проектом это невозможно, и я на горьком опыте выяснил, что лучше потратить лишние циклы на инкапсуляцию процедуры, чем потом мучительно отлавливать баг, который появился из-за того, что какой-то регистр внепланово изменил значение.
                    xor ax,ax                    mov ah,0Fh
                    int 10h
                    cmp al,03h
                    je @@exit_good
Этот блок нужен для того, чтобы остановить выполнение процедуры, если режим 3 уже установлен. Сначала регистр AX обнуляется, чтобы мы точно знали его значение. Функция 15 (0Fh) прерывания 10h возвращает в регистре AL номер установленного режима. Далее мы сравниваем результат с нужным значением (3), и если они равны, то переходим к завершению процедуры, метка @@exit_good. Если режим 3 не установлен, продолжаем.
                    mov ax,0003h                    int 10h
Здесь мы опять вызываем прерывание 10h со следующими параметрами: AH = 0(функция 0, установка видеорежима), AL = 3(номер режима). После этого снова идёт проверка режима. Если AL всё ещё не равен 0 - вероятно, возникла проблема, которую мы не сможем решить. Переходим к метке @@exit_bad. Если AL равен 3, продолжаем от @@exit_good. Первым делом - сохраняем в переменные параметры режима, которые вернуло прерывание 10h. В AL - номер режима; в AH - количество колонок символов; в BH - активную страницу видеопамяти (об этом позже). Параметр vrow не возвращается, потому что технически количество символьных строк ограничено только объёмом видеопамяти, а не размером дисплея. После этого восстанавливаем сохранённые в начале регистры, устанавливаем CF (флаг переноса) в нужное положение и завершаем процедуру.
Готово. Теперь в основной модуль после call read_BPB добавляем call set_vmode3 и после этого jc panic. JC - инструкция условного перехода. Переход выполняется при установленном флаге CF. То есть, если наша процедура set_vmode3 завершилась неудачно, программа продолжится от метки panic. Так как невозможность установить видеорежим говорит либо о серьёзных неполадках, либо о несовместимом оборудовании, продолжать выполнение смысла нет. После метки panic останавливаем программу инструкциями cli и hlt.
Далее стоит вывести какое-нибудь приветствие или заголовок, просто чтобы уведомить пользователя о том, что программа работает. Для этого первым делом стоит узнать положение курсора, ведь на экране скорее всего уже есть какой-то текст от BIOS. Это тоже можно сделать с помощью прерывания 10h. Добавьте в io.inc процедуру get_cursor_pos:
get_cursor_pos proc                                      push ax
                                      push bx
                                      push cx
                                      push dx
                                      pushf

                                      mov ah,03h
                                      mov bh,byte ptr vpage
                                      int 10h
                                      mov byte ptr cursor_X,dl
                                      mov byte ptr cursor_Y,dh

                                      popf
                                      pop dx
                                      pop cx
                                      pop bx
                                      pop ax
                                      ret
get_cursor_pos endp
Про сохранение/восстановление используемых регистров объяснять больше не буду, а в остальном тут всё просто: вызываем функцию 3 прерывания 10h, в BH передаём активную страницу видеопамяти. Прерывание возвращает в DL позицию курсора по X, а в DH - по Y. Сохраняем в переменных. Готово. Далее нам понадобится процедура для прокрутки содержимого дисплея. Тут чуть сложнее, добавьте в io.inc:
scroll_up proc                                     push ax
                                     push bx
                                     push cx
                                     push dx
                                     pushf

                                     mov ah,06h
                                     mov bh,CS_DEFAULT
                                     xor cx,cx
                                     mov dl,byte ptr vcol
                                     dec dl
                                     mov dh,byte ptr vrow
                                     dec dh
                                     int 10h

                                     popf
                                     pop dx
                                     pop cx
                                     pop bx
                                     pop ax
                                     ret
scroll_up endp
Функция прокрутки экрана BIOS требует, во-первых, цветовую схему, которой будут заполнены очищенные строки, а во-вторых, координаты верхнего левого и правого нижнего углов сдвигаемой области. Цветовая схема передаётся в регистре BH и состоит из цвета фона и цвета символа. Мы ещё не объявляли константы, давайте посмотрим, как это делается. Константы в отличие от подключаемых файлов можно объявлять где угодно, так как они нужны только на этапе компиляции и не попадают в исполняемый файл. В TASM для объявления констант используется инструкция equ. Вся конструкция выглядит так: ИМЯ КОНСТАНТЫ equ ЗНАЧЕНИЕ КОНСТАНТЫ. Так как в стандартной палитре третьего режима всего 16 цветов, уместно будет определить их в виде констант. Добавьте в код такую запись:
;Цвета фона.BC_BLACK               equ byte ptr 00h
BC_BLUE                 equ byte ptr 10h
BC_GREEN              equ byte ptr 20h
BC_CYAN                 equ byte ptr 30h
BC_RED                   equ byte ptr 40h
BC_MAGENTA          equ byte ptr 50h
BC_BROWN             equ byte ptr 60h
BC_LIGHTGRAY      equ byte ptr 70h

;Цвета символа.
SC_BLACK               equ byte ptr 00h
SC_BLUE                 equ byte ptr 01h
SC_GREEN              equ byte ptr 02h
SC_CYAN                 equ byte ptr 03h
SC_RED                   equ byte ptr 04h
SC_MAGENTA         equ byte ptr 05h
SC_BROWN             equ byte ptr 06h
SC_LIGHTGRAY      equ byte ptr 07h
SC_DARKGRAY       equ byte ptr 08h
SC_LIGHTBLUE       equ byte ptr 09h
SC_LIGHTGREEN   equ byte ptr 0Ah
SC_LIGHTCYAN      equ byte ptr 0Bh
SC_LIGHTRED        equ byte ptr 0Ch
SC_LIGHTMAGENTA equ byte ptr 0Dh
SC_LIGHTBROWN  equ byte ptr 0Eh
SC_WHITE               equ byte ptr 0Fh

;Несколько готовых цветовых схем.
CS_DEFAULT           equ BC_BLACK or SC_CYAN
CS_CLASSIC           equ BC_BLACK or SC_LIGHTGRAY
CS_DARK                 equ BC_BLACK or SC_DARKGRAY
CS_BLUE                 equ BC_BLUE or SC_LIGHTBLUE
CS_ALARM              equ BC_BLACK or SC_RED
CS_DEBUG              equ BC_BLUE or SC_WHITE
CS_INVERT             equ BC_LIGHTGRAY or SC_BLACK
CS_PANIC                equ BC_RED or SC_BLACK
Как видите, для передачи цвета фона/символа используется один байт. Нижние 4 бита отвечают за цвет символа, верхние - за цвет фона и некоторые другие эффекты (подчёркивание, мигание), которые нам сейчас не нужны. Теперь разберёмся с рабочей областью. Мы хотим сдвинуть вверх весь экран, поэтому верхняя левая точка будет в (0,0), а правая нижняя - в (число символов по X-1,число символов по Y-1). Первая передается в CX, вторая - в DX. Таким образом, код процедуры расшифровывается так:
AH=номер функции (6)BH=цветовая схема
CX=верхний левый угол рабочей области (0,0)
DX=правый нижний угол
Вызвать прерывание 10h
Процедура принимает число строк, на которое нужно прокрутить экран вверх, в AL. Добавьте после jc panic такой код:
                           call get_cursor_pos                           mov al,01h
                           call scroll_up
Последнее, что мы рассмотрим сегодня - вывод строки. Это функция 19 прерывания 10h. В качестве параметров она требует: сегмент и смещение строки в ES:BP; цветовую схему в BL; активную страницу видеопамяти в BH; позицию начала вывода по X в DL; позицию начала вывода по Y в DH; длину строки в CX; режим вывода в AL. Номер функции как всегда передаётся в AH.
Зная всё это, давайте подумаем, как организовать процедуру. В принципе тут всё почти однозначно, но что\ делать с длиной строки? Заносить в CX вручную перед каждым вызовом процедуры? Можно, но зачем раздувать код. Лучше включить эту информацию в саму строку. Например, приняв, что первые 16 бит строки будут содержать число символов в ней. У меня строка с заголовком загрузчика выглядит так:
str_title          dw 31                                                     ;Длина строки.                      db '=== Tardigrada Loader v.1.1 ==='    ;Строка.
Теперь давайте напишем саму процедуру в io.inc.
print_string proc                                   push ax
                                   push bx
                                   push cx
                                   push dx
                                   push bp
                                   push es
                                   pushf

                                   mov ax,0050h
                                   mov es,ax
                                   mov ax,1300h
                                   mov bh,byte ptr vpage
                                   mov cx,es:[bp]
                                   mov dh,byte ptr cursor_Y
                                   mov dl,byte ptr cursor_X
                                   add bp,0002h
                                   int 10h

                                   popf
                                   pop es
                                   pop bp
                                   pop dx
                                   pop cx
                                   pop bx
                                   pop ax
                                   ret
print_string endp
Наша процедура будет принимать два параметра: смещение строки в BP и цветовую схему в BL. Практически весь код - это заполнение регистров для вызова прерывания. В основном модуле после call scroll_up добавим:
                                   mov bl,CS_DEFAULT                                   mov bp,offset str_title
                                   call print_string
                                   mov al,01h
                                   call scroll_up
Этот код выведет заголовок и прокрутит экран ещё на одну строку вверх. Если всё сделано правильно, должно получиться что-то вроде этого:
^ Bochs for Windows - Display
USER ,__£
m2
■+Щ
•te
ТА
Reset susPEno Rower-
Û *
ujf'tnu vvwet
ù ф
Please visit :
. http://bochs.sourceforge.net . http ://www.nongnu.org/vgab ios
Bochs UBE Display Adapter enabled
Bochs 2.6.10.sun BIOS - build: 01/05/20
^Revision: 13752 $ $Date:
Чистая дискета: https://drive.google.com/file/d/1Bold4ds8oEruHQ7fJZKHglVo7A2Vc5MR/view?usp=sharing

Урок ОСдева №8: основной загрузчик, вводная.

Дисклеймер: эта серия постов не про UEFI. Это не значит, что я не знаю о существовании UEFI.
Про UEFI будет отдельная серия постов. Почему я не пишу про UEFI прямо сейчас? Потому что
UEFI - это уровень абстракции над железом, а мне интересно именно железо и работа с ним.

Итак, мы закончили писать первичный загрузчик. Что дальше? Дальше - немного пугающая
свобода. Если в случае ПЗ ограничения на объём памяти ставят разработчика в жёсткие рамки
и ограничивают полёт фантазии, то теперь вы можете строить архитектуру программы
по своему усмотрению и добавлять фичи сколько душа пожелает. Те решения, которые принял
я, могут быть не самыми разумными и правильными. Не буду вам их навязывать. Считаете,
что сможете сделать лучше - пробуйте. В любом случае стоит держать в голове минимальный
набор задач, который должен будет выполнять ваш загрузчик:

- Переключать процессор из 16-битного в 32-битный или 64-битный режим.
- Включать доступ к расширенной памяти.
- Составлять карту RAM и передавать её ОС.
- Минимально взаимодействовать с устройствами ввода-вывода, чтобы показать сообщение об ошибке
или принять ввод пользователя.
- Находить, считывать и обрабатывать файл конфигурации (опционально, делает загрузчик более гибким).
- Находить на диске и загружать в указанную область памяти ядро ОС.
- Находить на диске и загружать в указанные области памяти дополнительные файлы (опционально).
- Предоставлять ОС базовую GDT (об этом позже).

Важный момент: рано или поздно перед вами встанет вопрос совмещения в одной программе
16-битного и 32-битного (или 64-битного) кода. Лично я предпочёл с этим не заморачиваться и
разбил загрузчик на отдельные файлы. Тем более, что это соответствует любимой мной аккуратной
модульной структуре. Если вы всё-таки хотите запихать весь код в один файл, стоит заранее
выбрать ассемблер, который это поддерживает.

Ещё один важный момент: достоверное определение аппаратных ресурсов может быть (было до
появления UEFI - вставят тут апологеты) сложной задачей. Там, где это можно сделать
простым способом - я буду использовать его. Там, где для этого нужно будет полагаться
на технологии, рассказ о которых выйдет слишком объёмным для одного-двух постов (ACPI, UEFI) -
пока я буду заявлять наличие устройства как обязательное. Например, для работы ОС на этом
этапе будет обязательно наличие VGA-совместимых видеокарты и дисплея.

На этом вступление закончено и можно начинать писать. 16-битный модуль загрузчика будет
в некоторых местах повторять первичный. Такие участки кода я буду давать без объяснений.
Ну и - этот пост можно назвать вводным в новый этап программы, так что кода будет
немного. Поехали.Если вы вместе со мной писали ПЗ, то сейчас, запустив машину, получите вот такое
сообщение:

Plex86/Bochs UGABios (PCI) 0.7b 03 Jan 2020 This UGA/UBE Bios is released under the GNU LGPL
Please visit :
. http://bochs.sourceforge.net . http //www.nongnu.org/vgab ios
Bochs UBE Display Adapter enabled
Bochs 2.6.10.svn BIOS - build: 01/05/20
^Revision: 13752 $ $Date: 2019-12-30 14:16:18

Создайте в папке boot папку stage2, а в ней - текстовый файл loader.asm. Заголовок и конец
модуля выглядят почти идентично ПЗ:

.386p
CSEG segment use16
ASSUME cs:CSEG, ds:CSEG, es:CSEG, fs:CSEG, gs:CSEG, ss:CSEG
LOCALS @@

begin:

CSEG ends
end begin

Добавилась только директива LOCALS @@. В TASM эта команда разрешает использование локальных
меток. Начинающаяся с символов @@ метка или переменная будет работать только в границах
процедуры или модуля, в котором прописана. Это позволит нам создавать в разных местах
переменные и указатели с одинаковыми именами. Вы оцените полезность фичи, когда начнёте писать
большие программы. Идём дальше. Первое, что нам нужно сделать - это установить значения
сегментных регистров. Совсем как в ПЗ, изменилось только значение. Если помните, loader.bin
у нас был загружен в 0050h:0000h. Выглядит так:

begin:
     ;DS, ES, FS, GS.
          mov ax,0050h     ;Сегмент загрузчика.
          mov ds,ax     ;Поместить это значение во все сегментные регистры.
          mov es,ax
          mov fs,ax
          mov gs,ax

     ;СЕГМЕНТ СТЕКА.
          cli     ;Запретить прерывания перед переносом стека.
          mov ss,ax     ;Поместить в SS адрес сегмента загрузчика.
          mov sp,0FFFFh     ;Указатель стека - на конец сегмента.
          sti     ;Разрешить прерывания.

          cli
          hlt

Регистр CS мы не трогаем, так как его значение было корректно установлено ПЗ. Для комфортной
работы нам понадобится информация из блока параметров BIOS. Мы знаем, что он был загружен в
память вместе с ПЗ, поэтому обращаться к диску нужды нет. Собственно, я мог бы просто
скопировать всю структуру вместе со значениями из него, но это не кажется мне правильным.
Будет лучше считать структуру из RAM. Для этого нам понадобится неинициализированный
дубликат BPB и процедура, которая его заполнит. Добавьте эти переменные в конец модуля:

;=======================================;
;Блок параметров BIOS, 33 байта.;
;Здесь хранятся характеристики;
;носителя.;
;=======================================;
BPB:BPB_OEMnamedb ?,?,?,?,?,?,?,?     ;0-7. Имя производителя. Может быть любым.
BPB_bytespersecdw ?     ;8-9. Размер сектора в байтаx.
BPB_secperclustdb ?     ;10. Количество секторов в кластере.
BPB_reserveddw ?     ;11-12. Число зарезервированныx секторов (1, загрузочный).
BPB_numFATsdb ?     ;13. Число FAT.
BPB_RDentriesdw ?     ;14-15. Число записей Корневой Директории.
BPB_sectotaldw ?     ;16-17. Всего секторов на носителе.
BPB_mediatypedb ?     ;18. Тип носителя. 0F0 - 3,5-дюймовая дискета с 18 секторами в дорожке.
BPB_FATsizedw ?     ;19-20. Размер FAT в сектораx.
BPB_secpertrackdw ?     ;21-22. Число секторов в дорожке.
BPB_numheadsdw ?     ;23-24. Число головок (поверxностей).
BPB_hiddensecdd ?     ;25-28. Число скрытыx секторов перед загрузочным.
BPB_sectotal32dd ?     ;29-32. Число секторов, если иx больше 65535.

;===============================================;
;Расширенный блок параметров BIOS, 26 байт.;
;Этот раздел используется в DOS 4.0.;
;===============================================;
EBPB_drivenumdb ?     ;0. Номер привода.
EBPB_NTflagsdb ?     ;1. Флаги в Windows NT. Бит 0 - флаг необxодимости проверки диска. Бит 1 - флаг необходимости диагностики ;поверхности.
EBPB_extsigndb ?     ;2. Признак расшренного BPB по версии DOS 4.0.
EBPB_volIDdd ?     ;3-6. "Серийный номер". Любое случайное число или ноль, без разницы.
EBPB_vollabeldb ?,?,?,?,?,?,?,?,?,?,?     ;7-17. Название диска. Устарело.
EBPB_filesysdb ?,?,?,?,?,?,?,?     ;18-25. Имя файловой системы.
db ?     ;Еще один байт для того, чтобы структура занимала 15 32-битных слов.

Как видите, в этом варианте переменным не присвоены значения. Кроме того, в конце
зарезервирован ещё один байт. Он нужен для того, чтобы размер таблицы делился на 4
и её можно было загрузить как последовательность 32-битных слов, не перетерев
ничего лишнего. Теперь нужна процедура.

read_BPB proc
     push cx     ;Сохранить регистры.
     push di
     push si
     push ds

     push 07C0h
     pop ds     ;DS=07C0h, сегмент первичного загрузчика.
     mov si,3     ;SI=смещение BPB в ПЗ.
     mov di,offset BPB     ;DI=смещение BPB в loader.bin
     mov cx,15     ;CX=счётчик для копирования.
     rep movsd     ;Скопировать 15 32-битных слов, размер BPB+1 байт.

     pop ds     ;Восстановить регистры.
     pop si
     pop di
     pop cx
     ret     ;Завершить процедуру.
read_BPB endp

Тут в общем всё просто. Устанавливаем пару регистров DS:SI на начало BPB в первичном загрузчике,
а ES:DI - на нашу неинициализированную структуру. Потом командой rep movsd копируем всю
конструкцию + 1 байт. Вызовите процедуру инструкцией call после установки сегмента стека -
и готово. Кстати, вопрос знатокам: что быстрее выполнит 32-битный процессор в 16-битном
режиме - копирование 15 32-битных слов или 30 16-битных?

На этом пока всё. Сделайте новый батч файл с примерно таким текстом, чтобы не возиться
с инструкциями вручную:

tasm project\boot\stage2\loader.asm
tlink loader.obj
exe2bin loader.exe

Слинкуйте файл, закиньте на дискету - и готово. Сообщение о том, что loader.bin
не найден, должно пропасть. В следующий раз - установка видеорежима и методы вывода
текста.

Ссылка на полный полный текст модуля: https://drive.google.com/file/d/1rFc3OcEDmf7Rs_hEJ6iX98-1SimN_dx_/view?usp=sharing

Урок ОСдева №7: первичный загрузчик, финал.

В прошлый раз мы написали процедуру загрузки данных и использовали ее для того, чтобы 
поместить корневую директорию нашей дискеты в оперативную память сразу после собственно
программы-загрузчика по адресу 07C0h:0200h. План действий на сегодня:

-Найти в КД номер первого кластера файла.
-Загрузить первый кластер.
-Следуя по цепочке записей в FAT, загрузить остальные кластеры.

Перед тем, как кодить дальше, давайте  разберёмся, что такое КД и как её использовать для
поиска файлов*.

По сути корневая директория в FAT12 - это таблица, в которой каждому файлу или
поддиректории соответствует одна 32-байтная запись. Давайте посмотрим, что в ней есть.

Байты 0-10: имя файла в формате 8:3. Этот формат подразумевает, что имя файла занимает
ровно 8 байтов, а расширение - 3. Если имя файла меньше 8 символов, оно дополняется
пробелами: так, файл 'loader.bin' в КД будет проходить под именем 'LOADER  BIN'.

Байт 11: атрибуты записи. Набор флагов, позволяющий придать записи особые свойства.
          00000001b = только для чтения
          00000010b = скрытый
          00000100b = системный
          00001000b = метка раздела
          00010000b = директория
          00100000b = архив
          00001111b = LFN (long file name), запись имеет особый формат, поддерживающий длинные
                              имена файлов.

Байт 12: зарезервирован для Windows NT.

Байт 13: время создания в десятых секунды (почему-то 0-199 согласно OSDev Wiki).

Байты 14-15: время, когда был создан файл. Младшие 5 бит - секунды/2 (то есть при интерпретации
значения, например, для вывода на экран, эту часть надо умножать на 2). Следующие 6 - минуты.
Последние 5 бит - часы.

Байты 16-17: дата создания файла. Примерно та же история. День(0-4), месяц(5-8), год(9-15).

Байты 18-19: дата последнего доступа в том же формате, что и дата создания.

Байты 20-21: старшие 16 бит номера первого кластера файла. В FAT12 и FAT16 не используется.

Байты 22-23: время последнего изменения в том же формате, что и время, когда был создан файл.

Байты 24-25: дата последнего изменения в том же формате, что и дата создания.

Байты 26-27: младшие 16 бит номера первого кластера файла.

Байты 28-31: размер файла в байтах.
Довольно много информации, но нас интересуют только два поля: имя записи и младшая часть номера
стартового кластера (старшая половина в FAT12 не используется). Вырисовывается в общих чертах
алгоритм поиска файла? Если нет, я помогу:

1. Переходим к началу КД
2. Считываем имя записи
3. Сравниваем имя записи с именем искомого файла
4. Если имена совпали, файл найден, SUCCESS!
5. Записи кончились?
6. Если кончились - файла нет, аварийно завершаемся
7. Переходим к следующей записи
8. goto 2
Вот таким нехитрым способом мы сможем найти на диске начало файла или, если его нет, уведомить
об этом пользователя и свернуть выполнение загрузчика. Я решил, начиная с этого поста, не
перепечатывать весь листинг из предыдущих уроков. Вместо этого я приложу ссылку на файл в
конце. Это позволит вместить в пост больше полезной информации, не растягивая его до
нечитабельных размеров. А теперь давайте выполним наш поисковый алгоритм в коде. После
call read_sectors пишите:

                   mov cx,BPB_RDentries
                   mov di,0200h
                   mov si,offset fname
                   mov bp,si

next_entry:   mov ax,cx
                   mov bx,di
                   mov cx,11
                   rep cmpsb
                   mov si,bp
                   mov di,bx
                   mov cx,ax
                   je load_FAT
                   add di,32
                   loop next_entry

                   mov ah,3
                   xor bh,bh
                   int 10h

                   mov ax,1300h
                   mov bx,0007h
                   mov cx,22
                   mov bp,offset fname
                   int 10h

                   cli
                   hlt
Что всё это значит? В строчке mov cx,BPB_RDentries мы устанавливаем счётчик основного
цикла. Напоминаю, что в переменной BPB_RDentries у нас хранится число записей корневой
директории. 0200h - смещение загруженной в RAM КД. В SI мы помещаем смещение строки с
именем искомого файла. Кстати, впишите в переменные fname db 'LOADER  BIN'. После этого
мы сохраняем это же смещение в регистре BP. Это может быть пока неочевидно, но позже вы
поймёте, зачем.

Следующий блок кода, начинающийся с метки next_entry, - это собственно цикл просмотра
записей КД и сравнения имён. Первым делом мы сохраняем счётчик цикла и смещение текущей
записи. Счётчик сохраняем потому, что будет вложенный цикл, а смещение - потому, что
строковые инструкции вроде cmpsb изменяют значения регистров SI и DI. Кстати, теперь вам
должно быть понятно, зачем мы сохраняли указатель на строку с именем в BP.

mov cx,11 - установка счётчика вложенного цикла. Имена в FAT12 хранятся в формате 8:3,
значит, нам нужно сравнить две строки по 11 символов. Надеюсь, тут вопросов нет?
Инструкция cmpsb сравнивает значения двух байтов (в нашем случае символов), находящихся
в DS:SI и ES:DI. Префикс rep повторяет инструкцию, пока не обнулится счётчик в CX.
Далее мы восстанавливаем счётчик основного цикла в CX, смещение текущей записи в DI и
смещение строки с именем файла в SI. В старых версиях здесь у меня были пары инструкций
push/pop, но потом я подумал, что трансфер из регистра в регистр быстрее, чем обращение
к стеку, и поменял. Никогда не вредно сэкономить пару циклов.

Если в результате rep cmpsb все символы совпали, в регистре флагов будет установлен бит
ZF. Команда je load_FAT выполняет переход к метке load_FAT если флаг ZF установлен.
В случае если строки не совпали, мы переводим DI к следующей записи в КД и продолжаем
цикл командой loop next_entry. Тут бы можно было и закончить, но нужно обработать
отсутствие файла. С этим набором инструкций мы уже знакомы по предыдущему посту.
Первый блок возвращает положение курсора в DH,DL, а второй выводит от этой позиции
сообщение. Отличается только само сообщение. Вместо 'Disk read error.' мы выводим строку
с именем файла. Внимание, тут небольшой хак. Идея в том, чтобы вывести следующий текст:
'{filename} not found!'. Вызвать вывод строки два раза, но зачем? Если поместить в
разделе переменных текст ' not found!' сразу после переменной fname, а при вызове int 10h
указать в CX не 11 символов, а 22, то выведется сначала имя файла, а потом ' not found!'
Конечно же, этот текст обязательно должен быть сразу после fname. Добавьте строчкой ниже
db ' not found!' После этого останавливаем процессор парой команд cli и hlt. Не так-то
сложно, да? Впрочем, файл ещё нужно загрузить.

Для этого нам нужно будет загрузить в память FAT и разобраться, как ею пользоваться.
Давайте начнём с первой задачи, она чисто техническая и не требует умственного напряжения.
После hlt набирайте:

Load_FAT:          mov ax,[di+26]
                         mov cluster,ax
                         mov ax,BPB_reserved
                         mov cx,total_FATs_size
                         mov bx,BPB_RDentries
                         shl bx,5
                         add bx,0200h
                         mov FAT_offset,bx
                         call read_sectors
В строчке mov ax,[di+26] мы считываем из записи КД номер первого кластера файла, а затем
сохраняем его в переменной cluster. Далее, мы помним, что FAT у нас идут сразу после
зарезервированных секторов, поэтому в AX помещаем BPB_reserved. В CX у нас будет число
секторов, которое надо загрузить, то есть total_FATs_size. Загружать FAT будем сразу после
КД, то есть в 07С0h:0200h+размер КД. Размер КД = число записей КД*размер записи (32 байта).
Помещаем в BX число записей (BPB_RDentries), умножаем на 32 (shl bx,5 эквивалентно умножению
на 32, но выполняется быстрее) и добавляем 0200h. Готово! Сохраняем на будущее в переменной
FAT_offset (кстати, объявите её рядом с прочими) и вызываем read_sectors.

А теперь время вернуться к теории. Что такое FAT? Не поверите, но это тоже таблица, и её
структура ещё проще, чем у КД. Каждая запись в FAT соответствует кластеру на диске. FAT
можно назвать оглавлением диска (украл с OSDev Wiki). Кластер может быть свободен, занят
частью файла, зарезервирован ОС или испорчен. Если кластер хранит часть файла, то его
запись в FAT будет содержать номер следующего кластера файла. Понятно? Зная номер первого
кластера файла, мы можем загрузить его в память, потом заглянуть в FAT, найти нужную запись
и считать номер следующего кластера. Повторять до конца файла. Звучит просто, но, как
всегда, есть большое "НО"! Размер записи в FAT12 - 12 бит. Мы не можем оперировать
12-битными ячейками. Мы можем считать 8 или 16. То есть, если мы загрузим в AX начало FAT,
то в регистре окажется первая запись и часть второй. А если сдвинемся на один байт, то
получим конец первой записи и всю вторую. Давайте попробую проиллюстрировать для
наглядности. В верхней строчке будет часть FAT, разделённая на записи, а в нижней она же,
но поделенная на 8-битные куски.

0 0 0 1 0 1 1 1 0 0 1 0|0 1 1 1 0 0 1 0 1 0 0 0|0 0 1 0 0 1 0 0 0 1 1 1          3 Записи.
0 0 0 1 0 1 1 1|0 0 1 0 0 1 1 1|0 0 1 0 1 0 0 0|0 0 1 0 0 1 0 0|0 1 1 1         4,5 байта.

Решение в том, чтобы, считывая каждый нечётный кластер, сдвигать значение на 4 бита вправо, а
у чётного - обнулять 4 старших бита. Зная всё это, давайте писать код:

                             push 0050h
                             pop es
                             xor bx,bx
read_cluster:           mov ax,cluster
                             sub ax,2
                             movzx cx,BPB_secperclust
                             mul cx
                             add ax,datasector
                             call read_sectors
                             mov ax,cluster
                             mov si,ax
                             shr ax,1
                             add si,ax
                             add si,FAT_offset
                             mov dx,[si]
                             mov ax,cluster
                             test ax,1
                             jnz odd_cluster
                             and dx,0000111111111111b
                             jmp short done
odd_cluster:           shr dx,4
done:                     mov cluster,dx
                             cmp dx,0FF7h
                             jb read_cluster
Финальный рывок. Первое, что мы делаем - устанавливаем сегмент для загрузки файла. Так как
BIOS нам больше не указ, выбирать можно самостоятельно. Я бы с удовольствием отправил его
в 0000h:0000h, но первые 1280 байт заняты важными вещами, о которых поговорим позже.
Ближайший свободный участок RAM - 0050h:0000h (или 0000h:0500h, это тот же самый адрес
если вы вдруг забыли правила адресации сегмент:смещение). Обнуляем BX, так чтобы пара
ES:BX указывала на 0050h:0000h. Считываем в AX номер первого кластера файла. Дальше мы
вычитаем 2 из этого номера. Зачем? Затем, что значения 0 и 1 в FAT зарезервированы и не
используются в качестве номеров, а номер, указанный в таблицах, на 2 больше, чем правильное
значение. Да, это идиотизм.

Загружать будем не сектор, а кластер (что в нашем случае одно и то же, но всё-таки),
поэтому в качестве числа секторов помещаем в CX переменную BPB_secperclust и на неё же
умножаем номер кластера. AX*CX в данном случае дадут нам номер первого сектора нужного
кластера. А так как кластеры в FAT начинают считаться от начала области данных,то для
абсолютного значения добавляем к AX datasector. Готово. Вызываем read_sectors и загружаем
первый кластер файла в RAM.

Дальше будет немножко математической магии, объяснять которую я не буду. Если интересно -
разберётесь самостоятельно, там не так сложно. Остальным предлагаю просто поверить, что
смещение записи кластера внутри FAT = 3/2 номера кластера. Значит, берём в AX номер
кластера, его же помещаем в SI, делим AX на 2 и прибавляем к SI. Вуаля, смещение
записи от начала FAT найдено. Добавляем к нему смещение FAT_offset и считываем в DX
значение записи.

Теперь надо проверить, чётная ли запись. Для этого опять берём в AX номер кластера и
делаем сравнение с 1. Если флаг ZF не установлен (то есть 0 бит значения равен 1),
значит, номер записи - нечётный, переходим к odd_cluster и сдвигаем значение вправо на
4 позиции. Если чётный - делаем логическое "И" с маской 0000111111111111b и обнуляем
тем самым 4 старших бита. Теперь у нас есть содержимое нужной записи без всяких
посторонних хвостов, то есть номер следующего кластера. Сохраняем его в переменной
cluster. Дальше у нас идёт сравнение с номера с числом 0FF7h. Дело в том, что,
начиная от этого значения в FAT идут специальные коды, которые могут означать конец
файла, испорченный сектор и т.д. Для нас это значит, что если в качестве номера
кластера мы получили 0FF7h или больше, продолжать загрузку не имеет смысла. Поэтому
продолжаем цикл только если DX меньше 0FF7h. Я умышленно оставляю здесь дыру и
предлагаю всем заинтересованным попытаться самостоятельно сделать обработку ошибки,
связанной с битым кластером (код 0FF7h). Код конца файла, кстати, 0FF8h. Вся необходимая
для этой задачи информация и примеры кода уже есть в этом посте.

А мне остаётся только добавить в конце три строчки:

                    push 0050h
                    push 0000h
                    retf
Этот набор команд мы уже помним из старых постов. Помещаем в стек сегмент, потом
смещение, и передаём управление загруженному файлу командой retf. Поздравим себя!
Первичный загрузчик готов. Да, он умеет немного, но и задача у него всего одна:
найти загрузчик второго уровня, поместить его в RAM и отдать штурвал. Если вы
скомпилируете файл без инструкций org 1FEh и dw 0AA55h, то увидите, что программа
занимает всего 447 байт. Значит, у нас есть в запасе ещё 63. Как раз должно
хватить на проверку успешного считывания кластеров. У меня вместе с ней вышло 497
байт. Можете подсмотреть в приложенном файле, хоть это и неспортивно. Если вы
поместили загрузчик на дискету и получили в bochs (или на реальной машине) вот такой
экран, то всё работает как надо!

Plex86/Bochs UGABios (PCI) current-cvs 08 Jul 2014 This UGA/UBE Bios is released under the GNU LGPL
Please visit :
. http://bochs.sourceforge.net . http ://www.nongnu.org/vgab ios
Bochs UBE Display Adapter enabled
Bochs BIOS - build: 07/10/14
$Revis ion: 12412 $ $Date: 2014-07-10 09:28:59

Чистая дискета:
https://drive.google.com/file/d/1Bold4ds8oEruHQ7fJZKHglVo7A2Vc5MR/view?usp=sharing

Листинг:
https://drive.google.com/file/d/1Q5EtKX5kyF4MWcBeD8a6Jz5cPtqZja9C/view?usp=sharing

Bochs:
https://drive.google.com/file/d/16k2Gpr7oPSekq4rAhmtBV0IPnIteDLlE/view?usp=sharing

* FAT поддерживает вложенные директории, и они ничем принципиально не отличаются
от корневой, так что всё нижеизложенное касается и их. 

Урок ОСдева №6: минидрайвер флоппи-привода.

В предыдущем посте мы вычислили значения, нужные для работы с FAT12. Пора писать драйвер!
Начнём с постановки задачи. Что должен уметь драйвер FAT12 для первичного загрузчика?
Очень просто: загружать файлы. Больше ничего.

Для этого мы будем использовать прерывание BIOS. Кстати, про прерывания мы ещё не
говорили. Пока давайте считать, что это функции, предоставляемые для нашего удобства
BIOS. Подробно об этом говорить будем позднее, так как тема очень большая. Кроме того,
позже мы напишем полноценный драйвер, который будет работать с флоппи-приводом напрямую,
без посредства BIOS. Сделать это прямо сейчас мы не можем из-за ограничения по размеру
программы: первичный загрузчик должен занимать не больше 512 байт - полновесный драйвер
флоппи-привода в такой объём не влезет.

Прерывания вызываются командой int, после которой идёт номер. Стандартное
прерывание BIOS для работы с дисками - 13h. Соответственно, команда выглядит так: int 13h.
Как правило прерывания требуют передачи параметров через определённые регистры. Так,
int 13h нужен номер функции в AH. Прерывание 13h - это целый набор функций для работы
с различными видами съёмных и постоянных носителей. Нас интересует функция 2, чтение
секторов с диска. Она в свою очередь требует указать количество считываемых секторов в AL,
номер цилиндра в CH, номер начального сектора в CL, головку в DH, привод в DL и адрес
в памяти, куда будут считаны данные, в ES:BX.

Итак, ещё раз: в нашем случае int 13h вызывается со следующими параметрами:
AH = 2 (номер функции)
AL = число секторов
CH = номер цилиндра
CL = номер начального сектора
DH = номер головки
DL = 0 (номер привода)
ES:BX = сегмент:смещение области для загрузки

Прерывание int 13h у нас будет вызываться в процедуре read_sectors. Этой последней нужно
будет передать три параметра: LBA в AX, число секторов в CX и адрес для загрузки в ES:BX.
Что такое LBA мы уже знаем: это более современная линейная схема адресации секторов. К сожалению,
прерывание 13h работает с устаревшим форматом CHS, так что придётся делать конверсию
внутри процедуры.

Кстати, стоит сразу остановиться на том, как оформляются и что представляют собой процедуры
в TASM. В коде процедура выглядит так:

(имя процедуры) proc
     (тело процедуры)
(имя процедуры) endp

Обе эти инструкции нужны только компилятору и в исполняемый файл не попадают. В случае
"плоского" бинарного файла процедура окажется именно там, где она расположена в тексте
программы - никакой отдельной области памяти для неё создаваться не будет. Процедура
вызывается инструкцией call (имя процедуры). Предварительно, конечно, надо поместить
нужные параметры в нужные регистры.

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

1. Загрузить в память Корневую Директорию диска.
2. Найти в КД запись, соответствующую файлу.
3. Считать из записи номер первого кластера файла в FAT.
4. Загрузить в память FAT.
5. Загрузить в память цепочку кластеров, которую занимает файл.

В этот раз мы ограничимся только первым пунктом, всё остальное будет в финальной статье
про первичный загрузчик. Давайте вспомним, как выглядела программа в конце прошлого поста:

.386p
CSEG segment use16
ASSUME cs:CSEG, ds:CSEG, es:CSEG, fs:CSEG, gs:CSEG, ss:CSEG
begin:                              jmp short execute;Точка входа. Перейти к исполняемой части.
                                       nop;Пустой оператор. Заполняет 3-й байт перед BPB.



;БЛОК ПАРАМЕТРОВ BIOS==========================================================;

     ;=======================================;
     ;Блок параметров BIOS, 33 байта.                                         ;
     ;Здесь хранятся характеристики                                            ;
     ;носителя. Должен быть в 3 байтах                                       ;
     ;от начала загрузочного сектора.                                          ;
     ;=======================================;
     BPB_OEMnamedb 'BOOTDISK';0-7. Имя производителя. Может быть любым.
     BPB_bytespersecdw 512;8-9. Размер сектора в байтаx.
     BPB_secperclustdb 1;10. Количество секторов в кластере.
     BPB_reserveddw 1;11-12. Число зарезервированныx секторов (1, загрузочный).
     BPB_numFATsdb 2;13. Число FAT.
     BPB_RDentriesdw 224;14-15. Число записей Корневой Директории.
     BPB_sectotaldw 2880;16-17. Всего секторов на носителе.
     BPB_mediatypedb 0F0h;18. Тип носителя. 0F0 - 3,5-дюймовая дискета с 18 секторами в дорожке.
     BPB_FATsizedw 9;19-20. Размер FAT в сектораx.
     BPB_secpertrackdw 18;21-22. Число секторов в дорожке.
     BPB_numheadsdw 2;23-24. Число головок (поверxностей).
     BPB_hiddensecdd 0;25-28. Число скрытыx секторов перед загрузочным.
     BPB_sectotal32dd 0;29-32. Число секторов, если иx больше 65535.

     ;===============================================;
     ;Расширенный блок параметров BIOS, 26 байт.                                         ;
     ;Этот раздел используется в DOS 4.0.                                                       ;
     ;===============================================;
     EBPB_drivenumdb 0;0. Номер привода.
     EBPB_NTflagsdb 0;1. Флаги в Windows NT. Бит 0 - флаг необxодимости проверки диска. Бит 1 - флаг необходимости диагностики             поверхности.
     EBPB_extsigndb 29h;2. Признак расшренного BPB по версии DOS 4.0.
     EBPB_volIDdd 0;3-6. "Серийный номер". Любое случайное число или ноль, без разницы.
     EBPB_vollabeldb 'BOOTLOADER ';7-17. Название диска. Устарело.
     EBPB_filesysdb 'FAT12   ';18-25. Имя файловой системы.



;ИСПОЛНЯЕМЫЙ БЛОК===============================================================;

;Шаг 1. Исправить значения сегментных регистров.
execute:
                    ;DS, ES, FS, GS.
                              mov ax,07C0h;Сегмент загрузчика.
                              mov ds,ax;Поместить это значение во все сегментные регистры.
                              mov es,ax
                              mov fs,ax
                              mov gs,ax

                    ;СЕГМЕНТ СТЕКА.
                              cli;Запретить прерывания перед переносом стека.
                              mov ss,ax;Поместить в SS адрес сегмента загрузчика.
                              mov sp,0FFFFh;Указатель стека - на конец сегмента.
                              sti;Разрешить прерывания.

                    ;СЕГМЕНТ КОДА.
                              push ax;Поместить в стек сегмент.
                              mov ax,offset jump;Указатель на инструкцию после retf.
                              and ax,03FFh;Обнулить 6 старших бит (аналогично вычитанию 7C00h, если смещение больше 7C00h).
                              push ax;Поместить в стек смещение.
                              retf;Дальний возврат для смены CS.

jump:                     mov byte ptr EBPB_drivenum,dl;BIOS должен вернуть номер загрузочного устройства в DL. Сохранить его в BPB.

                             mov ax,BPB_RDentries;Число записей КД
                             shl ax,5;*32 (размер записи в байтах) = размер КД в байтах.
                             div BPB_bytespersec;AX/размер сектора в байтах = размер КД в секторах.
                             mov cx,ax;Поместить его в CX (будет счетчиком для загрузки КД).
                             xor ax,ax;Обнулить AX.
                             mov al,byte ptr BPB_numFATs;Число FAT
                             mul BPB_FATsize;*размер FAT в секторах = общий размер всех FAT в секторах.
                             mov total_FATs_size,ax;Сохранить результат в переменной.
                             add ax,BPB_reserved;AX+число зарезервированных секторов = стартовый сектор КД.
                             mov datasector,ax;Стартовый сектор КД + размер КД в секторах =
                             add datasector,cx;= стартовый сектор области данных. Сохранить его в переменной.

                             cli
                             hlt

;ПЕРЕМЕННЫЕ==================================================================;
     total_FATs_size dw ?;Переменная для хранения общего размера FAT в секторах.
     datasector dw ?;Переменная для хранения номера стартового сектора области данных.

     org 1FEh;Заполняет память нулями до 511-го байта.
     dw 0AA55h;Байты 511 и 512. Признак загрузочного сектора.

CSEG ends
end begin

Добавим следующий код между add datasector,cx и cli:

                              mov bx,0200h
                              call read_sectors

Теперь загрузчик перед тем, как остановить процессор, вызывает процедуру read_sectors. Но передали ли
мы все нужные параметры? Напоминаю, в AX должен быть LBA первого загружаемого сектора. И в AX у нас
как раз номер первого сектора КД! CX должен содержать число загружаемых секторов. И, большая удача,
именно оно в CX и есть. Сегмент в ES у нас уже установлен, а смещение в BX мы явно задали перед
вызовом процедуры. Всё отлично! Осталась самая малость: написать саму процедуру.

Где-нибудь между hlt и переменными сделайте шаблон:

read_sectors proc
                              ;ПУСТО
read_sectors endp

Алгоритм работы в общих чертах представляется нам как-то так: перевести LBA в CHS, установить
значения регистров для int 13h, вызвать прерывание... Профит! Не будем медлить. Пишите:

read_sectors proc
                              div BPB_secpertrack;Разделить LBA в AX на число секторов в дорожке.
                              inc dl;Остаток + 1 = номер сектора, т.к. нумерация с 1.
                              mov cl,dl;Поместить номер сектора в CL для int 13h
                              xor dx,dx;Обнулить перед делением.
                              div BPB_numheads;Разделить результат на число головок.
                              shl dx,8;Остаток = номер головки, сдвинуть его в DH.
                              mov ch,al;Частное = номер дорожки, его поместить в CH для int 13h.
                              mov dl,0;DL = 0, флоппи-диск А.
                              mov ax,0201h;Функция 2 int 13h, загрузка секторов с диска. В AL - число секторов.
                              int 13h
                              ret
read_sectors endp

Поздравим себя, на сегодня дело сделано. Шутка. Включаем голову. Во-первых, носители - а особенно
флоппи-диски! - имеют свойство не читаться с первого раза. На этот случай int 13h возвращает
статус операции в CF: если флаг обнулён - всё хорошо, если установлен - была ошибка чтения.
Во-вторых, даже в случае успеха мы загрузили только один сектор: значение в CX до сих пор не
используется. Начнём со второй проблемы:

read_sectors proc
                              mov bp,BPB_bytespersec;Размер сектора, понадобится внутри цикла.

main:                      pusha;Сохранить регистры общего назначения.
                              div BPB_secpertrack;Разделить LBA в AX на число секторов в дорожке.
                              inc dl;Остаток + 1 = номер сектора, т.к. нумерация с 1.
                              mov cl,dl;Поместить номер сектора в CL для int 13h
                              xor dx,dx;Обнулить перед делением.
                              div BPB_numheads;Разделить результат на число головок.
                              shl dx,8;Остаток = номер головки, сдвинуть его в DH.
                              mov ch,al;Частное = номер дорожки, его поместить в CH для int 13h.
                              mov dl,0;DL = 0, флоппи-диск А.
                              mov ax,0201h;Функция 2 int 13h, загрузка секторов с диска. В AL - число секторов.
                              int 13h
                              popa;Восстановить сохраненные регистры.

                              inc ax;Увеличить LBA.
                              add bx,bp;Сместить указатель загрузки на длину сектора.
                              loop main;Продолжить цикл.
                              ret;Завершить процедуру.
read_sectors endp

Процедура теперь организована в виде цикла со счётчиком в CX. Команда loop возвращает
указатель инструкции к указанной метке при условии, что CX не равен 0. CX при этом
уменьшается на 1. Обратите внимание, что в начале процедуры мы помещаем в BP
размер сектора в байтах, а блок кода из прошлой версии теперь обрамляется инструкциями
pusha и popa. Последнее нужно для того, чтобы после выполнения шага цикла вернуть вводные
значения в соответствующие регистры. Перед началом следующего шага LBA в AX увеличивается
на 1, а смещение области загрузки увеличивается на размер сектора. Время разобраться
с возможными ошибками чтения.

read_sectors proc
                              mov bp,BPB_bytespersec;Размер сектора, понадобится внутри цикла.
main:                      mov di,5;Число попыток чтения в случае ошибки.

load_sector:            pusha;Сохранить регистры общего назначения.
                              div BPB_secpertrack;Разделить LBA в AX на число секторов в дорожке.
                             inc dl;Остаток + 1 = номер сектора, т.к. нумерация с 1.
                             mov cl,dl;Поместить номер сектора в CL для int 13h
                             xor dx,dx;Обнулить перед делением.
                             div BPB_numheads;Разделить результат на число головок.
                             shl dx,8;Остаток = номер головки, сдвинуть его в DH.
                             mov ch,al;Частное = номер дорожки, его поместить в CH для int 13h.
                             mov dl,0;DL = 0, флоппи-диск А.
                             mov ax,0201h;Функция 2 int 13h, загрузка секторов с диска. В AL - число секторов.
                             int 13h
                             jnc sector_loaded;Если CF не установлен, сектор загружен успешно.

                             xor ax,ax;Функция 0 int 13h, сброс головок.
                             xor dl,dl;DL = номер привода, флоппи-диск А.
                             int 13h
                             popa;Восстановить сохраненные регистры.
                             sub di,1;Уменьшить счетчик попыток.
                             jnz load_sector;Если счетчик не обнулился, перейти к загрузке сектора.
                             ret

sector_loaded:        popa;Восстановить сохраненные регистры.
                             inc ax;Увеличить LBA.
                             add bx,bp;Сместить указатель загрузки на длину сектора.
                             loop main;Продолжить цикл.
                             ret;Завершить процедуру.
read_sectors endp

После вызова прерывания у нас теперь стоит jnc sector_loaded. Эта инструкция делает переход к
указанной метке, но только если флаг CF не установлен. Таким образом, к инициализации переменных
для следующего шага цикла мы попадаем только если предыдущий завершился успешно. Если же CF
установлен, начинается обработка ошибки. Функция 0 int 13h возвращает читающие головки привода к
0 сектору 0 дорожки, это должно уменьшить вероятность ошибки при следующем чтении. После
этого мы уменьшаем счётчик попыток на 1 и, если он не обнулился (инструкция jnz), делаем повторную
попытку. Теперь процедура почти готова. Остались финальные штрихи. Во-первых, сброс головок
тоже может пойти с ошибкой, и для пущей уверенности операцию стоит повторить несколько раз.
Во-вторых, было бы неплохо, если бы процедура обрабатывала ситуацию, когда все попытки чтения
завершились неудачей. Сейчас она просто завершается, как и в случае успеха. Начнём опять со второй задачи.

read_sectors proc
                             mov bp,BPB_bytespersec;Размер сектора, понадобится внутри цикла.
main:                     mov di,5;Число попыток чтения в случае ошибки.

load_sector:            pusha;Сохранить регистры общего назначения.
                             div BPB_secpertrack;Разделить LBA в AX на число секторов в дорожке.
                             inc dl;Остаток + 1 = номер сектора, т.к. нумерация с 1.
                             mov cl,dl;Поместить номер сектора в CL для int 13h
                             xor dx,dx;Обнулить перед делением.
                             div BPB_numheads;Разделить результат на число головок.
                             shl dx,8;Остаток = номер головки, сдвинуть его в DH.
                             test ah,ah;Проверить AH. Если больше нуля, что-то пошло не так.
                             jnz error;Т.к. на диске не может быть больше 255 дорожек, завершить с ошибкой.
                             mov ch,al;Частное = номер дорожки, его поместить в CH для int 13h.
                             mov dl,0;DL = 0, флоппи-диск А.
                             mov ax,0201h;Функция 2 int 13h, загрузка секторов с диска. В AL - число секторов.
                             int 13h
                             jnc sector_loaded;Если CF не установлен, сектор загружен успешно.

                             xor ax,ax;Функция 0 int 13h, сброс головок.
                             xor dl,dl;DL = номер привода, флоппи-диск А.
                             int 13h
                             popa;Восстановить сохраненные регистры.
                             sub di,1;Уменьшить счетчик попыток.
                             jnz load_sector;Если счетчик не обнулился, перейти к загрузке сектора.
                             ret

sector_loaded:        popa;Восстановить сохраненные регистры.
                             inc ax;Увеличить LBA.
                             add bx,bp;Сместить указатель загрузки на длину сектора.
                             loop main;Продолжить цикл.
                             ret;Завершить процедуру.

error:                     popa;Попытки кончились. Восстановить сохраненные регистры.
                             mov ax,07c0h;Сегмент загрузчика
                             mov es,ax;поместить в ES для int 10h.
                             mov ah,03h;Функция 3 прерывания 10h, получить позицию курсора в DH, DL.
                             xor bh,bh;BH = номер видеостраницы.
                             int 10h;DH = строка, DL = столбец.

                             mov ax,1300h;Функция 19 прерывания 10h, вывод строки. AL = режим вывода.
                             mov bx,0007h;BH = страница, BL = атрибуты символа.
                             mov cx,0010h;CX = длина строки.
                             mov bp,offset msg_DRE;ES:BP = указатель на строку.
                             int 10h;Вывести строку в DH,DL без обновления курсора.
                             cli;Запретить прерывания
                             hlt;и остановить процессор.
read_sectors endp

В первом сегменте кода после shl dx,8 у нас появилась проверка на ошибочность результата. Она
явно избыточна, но пусть будет. Если в результате деления у нас получился номер дорожки больше
255, то что-то пошло не так. Программа переходит к метке error, после которой происходит
следующее: первый сегмент кода опустошает стек, а потом с помощью прерывания 10h
(прерывание для работы с дисплеем) считывает положение курсора на экране, а второй выводит сообщение
об ошибке и останавливает процессор. Строку с сообщением можно хранить рядом состальными переменными,
выглядит она так:

msg_DRE db 'Disk read error.' ;Сообщение об ошибке чтения с диска.

Осталась самая малость. Обработать повторные попытки сброса головок. Финальная версия прецедуры
будет выглядеть так:

read_sectors proc
                             mov bp,BPB_bytespersec;Размер сектора, понадобится внутри цикла.
main:                     mov di,5;Число попыток чтения в случае ошибки.

load_sector:            pusha;Сохранить регистры общего назначения.
                             div BPB_secpertrack;Разделить LBA в AX на число секторов в дорожке.
                             inc dl;Остаток + 1 = номер сектора, т.к. нумерация с 1.
                             mov cl,dl;Поместить номер сектора в CL для int 13h
                             xor dx,dx;Обнулить перед делением.
                             div BPB_numheads;Разделить результат на число головок.
                             shl dx,8;Остаток = номер головки, сдвинуть его в DH.
                             test ah,ah;Проверить AH. Если больше нуля, что-то пошло не так.
                             jnz error;Т.к. на диске не может быть больше 255 дорожек, завершить с ошибкой.
                             mov ch,al;Частное = номер дорожки, его поместить в CH для int 13h.
                             mov dl,0;DL = 0, флоппи-диск А.
                             mov ax,0201h;Функция 2 int 13h, загрузка секторов с диска. В AL - число секторов.
                             int 13h
                             jnc sector_loaded;Если CF не установлен, сектор загружен успешно.

                             mov cx,0003h;Счетчик попыток сброса головок.
reset:                     xor ax,ax;Функция 0 int 13h, сброс головок.
                             xor dl,dl;DL = номер привода, флоппи-диск А.
                             int 13h
                             jnc reload;Если не было ошибки - повторить попытку чтения сектора.
                             loop reset;Попробовать сбросить головки еще раз, если CX не обнулился.
error:                     popa;Попытки кончились. Восстановить сохраненные регистры.
                             jmp short disk_read_error;Сообщить об ошибке и завершить программу.

reload:                   popa;Восстановить сохраненные регистры.
                             sub di,1;Уменьшить счетчик попыток.
                             jnz load_sector;Если счетчик не обнулился, перейти к загрузке сектора.
                             jmp short disk_read_error;Сообщить об ошибке и завершить программу.

sector_loaded:        popa;Восстановить сохраненные регистры.
                             inc ax;Увеличить LBA.
                             add bx,bp;Сместить указатель загрузки на длину сектора.
                             loop main;Продолжить цикл.
                             ret;Завершить процедуру.

disk_read_error:      mov ax,07c0h;Сегмент загрузчика
                             mov es,ax;поместить в ES для int 10h.
                             mov ah,03h;Функция 3 прерывания 10h, получить позицию курсора в DH, DL.
                             xor bh,bh;BH = номер видеостраницы.
                             int 10h;DH = строка, DL = столбец.

                             mov ax,1300h;Функция 19 прерывания 10h, вывод строки. AL = режим вывода.
                             mov bx,0007h;BH = страница, BL = атрибуты символа.
                             mov cx,0010h;CX = длина строки.
                             mov bp,offset msg_DRE;ES:BP = указатель на строку.
                             int 10h;Вывести строку в DH,DL без обновления курсора.
                             cli;Запретить прерывания
                             hlt;и остановить процессор.
read_sectors endp

Поздравим себя! Теперь у нас есть процедура, считывающая данные с диска. Добавленный нами
вызов read_sectors в конце программы помещает КД диска в память сразу после самого
загрузчика. Следующий пост будет последним на тему первичного загрузчика. В нём мы научимся
пользоваться КД и FAT и загружать файлы.

Урок ОСдева №5: подготовка к работе с файловой системой FAT12.

В прошлый раз мы инициализировали сегментные регистры и обеспечили загрузчику безопасную среду
обитания. Сегодня будем готовить почву для работы с файловой системой FAT12. Для начала стоит
поподробнее ознакомиться с её структурой. В FAT12 пространство носителя можно разделить на
несколько областей. Они могут быть разного размера, но идут всегда в следующем порядке:

ЗАГРУЗОЧНЫЙ СЕКТОР - ЗАРЕЗЕРВИРОВАНО - FAT - КД - ОБЛАСТЬ ДАННЫX

Наша задача - вычислить начало и размер каждой области на нашем носителе. Эта информация понадобится
для загрузки файлов. Важный момент: при работе с контроллером флоппи-привода мы оперируем секторами,
а не байтами. То есть, когда я пишу "размер", я имею в виду количество секторов, занятыx
областью.

Загрузочный сектор - место, где обитает наша программа. Это всегда сектор
номер 1 на носителе, и занимает он ровно 1 сектор. Было несложно.

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

FAT, таблица распределения файлов. Будет чуть сложнее. Размер FAT в секторах xранится в переменной
BPB_FATsize. Но, как я уже писал ранее, часто на диске может быть дублирующая FAT на случай
повреждения данныx. Количество FAT на диске указано в переменной BPB_numFATs. Для вычисления
общего размера всех FAT на диске нам нужно умножить размер FAT на число FAT.

Дальше у нас идёт корневая директория. Это набор записей о размещении файлов. Размер записи КД
в FAT12 - 32 байта. Количество записей указано в переменной BPB_RDentries. Берём размер записи
и умножаем на число записей. Всё? Нет. Так мы получим размер в байтах, его нужно перевести
в секторы. Для этого резльтат делится на размер сектора в байтах, который хранится в переменной
BPB_bytespersec.

Вот теперь всё. Вспомним, как выглядела программа в конце прошлого поста:

.386p
CSEG segment use16
ASSUME cs:CSEG, ds:CSEG, es:CSEG, fs:CSEG, gs:CSEG, ss:CSEG

begin:                    jmp short execute                    ;Точка входа. Перейти к исполняемой части.
                            nop                                         ;Пустой оператор. Заполняет 3-й байт перед BPB.



;БЛОК ПАРАМЕТРОВ BIOS======================================================================;

;=======================================;
;Блок параметров BIOS, 33 байта.;
;Здесь хранятся характеристики;
;носителя. Должен быть в 3 байтах;
;от начала загрузочного сектора.;
;=======================================;
          BPB_OEMname db 'BOOTDISK'          ;0-7. Имя производителя. Может быть любым.
          BPB_bytespersec dw 512                  ;8-9. Размер сектора в байтаx.
          BPB_secperclust db 1                        ;10. Количество секторов в кластере.
          BPB_reserved dw 1                          ;11-12. Число зарезервированныx секторов (1, загрузочный).
          BPB_numFATs db 2                          ;13. Число FAT.
          BPB_RDentries dw 224                     ;14-15. Число записей Корневой Директории.
          BPB_sectotal dw 2880                      ;16-17. Всего секторов на носителе.
          BPB_mediatype db 0F0h                   ;18. Тип носителя. 0F0 - 3,5-дюймовая дискета с 18 секторами в дорожке.
          BPB_FATsize dw 9                           ;19-20. Размер FAT в сектораx.
          BPB_secpertrack dw 18                    ;21-22. Число секторов в дорожке.
          BPB_numheads dw 2                        ;23-24. Число головок (поверxностей).
          BPB_hiddensec dd 0                        ;25-28. Число скрытыx секторов перед загрузочным.
          BPB_sectotal32 dd 0                        ;29-32. Число секторов, если иx больше 65535.

;===============================================;
;Расширенный блок параметров BIOS, 26 байт.;
;Этот раздел используется в DOS 4.0.;
;===============================================;
          EBPB_drivenumdb 0                         ;0. Номер привода.
          EBPB_NTflagsdb 0;1. Флаги в Windows NT. Бит 0 - флаг необxодимости проверки диска. Бит 1 - флаг необходимости диагностики поверхности.
          EBPB_extsigndb 29h;2. Признак расшренного BPB по версии DOS 4.0.
          EBPB_volIDdd 0;3-6. "Серийный номер". Любое случайное число или ноль, без разницы.
          EBPB_vollabeldb 'BOOTLOADER ';7-17. Название диска. Устарело.
          EBPB_filesysdb 'FAT12   ';18-25. Имя файловой системы.



;ИСПОЛНЯЕМЫЙ БЛОК========================================================================;

;Шаг 1. Исправить значения сегментных регистров.
execute:
         ;DS, ES, FS, GS.
                   mov ax,07C0h                    ;Сегмент загрузчика.
                   mov ds,ax                          ;Поместить это значение во все сегментные регистры.
                   mov es,ax
                   mov fs,ax
                   mov gs,ax

          ;СЕГМЕНТ СТЕКА.
                   cli                                      ;Запретить прерывания перед переносом стека.
                   mov ss,ax                           ;Поместить в SS адрес сегмента загрузчика.
                   mov sp,0FFFFh                   ;Указатель стека - на конец сегмента.
                   sti                                      ;Разрешить прерывания.

          ;СЕГМЕНТ КОДА.
                   push ax                              ;Поместить в стек сегмент.
                   mov ax,offset stop               ;Указатель на инструкцию после retf.
                   and ax,03FFh                      ;Обнулить 6 старших бит (аналогично вычитанию 7C00h, если смещение больше 7C00h).
                   push ax                              ;Поместить в стек смещение.
                   retf                                    ;Дальний возврат для смены CS.

stop:            cli
                   hlt

          org 1FEh;Заполняет память нулями до 511-го байта.
          dw 0AA55h;Байты 511 и 512. Признак загрузочного сектора.

CSEG ends
end begin


Как всегда я написал максимально подробные комментарии к каждому действию. Теперь после retf добавьте
следующий код вместо cli и hlt:

stop:           mov byte ptr EBPB_drivenum,dl

                  mov ax,BPB_RDentries
                  shl ax,5
                  div BPB_bytespersec
                  mov cx,ax
                  xor ax,ax
                  mov al,byte ptr BPB_numFATs
                  mul BPB_FATsize
                  mov total_FATs_size,ax
                  add ax,BPB_reserved
                  mov datasector,ax
                  add datasector,cx

                  cli
                  hlt


Давайте разбираться. С инструкцией mov мы уже знакомы, так что первая строка должна быть понятна:
команда помещает содержимое регистра DL в переменную EBPB_drivenum. Но что за byte ptr?
Это префикс смены разрядности. Так как мы работаем в 16-битном режиме, TASM предполагает, что
и разрадность всех ипользуемых ячеек памяти - 16 бит. Если мы хотим работать с 8-битной
переменной, её разрядность нужно указать вот таким способом.

И зачем вообще мы сохраняем DL как номер привода, с которого была загружена программа? Дело в
том, что по идее BIOS должна вернуть его в DL. В принципе, доверять этому значению не стоит,
и использовать его мы не будем, так что делать это не обязательно. Просто я педант.

Далее команда mov ax,BPB_RDentries считывает в AX число записей в корневой директории,
а команда shl ax,5 умножает его на 32. Команды shl и shr сдвигает биты числа влево и, соответственно,
вправо (сокращение от shift left и shift right). Сдвиг числа влево на 1 эквивалентен умножению
на 2. Сдвиг на 5 эквивалентен умножению на 32. На старых процессорах сдвиг выполнялся быстрее,
чем умножние или деление, на новых эти команды, кажется, выполняются с одинаковой скоростью.

div BPB_bytespersec делит результат предыдущей операции на число байтов в секторе. Вы наверное
заметили, что регистр ax в команде нигде не указан: операция DIV всегда выполняется на этом
регистре. В результате деления мы получаем чсло секторов, которые занимает КД. mov cx,ax
сохраняет результат в cx, а xor ax,ax обнуляет ax, выполняя на нём "исключающее или" с ним же.

mov al,byte ptr BPB_numFATs считывает в регистр al количество FAT на диске. Кстати! Регистров
al и dl не было в списке, который я приводил на прошлом уроке. Сейчас поясню.
Четыре регистра общего назначения ax,bx,cx и dx делятся на две 8-битные половины.
ax на al и ah, bx на bl и bh, ... l в данном слуае значит low, то есть младшие 8 бит.
h, соответственно, старшие high. Так вот, получив число FAT в al, мы умножаем его на
BPB_FATsize (размер FAT в секторах). Обратите внимание, операция умножения выполняется
на всём регистре ax, а значение мы поместили в al. Для этого мы и обнуляли ax операцией
раньше. Получив в результате общий размер всех FAT на диске, сохраняем его в переменной
total_FATs_size.

Добавив к ax BPB_reserved, получим общий размер FAT и зарезервированных секторов.
Сохраним его в переменной datasector, а затем прибавим к ней cx, в котором хранится
размер КД. Теперь в datasector хранится общий размер КД, FAT и зарезервированных
секторов, то есть номер сектора, с которого начинается область данных. Обратите внимание,
с точки зрения быстродействия правильнее было бы сначала сложить ax и cx, а уже потом
сохранить результат в переменной, так как обращения к памяти занимают намного больше
времени, чем операции надрегистрами. Зачем я сделал именно так, станет понятно в
следующий раз. А на сегодня всё! Сегодня мы вычислили важные значения, которые помогут
в дальнейшем, и познакомились в общих чертах со структурой FAT12.

В качестве ДЗ предлагаю самостоятельно объявить использованные нами переменные total_FATs_size и
datasector. Обе 16-битного формата. Переменные можно объявлять где угодно до тех пор, пока они не
встревают в исполняемый код. Например, можно вставить между dw 0AA55h и CSEG ends

Урок ОСдева №4: работа с RAM, адресация в 16-битном режиме, регистры процессора.

Поздравим себя. В прошлый раз мы добавили блок параметров BIOS, после чего винда перестала
ругаться на дискету. Пора начинать писать загрузчик. Но перед этим надо подробнее
разобраться в специфике программирования на ассемблере. Всё-таки он сильно отличается от языков
высокого уровня. Давайте вспомним, как выглядела программа в конце прошлого поста.

.386p
CSEG segment use16
ASSUME cs:CSEG, ds:CSEG, es:CSEG, fs:CSEG, gs:CSEG, ss:CSEG

begin:            jmp short execute                   ;Точка входа. Перейти к исполняемой части.
                     nop                                       ;Пустой оператор. Заполняет 3-й байт перед BPB.

;БЛОК ПАРАМЕТРОВ BIOS===================================================================;
;=======================================;
;Блок параметров BIOS, 33 байта.
;Здесь хранятся характеристики
;носителя. Должен быть в 3-х байтах
;от начала загрузочного сектора.
;=======================================;
BPB_OEMname    db 'BOOTDISK'    ;0-7. Имя производителя. Может быть любым.
BPB_bytespersec  dw 512              ;8-9. Размер сектора в байтаx.
BPB_secperclust   db 1                  ;10. Количество секторов в кластере.
BPB_reserved      dw 1                  ;11-12. Число зарезервированныx секторов (1, загрузочный).
BPB_numFATs     db 2                   ;13. Число FAT.
BPB_RDentries    dw 224               ;14-15. Число записей Корневой Директории.
BPB_sectotal       dw 2880             ;16-17. Всего секторов на носителе.
BPB_mediatype   db 0F0h              ;18. Тип носителя. 0F0 - 3,5-дюймовая дискета с 18 секторами в дорожке.
BPB_FATsize       dw 9                   ;19-20. Размер FAT в сектораx.
BPB_secpertrack dw 18                  ;21-22. Число секторов в дорожке.
BPB_numheads   dw 2                   ;23-24. Число головок (поверxностей).
BPB_hiddensec    dd 0                  ;25-28. Число скрытыx секторов перед загрузочным.
BPB_sectotal32    dd 0                  ;29-32. Число секторов, если иx больше 65535.

;===============================================;
;Расширенный блок параметров BIOS, 26 байт.
;Этот раздел используется в DOS 4.0.
;===============================================;
EBPB_drivenum   db 0                       ;0. Номер привода.
EBPB_NTflags      db 0                      ;1. Флаги в Windows NT. Бит 0 - флаг необxодимости проверки диска.
EBPB_extsign      db 29h                   ;2. Признак расшренного BPB по версии DOS 4.0.
EBPB_volID         dd 0                      ;3-6. "Серийный номер". Любое случайное число или ноль, без разницы.
EBPB_vollabel     db 'BOOTLOADER '  ;7-17. Название диска. Устарело.
EBPB_filesys       db 'FAT12   '           ;18-25. Имя файловой системы.



;ИСПОЛНЯЕМЫЙ БЛОК=====================================================================;
execute:                     cli
                                 hlt

           org 1FEh                              ;Заполняет память нулями до 511-го байта.
           dw 0AA55h                           ;Байты 511 и 512. Признак загрузочного сектора.
CSEG ends
end begin

Я снабдил всё подробными комментариями. Надеюсь, они помогут вам освежить память. Вкратце -
после запуска программы процессор выполняет переход к метке execute - и после этого останавливается
командами cli и hlt. Давайте добавим следующий код после execute, а потом разберём его.

execute:             mov ax,07C0h
                         mov ds,ax
                         mov es,ax
                         mov fs,ax
                         mov gs,ax

                         cli
                         mov ss,ax
                         mov sp,0FFFFh
                         sti

                         push ax
                         mov ax,offset stop
                         and ax,03FFh
                         push ax
                         retf

stop:                  cli
                         hlt

           org 1FEh                                     ;Заполняет память нулями до 511-го байта.
           dw 0AA55h                                  ;Байты 511 и 512. Признак загрузочного сектора.

Целая куча новых команд. Для того, чтобы их понять, придётся освоиться с новыми понятиями.

Регистр - ячейка памяти процессора, которая может выполнять какую-то конкретную задачу
или иметь общее назначение. Программируя на ассемблере, вы постоянно будете оперировать
регистрами: помещать в них данные, извлекать, модифицировать и т.д. В 16-битном режиме
процессор использует следующий набор регистров: AX, BX, CX, DX, SI, DI, BP, SP, flags, CS, DS, ES,
FS, GS, SS. С функциями каждого из них будем разбираться по мере надобности.

Сегмент:смещение - устаревшая система адресации, применявшаяся в эпоху 16-битных процессоров.
Тем не менее, для нас она важна, так как ради обратной совместимости именно в этом
режиме BIOS оставляет систему перед запуском загрузчика.

Постараюсь объяснить. 16-битная разрядность процессора подразумевает, что за раз он может обработать
16 бит данных. Максимальное значение, которое можно передать 16 битами - 65535. Это ограничение
касается и адресации памяти. Выходит, процессору доступно всего (65536/1024) 64 килобайта RAM. Чтобы
обойти это ограничение, была придумана модель адресации segment:offset. Сегмент в ней - это базовый адрес,
от которого считается смещение. Регистры процессора CS, DS, ES, FS, GS и SS - сегментные. Они используются
для указания адреса в памяти, от какого отсчитывается смещение. Например, DS:0050h означает байт 0050h
от значения, помещённого в DS. Вернее, от значения в DS*16. Это называется гранулярностью. Единица,
помещённая в регистр DS, устанавливает основание сегмента не в 1-й байт, а в 16-й. За счёт этого нам
становится доступен целый мегабайт оперативной памяти! (или даже больше с некоторыми ухищрениями,
но рассказывать о них я большого смысла не вижу, т.к. мы всё равно скоро покинем 16-битное царство)

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

Команда mov op1,op2 используется для того, чтобы переместить значение op2 в op1. В качестве
операнда op1 может выступать адрес ячейки памяти или регистр. В качестве op2 может быть ячейка
памяти, регистр или конкретное значение. Есть два ограничения: операнды должны совпадать по
разрядности (нельзя поместить содержимое 16-битного регистра в 32-битный, например) и в качестве
обоих операндов не могут быть адреса в памяти. Так что делает этот код?

mov ax,07C0h
mov ds,ax
mov es,ax
mov fs,ax
mov gs,ax

Правильно, он помещает значение 07С0h в регистр AX,  потом копирует AX в сегментные регистры
DS, ES, FS и GS. Зачем? Затем, что BIOS копирует загрузочный сектор в 07С0h:0000h. Так как
наш загрузчик находится по этому адресу, будет правильным установить значения сегментных регистров
так, чтобы они указывали туда же. По какой-то причине (ей-богу не помню!) присваивать значения
сегментным регистрам напрямую нельзя, но можно через другой регистр - поэтому сначала мы загружаем
его в AX, а уже AX копируем в сегментные регистры. Вы наверое уже обратили внимание, что сегментные
регистры здесь не все. Для модификации оставшихся надо немного поплясать с бубном.

cli
mov ss,ax
mov sp,0FFFFh
sti

Что происходит здесь? Пара команд cli и sti запрещает и разрешает прерывания. Прерывания - то, при помощи
чего разные устройства в компьютере общаются с процессором. Они могут поступать от таймеров, дисковых
контроллеров и из множества других источников. Позже мы ещё поговорим о них подробно, а сейчас достаточно
знать, что команда cli вешает на процессор знак "не беспокоить". sti, соответственно, его снимает.
Дело в том, что SS - это сегментный регистр стека. При манипуляциях с ним лучше убедиться, что в
неподходящий момент не произойдёт переключение задачи. Обратите внимание: сегмент стека у нас там же,
где и загрузчик. Получается, помещая данные в стек, мы затрём часть собственного кода? Нет. Позиция стека
передаётся парой регистров SS:SP. SS - сегмент, а SP - смещение. mov sp, 0FFFFh устанавливает начало
стека в конец сегмента. Получается, ему некуда расти? Тоже нет. Стек растёт в обратном направлении.
Если мы командой push отправим в стек 16-битное слово, то указатель изменит значение на 0FFFDh. Таким
образом, загрузчик и стек находятся в разных концах 64-килобайтного сегмента, и расстояние между ними
вполне приличное.

push ax
mov ax,offset stop
and ax,03FFh
push ax
retf

Соберитесь, последний на сегодня кусок кода. Здесь мы модифицируем сегментный регистр кода, CS. К нему
тоже нужен особый подход. Кстати, самое время поговорить о том, как процессор узнаёт, какую команду выполнять
следующей. Как и в случае стека, существует указатель в виде пары регистров CS:IP. Каждый раз после
считывания из памяти инструкции IP увеличивается на её размер в байтах. Все модели BIOS помещают загрузчик
в 07C0h:0000h, но вот состояние CS:IP может быть разным: например, 07C0h:0000h и 0000h:7C00h указывают на
один и тот же байт в памяти, но во втором случае у нас могут быть проблемы. В каком именно состоянии
оказались регистры CS:IP при старте загрузчика, мы не знаем, поэтому лучше перестраховаться и установить
своё значение.

Как установить значение CS:IP? Например, при помощи инструкции дальнего возврата retf. Обычно она
используется для возврата из процедур, но подойдёт и нам, так кк делает именно то, что нужно: меняет
значения CS:IP. Сегмент и смещение для возврата должны быть в стеке. В AX у нас значение сегмента, 07C0h,
так что командой push отправляем его в стек. А вот с IP придётся повозиться. Щас объясню. CS в данный момент
может быть установлен либо в 07C0h, либо в 0000h. Значит, любое считанное нами смещение относительно его
начала будет равно или X или X+7C00h. Нам нужно однозначно привести его к X. Как это сделать? Команда
mov AX,offset stop помещает в AX смещение метки stop (то есть, конечно, команды cli, сами метки
в исполняемом файле физически не присутствуют и места не занимают). 7С00h, если его перевести в
двоичный вид, будет равно 111110000000000b. Соответственно, искомый X помещается в восьми нулях в начале
значения.  обнуление старших пяти единиц будет эквивалентно уменьшению значения на 7С00h, что нам и нужно.
Про логические операции поговорим позже, но пока знайте, что команда and AX,03FFh делает как раз это:
обнуляет все старшие разряды AX, начиная с первой единицы в 111110000000000b. 03FFh, кстати, в
двоичном виде будет выглядеть так: 1111111111b. Заметили связь? В общем, если кто-то не разбирается в
логических операциях, то ДЗ на сегодня - просветиться по этой теме.

Фух, чёрт возьми, на сегодня всё! Теперь наш загрузчик будет работать в предсказуемой среде, что сэкономит
нам море усилий.

Уроки ОСдева №3: блок параметров BIOS

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

1. При попытке открыть образ дискеты Wndows говорит, что она не отформатирована.
2. BIOS не воспринимает дискету как загрузочную и пишет что-нибудь вроде "no bootable device found".
Давайте разбираться. Если помните, в конце прошлого поста наша будущая программа-загрузчик выглядела так:
.386p
CSEG segment use16
ASSUME cs:CSEG, ds:CSEG, es:CSEG, fs:CSEG, gs:CSEG, ss:CSEG
begin:                    cli
                             hlt
CSEG ends
end begin

По сути это просто заглушка, которая при запуске должна останавливать процессор. Причина ругани Виндоус в том, что в нулевом секторе на отформатированном носителе хранится важная структура данных - блок параметров BIOS (BPB). Записав туда же нашу программу, мы его пот+ёрли. Для того, чтобы этого избежать, нам придётся воссоздать BPB в тексте программы. Для этого нужно знать геометрию носителя. К счастью, в наше время сохранился только один тип дискет.

Непроверенный метод: возможно, если с помощью утилиты debug записать программу не в начало сектора, а со смещением, достаточным, чтобы пропустить оригинальный BPB, то он не пострадает, но я не проверял. По-моему, debug всё равно забьёт остатки сектора каким-то мусором.

Во-первых, измените программу вот так:

.386p
CSEG segment use16
ASSUME cs:CSEG, ds:CSEG, es:CSEG, fs:CSEG, gs:CSEG, ss:CSEG
begin:                     jmp short execute
                             nop
execute:                 cli
                             hlt
CSEG ends
end begin

У нас появились две новые инструкции: jmp short и nop. Последняя - просто пустой оператор. Процессор пропускает его, не выполняя никаких действий. Занимает 1 байт. jmp - инструкция перехода. jmp short - переход в пределах 127 байт от текущего положения. Исполняется гораздо быстрее jmp, так что везде где возможно - используйте его. Занимает 2 байта. execute - название метки, на которую указывает инструкция jmp short.

Зачем всё это и зачем nop? BPB должен располагаться строго в трёх байтах от начала нулевого сектора. Эти три байта и занимают инструкции jmp short execute и nop. Таким образом, когда программа начнёт исполняться, первой инструкцией, которую выполнит процессор, будет пропустить BPB. В противном случае он бы попытался исполнить его как код, что привело бы к катастрофе.
Теперь давайте вставим сам блок параметров BIOS между nop и меткой execute.

.386p
CSEG segment use16
ASSUME cs:CSEG, ds:CSEG, es:CSEG, fs:CSEG, gs:CSEG, ss:CSEG
begin:                     jmp short execute
                             nop

                BPB_OEMname          db    'BOOTDISK'
                BPB_bytespersec       dw    512
                BPB_secperclust        db     1
                BPB_reserved            dw    1
                BPB_numFATs           db     2
                BPB_RDentries          dw    224
                BPB_sectotal             dw    2880
                BPB_mediatype         db     0F0h
                BPB_FATsize             dw     9
                BPB_secpertrack        dw     18
                BPB_numheads         dw     2
                BPB_hiddensec          dd     0
                BPB_sectotal32          dd     0
                EBPB_drivenum         db     0
                EBPB_NTflags            db     0
                EBPB_extsign            db     29h
                EBPB_volID               dd     0
                EBPB_vollabel            db     'BOOTLOADER '
                EBPB_filesys              db     'FAT12   '

execute:                 cli
                             hlt

CSEG ends
end begin

BPB - это блок данных, и здесь мы впервые объявляем переменные. В TASM это выглядит так: BPB_OEMname (имя) -пробел- db, dw, dd или dq -пробел- 'BOOTDISK' (значение). Имени может и не быть, но тогда к переменной нужно будет обращаться по смещению, это не очень удобно. DB, DW, DD и DQ - сокращение от define byte (word, double word или quad word) - обозначают размер переменной. Соответственно, 1, 2, 4 или 8 байт. Инстркция этого типа позволяют объявлять целые серии значений через запятую: myvalue dw 2, 5, 165, 776. С помощью инструкции db можно объявлять строки: mytext db 'Allo, Yoba!' Обратите внимание, что в плоском бинарнике переменные при компиляции не выносятся в какую-то специальную область данных. В исполняемом файле они будут именно там, где вы их объявили в тексте программы. Ещё важный момент: имена переменных только для вашего личного пользования, в исполняемый файл они не попадут, так что вы не обязаны копировать названия у меня. Теперь давайте посмотрим, что за информация хранится в BPB.

BPB_OEMname - 8 байт: по идее здесь должно быть название производителя, но по факту вы можете писать что угодно, никто на это значение не смотрит.
BPB_bytespersec - 2 байта: размер сектора в байтах, для дискет как правило 512.
BPB_secperclust - 1 байт: число секторов в кластере. Про кластеры мы поговорим позже, но в случае с дискетами секторы и кластеры соответствуют друг другу.
BPB_reserved - 2 байта: число зарезервированных секторов, недоступных файловой системе. В нашем случае такой один, это наш загрузочный сектор.
BPB_numFATs - 1 байт: количество FAT (file allocation table), таблиц распределения файлов. Так как носители информации (особенно дискеты) подвержены порче, а FAT - очень важная часть файловой системы, для неё часто делается резервная копия.
BPB_RDentries - 2 байта: количество записей в корневой директории (Root Directory). Про корневую директорию тоже будем говорить в другой раз, но пока можете представить её как список файлов с указанием их физического расположения на носителе.
BPB_sectotal - 2 байта: число секторов на диске, если их не больше 65535. Если больше, здесь должен быть 0.
BPB_mediatype - 1 байт: тип носителя. F0 - код для 3,5-дюймовой дискеты с 18 секторами в дорожке.
BPB_FATsize - 2 байта: размер одной FAT в секторах.
BPB_secpertrack - 2 байта: число секторов в дорожке.
BPB_numheads - 2 байта: число головок.
BPB_hiddensec - 4 байта: количество скрытых секторов перед загрузочным, в нашем случае 0.
BPB_sectotal32 - 4 байта: число секторов, если их больше 65535. Если меньше, здесь должен быть 0.

Здесь стандартный BIOS Parameter Block заканчивается и начинается расширенный, который появился в поздних версиях DOS.

EBPB_drivenum - 1 байт: бесполезная переменная, хранящая номер привода, в который был вставлен носитель при форматировании.
EBPB_NTflags - 1 байт: флаги Вин НТ. Если установлен бит 0, необходимо проверить носитель на битые секторы. Значения других флагов не знаю.
EBPB_extsign - 1 байт: признак расширенного BPB. Для нашей версии должно быть 29h.
EBPB_volID - 4 байта: случайный номер, который присваивается при форматировании. В общем бесполезен.
EBPB_vollabel - 11 байт: имя носителя.
EBPB_filesys - 8 байт: имя файловой системы.

Если вы теперь заново скомпилируете программу и запишите на дискету, то она отлично откроется в Windows. Первая проблема решена, но осталась вторая: дискета всё ещё не опознаётся как загрузочная. Вспоминаем: для этого последние 2 байта загрузочного сектора должны иметь значения AAh и 55h. Добавим ещё две строчки в нашу программу:

.386p
CSEG segment use16
ASSUME cs:CSEG, ds:CSEG, es:CSEG, fs:CSEG, gs:CSEG, ss:CSEG
begin:                     jmp short execute
                             nop

                BPB_OEMname          db    'BOOTDISK'
                BPB_bytespersec       dw    512
                BPB_secperclust        db     1
                BPB_reserved            dw    1
                BPB_numFATs           db     2
                BPB_RDentries          dw    224
                BPB_sectotal             dw    2880
                BPB_mediatype         db     0F0h
                BPB_FATsize             dw     9
                BPB_secpertrack        dw     18
                BPB_numheads         dw     2
                BPB_hiddensec          dd     0
                BPB_sectotal32          dd     0

                EBPB_drivenum         db     0
                EBPB_NTflags            db     0
                EBPB_extsign            db     29h
                EBPB_volID               dd     0
                EBPB_vollabel            db     'BOOTLOADER '
                EBPB_filesys              db     'FAT12   '

execute:                 cli
                             hlt

               org 510
                dw 0AA55h

CSEG ends
end begin

Команда org 510 заполнит нулями место от текущей позиции до 510 байта, а в последние два мы поместили метку загрузочного сектора. Вуаля, проблема 2 решена.

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

Нужны ли туторы по UEFI?
Да
88(69,29%)
Нет
8(6,3%)
Я не знаю, заебал свою гиковскую xуйню сюда постить, вали на гитxаб!
31(24,41%)

Уроки ОСдева №2: схемы адресации и твоя первая программа на ассемблере

Если кто-то решил следовать этим туторам, самое время обзавестись необходимыми инструментами. Скачайте turbo assembler (TASM) или любой другой привычный вам. Установите виртуальную машину с Windows XP. Работать с большинством используемых программ проще в ней. Я бы посоветовал Oracle VirtualBox.

CHS

В прошлый раз мы выяснили несколько важных вещей:
- для того, чтобы загрузить ОС с дискеты, нам нужна программа-загрузчик.
- программа-загрузчик должна занимать ровно 512 байт, причём последние два байта должны содержать метку загрузчика (AA55h).
- программа-загрузчик должна располагаться в первом физическом секторе носителя.

Эта информация в свою очередь ставит перед нами новый вопрос: как поместить загрузчик на полагающееся ему место? Придётся разбираться в схемах адресации данных на цифровых носителях. Тема не очень простая, но необходимая, так что крепитесь.

,geek,Прикольные гаджеты. Научный, инженерный и  айтишный юмор,OSDev,Операционная система,разработка,программирование,ассемблер,песочница

На картине выше - структура дискового носителя. Это может быть жёсткий диск, дискета или CD, суть не поменяется. Головка, она же лицо, - это одна сторона поверхности носителя (пластины). У жёсткого диска на рисунке 4 двусторонних пластины, то есть 8 головок. Головка в свою очередь разбита на концентрические дорожки (они же цилиндры), а те - на одинакового размера (как правило 512 байт) секторы. Независимо от радиуса цилиндра в каждом из них одинаковое число секторов, что позволяет для доступа к любому конкретному сектору использовать трёхкомпонентную координату Цилиндр-Головка-Сектор (Cylinder-Head-Sector, CHS). Важно помнить, что в системе CHS цилиндры и головки нумеруются с 0, а секторы - с 1. То есть для доступа к первому физическому сектору носителя наши координаты будут иметь значения: 0(цилиндр), 0(головка), 1(сектор).

Проблема в том, что привычные нам файловые системы скрывают физическую геометрию носителей за набором абстракций вроде файлов и папок. К счастью, есть альтернативы. Например, в Windows есть команда debug. Выглядит так:

debug boot.bin(имя файла, загружается в память по адресу 100h)
-w(запись) 100(адрес загруженного файла) 0(номер привода) 0(номер первого сектора) 1(число секторов для загрузки)
-q(выход)

Я выше писал, что секторы нумеруются с 1, но debug закон не писан. Всё остальное время правило в силе.

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

LBA

Информацию из этого раздела мы пока использовать не будем, но позже она пригодится. Формат CHS на сегодняшний день устарел. Практически все устройства нативно поддерживают схему адресации под названием LBA (Linear Block Addressing), в которой к секторам обращаются просто по их порядковому номеру, без учёта геометрии носителя. Тем не менее, CHS поддерживается на уровне эмуляции и для работы с дискетами пользоваться мы будем именно ей.

boot.asm

Итак, мы разобрались, как поместить нашу программу в нужное место на дискете. Самое время приступать к написанию кода! Мы, конечно, не будем в этот раз писать полный загрузчик. Это будет просто небольшая вводная в турбо ассемблер. Она предназначена для людей без опыта в ассемблере и подразумевает, что вы будете пользоваться TASM. Если вы уже знаете x86 ассемблер и намерены пользоваться каким-то другим - смело пропускайте конец статьи.

Скачали TASM? Отлично, инсталлируйте его в какое-нибудь легкодоступное место на диске и в папке BIN/ создайте папки OS/boot/stage1/.

Теперь в stage1/ создайте пустой текстовый файл, переименуйте его в boot.asm и откройте текстовым редактором. В файле создайте следующий текст:

.386p
CSEG segment use16
ASSUME cs:CSEG, ds:CSEG, es:CSEG, fs:CSEG, gs:CSEG, ss:CSEG
begin:

CSEG ends
end begin

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

.386p - указание компилятору сверять инструкции с набором для 386 процессора в защищённом (protected, отсюда p) режиме. Соответственно, если компилятор встретит инструкцию, которую данный процессор не поддерживает, он выдаст ошибку.

CSEG - название сегмента, в котором будет содержаться код нашей программы.

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

begin - метка начала программы. Отсюда будет начинаться собственно код.

CSEG ends - конец сегмента CSEG.

end begin - метка конца программы.

Скомпилировать программу в таком виде не выйдет, так как в ней нет ни одной инструкции. Давайте добавим парочку.

.386p
CSEG segment use16
ASSUME cs:CSEG, ds:CSEG, es:CSEG, fs:CSEG, gs:CSEG, ss:CSEG
begin:                    cli
                             hlt
CSEG ends
end begin
Мы добавили 2 команды: cli и hlt. Первая запрещает прерывания, то есть не даёт устройствам отправить сигнал процессору, а вторая останавливает сам процессор. Таким образом, сейчас наша программа при запуске просто "вешает" компьютер. Тем не менее, её уже можно превратить в исполняемый файл.

Откройте командную строку, перейдите в папку BIN/ и введите: tasm OS/boot/stage1/boot.asm. После нажатия enter в папке BIN/ появится файл BOOT.OBJ. Это ещё не конец. Файл .obj - это инструкция для линкера, так что пишите: tlink boot.obj. Теперь у вас добавились два новых файла, BOOT.MAP и BOOT.EXE, последний из которых - и есть исполняемый файл, который нам нужен! Незадача в том, что мы пишем не приложение под DOS, а операционную систему, которая должна будет работать на голом железе. exe-формат содержит таблицы релокации и всякие прочие данные, которые нам будут мешать. В нашем исполняемом файле должны быть только инструкции программы и больше ничего. К счастью, и тут есть готовое решение. Пишите в командной строке exe2bin boot.exe и жмите enter. Появился файл boot.bin. Проверьте его размер, он должен занимать ровно 2 байта, по одному байту на инструкции cli и hlt. Успех!

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

Уроки ОСдева №1

В прошлом посте я представил свою операционку и игру, которую пишу под неё в свободное время. Там же я сделал несколько заявок на будущие посты. Среди прочего я сказал, что, возможно, сделаю серию образовательных постов про разработку операционных систем и низкоуровневое программирование. Сегодня будет первый такой пост. Для понимания материала не нужно знать ассемблер или разбираться в устройстве ОС - про всё это я буду рассказывать. Нужно иметь представление об архитектуре компьютера в общих чертах: понимать, что такое BIOS, процессор, материнская плата, оперативная память, видео-, звуковая и сетевая карты, жёсткий диск, оптические и флоппи-приводы и как примерно всё это между собой скрепляется. Неплохо бы знать, что такое бит, байт и слово. Вообще супер, если вы в курсе, чем десятичная система счисления отличается от двоичной и шестнадцатеричной и умеете переводить из одной в другую. Поехали.

P.S.: если знаете английский, советую зайти сюда. Это довольно старая серия уроков по ОСдеву для новичков. Я в своё время почерпнул там очень много и в своих постах наверняка буду невольно цитировать оттуда.


Часть 1, теоретическая.

С чего начать? Вопрос, который возникает в голове любого, кто собрался писать ОС с нуля. В интернете полно тематических ресурсов, но не так много обучающих, где бы задача написания операционной системы разбивалась на небольшие последовательные этапы-уроки. Например, на OSDev.org очень много информации, распределённой по тематическим разделам, но составить на её основе у себя в голове необходимую последовательность действий для новичка будет очень сложно.

Я думаю, стоит начать с включения компьютера. Это не шутка: чтобы создать свою операционную систему, надо до определённой степени понимать как работает компьютер. Что происходит, когда вы нажимаете кнопку POWER на системном блоке и как у вас на экране оказывается ваш заваленный ярлыками и "новыми папками" рабочий стол? Для того, чтобы программа (а наша ОС - это, конечно же, программа) начала исполняться, она должна сначала попасть в оперативную память. Содержимое оперативной памяти же на момент включения пусто. Если среди читающих есть инженеры, советую зажмуриться и пропустить до следующего абзаца: сейчас будет упрощённая модель.

1. Нажатая кнопка POWER посылает электрический сигнал на материнскую плату.
2. Сигнал доходит до материнской платы и отправляется к блоку питания.
3. Блок питания просыпается и начинает подавать энергию подключенным устройствам.
4. Блок питания посылает сигнал на материнскую плату, начинает исполняться программа BIOS.
5. BIOS проводит POST (power-on self-test), посылая сигналы разным устройствам и получая (или не получая) от них ответ. Если устройство не отправило ответ, оно помечается как нерабочее или отсутствующее. Тут же BIOS определяет количество оперативной памяти и некоторые другие параметры системы.
6. Если POST окончен и никаких критических поломок не выявлено, BIOS сверяется со списком загрузочных устройств. Наверняка вы хоть раз его видели, если устанавливали Windows: его обычно можно настроить через интерфейс BIOS, выбрав, с чего загружать ОС (floppy, HDD, USB, ...).

Вот тут начинается часть, которая интересует нас. Предположим, мы вставили дискету с нашей ОС в привод и настроили приоритет загрузки следующим образом: CD-ROM, флоппи-привод, жёсткий диск, USB. Как BIOS определит, что на одном из носителей есть операционная система для загрузки? Физическое устройство цифровых носителей и способы доступа к информации на них это тема для отдельного урока или даже нескольких, так что пока удовольствуемся упрощённой схемой: BIOS считывает с 0 по 511 байты носителя и проверяет, чему равны байты 510 и 511. Если они равны 170 и 85 (AAh и 55h в шестнадцатеричной системе), BIOS считает, что нашёл программу-загрузчик. После этого считанный участок носителя размером в 512 байт загружается в оперативную память и запускается центральный процессор компьютера, который начинает выполнять загруженную программу. Программа-загрузчик догружает остальные файлы ОС и располагает их в памяти нужным образом, а потом говорит процессору, откуда нужно начать выполнение ОС.

Какие выводы можно сделать из полученной информации?

1. Помимо операционной системы на носителе должна быть программа-загрузчик, т.к. компьютер не знает, как именно структурированы файлы ОС и куда их надо загружать. Загрузка ОС - тоже задача разработчика.
2. Загрузчик должен быть не больше 510 байт, ведь BIOS считывает 512 и последние два из них заняты меткой загрузчика.
3. Последние два байта программы-загрузчика должны быть равны AA55h.
3. Загрузчик должен занимать строго определённое место на носителе: с 0 по 511 байты. Думаю, вы замечали, что когда копируете файлы на носитель обычными методами, никто не спрашивает вас, в какой именно участок памяти вы хотите их поместить. Значит, копировать загрузчик нужно каким-то особенным способом.

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


Продолжать?
Да
306(59,77%)
Единая Россия
206(40,23%)
Здесь мы собираем самые интересные картинки, арты, комиксы, мемасики по теме OSDev (+10 постов - OSDev)