Статья ASM. Программирование ОС [2] – ядро Kernel

Marylin

Mod.Assembler
Red Team
05.06.2019
342
1 513
OSDevLogo2.webp

В первой части обсуждалась тема создания первичного загрузчика самописной ОС, теперь перейдём к коду ядра, которое он собственно загружает. Особое внимание уделяется здесь перефирийной шине PCI современных чипсетов, её архитектурным особенностям и метом подключения к ней физических устройств. Под катом рассматривается и создаваемое биосом конфигурационное простраство PCI, плюс сбор информации о свободных регионах памяти ОЗУ.

Часть #2.

1. Ядро операционной системы
• Сбор информации об устройствах​
• Топология шины PCI​
• Конфигурационное пространство устройств​

2. Память
• Карта свободных регионов памяти​
• Обзор функции AX=E820h прерывания INT-15h​



1. Топология шины PCI

Ещё со-времён процессоров i386, основной шиной на материнской плате была и остаётся PCI (Peripheral-Component-Interconnect). Более того, вплоть до появления памяти DDR (Dual-Data-Rate, передача данных по обоим фронтам синхроимпульсов), PCI связывала северный мост чипсета с самим процессором, исполняя роль внешней его шины. Однако требованиям DDR2 она уже не удовлетворяла, и её пришлось заменить на фронтальную FSB (Front-Side-Bus). Так PCI стала периферийной, к которой и по сей день подключаются буквально все физические устройства с параллельным интерфейсом, кроме последовательных PCI-Express. Начиная с DDR3, и FSB уже отправилась на свалку истории, уступив место более производительным последовательно-параллельным шинам DMI (Direct Media Interface) или QPI (QuickPath Interconnect) – в архитектуре Intel именно они сейчас связывает хаб чипсета PCH (Platform-Controller-Hub) с центральным процессором CPU, хотя AMD использует свою HT (HyperTransport).

Конструктивно PCI является древовидной шиной, кусты которой собираются в сегменты. Для согласования линий передачи, первичная шина PCI подключается к чипсету через главный мост Host-Bridge, а к основной шине через аналогичные мосты PCI-to-PCI могут подключаться ещё до 255 вторичных шин. На каждой из этих шин могут висеть максимум до 32-х устройств, и наконец каждое из девайсов может иметь макс.8 функций. Таким образом, полностью загруженная шина PCI способна унести на себе 256х32х8=65.536 физических устройств, что намного превышает разумный предел на мат.плате.

Из-за такой топологии, для адресации PCI-устройств процессор использует трехмерный адрес Bus : Device : Function, или более краткую его форму BDF. Именно функция здесь представляет конечное устройство, в то время как Device играет роль контейнера. Всё потому, что девайсы могут быть многофункциональными. Например часто на внешней звуковой карте присутствует и гейм-порт для подключения джойстиков. Тогда имеем 1 устройство, и 2 функции в нём. Многофункциональные контролёры широко используются на практике, т.к. позволяют экономить всего 4 линии выбора PCI-слотов INT#A/B/C/D.

PCI-Arch.webp

Как видно из рис.выше, архитектура последовательной PCI-Express в корень отличается от параллельной PCI – она организована по схеме «Звезда», где центром вселенной является рут-хаб. Благодаря соединению точка-точка (одноранговая сеть Peer-to-Peer), девайсы PCI-Ex могут напрямую обмениваться между собой, правда для этого один из них должен поддерживать механизм «Bus-Mastering» (ведущее устройство, мастер). А так, если не учитывать конструктивные особенности, на более высоком уровне имеем всё ту-же адресацию BDF.

Кстати обратите внимание на мост PCI-to-LPC. Он присутствует во-всех современных чипсетах, и служит для согласования 32-битной шины PCI, с 7-битной LPC (Low-Pin-Count), к которой подключается например клавиатура PS/2, флэшка биоса и прочие железяки, т.е. все, кому для работы хватает 2-4 линии обмена. У шины LPC всего 7 обязательных проводников – 4 для адреса/данных, и 3 для сигналов управления. Она работает на такой-же частоте 33 МГц, как и шина PCI.

