Приклади, які можна виконати
Технічно програма, яка працює без ОС, - це ОС. Тож давайте подивимось, як створити та запустити кілька мізерних привіт світових ОС.
Код усіх прикладів, наведених нижче, присутній у цьому репортажі GitHub .
Завантажувальний сектор
На x86 найпростіша річ і найнижчий рівень - це створити головний завантажувальний сектор (MBR) , який є типом завантажувального сектора , а потім встановити його на диск.
Тут ми створюємо один printf
заклик:
printf '\364%509s\125\252' > main.img
sudo apt-get install qemu-system-x86
qemu-system-x86_64 -hda main.img
Результат:
Тестовано на Ubuntu 18.04, QEMU 2.11.1.
main.img
містить наступне:
\364
у octal == 0xf4
в шістнадцятковій версії: кодування hlt
інструкції, яке повідомляє ЦП припинити роботу.
Тому наша програма нічого не зробить: лише запустити та зупинити.
Ми використовуємо восьмеричні, оскільки \x
шестинадцяткові числа не визначені POSIX.
Ми могли легко отримати це кодування за допомогою:
echo hlt > a.asm
nasm -f bin a.asm
hd a
але 0xf4
кодування також задокументоване в посібнику Intel, звичайно.
%509s
виробляють 509 просторів. Потрібно заповнити файл до байта 510.
\125\252
у octal ==, 0x55
а далі 0xaa
: магічні байти, необхідні для обладнання. Вони повинні бути байтами 511 та 512.
Якщо його немає, апаратне забезпечення не розглядатиме це як завантажувальний диск.
Зауважте, що навіть не роблячи нічого, на екрані вже надруковано кілька символів. Вони друкуються прошивкою та служать для ідентифікації системи.
Запуск на реальному обладнання
Емулятори - це цікаво, але апаратне забезпечення - справжня справа.
Однак зауважте, що це небезпечно, і ви можете помити свій диск помилково: робіть це лише на старих машинах, які не містять критичних даних! Або ще краще, такі розробки, як Raspberry Pi, див. Приклад ARM нижче.
Для типового ноутбука вам потрібно зробити щось на кшталт:
Запишіть зображення на USB-накопичувач (знищить ваші дані!):
sudo dd if=main.img of=/dev/sdX
підключіть USB до комп'ютера
Увімкніть його
скажіть, щоб він завантажувався з USB.
Це означає змусити прошивку вибрати USB перед жорстким диском.
Якщо це не поведінка вашої машини за замовчуванням, продовжуйте натискати клавіші Enter, F12, ESC або інші такі дивні клавіші після ввімкнення живлення, поки не з’явиться меню завантаження, де ви можете вибрати для завантаження з USB.
Часто можна налаштувати порядок пошуку в цих меню.
Наприклад, на моєму старому Lenovo Thinkpad T430, UEFI BIOS 1.16, я бачу:
Привіт Світ
Тепер, коли ми зробили мінімальну програму, переходимо до привітного світу.
Очевидне питання: як зробити IO? Кілька варіантів:
- попросіть прошивку, наприклад, BIOS або UEFI, чи робити це для нас
- VGA: спеціальна область пам'яті, яка надрукується на екрані, якщо записана на. Можна використовувати в захищеному режимі.
- написати драйвер і поговорити безпосередньо з обладнанням дисплея. Це "правильний" спосіб це зробити: більш потужний, але складніший.
серійний порт . Це дуже простий стандартизований протокол, який надсилає та витягує символи з хост-терміналу.
Джерело .
Він, на жаль, не представлений на більшості сучасних ноутбуків, але це звичайний шлях для розробки плат, див. Приклади ARM нижче.
Це справді прикро, оскільки такі інтерфейси дуже корисні, наприклад, для налагодження ядра Linux .
використовувати функції налагодження чіпів. ARM називає їх напівхостинг, наприклад. Що стосується реального обладнання, воно потребує додаткової апаратної та програмної підтримки, але на емуляторах це може бути безкоштовна зручна опція. Приклад .
Тут ми зробимо приклад BIOS, як це простіше на x86. Але зауважте, що це не самий надійний метод.
головний.С
.code16
mov $msg, %si
mov $0x0e, %ah
loop:
lodsb
or %al, %al
jz halt
int $0x10
jmp loop
halt:
hlt
msg:
.asciz "hello world"
link.ld
SECTIONS
{
. = 0x7c00;
.text :
{
__start = .;
*(.text)
. = 0x1FE;
SHORT(0xAA55)
}
}
Зберіть і зв’яжіть із:
gcc -c -g -o main.o main.S
ld --oformat binary -o main.img -T linker.ld main.o
Результат:
Тестовано на: Lenovo Thinkpad T430, UEFI BIOS 1.16. Диск, створений на хості Ubuntu 18.04.
Окрім стандартних інструкцій зі зборки, ми маємо:
.code16
: повідомляє GAS виводити 16-бітний код
cli
: вимкнути переривання програмного забезпечення. Це може змусити процесор знову почати працювати післяhlt
int $0x10
: робить виклик BIOS Це те, що друкує персонажів один за одним.
Важливими прапорами посилань є:
--oformat binary
: виводить необроблений двійковий код складання, не перекручуйте його у файлі ELF, як це стосується звичайних виконуваних файлів.
Використовуйте C замість складання
Оскільки C збирається до складання, використовуючи C без стандартної бібліотеки досить просто, вам в основному просто потрібно:
- сценарій посилання, щоб розмістити речі в пам’яті в потрібному місці
- прапори, які повідомляють GCC не використовувати стандартну бібліотеку
- крихітна точка вступу для складання, яка встановлює необхідний стан C
main
, зокрема:
TODO: посилання на такий приклад x86 на GitHub. Ось ARM, який я створив .
Речі стають веселішими, якщо ви хочете використовувати стандартну бібліотеку, оскільки у нас немає ядра Linux, яке реалізує більшу частину стандартної функціональності бібліотеки С через POSIX .
Кілька можливостей, не звертаючись до повномасштабної ОС, як Linux, включають:
Ньюліб
Детальний приклад за адресою: https://electronics.stackexchange.com/questions/223929/c-standard-libraries-on-bare-metal/223931
У Newlib вам належить реалізувати системні дзвінки самостійно, але у вас виходить дуже мінімальна система, і їх дуже легко реалізувати.
Наприклад, ви можете переспрямувати printf
на системи UART або ARM або реалізувати за exit()
допомогою напівгостінгу .
вбудовані операційні системи, такі як FreeRTOS та Zephyr .
Такі операційні системи, як правило, дозволяють вимкнути попереднє планування, тому надаючи вам повний контроль над виконанням програми.
Їх можна розглядати як свого роду попередньо реалізований Newlib.
ARM
У ARM загальні ідеї однакові. Я завантажив:
Для Raspberry Pi https://github.com/dwelch67/raspberrypi виглядає як найпопулярніший підручник, доступний сьогодні.
Деякі відмінності від x86 включають:
IO робиться шляхом прямого написання на магічні адреси, немає in
і out
інструкцій.
Це називається IO пам'яті, відображеної в пам'яті .
для деяких справжніх апаратних засобів, таких як Raspberry Pi, ви можете самостійно додати прошивку (BIOS) до образу диска.
Це добре, оскільки робить оновлення програмного забезпечення більш прозорим.
Прошивка
По правді кажучи, ваш завантажувальний сектор - це не перше програмне забезпечення, яке працює на центральному процесорі.
Насправді спочатку працює так звана прошивка , яка є програмним забезпеченням:
- виготовлені виробниками обладнання
- як правило, із закритим джерелом, але ймовірно, на основі С
- зберігається в пам'яті лише для читання, і тому важче / неможливо змінити без згоди продавця.
Добре відомі прошивки включають:
- БІОС : стара загальноприйнята прошивка x86. SeaBIOS - це реалізація з відкритим кодом за замовчуванням, що використовується QEMU.
- UEFI : наступник BIOS, краще стандартизований, але більш здатний і неймовірно роздутий.
- Coreboot : шляхетна спроба з відкритим вихідним кодом
Прошивка робить такі речі:
петлю на кожен жорсткий диск, USB, мережу тощо, поки ви не знайдете щось завантажувальне.
Коли ми запускаємо QEMU, -hda
каже, що main.img
це жорсткий диск, підключений до обладнання, і
hda
є першою, яку випробували, і вона використовується.
завантажте перші 512 байти на адресу оперативної пам’яті 0x7c00
, покладіть туди RIP процесора та нехай він працює
показувати на дисплеї такі речі, як меню завантаження або виклики друку BIOS
Прошивка пропонує ОС-подібний функціонал, від якого залежить більшість ОС. Наприклад, підмножина Python перенесена для запуску в BIOS / UEFI: https://www.youtube.com/watch?v=bYQ_lq5dcvM
Можна стверджувати, що прошивки не відрізняються від ОС, і що прошивка - це єдине "справжнє" програмування голих металів.
Як каже цей розробник CoreOS :
Важка частина
Коли ви вмикаєте ПК, мікросхеми, що складають чіпсет (northbridge, southbridge та SuperIO), ще не ініціалізуються належним чином. Навіть незважаючи на те, що BIOS ROM настільки віддалений від процесора, як це міг бути, це доступно процесору, оскільки він повинен бути, інакше процесор не матиме інструкцій виконувати. Це не означає, що BIOS ROM повністю відображений, як правило, ні. Але достатньо лише відобразити карту, щоб розпочати процес завантаження. Будь-які інші пристрої, просто забудьте про це.
Коли ви запускаєте Coreboot під QEMU, ви можете експериментувати з вищими шарами Coreboot і з корисними навантаженнями, але QEMU пропонує мало можливостей експериментувати з початковим кодом низького рівня. З одного боку, оперативна пам’ять просто працює з самого початку.
Опублікувати початковий стан BIOS
Як і багато речей у техніці, стандартизація є слабкою, і одна з речей, на яку не слід покладатися, - це початковий стан регістрів, коли ваш код починає працювати після BIOS.
Тож зробіть собі прихильність і використовуйте код ініціалізації, наприклад: https://stackoverflow.com/a/32509555/895245
Реєстри подобаються %ds
і %es
мають важливі побічні ефекти, тому вам слід скасувати їх, навіть якщо явно їх не використовуєте.
Зауважте, що деякі емулятори приємніші за реальне обладнання та дають хороший початковий стан. Потім, коли ви переходите на реальне обладнання, все зламається.
GNU GRUB Multiboot
Завантажувальні сектори прості, але вони не дуже зручні:
- на диску ви можете мати лише одну ОС
- код навантаження повинен бути дійсно невеликим і вміщуватися в 512 байт. Це можна вирішити за допомогою виклику BIOS int 0x13 .
- вам доведеться зробити багато запуску самостійно, як перейти в захищений режим
Саме з цих причин GNU GRUB створив більш зручний формат файлу під назвою multiboot.
Мінімальний робочий приклад: https://github.com/cirosantilli/x86-bare-metal-examples/tree/d217b180be4220a0b4a453f31275d38e697a99e0/multiboot/hello-world
Я також використовую його на моїх прикладах репортажу GitHub, щоб мати можливість легко запускати всі приклади на реальному обладнання, не спалюючи USB мільйон разів. У QEMU це виглядає приблизно так:
Якщо ви готуєте вашу ОС як багатозавантажувальний файл, то GRUB зможе знайти її у звичайній файловій системі.
Саме це робить більшість дистрибутивів, підкладаючи зображення ОС /boot
.
Файли з багатозавантажуваними файлами - це в основному файл ELF зі спеціальним заголовком. Вони визначені GRUB за адресою: https://www.gnu.org/software/grub/manual/multiboot/multiboot.html
Ви можете перетворити багатозавантажений файл на завантажувальний диск за допомогою grub-mkrescue
.
El Torito
Формат, який можна записати на компакт-диски: https://en.wikipedia.org/wiki/El_Torito_%28CD-ROM_standard%29
Можна також створити гібридне зображення, яке працює на ISO або USB. Це можна зробити за допомогою grub-mkrescue
( приклад ), а також зробити ядро Linux при make isoimage
використанні isohybrid
.
Ресурси