Пишем свой ROM BIOS
![fkkocvs21w-tya1t--_qs7jg8jw.jpeg](https://habrastorage.org/r/w780q1/webt/fk/ko/cv/fkkocvs21w-tya1t--_qs7jg8jw.jpeg)
Со стародавних времён хотел попробовать создать собственный ROM BIOS, который будет записан в отдельную микросхему и будет выполнять какие-то полезные действия. Например, ROM BIOS установлен в видеокартах (по крайне мере старых), контроллерах дисков и много где ещё — это фактически расширения обычного BIOS. Понимаю, что это всё старьё и тягаться с возможностями современного UEFI бессмысленно, но мне очень хотелось создать свой собственный образ, который находится в физически в отдельной микросхеме и работает при старте системы. Поэтому пришлось даже для этих целей найти старый комп.
Когда я только решил влезть в этот вопрос, столкнулся с тем, что достаточно мало толковой информации, которая была бы хорошо и чётко изложена. Возможно, я плохо искал, примеров того, как писать свою программу в бутсектор жёсткого диска было много, а вот толкового мануала по созданию BIOS Extension — кот наплакал.
В этой статье мы с вами разберём создание своего ROM BIOS, дальше я расскажу про тонкости и нюансы, напишу низкоуровневый «Hello world», сделаю настоящий интерпретатор BASIC ROM, который стартует при включении компьютера, как в первых IBM PC.
❯ Теория работы расширения BIOS
Думаю, все знают, что такое BIOS, многие заходили в его настройки при старте компьютера. Но, оказывается, есть возможность делать для него модули расширения и создавать там свой функционал.
Если говорить об архитектуре IBM BIOS, то он может быть расширен отдельными модулями, и всё будет работать как единый код. Поэтому в персональную ЭВМ могут быть добавлены дополнительные микросхемы ПЗУ, которые будут включать в себя эти модули.
Многие из вас видели эти микросхемы на сетевых картах, но они также присутствуют в видеокартах VGA Video BIOS, системный (основной) BIOS, BIOS IDE жёсткого диска и многие другие.
Для того чтобы системный BIOS нашёл дополнительный модуль в памяти, есть специальная процедура, которая позволяет его найти. Во время самотестирования POST, после загрузки векторов прерывания в ОЗУ, резидентный код BIOS начинает проверять ПЗУ на наличие специальных байтов заголовка, которые помечают начало дополнительного модуля BIOS. Поиск этих заголовков идёт в диапазоне абсолютных адресов 0C8000h - 0F4000h
.
Заголовок имеет определённый вид:
- Два байта обозначают начало секции:
0x55
и0xAA
. - Сразу за двумя первыми байтами заголовка, следует третий байт, который содержит длину дополнительного кода BIOS. Это количество блоков, длиной 512 байт, необходимых для хранения этого когда. Это ещё значит, что раздел кода расширения BIOS не может быть длиннее:
После того как заголовок был найден, проверяется — является ли последующий раздел настоящим расширением BIOS. Для этого выполняется проверка целостности для указанного количества блоков по 512 байт. Значения каждого байта в блоке суммируются сложением по модулю 0×100 — эффект такой же, как при делении суммы всех байтов на 4096(d). Остаток 0 указывает, что расширение BIOS содержит корректный код. Проще говоря, последний байт должен хранить контрольную сумму, сумма всех предыдущих байтов, с которой даёт нуль.
Как только заголовок идентифицирован, контрольная сумма проверена, то управление будет передано к третьему байту этого расширения, и начинается выполнение программы, содержащейся в этом расширении. Например, видеокарта инициализирует свои регистры, благодаря которым вы можете видеть что-либо на мониторе после включения. Или находят диски, подключённые к компу.
Дальше, когда код был выполнен, управление вновь передаётся к резидентному BIOS, и поиск в памяти дополнительных блоков BIOS продолжается. Этот процесс завершается, когда достигается абсолютный адрес 0F4000h
, после которого уже и начинается загрузка с диска.
Внимательный читатель может заметить, что в одной и той же области памяти не может быть двух одинаковых образов, поэтому производители плат расширения для компьютеров дают возможность переназначать адреса, которые использовались с их расширениями BIOS, чтобы не было конфликтов.
Например, в большинстве систем тех лет, есть три стандартных BIOS, которые, как правило, всегда расположены в одном и том же месте:
- Системный BIOS: основной системный BIOS расположен в блоке памяти размером 64 КБ от
F0000h
доFFFFFh
. - VGA Video BIOS: это BIOS, который нужен для инициализации и управления видеокартой. Обычно он находится в блоке размером 32 КБ от
C0000h
доC7FFFh
. Находится непосредственно на плате видеокарты. - BIOS жёсткого диска IDE: BIOS, который управляет жёстким диском IDE, если в вашей системе есть IDE, расположен от
C8000h
доCBFFFh
. На старых платах расположен на плате контроллера жёстких дисков, на новой системе, как я понимаю, непосредственно на материнской плате.
Например, можно взять программу RAMVIEW, которую я правил в прошлом посте, и посмотреть BIOS видеокарты.
Просмотр памяти BIOS реальной видеокарты
Переходим по смещению C000h
, что как раз соответствует физическому адресу C0000h
. Можно сразу увидеть два «магических» числа заголовка: 55h
и AAh
, затем идёт число 7Ch
, которое говорит нам, что это расширение занимает 63488 байта.
Из теории должно быть понятно, что если мы хотим написать программу, то нам надо в начале кода делать заголовок, который будет содержать волшебные числа и размер кода в секторах, а в конце кода считать контрольную сумму. И если с первым всё более-менее понятно, то сложность доставляет добавление контрольной суммы.
В общем-то, этой информации должно быть достаточно, чтобы взять и написать свой BIOS.
❯ Пишем «Hello Wold» в расширении BIOS.
Когда начинал искать информацию по теме, наткнулся на следующую PDF, которая и стала кладезем информации. Это какая-то презентация, как собирать BIOS-расширения. В конце неё даётся ссылка на репозиторий с прекрасным примером и описанием. Это замечательный пример того, для чего можно было создавать свои расширения в BIOS — защищённая загрузка на жёсткий диск с паролем. Да, во времена UEFI всё это потеряло всякий смысл. Но мы хотим разобраться с этой частью.
Попробовал это расширение, оно у меня высыпалось с ошибкой. Но я понял, что оно работает, потому что оно запустилось и напечатало ошибку. Значит можно взять код, который выводил ошибку и сделать из него «Hello World».
Обкорнав этот пример, получилось настоящий «Привет мир». Сам ассемблеровский код разбирать не буду, он достаточно очевиден, но обращу внимание на некоторые моменты. Код весь можно посмотреть в моём репозитории. Программа собирается транслятором nasm, на мой взгляд, самый удобный и простой транслятор для начинающих, работает как в DOS, Windows, так и в Linux. Для удобства сборки написал Makefile.
Пробежимся по коду hello.asm:
org 0
rom_size_multiple_of equ 512
bits 16
; PCI Expansion Rom Header
; ------------------------
db 0x55, 0xAA ; signature
db rom_size/512; initialization size in 512 byte blocks
entry_point: jmp start
start:
Начинаем работать с нулевого смещения. Вначале мы определяем define rom_size_multiple_of equ 512
, который содержит размер блока. Bits 16 говорит, что NASM должен генерировать код для процессора, работающего в шестнадцати битном режиме.
db 0x55, 0xAA
— это заголовок, магические числа, которые обозначают начало область начала расширения BIOS.db rom_size/512
— это один байт, который определяет размер секторов нашей программы. Рассчитывается автоматически с помощью макроса, для этого в конце программы добавлен следующий макрос:
rom_end equ $-$$
rom_size equ (((rom_end-1)/rom_size_multiple_of)+1)*rom_size_multiple_of
Макрос rom_end
— получает размер программы в байтах. Далее идёт расчёт размер памяти, кратно блоку 512 байт и сохраняется в макрос rom_size
. И это значение уже подставляется в байт в памяти, где рассчитывается количество блоков. Надеюсь, вы не запутались, тут просто происходит рекурсивная трансляция.
После того как заголовок определён, идёт команда jmp start
, которая осуществляет прыжок на начало исполнения программы. В конце кода программы, перед макросами расчёта её размеров, резервируется 1 байт для расчёта контрольной суммы:
db 0 ; reserve at least one byte for checksum
В качестве способа ввода-вывода используется стандартное прерывание BIOS int 10h
, которое расписывается в любой книжке по ассемблеру, поэтому не вижу смысла подробно на этом останавливаться. Многие студенты пишут подобные опусы на лабораторках в институте.
Самое интересное — расчёт контрольной суммы, и он осуществляется с помощью дополнительной программы addchecksum.c. Её исходники я также позаимствовал из проекта AHCI BIOS Security Extension. Код этой программы тоже стоит разобрать, потому что он интересен и полезен. Не так много работающих примеров, которые умеют считать контрольные суммы для расширения BIOS.
...
int main(int argc, char *argv[]) {
...
FILE *f=fopen(argv[1], "r+");
...
fseek(f, 0, SEEK_END);
int f_size=ftell(f);
fseek(f, 0, SEEK_SET);
unsigned char sum=0;
int i;
for(i=0;i
С помощью перехода в конец файла fseek(f, 0, SEEK_END);
мы получаем размер файла int f_size=ftell(f);
. Далее с начала файла образа читаем побайтно и суммируем полученные значения в однобайтовой переменной sum
(то есть отбрасывая бит переноса). После чего инвертируем полученное значение и записываем его в последний байт нашей программы fputc((0x100-sum)&0xff, f);
, где мы заранее зарезервировали нулевой байт данных. В результате, если просуммировать весь код программы, он должен быть равен нулю, вместе с нашей контрольной суммой (последний байт хранит отрицательное число контрольной суммы).
Собрать это всё можно следующими двумя командами:
gcc addchecksum.c -o addchecksum
nasm hello.asm -fbin -o hello.rom
./addchecksum hello.rom || rm hello.rom
Результатом всех операций, если вы всё сделали правильно, будет файл hello.rom
. Осталось найти способ протестировать работоспособность этого бинарника.
❯ Способы отладки BIOS Extension
Самый очевидный и проблемный способ — это прошить программатором микросхему ПЗУ, вставить в специальную панельку на плате ISA или PCI, затем эту карту вставить в комп, включить его и убедиться, что ничего не работает. Есть вообще путь настоящего джедая — это встроить код прям в BIOS на материнской плате, но всё это долго, муторно и неудобно. Есть более простые и удачные решения.
Всё хорошее придумано для нас — это виртуальные машины, в данном случае использовал VirtualBox и qemu. Второй вариант быстрее и проще, первый — доступен в Windows, а поскольку большую часть времени работаю в wsl под Windows, для меня это актуально. Да, я знаю, что qemu есть для Windows, но таков путь.
VirtualBox
Для того чтобы протестировать ROM BIOS Extension, я создал пустую виртуальную машину. Там не требуется даже подключать жёсткий диск, он не нужен. Далее необходимо подключить данный образ к виртуальной машине. В Windows делал следующим образом, запускаю PowerShell и выполняю:
cd "C:\Program Files\Oracle\VirtualBox\"
.\VBoxManage.exe setextradata testrom "VBoxInternal/Devices/pcbios/0/Config/LanBootRom" "c:\tmp\hello.rom"
После этого можно просто подменять файл c:\tmp\hello.rom
и всё будет работать. В Linux тоже всё работает, команда такая же, разве что не требуется лезть по явному пути, так как путь до VBoxManage уже содержится в переменной PATH.
Копирую файл hello.rom
в c:\tmp\
и пробую запустить виртуальную машину.
Тестирование образа ROM в VirtualBox
Всё отлично работает. Но для меня qemu показалось намного удобнее и проще, без всех этих плясок с созданием машины и прописыванием путей до образа. И запустить её можно из любой папки.
Запуск BIOS в qemu
Протестировать в виртуальной машине qemu можно одной командой, прямо из папки с получившимся бинарником:
qemu-system-i386 -net none -option-rom hello.rom
-net none
говорит, что сети у нас нет.-option-rom
— указывает образ ROM-файла.
Очень просто и удобно. Плюс можно сразу встроить в Makefile, чтобы собрать и проверить.
Запускаем тестирование программы из Makefile
Оба инструмента имеют право на жизнь, но мой выбор — это qemu, плюс там удобно будет ещё и посмотреть, что происходит с моей программой.
❯ Тестирование на реальном железе
Если первая часть статьи показалась вам сложной, то нет — это была самая простая часть проекта. Самая сложная часть, которая отняла у меня большую часть времени — это запуск всего на реальном железе.
Вообще, не так много способов установить свой собственный ROM в память компьютера, самый простой — это подобрать сетевую карту, у которой есть кроватка для установки ROM. Вроде бы прошивай ПЗУ, устанавливай в кроватку и вперед, но как обычно, путь был тернист.
Попытка на 386 машине
Изначально планировал весь проект реализовать на 386 машине, железо которой мне было любезно предоставлено spiritus_sancti.
386 материнская плата
Чтобы оживить его, пришлось пройти настоящий квест по разворачиванию DOS на Compact Flash, оказалась не такая простая задачка, с танцами. О чём подробно описал у себя в ЖЖ. Все эти мытарства были нужны для того, чтобы увидеть образ ПЗУ в памяти ЭВМ.
Для прошивки взял микросхему M2764, программатор долго её не хотел видеть, всё давал ошибки ножек. Но всё же дал её прошить.
Прошивка микросхемы
После прошивки вставил её в панельку сетевой карты и стал ждать чуда.
ПЗУ M2764 в панельке сетевой карты
Но чуда не произошло, сколько я не плясал с бубном, но видел при загрузке только такую картину:
BIOS найден не был, попытка загрузиться на дискетку
Тогда я тщетно пытался разыскать свой код в памяти с помощью утилиты debug (для этого и ставил DOS). Но безуспешно, в результате решил взять железо посвежее для своих опытов.
Pentium 4 и сетевая карта Realtek RTL8139
Поскребя по сусекам в своём гараже, обнаружил там системный блок с Pentium 4, который долгое время стоял под дождём, но оказался живым. И подумал, что он вполне может подойти для моих целей. Винт был мёртв и хрустел французской булкой, но меня это не волновало, главное, чтобы работал BIOS и PCI.
Потрошка подопытного, все шлейфы отключены, во избежание
При включении оказался вполне себе бодрым и живым, разве что пришлось заменить батарейку BIOS.
Старт, всё живое, дисков нет
Чтобы код с сетевой карты выполнился, необходимо обязательно указать загрузку с LAN. Как это ни странно, по умолчанию выполняться он не будет, хотя мануалы гласят об обратном.
Включаем исполнение кода на сетевой карте
Есть верный способ сделать что-то хорошо — это повторить за кем-то. Поэтому, решил взять не только куски кода из проекта AHCI BIOS, но и рекомендации по выбору сетевой карты:
This one uses the Realtek RTL8139 controller, which is my recommendation. Network cards with the RTL8139 are easy and cheap to find on the second hand market. The network card should come with an unpopulated DIP-28 socket, so you will also need to get a DIP-28 EEPROM as the actual option ROM. Any ROM that sells as 28C64 should be compatible.
Вместо 28C64 решил использовать микросхему M2764, которая у меня уже была в наличии. Карта очень распространённая и купить её не проблема, главное следить, чтобы в ней была панелька.
Как я узнал из статьи «Boot manager для 486-го компьютера», для работы BOOT ROM сетевую карту нужно конфигурировать. Нашёл с трудом драйвера для RTL8139 под ДОС и даже настроил, чтобы всё корректно работало.
Настройка сетевой карты в драйверах
Ставлю прошитое ранее ПЗУ в эту сетевую карту, ииии…
Установленное ПЗУ. Железка снята для удобства многократной установки платы
Устанавливаю её в системный блок, загружаюсь и ничего. Как я не бился, как ни крутился, что не делал — не получается. Именно тогда я озадачился поиском программы просмотра памяти. С помощью RAMVEW смотрел всю память, но так и не встретил там участка с моим образом (ключевое слово «Hello Word» в начале сегмента).
Стало понятно, что нужно брать сетевую карту, у которой уже есть настоящий, живой Boot ROM и смотреть как же он работает. Это сетевые карты типа PXE.
Рабочий вариант: сетевуха с образом PXE
Идея простая, найти сетевую карту с рабочим ПЗУ, затем посмотреть, как оно устроено. Отпаять с неё микросхему ПЗУ, зашить свою прошивку и припаять обратно. Но потом я подумал:, а зачем отпаивать, если можно поставить просто панельку. Так я и поступил.
PXE-сетевые карты стоят на досках объявлений дешевле, чем микросхемы ПЗУ в магазине. Поэтому, купил сразу несколько плат, чтобы было место для экспериментов. Выбор пал на распространённую модель: 3Com 3C905C-TXM.
Сетевая карта с PXE
Ставлю её в компьютер, и, о чудо, она грузится!
PXE ROM успешно обнаружен!
Можно даже посмотреть, как выглядит образ PXE ROM в памяти компьютера, с помощью утилиты RAMVIEW.
Образ PXE ROM в памяти компьютера
Здесь для меня была ценная информация, где я смогу найти образ своей программы в памяти компьютера. Всё точно так же, магические два числа и размер образа в секторах.
Дело стало за малым: отпаять микросхему и припаять панельку. На деле оказалась не такая простая процедура, как я думал, даже пришлось носить к профессиональному монтажнику (благо есть такая возможность), но у меня всё получилось.
Снимаем микросхему ПЗУ
Монтируем панель для микросхем
Прошиваем
Устанавливаем микросхему на своё законное место. Для сравнения плата с панелькой и микросхемой, запаянной штатно:
Всё, теперь всё готово к тестированию. Вставляем плату в компьютер, и всё работает!
Суровый аппаратный Hello World
Этот удивительный момент даже заснял на видео.
Теперь даже можно проверить, что в памяти видно эту программу. Обратите внимание на строку «Hello world.» в конце.
По адресу PXE теперь живёт «Привет мир»
Всё это конечно прикольно, но хочется чего-то более полезного и функционального, ведь умение делать свой ROM BIOS дарит много новых возможностей. А давайте сделаем настоящий ROM BASIC?
❯ BIOS ROM BASIC
Старожилы помнят, что в первых персональных компьютерах IBM интерпретатор BASIC был встроен в ROM BIOS. И мне показалась забавной загружаться вместо ОС в интерпретатор BASIC.
Главная задача, которая стояла предо мной — это найти хорошие исходники BASIC, написанные на ассемблере. Может показаться забавным, но Microsoft официально, в своём репозитории выложила исходники GW-BASIC. Однако, внимательно посмотрел на них, почитал статьи тех, кто пытался их собрать, например, и понял, что дело гиблое. В результате я отказался от идеи использовать этот исходник, хотя, конечно, это было бы очень аутентично.
Мне же посоветовали исходники bootBASIC, который занимает всего 512 байт, и рассчитан для работы в загрузочном секторе жёсткого диска. По счастливому стечению обстоятельств, в качестве транслятора там тоже использовался nasm.
Я сделал клон этого проекта, за пару часов добавил функционал, чтобы он компилировал ещё образ для ROM сетевой карты: добавил в начале магическое число, размер и в конце контрольную сумму — теперь вы всё знаете и сами умеете.
Осталось только собрать образ ROM простой командой:
make basic.rom
И проверить его:
make runqemurom
Но всё это не спортивно, интересно попробовать в реальном железе. И таки да, оно работает!
Как говорится, лучше один раз увидеть, чем тысячу раз прочитать. Поэтому вот вам небольшое кинцо.
❯ Заключение
Кто бы мог подумать, что создание своего образа BIOS окажется таким интересным и продолжительным квестом. Мне пришлось купить около десятка сетевых карт, чтобы наконец найти рабочую. Проблемы с ROM, которые у меня были, вероятнее всего связаны с плохим качеством микросхемы ПЗУ.
Очень много этапов экспериментов не вошло в статью, как минимум у меня около десятка различных вариаций «Hello world», написанных в разной стилистике ассемблера. Поле для экспериментов просто непаханое. Несмотря на то, что это старьё, такой опыт полезен для разработки на голом железе и может быть использован при программировании других модулей.
Хоть литературы по теме немного, создавать свой ПЗУ BIOS совершенно несложно. Это не сильно отличается от примеров создания загрузчика в бутсектор жёсткого диска. Просто нужно помнить, что есть ещё контрольная сумма, которую тоже нужно уметь считать и записывать в конец образа ROM.
❯ Полезные ссылки
- Краткое описание что такое BIOS Extension.
- Презентация, которая сдвинула меня с мёртвой точки.
- Репозиторий проекта AHCI BIOS Security Extension.
- Мой репозиторий BIOS «Hello world.».
- Оригинальный репозиторий bootBASIC.
- Мой репозиторий, который умеет грузиться в ROM BIOS.
- Интересная статья по теме «Boot manager для 486-го компьютера».
P.S. Если вам интересно моё творчество, вы можете следить за мной ещё в телеграмме.