Все внешние и внутренние устройства на мат.плате классифицируются по назначению. Например диски могут иметь разный интерфейс обмена данными ATA-SATA-SCSI-SAS-NVMe-USB-Floppy, хотя все они выполняют роль накопителя MassStorage. Таким образом, к понятию BDF присавокупляется и триада Class : Subclass : Interface, которую описывает дока организации PCI-Sig под названием .

Чтобы продемонстрировать древо устройств на шине PCI, воспользуемся командой отладчика WinDBG !pcitree. Судя по логу ниже, на моей машине всего 4 шины Bus[0:3], и на каждой висит хотя-бы один девайс со-своей функцией D:F. Если функция имеет отличное от нуля значение, значит устройство является многофункциональным – ярким его представителем здесь можно считать сегменты на шинах 2,3. Помимо BDF, в логе имеем разделённые двоеточием идентификаторы производителя Vendor&Device 8086:29c0, и коды Class&Subclass 0600 = HostBridge.

Код:
0: kd> !pcitree
Bus 0x0 (FDO Ext fffffa80043bb250)
   (d=0,  f=0)  8086:29c0  0600  Bridge/HOST to PCI
   (d=2,  f=0)  8086:29c2  0300  Display Controller/VGA
   (d=1b, f=0)  8086:27d8  0403  Multimedia Device/ High Definition Audio
   (d=1c, f=0)  8086:27d0  0604  Bridge/PCI to PCI

Bus 0x1 (FDO Ext fffffa8004470190)
   (d=1c, f=1)  8086:27d2  0604  Bridge/PCI to PCI

Bus 0x2 (FDO Ext fffffa8004470b40)
   (d=0,  f=0)  10ec:8136  0200  Network Controller/Ethernet
   (d=1d, f=0)  8086:27c8  0c03  Serial Bus Controller/USB
   (d=1d, f=1)  8086:27c9  0c03  Serial Bus Controller/USB
   (d=1d, f=2)  8086:27ca  0c03  Serial Bus Controller/USB
   (d=1d, f=3)  8086:27cb  0c03  Serial Bus Controller/USB
   (d=1d, f=7)  8086:27cc  0c03  Serial Bus Controller/USB
   (d=1e, f=0)  8086:244e  0604  Bridge/PCI to PCI

Bus 0x3 (FDO Ext fffffa8004471190)
   (d=1f, f=0)  8086:27b8  0601  Bridge/PCI to LPC
   (d=1f, f=1)  8086:27df  0101  Mass Storage Controller/IDE
   (d=1f, f=2)  8086:27c0  0101  Mass Storage Controller/IDE
   (d=1f, f=3)  8086:27da  0c05  Serial Bus Controller/SMBus

Поскольку на одной шине обычно резвятся как дети сразу несколько устройств, вполне возможны их конфликты. Например кого обслужить первым, когда 2 устройства одновременно решили воспользоваться шиной? Значит необходим механизм распределения им аппаратных и программных ресурсов, чем занимается как-правило диспетчер Plug&Play в биос. Однако биос, в данном случае, не может самостоятельно принимать такие важные решения, т.к. заранее не знает, с какого типа устройством ему придётся иметь дело. Другим словами нужна автоматизация, которая реализуется весьма интересным способом.

На рисунке ниже представлена схема микропроцессорного устройства, которой придерживаются буквально все девайсы с контролёром на борту. Это касается не только компьютерных устройств, но и всех остальных, например телевизоры, камеры, электронные часы, дроны, и многое другое. В обвязке центрального контролёра всегда будет присутсвовать шина адреса/данных AD[0:32], пин для сброса в дефолт Reset, задающий тактовую частоту работы кварцовый резонатор МГц, порт с атрибутами R/W для внешнего управления, и постоянная память ПЗУ-ROM, куда сохраняется прошивка программного обеспечения. Это базовый минимум, который может быть расширен внешней памятью ОЗУ.

