- 05.06.2019
- 342
- 1 513
В первой части обсуждалась тема создания первичного загрузчика самописной ОС, теперь перейдём к коду ядра, которое он собственно загружает. Особое внимание уделяется здесь перефирийной шине 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-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, куда сохраняется прошивка программного обеспечения. Это базовый минимум, который может быть расширен внешней памятью ОЗУ.При включении компьютера, диспетчер PnP в биос получает управление, и приступает к распределению ресурсов аппаратным устройствам. Основную проблему здесь создают линии прерываний IRQ, по которым девайсы привлекают к себе внимание центрального процессора. В идеале у каждого устройства должна быть своя индивидуальная линия, тогда и конфликтов можно будет избежать. Однако на практике, в момент загрузки системы кол-во линий ограничено значением 15, а устройств на современных мат.платах намного больше. Пока операционная система не активирует расширенный контролёр прерываний APIC, биос так и будет зажат в рамки
IRQ 0:15
, иначе ни о какой совместимости с устаревшими LegacyDevice не может быть и речи.Поэтому производители устройств прошивают в ROM своих железяк минимальные требования к системе, а диспетчер PnP на этапе конфигурирования запрашивает их. Такие циклы на шине PCI назвали «Специальными» – биос посылает запрос девайсу, а тот отвечает 256-байтным блоком данных. Далее блоки всех обнаруженных устройств собираются в специально предназначенную для этих целей область памяти, которая известна как «Конфигурационное пространство PCI», или на английский манер «PCI Config Space». Первые 64-байта в каждом из блоков одинаковы для всех устройств (заголовок Header), а остальные 192 зависят от настроения производителя «Vendor-Specific». В таблице ниже перечислены поля базового блока данных размером
40h=64
байт из ПЗУ любого (поддерживающего технологию PnP) устройства:Приоритетные для нас поля я выделил здесь цветом. После того-как наша ОС получит управление от биоса, мы должны активировать контролёр прерываний 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-ми разрядность полей авансом предрекает нам активное использование логических инструкций в коде, со-сдвигами влево/вправо по горизонту.Чтобы получить полный список устройств на всех шинах 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.В следующем пунке(2) отображается дамп конфигурационного пространства PCI где видно, что на моей виртуальной машине VirtualBox архитектурно имеется всего 1 шина
Bus(0)
, хотя при тестах на реальном железе я обнаружил все 4. Как уже упоминалось выше, если в регистрах BAR
взведён младший бит, то содержимое нужно воспринимать как номер физического порта, а если он сброшен – значит порт отображается на память, и в регистре лежит указатель на него.И на конец в окно пункта(3) меню логируются базовые сведения о центральном процессоре (возвращает инструкция cpuid), а так-же состояние регионов памяти ОЗУ. По значению в столбце «Lenght» можно вычислить примерный объём установленной на машине плашки DDR-SDRAM. В данном случае, макс.значением является
0x1EF0000
, что в 10-тичном представлении будет 32.440.320
байта, или ~32 МБ. Именно этот объём установлен в настройках моей вирт.машины для CodebyOS.Обратите внимание на указатель стека
ESP=00007BFC
. Не смотря на то, что код ядра загружен по адресу 0000:0600
, стековая память осталась на прежнем месте, как установил её первичный загрузчик ОС. Теперь можно смело расширять ядро всё новым и новым кодом не беспокоясь о том, что ядро столкнётся со-стеком и затерёт его своими данными. Конечно рано или поздно это неизбежно произойдёт, но в этот момент мы уже будем в защищённом режиме с виртуальной памятью, и соответственно перенастроим весь стек.3. Заключение
В следующей части(3) уделяется внимание разбору таблицу SMBIOS в пункте(4) меню, а так-же продемонстрируем различные эффекты в текстовом режиме типа бегущая строка, скрол экрана вверх/вниз, работу с цветами, и всё остальное в этом духе. Последняя часть(4) будет полностью посвящена защищённому режиму, который активирует пункт(6) меню. В скрепке найдёте два исходника Boot и Kernel.asm, плюс уже скомпилированный образ флопика для тестов. До скорого, пока!
_
Вложения
Последнее редактирование модератором: