Эта третья часть темы о программировании кастомных операционных систем. Здесь рассмотрим интерфейсы «Desktop Management» DMI с усовершенствованной его версией «System Management» SMBIOS, а так-же работу с дисковыми накопителями «SATA Storage Device» в режиме AHCI. Поскольку тесты на виртуальных машинах типа QEMU и VirtualBox сильно ограничивают возможности и соответственно конечный результат, особое внимание уделяется созданию загрузочных флэшек USB без стороннего софта, в HEX-редакторе. Это позволит протестировать нашу ОС на реальном железе. Вот линки на первую и вторую части статьи.
Часть #3.
1. Создание загрузочной USB-Flash
• Внутренняя организация Flash накопителей
• Вычисление размера в секторах LBA
• Копирование загрузчика и ядра ОС
2. Двоичная база SMBIOS
• Назначение
• Поиск таблицы в памяти
• Формат содержимого
1. Создание загрузочной USB-Flash
Виртуальные машины – это хороший полигон для тестов всевозможного рода программного обеспечения. Внутри их периметра можно наблюдать, например, за малварью не беспокоясь о том, что последствия окажут влияние на хостовую/основную систему. Но когда дело доходит до работы с физ.оборудованием на материнской плате, полностью доверять ВМ уже не приходится, поскольку на виртуализацию железа софт данного класса просто не рассчитан. В этом случае, с вирт.платформы нам остаётся только мигрировать на реальную, для чего как-нельзя лучше подходит загрузка с Flash-накопителей (время флопиков безвозвратно ушло).
Сейчас в инете можно найти огромное кол-во готового софта для создания Boot-USB, в числе которых Rufus, Sadru, AIO-Boot и многие другие. Однако проблема в том, что почти все эти зверьки заточены под создание установочных Win/Linux/MAC/DOS, а если и предлагает иные варианты операционных систем (как в нашем случае), то нужно быть готовым к длительным танцам с бубном. Именно по этой причине мы создадим загрузочный флэш-брелок вручную без специальных утилит, тем более что решается это дело буквально за пару минут.
1.1. Внутренняя организация Flash накопителей
USB диски отличаются от жёстких HDD только способом хранения данных – в USB роль запоминающих ячеек выполняют мосфеты (транзисторы) с устойчивым состоянием, а в HDD слой магнитного напыления. А вот логическая организация уже одинаковая: всё та-же таблица разделов в первом секторе «PartitionTable», и те-же секторы с нумерацией LBA «Logical Block Address». Однако есть и пара моментов, на которые стоит обратить внимание.
1. В классическом варианте USB поддерживает всего 1 раздел Volume, в то время как на HDD с разметкой MBR основных томов может быть макс 4. Конечно ничто не мешает создать и USB с несколькими разделами, но это уже нетипичная ситуация – вполне возможно Linux и проглотит подобного рода каприз флэшки, а вот Windows может и не принять пассажира на свой борт.
2. Как правило, во-всех HDD первый раздел всегда начинается с сектора LBA(64) – это дань прошлому, когда использовалась трёхмерная геометрия пространства CHS (цилиндр, головка, сектор). Поскольку в одном цилиндре было строго 63 сектора, то раздел априори начинался с нового цилиндра, а не с его середины. Кстати это касается и твёрдотелых накопителей SSD, т.к. к ним предъявляется требование обратной совместимости. Но в силу того, что у USB нет физических цилиндров (концентрических треков на поверхности), их первый и единственный раздел может начинаться с любого сектора, хотя почему-то всеми утилитами форматирования был выбран именно LBA(128). Таким образом, между загрузчиком и первым разделом USB-Flash всегда имеется свободное пространство как минимум в 128-секторов, а это
128*512=64
Кбайт.1.2. Вычисление ёмкости USB-накопителя в секторах
Таким образом, вооружившись теорией можно создать загрузочную флэш вручную, просто открыв носитель в двоичном редакторе HxD и оформить подходящую «PartitionTable» для одного раздела. Однако заниматься таким фехтованием не имеет смысла, когда можно автоматизировать весь процесс например в виндовой утилите ком.строки DISKPART – это избавит нас от нудного вычисления общего кол-ва секторов на флэшке, чтобы установить валидный её размер. Значит жмём комбинацию
Win+R --> diskpart
, и дождавшись приветствия стучим по клаве в следующей последовательности:
Код:
DISKPART> list disk <--------- Запрашиваем список имеющихся в наличии дисков
Диск ### Состояние Размер Свободно Дин GPT
-------- --------- ------------- ---------- --- ---
Диск 0 В сети 74 Gбайт 1024 Kбайт
Диск 1 В сети 74 Gбайт 0 байт
Диск 2 В сети 3854 Mбайт 0 байт
DISKPART> select disk 2 <--------- Установить фокус на USB диск (в моём случае №2)
Выбран диск 2.
DISKPART> detail disk <--------- Тест, что это реально наша Flash, а не хард с полезной инфой
ADATA USB Flash Drive
ИД диска : 00000000
Тип : USB
Состояние : В сети
Путь : 0
Конечный объект : 0
ИД LUN : 0
Путь к расположению : UNAVAILABLE
Только для чтения : Нет
Загрузочный диск : Нет
Диск файла подкачки : Нет
Диск спящего режима : Нет
Диск аварийного дампа: Нет
Кластерный диск : Нет
Том ### Имя Метка ФС Тип Размер Состояние
----- --- --- --------- ----- -------- ------- ---------
Том 5 H RAW Сменный 3853 Mб Исправен
DISKPART> clean <--------- Обнулить текущий формат и геометрию
Очистка диска выполнена успешно.
DISKPART> create partition primary <--------- Создать основной раздел (по факту создаётся новая PartTable)
Указанный раздел успешно создан.
DISKPART> select partition 1 <--------- Установить на него фокус
Выбран раздел 1.
DISKPART> active <--------- Сделать раздел активным/загрузочным (сигнатура 80h)
Раздел помечен как активный.
DISKPART> assign <--------- Задать букву флэшке USB (иначе ОС потом её не увидит)
Назначение точки подключения выполнено успешно.
DISKPART> exit <--------- Game Over!
Завершение работы DiskPart...
С этого момента флэшка превратилась в загрузочную, а что она будет загружать – это уже зависит от нашего настроения. Обратите внимание, что мы не форматируем USB-Flash оставляя сырую файловую систему RAW, поскольку ядро нашей ОС будет работать без Linux/Win на чистом энтузиазме процессора CPU. Если сейчас вытащить брелок c порта и вставить вновь на место, то Win тут-же предложит нам отформатировать его, на что необходимо ответить категорическим отказом, иначе всё проделанное выше прямиком отправится коту под хвост.
1.3. Копирование загрузчика и ядра ОС на Flash
Теперь запустим двоичный редактор HxD, и через меню «Инструменты» откроем в нём новоиспечённую Boot-Flash, не забыв при этом снять галку «Только для чтения». Всё, кроме выделенной на скрине ниже «Partition Table» нам не нужно, а потому смело забиваем с адреса нуль этот хлам нулями, после чего сохраняемся по
Ctrl+S
.Если скопировать выделенную область в новый файл, то получим наглядную картину геометрии нашей Boot-флэш. Критически важными здесь являются всего три поля – это флаг загрузочного раздела
80h
в первом байте, и 2 дворда в хвосте. Значение первого равно 00000080h
и это сектор LBA(128) с которого начинается раздел Volume, а в последние 4-байта утилита DISKPART на автомате вычислила и прописала общее число секторов в разделе. В данном случае видим значение 7.892.864
и если умножить его на размер одного сектора =512 байт, то получим ёмкость накопителя ~4 GB.Таким образом, если у вас уже была отформатированная рабочая USB-Flash с валидно указанной геометрией в двух последних двордах, то чтобы сделать из неё загрузочную, достаточно просто указать флаг
80h
в первом байте. Всё! ..и никаких сторонних утилит не нужно.Прежний загрузчик нашей ОС был заточен под Floppy(A), но поскольку теперь в качестве носителя будем использовать USB-брелок, то придётся изменить стандартную функцию чтения секторов
AH=2
прерывания int-13h
на расширенную AH=42h
. Аргументы ей передаются через специальный пакет-данных, и в примере ниже я выделил для этого пакета фрейм в стековой памяти. Обратите внимание, что первый сектор раздела вычисляется динамически по значению первого дворда offset 1С6h
, в результате чего данный загрузчик можно будет использовать не только для USB-Flash, но и для жёстких дисков.
C-подобный:
mov ax,word[$$+0x01c6] ;// Первый сектор раздела из PTable (см.предыдущий скрин)
pushd 0 ;// StartLBA Hight ------/----------> Итого 64 бита
push 0 ax ;// StartLBA Low -----/
push 0 ;// Buffer Segment ----------/------> Куда копировать (приёмный буфер в памяти)
push 0x0600 ;// Buffer Offset ---------/
push 32 ;// Sector Counter -----------------> Сколько секторов: 32*512=64К
push 16 ;// Sizeof Packet (размер пакета)
mov ah,42h ;// функция 42h = чтение секторов с диска в память
mov dx,0x80 ;// номер диска
mov si,sp ;// указатель на пакет
int 13h ;// зовём сервис диска
add sp,16 ;// восстановить стек от 'пушей
push cs 0x0600 ;// передать управление ядру ОС!
retf
В скрепку положу готовый бинарь лоадера, а само ядро Kernel соответственно кладём в сектор
LBA(128)
флэшки, что представлено на скрине ниже. Повторюсь, что вставлять данные в редакторе HxD нужно не привычной комбинацией Ctrl+C
, а «Вставкой с заменой» по Ctrl+B
. Иначе размер носителя увеличится и получим ошибку.2. Информационная база SMBIOS
Ещё в 2000-х Microsoft выдвинула жёсткие требования всем разработчикам материнских плат, мол если хотите, чтобы мы гоняли свою ОС на вашем железе, то предоставьте нам всю информацию о бортовом оборудовании своего «авто». Так от производителей появился сначала программный интерфейс DMI (Desktop Management), который позже был усовершенствован в текущий SMBIOS (System Management).
Суть в том, что в глобальном ROM-BIOS системы (которую пишут прогеры производителей мат.плат) имеется процедура для создания информационной базы, куда сбрасывается паспорт всех живых тушканчиков на мат.плате, начиная от CPU с чипсетом, и заканчивая портами подключения внешних устройств. Поскольку эта база создаётся биосом задолго до загрузки ОС, последние приходят уже на готовое – для выбора подходящего драйвера им остаётся только прочитать SMBIOS.
Такие были планы, но на практике Win/Linux сами при загрузке сканируют почву под ногами, не доверяя сведениям разрабов (да и технология Plug&Play предусматривает горячее подключение девайсов уже после загрузки ОС). Не смотря на это, интерфейс SMBIOS активно развивается – начиная с v3.0 имеется поддержка х64, а последняя v3.8 датируется августом 2024-года. В общем как ни крути, но для осдева это как-раз то, что доктор прописал.
2.1. Формат содержимого инфо-базы
На данный момент база SMBIOS включает в себя 46 таблиц. Каждая из них описывает отдельно взятый элемент материнской платы: например в таблице(0) хранится инфа о системном BIOS, в таблице(2) сведения о чипсете, в таблице(4) паспорт процессора CPU, и т.д. Размеры таблиц не фиксированы, и зависят от возможных свойств конкретного оборудования. Внутри базы все таблицы плотно прижаты друг к другу – где заканчивается одна, там сразу начинается следующая. Маркером окончания каждой из таблиц служит пара двоичных нулей
0000h
.Таблицы начинаются с одинакового для всех 4-байтного заголовка «Header», после которого идёт полезная нагрузка в виде бинарных данных, и в хвосте располагаются уже нуль-терминальные текстовые строки (если таковые имеются). Первый байт в заголовке хранит номер/идентификатор таблицы, а второй – размер бинарных данных в ней. В некоторых случаях данные кодируются, а коды оговариваются в спецификации, которую я прикрепил в скрепку к статье. Заголовок заканчивается 2-байтным дескриптором – для нас он не представляет интереса. Вот как это выглядит в утилите RW, где красным я выделил Header:
Такой формат позволяет нам по первому байту определять тип таблицы в глобальной базе SMBIOS, а по второму байту прыгать сразу к её строкам. Например, в данном случае во-втором байте лежит значение(18h), и если использовать его как смещение Offset от начала текущей таблицы (см.зелёный блок), то упрёмся сразу в её текстовые строки. Каждая строка заканчивается одним терминальным нулём, а если встретим пару нулей, значит это конец текущей таблицы, и за ней будет идти сразу следущая.
2.2. Поиск базы SMBIOS в памяти ОЗУ
Адрес базы SMBIOS в системной памяти не регламентируется, и каждый вендор материнских плат волен сам выбирать её местоположение. Она может находится в любом месте начиная с сегментного адреса
F000:0000
. Найти базу можно сканированием памяти по сигнатуре SM на границе параграфа 16-байт. То-есть ставим указатель на сегмент F000:0000
и с шагом в 16-байт ищем строку «SM». Обнаружив сигнатуру, упрёмся в заголовок самой базы, который числится в доках как SMBIOS_ENTRY. В этом заголовке прописывается версия базы, её размер и прочее, а так-же приоритетный для нас ещё один блок инфы с сигнатурой уже DMI (дань прошлому). Именно в этом блоке и будет лежать указатель на первую таблицу(0) в базе, после которой располагаются и все остальные. Кстати заголовок базы SMBIOS как-правило лежит в одном месте ОЗУ, а непосредственно массив таблиц уже вообще в другом. Структура заголовка базы имеет такой прототип:
C-подобный:
struct SMBIOS_ENTRY ;// Offset
SmbSignature dd 0 ;// 00 _SM_
Checksum db 0 ;// 04
Length db 0 ;// 05
Version db 0,0 ;// 06
MaxStructSize dw 0 ;// 08
EntryRevision db 0 ;// 10
Padding db 5 dup(0) ;// 11
DmiSignature db 5 dup(0) ;// 16 _DMI_
DmiChecksum db 0 ;// 21
DmiTableLength dw 0 ;// 22
DmiTableAddr dd 0 ;// 24 <--------- Адрес таблицы(0) в базе SMBIOS
DmiStructCount dw 0 ;// 28
wNumSMBStruc dw 0 ;// 30
ends
Немаловажную роль играет версия SMBIOS в заголовке базы, т.к. некоторые урезаны и имеют неполное кол-во таблиц. Например в первой (наиболее стабильной) версии v2.0 было всего 16 таблиц, а в v2.8 вообще 9 штук. У меня имеются несколько перечисленных ниже спецификаций, из которых я собрал такой сводный лист – имейте это в виду. В своём ядре ОС я буду выводить информацию только из выделенных коричневым таблиц:
Полный исходник сбора информации из инфо-базы SMBIOS положу в скрепке, а здесь приведу лишь пример поиска её в системной памяти:
C-подобный:
;//********************************************************************
;//****************** Меню SMBIOS ***********************************
;//********************************************************************
align 16
@SmbiosPage:
call ClearScreen ;// очистить окно
mov di,(160*2)+74 ;//
call SetCheckBox ;// вставим подчёркивание меню
mov dword[CurrentType],0 ;// номер таблицы(0)
push es 0xf000 ;// поиск сигнатуры "_SM_"
pop es ;// от 000F:0000 до 000F:FFFF
xor di,di ;//
@@: cmp dword[es:di],'_SM_' ;// сравнить с шаблоном
je @f ;// если нашли
add di,16 ;// иначе сл.параграф (шаг 16 байт)
cmp di,0xffff ;// всё проверили?
jnz @b ;// нет – на повтор
pop es ;//
jmp @exitSmBios ;// иначе: на выход
;//----- Нашли заголовок базы SMBIOS ---------------------------------
@@: mov ax, [es:di+6] ;// версия базы в формате BCD
ror ax,8 ;// ...(коррекция Major/Minor)
mov dx, [es:di+22] ;// размер базы
mov ebx,[es:di+24] ;// адрес таблицы(0) в базе
pop es ;//
mov [SmbEntry],di ;// запомнить данные в переменных
mov [SmbVersion],ax ;// ^^^^
mov [SmbSize],dx ;// ^^^^
mov [DmiEntry],ebx ;// ^^^^
Как результат, на двух/своих стационарах я получил следующие логи, которые совпадают с отчётами сторонних утилит по сбору информации о системе. Так, в первом блоке видим паспорт биоса мат.платы, во-втором модель и версию чипсета, в третьем достаточно полная инфа о процессоре, включая частоту внешней шины и кол-во логических/физических ядер, и даже тип сокета (правда кэши я поленился пропарсить). Но что наиболее актуально, так это размер установленной физической памяти DDR-SDRAM в слотах, и если повезёт – её производителя как на втором скрине. А вот тип DDR(2,3,4) к сожалению не указывается в базе SMBIOS и если нужно, то придётся организовать чтение м/схемы SPD на модуле памяти, для чего потребуется доступ к шине
i2c
.Зато у нас уже есть размер физ.памяти (на моих машинах это 4 и 16 гигов), который нельзя получить в реальном режиме никакими иными средствами. Он нам потребуется в части(5) статьи, для организации перехода в защищённый режим с длинной моделью памяти LongMode x64 (см.предпоследний пункт меню).
Сори за качество скринов, т.к. делал их на свой смартфон:
3. Заключение
Чтобы не было каламбура, идентификацию и работу с накопителями ATA/SATA оставим для следующей части, тем-более, что для них в пользовательском интерфейсе нашей ОС выделен отдельный пункт меню. В скрепке лежит спека на SMBIOS v3.8, а так-же исходники с готовыми бинарями USB-загрузчика и ядра Kernel. Всем удачи, пока!