Device.webp

При включении компьютера, диспетчер PnP в биос получает управление, и приступает к распределению ресурсов аппаратным устройствам. Основную проблему здесь создают линии прерываний IRQ, по которым девайсы привлекают к себе внимание центрального процессора. В идеале у каждого устройства должна быть своя индивидуальная линия, тогда и конфликтов можно будет избежать. Однако на практике, в момент загрузки системы кол-во линий ограничено значением 15, а устройств на современных мат.платах намного больше. Пока операционная система не активирует расширенный контролёр прерываний APIC, биос так и будет зажат в рамки IRQ 0:15, иначе ни о какой совместимости с устаревшими LegacyDevice не может быть и речи.

Поэтому производители устройств прошивают в ROM своих железяк минимальные требования к системе, а диспетчер PnP на этапе конфигурирования запрашивает их. Такие циклы на шине PCI назвали «Специальными» – биос посылает запрос девайсу, а тот отвечает 256-байтным блоком данных. Далее блоки всех обнаруженных устройств собираются в специально предназначенную для этих целей область памяти, которая известна как «Конфигурационное пространство PCI», или на английский манер «PCI Config Space». Первые 64-байта в каждом из блоков одинаковы для всех устройств (заголовок Header), а остальные 192 зависят от настроения производителя «Vendor-Specific». В таблице ниже перечислены поля базового блока данных размером 40h=64 байт из ПЗУ любого (поддерживающего технологию PnP) устройства:

PciCfg.webp

Приоритетные для нас поля я выделил здесь цветом. После того-как наша ОС получит управление от биоса, мы должны активировать контролёр прерываний APIC, в результате чего кол-во линий IRQ увеличится с 15-ти сразу до 256. На следующем этапе можно будет уже переназначить установленные линии, для чего потребуется обход всего конфиг.пространства PCI, с записью новых значений в последнее поле блока «Interrupt-Line».

Обнаружение устройств подразумевает чтение первого дворда с идентификаторами Vendor&Device. Диспетчер PnP работает в тесной связке с диспетчером шины PCI, а потому если устройства на шине нет, то последний должен обязательно вернуть нам 0xFFFFFFFF. Если получим любое другое значение Ven&Dev, значит всё ОК и прочитав регистр(2) сможем опознать девайс по его кодам Class/Subclass/Interface.

Регистры базовых адресов BAR[0:5] открывают нам доступ к портам самого контролёра устройства. Младший бит(0) выступает здесь в качестве флага – если в любом из BAR`ов он взведён, значение нужно воспринимать как номер физ.порта в диапазоне 0xFFFF, иначе порт отображается на память, а значение представляет собой её базовый адрес в ОЗУ (как правило самые верхние адреса доступной памяти). Сложив 2 соседних BAR, можно получить и 64-битный адрес – это актуально для работы процессора в режиме LongMode x64. Зашитые вендором значения регистров BAR можно получить и в произвольный момент времени. Для этого достаточно записать в них тестовое 0xFFFFFFFF, и покопавшись в своих широких штанинах контролёр вернёт нам инверсный адрес базы (требуется инструкция NOT), а так-же размер пространства памяти портов. Команда !devext отладчика WinDBG в курсе всех событий на шине PCI, и возвращает следующий лог (запрос устройства VGA):

Код:
0: kd> !pcitree
Bus 0x0    FDO Ext fffffa80043a6190
   (d=0,  f=0) 8086:29c0  devext 0xfffffa80043a8b60  0600  Bridge/HOST to PCI
   (d=2,  f=0) 8086:29c2  devext 0xfffffa80043a91b0  0300  Display Controller/VGA  <------//
.......
Total PCI Segments processed = 1
;-------------------------------------------------------

0: kd> !devext 0xfffffa80043a91b0
PDO Extension, Bus 0x0, Device 2, Function 0.

  Vendor Id 8086 (Intel)  Device Id 29C2
  Subsys Vendor Id 0000,  Subsys Id 7529
  Header Type 0,  Class/Subclass 03/00  (Display Controller/VGA)
  Interface: 00,  Revision: 10,  IntPin: 01,  IrqLine 10

  Requirements:     Alignment Length    Minimum          Maximum
    BAR0    Mem:    00080000  00080000  0000000000000000 00000000ffffffff
    BAR1     Io:    00000008  00000008  0000000000000000 000000000000ffff
    BAR2    Mem:    10000000  10000000  0000000000000000 00000000ffffffff
    BAR3    Mem:    00100000  00100000  0000000000000000 00000000ffffffff
  Resources:        Start             Length
    BAR0    Mem:    00000000fea80000  00080000
    BAR1     Io:    000000000000dc00  00000008
    BAR2    Mem:    00000000d0000000  10000000
    BAR3    Mem:    00000000fe900000  00100000

  Interrupt Requirement:
    Line Based   :  MinVector = 0x0, MaxVector = 0xffffffff
    Message Based:  Type - Msi, 0x1 messages requested
    Int Resource :  Type - Line Based, Irq Line = 0x10
0: kd>


1.1. Программный доступ к «PCI-Config-Space»

Будем считать, что с теорий разобрались – теперь рассмотрим программную реализацию доступа к настройкам устройств PCI.
Процессор имеет 2 порта ввода-вывода для чтения/записи конфиг.пространства - в порту CONFIG_ADDRESS=0x0CF8 указываем адрес девайса на шине в формате BDF, после чего из порта CONFIG_DATA=0x0CFC можно будет прочитать или запись данные. Непосредственно операции R/W осуществляются инструкциями IN (чтение) и OUT (запись), при этом минимальной порцией является 32-битное значение dword. То есть если нам нужен 1-байт, придётся всё-равно прочитать 4, и далее сдвигами shr/shl или логикой and/or выделить 8 из 32-х бит. Костыль конечно, но именно такой конституции придерживается изначально механизм.

Описанные в таблице выше поля в терминологии PCI называют регистрами, и в записываемом в порт 0x0CF8 32-битном адресе, под номер регистра отводятся всего 6-бит. Как результат имеем доступ к 2^6=64 регистрам, что в сумме даёт 64х4=256 байт данных из ПЗУ девайса. В частности это означает, что инструкциями in/out мы не сможем получить всё содержимое устройств на шине PCI-Express, поскольку на этапе конфигурации они как-правило возвращают диспетчеру PnP не только базовые 256-байт, но и вплоть до 4 КБ расширенных данных из своей ROM. Однако радует то, что в большинстве случаях читать (а тем более записывать) данные за пределами первых 256-байт нам и не нужно – вендору лучше знать их назначение, в противном случае вообще теряется смысл в обмене с ПЗУ, и конфигурационном пространстве в целом.

На рис.ниже представлен формат 32-битного адреса, который мы должны будем отправить в порт 0x0CF8. Адрес типа(0) нас не интересует, и представлен здесь чисто для сведения – такие запросы не выходят за пределы главного моста «Host-Bridge» (адрес BD всегда равен 0), который связывает первичную шину PCI с чипсетом платформы PCH. Более того, доступ к этому мосту в полне можно получить и указав полный адрес типа(1), а потому мы будем использовать исключительно его. Два младших бита (выделены красным) заполняет диспетчер шины PCI, а от нас требуется лишь зарезервировать для них место, сдвинув всю конструкцию BDFR на 2-бита влево. Не кратная 8-ми разрядность полей авансом предрекает нам активное использование логических инструкций в коде, со-сдвигами влево/вправо по горизонту.

PciAddr.webp

Чтобы получить полный список устройств на всех шинах PCI, мы должны использовать следующий алгоритм:

0. Взводим в пакете адреса старший бит(31), иначе диспетчер шины в биос проигнорирует наш запрос.​
1. Ставим адрес BDFR=00:00:00:00, и отправляем запрос на чтение первого устройства в порт out 0x0CF8.​
2. Читаем с порта in 0x0CFC, и если в ответ получили 0xFFFFFFFF, значит на шине нет устройства с таким адресом, иначе обрабатываем данные.​
3. Делаем +1 номеру функции в пакете полного адреса, и проверяем её на макс.значение 8.​
4. Если меньше, то уходим на повтор к пункту(1), иначе сбрасываем поле Func в нуль, и делаем +1 полю Device.​
5. Проверяем его на макс.значение 32 – если TRUE сбрасываем Device в нуль, и делаем +1 полю Bus.​
6. Делаем инкремент поля Bus, пока не упрёмся в потолок 255 (отсчёт с нуля).​

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

C-подобный:
;//----- Поиск устройств в PCI-Config-Space
         xor    bx,bx           ;// BH = Bus,  BL = Device
         xor    cx,cx           ;// CH = Func, CL = Register

@find:   call   ReadPciCfg      ;// Процедуре чтения с порта передаются аргументы в BX/CX
         cmp    eax,-1          ;// проверить выхлоп на ошибку
         jnz    @deviceFound    ;// нашёлся какой-то тушканчик

@next:   inc    ch              ;// иначе: цикл по функциям
         cmp    ch,8            ;// все функции проверили ???
         jnz    @find           ;// нет - мотать цикл дальше
         xor    ch,ch           ;// иначе: сбросить Func в дефолт =0

         inc    bl              ;// Цикл по устройства
         cmp    bl,32           ;// всего устройств
         jnz    @find
         xor    bl,bl

         inc    bh              ;// Цикл по шинам
         cmp    bh,255          ;// всего вторичных шин
         jz     @stopFind       ;// если последняя - на выход!
         jmp    @find

;//----- Формируем в EAX адрес PCI для чтения/записи
ReadPciCfg:
         xor    eax,eax            ;// очистить регистр адреса
         mov    al,bh              ;// байт с номером шины
         or     ah,10000000b       ;// бит доступа =1
         shl    eax,16             ;// сдвинуть в ст.часть 32-бит адреса!
         mov    ah,bl              ;// номер устройства на шине
         shl    ah,3               ;// сдвинуть на 3-бита влево
         or     ah,ch              ;// добавить номер функции
         mov    al,cl              ;// записать в мл.байт адреса номер регистра
         and    al,11111100b       ;// сбросить 2-мл.бита для типа-адреса 0/1
         mov    [pciAddress],eax   ;// запомнить полный адрес устройства
         mov    dx,0x0CF8          ;// порт CONFIG_ADDRESS
         out    dx,eax             ;// указать адрес для доступа в порту!
         nop                       ;// ...пауза в 1-такт процессора
         mov    dx,0x0CFC          ;// порт CONFIG_DATA
         in     eax,dx             ;// прочитать данные с порта!
         nop                       ;// ...пауза в 1-такт процессора
ret

Поскольку на каждой итерации цикла у нас уже будет полностью сформированный адрес устройства на шине, а так-же возвращённые первым запросом идентификаторы Ven&Dev, ничто не мешает читать и остальные поля текущего блока в пространстве PCI-Config-Space. Для этого достаточно изменить в адресе лишь номер регистра, который (как мы уже знаем) может принимать значения в диапазоне 0-63. Например чтобы прочитать поле с классом текущего устройства, заносим в поле адреса Registers=2, а для регистров базовых адресов BAR, поле Registers=4 (см.формат в таблице выше).

C-подобный:
;//----- Class\Subclass\Interface ---------------------------
         mov    eax,[pciAddress]       ;// предварительно сохранённый адрес BDFR
         mov    bl,2                   ;// номер регистра =2
         shl    bl,2                   ;// сдвинуть на 2-бита влево (на рис.выше выделены красным)
         mov    al,bl                  ;// изменить номер регистра в пакете адреса!
         call   ReadPciConfigPort      ;// процедура чтения указанного регистра
         push   ecx                    ;// запомнить считанное значение Class/Subclass/Interface
         add    [cursor],8             ;// (позиция курсора в окне)
         mov    [dataSize],6           ;// (тетрад для вывода = 3 байта)
         call   PrintHex               ;// печать значений!
         pop    eax                    ;//
         shr    eax,24                 ;// оставить только ClassCode
         mov    [classCode],ax         ;//

;//----- Дамп 4-х регистров BAR в цикле ---------------------
         add    [cursor],18            ;// Здесь аналогично,
         mov    bp,4                   ;// ..только вывод 4-х BAR[0:3] в цикле
         mov    bx,4                   ;// номер регистра в PCI-Cfg =4
@@:      push   bx bp                  ;//
         mov    eax,[pciAddress]       ;//
         shl    bl,2                   ;// сдвинуть на 2-бита влево (на рис.выше выделены красным)
         mov    al,bl                  ;// изменить номер регистра в пакете адреса!
         call   ReadPciConfigPort      ;//
         mov    [dataSize],8           ;// печатать 8 тетрад = DWORD
         call   PrintHex               ;//
         add    [cursor],18            ;//
         pop    bp bx                  ;//
         inc    bl                     ;// номер регистра +1
         dec    bp                     ;// счётчик цикла  -1
         jnz    @b                     ;// промотать..

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


2. Создание карты свободных участков ОЗУ

Так, мелкими перебежками мы подобрались ко-второму пункту меню в окне кастомной ОС.
На следующем этапе необходимо выяснить, какие блоки оперативной памяти доступны нам для использования после перехода в защищённый режим. В этом деле нужно быть предельно осторожным, чтобы случайно не затереть, например, всё то-же конфигурационное пространство PCI. При включении машины ладно биос проделал за нас всю черновую работу, однако PnP-устройства могут подключаться к ОС и уже в процессе её работы, например флэшки USB, принтеры, и прочее барахло. Поэтому отведённую под PCI-Cfg память трогать нельзя ни при каких обстоятельствах, иначе придётся с нуля создавать её вновь. Под эту категорию подпадают ещё 2 таблицы – это создаваемая биосом ACPI (Advanced Configuration & Power Interface), а так-же таблица системного менеджемента DMI (в младенчестве SMBios). В общем как ни крути, а карта свободных регионов памяти требуется нам позарез.

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

В реальном режиме процессора где мы находимся на данный момент, можно воспользоваться услугами сервиса биос int-15h с функцией AX=E820h. Она просто обязана существовать во-всех BIOS/EFI начиная с 1992-года, поскольку замены ей попросту нет. Чтобы получить полную карту, данное прерывание нужно вызывать в цикле, пока в регистре EBX не получим ошибку нуль. При каждом вызове, в специально оформленный буфер размером 28-байт, возвращается всего 1 регион непрерывной памяти, а доступен он пользователю или нет, указывается в поле по смещению(16) «RegionType» приёмного буфера. Значение в регистре EBX представляет собой индекс региона в таблице (т.е. его порядковый номер) и при каждом вызове, прерывание увеличивает его на 1. От сюда следует, что необходимо сохранять EBX для последующих вызовов в цикле, иначе план рухнет как карточный дом. Вот как это реализовано у меня:

C-подобный:
;//----- Сбор информации о свободных регионах памяти

         mov    [index],0               ;// на старте индекс = 0
@NextMemoryMap:                         ;//
         mov    ebx,[index]             ;// отправляем его в EBX
         mov    di,AddressRangeDesc     ;// ES:DI = указатель на буфер
         mov    ecx,24                  ;// размер буфера
         mov    edx,0x534d4150          ;// строка 'SMAP' (см.ссылки ниже)
         mov    eax,0xE820              ;// номер функции
         int    15h                     ;// зовём прерывание!
         or     ebx,ebx                 ;// проверим на ошибку (0 = нет больше регионов)
         jz     @stopMemoryMap          ;// на выход, если ошибка
         mov    [index],ebx             ;// иначе: запомнить индекс для сл.вызова
         jmp    @NextMemoryMap          ;// на повтор..

@stopMemoryMap:
         jmp    @GetMenuItem            ;//

;// https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/  #15
;// https://wiki.osdev.org/Detecting_Memory_(x86)
;//-----------------------------------------------------
AddressRangeDesc:        ;// Приёмный буфер функции E820h
BaseAddrLow     dd  0    ;// мл.часть базового адреса региона,
BaseAddrHigh    dd  0    ;// ...и его старшие 32-бита.
LenghtLow       dd  0    ;// мл.часть размера/длины региона
LenghtHigh      dd  0    ;// ... и его старшие 32-бита.
RegionType      dd  0  ;//-------+     Тип региона
ExtAttributes   dd  0    ;//     |     (расширенные атрибуты нам не нужны)
                         ;//     V
FreeMemory      = 1      ;// свободно
Reserved        = 2      ;// занято
ACPI            = 3      ;// используется таблицей ACPI
NVS             = 4      ;// используется в режиме сна
Bad             = 5      ;// регион памяти имеет ошибки (не доступен для ОС)
Disabled        = 6      ;// неотображаемая Shadow-Memory
RomMemory       = 7      ;// проекция энерго-независимой памяти ПЗУ
OEM             = 12     ;// используется поставщиком

Обратите внимание на размеры полей «xxLow/High». Такая конструкция позволяет из двух частей собирать один 64-битный адрес, как того требует режим Long х64. По идее нужно выделять буфер приличного размера, и сбрасывать в него последовательно информацию сразу о всех регионах – она пригодится позже диспетчеру памяти, если таковой планируется в самописной ОС. Но в данном случае моя тупо ведёт логи, а потому при каждом вызове я перезаписываю данные в уже отработанном текущем буфере. Распространёнными являются только первые три типа регионов «Свободно, Занято, ACPI», и за свою практику я других значений не встречал.

Здесь нужно отметить, что функция возвращает именно непрерывные регионы памяти с последовательными адресами. Например если имеется блок свободной ОЗУ размером 10 МБайт, но посередине в нём имеются 100-байт занятых, то функа вернёт уже три отдельных региона. Более того, она заточена для сбора инфы при переходе в защищённый режим, и соответственно рассматривает весь первый мегабайт (где находится таблица прерываний IVT и данные биоса BDA) свободным для использования. В общем прерывание не фонтан, но в выборе мы сильно ограничены.


3. Практическая часть – пишем ядро ОС

Ну и под занавес приведу код ядра. Чтобы не потеряться в навигации по меню, имеется подсветка текущего выбора под буттоном. Сами-же меню выбираются цифровыми клавишами 1-7. Первичный загрузчик копирует ОС с образа диска в память по сегментному адресу 0000:0600 так, что в нашем распоряжении оказывается чуть меньше одного сегмента, а это 64 КБ. Поэтому дальних переходов в коде нет, и можно прыгать на любой участок обычным jmp.

Для вывода на экран HEX-значений используется универсальная процедура, которая способна печатать любое значение Byte/Word/Dword в пределах 32-бит. Поскольку вывод осуществляется сдвигами влево тетрад по 4-бита, число разрядностью Word и Byte должно находится в старшей половине регистра. То есть если нужно вывести 2-байта Word, то передаваемое в аргументе исходное число нужно предварительно сдвинуть влево на 16-бит, а если 1-байт, то сдвинуть на 24-бита. Вот пример:

C-подобный:
;//----  Процедура выводит ECХ на экран в HEX ------------
PrintHex:
         pusha                   ;// Сохранить все регистры
         push   es 0xb800        ;//
         pop    es               ;// ES = сегмент видеобуфера в txt-режиме 80х25/16
         mov    di,[cursor]      ;// ES:DI = позиция символа на экране
         xchg   edx,ecx          ;// EDX = любое 32-битное число
         mov    ecx,[dataSize]   ;// кол-во символов для вывода (в байте 2 символа, включая первый нуль)
@@:      shld   eax,edx,4        ;// получить из EDX в AL очередную цифру сдвигом влево (одна тетрада)
         rol    edx,4            ;// удалить её из EDX
         and    al,0x0f          ;// оставить в AL только эту цифру
         cmp    al,0x0a          ;// три команды перевода
         sbb    al,0x69          ;//    ..hex цифры из AL
         das                     ;//        ..в соответствующий ASCII-код
         mov    ah,Gray          ;// цвет символа
         stosw                   ;// вывод AХ в видеобуфер!
         loop   @b               ;// повторить для всех цифр
         pop    es               ;// вернуть ES в свой сегмент
         popa                    ;// ...и все остальные регистры
ret

Весь исходник ОС большой, а потому я положу его в скрепку.
После компиляции, открываем HEX-редактор HxD и копируем получившийся бинарь во-второй сектор образа дискеты (см.часть 1), сразу после загрузчика. Чтобы размер образа при этом не увеличился, вставлять в редакторе нужно не обычной комбинацией Ctrl+V, а её альтернативой Ctrl+B (см.меню «Правка» в HxD). Для навигации по коду в окне HEX-редактора, зарезервирована комбинация Ctrl+G. Как результат получим следующие пункты меню нашей ОС, из которых пока реализованы только 1,2,3.

VirtualBox_Codeby_27_04_2025_20_45_34.webp

В следующем пунке(2) отображается дамп конфигурационного пространства PCI где видно, что на моей виртуальной машине VirtualBox архитектурно имеется всего 1 шина Bus(0), хотя при тестах на реальном железе я обнаружил все 4. Как уже упоминалось выше, если в регистрах BAR взведён младший бит, то содержимое нужно воспринимать как номер физического порта, а если он сброшен – значит порт отображается на память, и в регистре лежит указатель на него.

VirtualBox_Codeby_27_04_2025_20_46_05.webp

И на конец в окно пункта(3) меню логируются базовые сведения о центральном процессоре (возвращает инструкция cpuid), а так-же состояние регионов памяти ОЗУ. По значению в столбце «Lenght» можно вычислить примерный объём установленной на машине плашки DDR-SDRAM. В данном случае, макс.значением является 0x1EF0000, что в 10-тичном представлении будет 32.440.320 байта, или ~32 МБ. Именно этот объём установлен в настройках моей вирт.машины для CodebyOS.

Обратите внимание на указатель стека ESP=00007BFC. Не смотря на то, что код ядра загружен по адресу 0000:0600, стековая память осталась на прежнем месте, как установил её первичный загрузчик ОС. Теперь можно смело расширять ядро всё новым и новым кодом не беспокоясь о том, что ядро столкнётся со-стеком и затерёт его своими данными. Конечно рано или поздно это неизбежно произойдёт, но в этот момент мы уже будем в защищённом режиме с виртуальной памятью, и соответственно перенастроим весь стек.

VirtualBox_Codeby_27_04_2025_20_46_25.webp


3. Заключение

В следующей части(3) уделяется внимание разбору таблицу SMBIOS в пункте(4) меню, а так-же продемонстрируем различные эффекты в текстовом режиме типа бегущая строка, скрол экрана вверх/вниз, работу с цветами, и всё остальное в этом духе. Последняя часть(4) будет полностью посвящена защищённому режиму, который активирует пункт(6) меню. В скрепке найдёте два исходника Boot и Kernel.asm, плюс уже скомпилированный образ флопика для тестов. До скорого, пока!
_
 

Вложения

Последнее редактирование модератором:
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!