Приклади, які можна виконати
Давайте створимо та запустимо декілька мізерних програм зі світовим привітним голим привіт, які працюють без ОС на:
Ми також максимально випробуємо їх на емуляторі QEMU, оскільки це безпечніше і зручніше для розвитку. Тести QEMU пройшли на хості Ubuntu 18.04 з попередньо упакованим QEMU 2.11.1.
Код усіх прикладів x86, наведених нижче, та більше можна знайти на цьому репортажі GitHub .
Як запустити приклади на реальному технічному забезпеченні x86
Пам’ятайте, що використання прикладів із реальним обладнанням може бути небезпечним, наприклад, ви можете помилкою витерти диск або заштрихувати це обладнання: зробіть це лише на старих машинах, які не містять критичних даних! Або ще краще, використовуйте дешеві напів одноразові девборди, такі як Raspberry Pi, дивіться приклад ARM нижче.
Для типового ноутбука x86 ви повинні зробити щось на кшталт:
Запишіть зображення на USB-накопичувач (знищить ваші дані!):
sudo dd if=main.img of=/dev/sdX
підключіть USB до комп'ютера
Увімкніть його
скажіть це завантажуватися з USB.
Це означає змусити прошивку вибрати USB перед жорстким диском.
Якщо це не поведінка вашої машини за замовчуванням, продовжуйте натискати клавіші Enter, F12, ESC або інші такі дивні клавіші після ввімкнення живлення, поки ви не отримаєте меню завантаження, де ви можете вибрати для завантаження з USB.
У цих меню часто можливо налаштувати порядок пошуку.
Наприклад, на своєму T430 я бачу таке.
Після ввімкнення в цей момент мені потрібно натиснути Enter, щоб увійти до меню завантаження:
Потім тут я повинен натиснути F12, щоб вибрати USB як завантажувальний пристрій:
Звідти я можу вибрати USB як завантажувальний пристрій, як це:
Крім того, щоб змінити порядок завантаження і вибрати USB, щоб він мав вищий пріоритет, тому мені не доведеться щоразу вибирати його вручну, я натискаю клавішу F1 на екрані "Меню переривання при запуску", а потім перейдіть до:
Завантажувальний сектор
На x86 найпростіша річ і найнижчий рівень - це створити головний завантажувальний сектор (MBR) , що є типом завантажувального сектора , а потім встановити його на диск.
Тут ми створюємо один printf
заклик:
printf '\364%509s\125\252' > main.img
sudo apt-get install qemu-system-x86
qemu-system-x86_64 -hda main.img
Результат:
Зауважте, що навіть не роблячи нічого, на екрані вже надруковано кілька символів. Вони надруковані вбудованим програмним забезпеченням і служать для ідентифікації системи.
А на T430 ми просто отримуємо порожній екран з миготливим курсором:
main.img
містить наступне:
\364
у octal == 0xf4
в шістнадцятковій версії: кодування hlt
інструкції, яке повідомляє ЦП припинити роботу.
Тому наша програма нічого не зробить: лише запустити та зупинити.
Ми використовуємо восьмеричні, оскільки \x
шістнадцяткові числа не визначені POSIX.
Ми могли легко отримати це кодування за допомогою:
echo hlt > a.S
as -o a.o a.S
objdump -S a.o
який виводить:
a.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <.text>:
0: f4 hlt
але це також задокументовано в посібнику Intel, звичайно.
%509s
виробляють 509 просторів. Потрібно заповнити файл до байта 510.
\125\252
в вісімковій == 0x55
слід 0xaa
.
Це 2 необхідні магічні байти, які повинні бути байтами 511 та 512.
BIOS проходить через усі наші диски, шукаючи завантажувальні, і він вважає завантажувальним лише ті, у яких є ці два магічні байти.
Якщо його немає, обладнання не сприйме це як завантажувальний диск.
Якщо ви не printf
майстер, ви можете підтвердити вміст за main.img
допомогою:
hd main.img
який показує очікуване:
00000000 f4 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 |. |
00000010 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 | |
*
000001f0 20 20 20 20 20 20 20 20 20 20 20 20 20 20 55 aa | U.|
00000200
де 20
пробіл в ASCII.
Прошивка BIOS зчитує ці 512 байти з диска, додає їх у пам'ять і встановлює ПК на перший байт, щоб почати їх виконання.
Привіт світовий завантажувальний сектор
Тепер, коли ми зробили мінімальну програму, перейдемо до привітного світу.
Очевидне питання: як зробити 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"
GitHub вище за течією .
link.ld
SECTIONS
{
/* The BIOS loads the code from the disk to this location.
* We must tell that to the linker so that it can properly
* calculate the addresses of symbols we might jump to.
*/
. = 0x7c00;
.text :
{
__start = .;
*(.text)
/* Place the magic boot bytes at the end of the first 512 sector. */
. = 0x1FE;
SHORT(0xAA55)
}
}
Зберіть і зв’яжіть із:
as -g -o main.o main.S
ld --oformat binary -o main.img -T link.ld main.o
qemu-system-x86_64 -hda main.img
Результат:
А на Т430:
Тестовано на: Lenovo Thinkpad T430, UEFI BIOS 1.16. Диск, створений на хості Ubuntu 18.04.
Окрім стандартних інструкцій зі зборки, ми маємо:
.code16
: повідомляє GAS виводити 16-бітний код
cli
: вимкнути переривання програмного забезпечення. Це може змусити процесор знову почати працювати післяhlt
int $0x10
: робить виклик BIOS Це те, що друкує персонажів один за одним.
Важливими прапорами посилань є:
--oformat binary
: виведіть необроблений бінарний код складання, не загортайте його у файл ELF, як це стосується звичайних виконуваних файлів.
Щоб краще зрозуміти частину сценарію зв’язувача, ознайомтеся з етапом переїзду посилання: Що роблять посилання?
Програми голого металу Cooler x86
Ось кілька складніших налаштувань голого металу, яких я досяг:
Використовуйте C замість складання
Підсумок: використовуйте багатозавантажувальний пристрій GRUB, який вирішить безліч прикрих проблем, про які ви ніколи не думали. Дивіться розділ нижче.
Основна складність x86 полягає в тому, що BIOS завантажує лише 512 байт з диска в пам'ять, і ви, ймовірно, підірвете ці 512 байти при використанні C!
Для вирішення цього питання ми можемо використовувати двоступеневий завантажувач . Це робить подальші дзвінки BIOS, які завантажують більше байтів з диска в пам'ять. Ось мінімальний приклад складання 2 етапу з нуля за допомогою int 0x13 викликів BIOS :
Як варіант:
- якщо вам це потрібно лише для роботи в QEMU, але не справжнього обладнання, скористайтеся
-kernel
опцією, яка завантажує в пам'ять весь файл ELF. Ось приклад ARM, який я створив за допомогою цього методу .
- для Raspberry Pi прошивка за замовчуванням піклується про завантаження для нас зображення з файлу ELF, названого так
kernel7.img
, як -kernel
це робить QEMU .
Для освітніх цілей, ось один етап приклад мінімальний C :
main.c
void main(void) {
int i;
char s[] = {'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd'};
for (i = 0; i < sizeof(s); ++i) {
__asm__ (
"int $0x10" : : "a" ((0x0e << 8) | s[i])
);
}
while (1) {
__asm__ ("hlt");
};
}
запис.S
.code16
.text
.global mystart
mystart:
ljmp $0, $.setcs
.setcs:
xor %ax, %ax
mov %ax, %ds
mov %ax, %es
mov %ax, %ss
mov $__stack_top, %esp
cld
call main
linker.ld
ENTRY(mystart)
SECTIONS
{
. = 0x7c00;
.text : {
entry.o(.text)
*(.text)
*(.data)
*(.rodata)
__bss_start = .;
/* COMMON vs BSS: /programming/16835716/bss-vs-common-what-goes-where */
*(.bss)
*(COMMON)
__bss_end = .;
}
/* /programming/53584666/why-does-gnu-ld-include-a-section-that-does-not-appear-in-the-linker-script */
.sig : AT(ADDR(.text) + 512 - 2)
{
SHORT(0xaa55);
}
/DISCARD/ : {
*(.eh_frame)
}
__stack_bottom = .;
. = . + 0x1000;
__stack_top = .;
}
бігати
set -eux
as -ggdb3 --32 -o entry.o entry.S
gcc -c -ggdb3 -m16 -ffreestanding -fno-PIE -nostartfiles -nostdlib -o main.o -std=c99 main.c
ld -m elf_i386 -o main.elf -T linker.ld entry.o main.o
objcopy -O binary main.elf main.img
qemu-system-x86_64 -drive file=main.img,format=raw
C стандартна бібліотека
Речі стають веселішими, якщо ви також хочете використовувати стандартну бібліотеку C, оскільки у нас немає ядра Linux, яке реалізує значну частину стандартних функцій бібліотеки С через POSIX .
Кілька можливостей, не звертаючись до повномасштабної ОС на зразок Linux, включають:
Напишіть своє. Це лише купа заголовків та файлів С, врешті-решт, правда? Правильно ??
Ньюліб
Детальний приклад за адресою: /electronics/223929/c-standard-libraries-on-bare-metal/223931
Newlib реалізує всі нудне , НЕ OS конкретних речей для вас, наприклад memcmp
, memcpy
і т.д.
Потім він надає вам декілька заглушок для здійснення системних дзвінків, які вам потрібні самі.
Наприклад, ми можемо реалізувати exit()
на ARM через напівгостінг із:
void _exit(int status) {
__asm__ __volatile__ ("mov r0, #0x18; ldr r1, =#0x20026; svc 0x00123456");
}
як показано в цьому прикладі .
Наприклад, ви можете перенаправити printf
на системи UART або ARM або реалізувати за exit()
допомогою напівгостінгу .
вбудовані операційні системи, такі як FreeRTOS та Zephyr .
Такі операційні системи, як правило, дозволяють вимкнути попереднє планування, тому надаючи вам повний контроль над виконанням програми.
Їх можна розглядати як свого роду попередньо реалізований Newlib.
GNU GRUB Multiboot
Завантажувальні сектори прості, але вони не дуже зручні:
- Ви можете мати лише одну ОС на диску
- код навантаження повинен бути дійсно невеликим і вміщуватися в 512 байт
- вам доведеться робити багато запуску самостійно, як перейти в захищений режим
Саме з цих причин GNU GRUB створив більш зручний формат файлу під назвою multiboot.
Мінімальний робочий приклад: https://github.com/cirosantilli/x86-bare-metal-examples/tree/d217b180be4220a0b4a453f31275d38e697a99e0/multiboot/hello-world
Я також використовую його на моїх прикладах GitHub repo, щоб мати можливість легко запускати всі приклади на реальному обладнання, не спалюючи USB мільйон разів.
Результат QEMU:
T430:
Якщо ви готуєте вашу ОС як багатозавантажувальний файл, то GRUB зможе знайти її у звичайній файловій системі.
Саме це робить більшість дистрибутивів, підкладаючи зображення ОС /boot
.
Файли з декількома завантаженнями - це в основному файл ELF зі спеціальним заголовком. Вони визначені GRUB за адресою: https://www.gnu.org/software/grub/manual/multiboot/multiboot.html
Ви можете перетворити багатозавантажений файл на завантажувальний диск за допомогою grub-mkrescue
.
Прошивки
По правді кажучи, ваш завантажувальний сектор - це не перше програмне забезпечення, яке працює на процесорі системи.
Насправді спочатку працює так звана прошивка , яка є програмним забезпеченням:
- виготовлені виробниками обладнання
- як правило, із закритим джерелом, але, ймовірно, на основі С
- зберігається в пам'яті лише для читання, а тому важче / неможливо змінити без згоди продавця.
Добре відомі прошивки включають:
- БІОС : стара загальноприйнята прошивка 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
мають важливі побічні ефекти, тому вам слід скасувати їх, навіть якщо явно їх не використовуєте.
Зауважте, що деякі емулятори приємніші за реальне обладнання та дають хороший початковий стан. Потім, коли ви переходите на реальне обладнання, все ламається.
El Torito
Формат, який можна записати на компакт-диски: https://en.wikipedia.org/wiki/El_Torito_%28CD-ROM_standard%29
Можна також створити гібридне зображення, яке працює на ISO або USB. Це можна зробити за допомогою grub-mkrescue
( приклад ), а також зробити ядро Linux при make isoimage
використанні isohybrid
.
ARM
У ARM загальні ідеї однакові.
Немає широко доступних напівстандартизованих попередньо встановлених мікропрограмних програм, таких як BIOS, для використання в IO, тому два найпростіші типи IO, які ми можемо зробити, це:
- серійний, який широко доступний на розробниках
- блимає світлодіод
Я завантажив:
кілька простих прикладів QEMU C + Newlib та необроблених прикладів монтажу тут, на GitHub .
Приклад prompt.c , наприклад , приймає дані від вашого терміналу хоста і повертає висновок всіх через моделюється UART:
enter a character
got: a
new alloc of 1 bytes at address 0x0x4000a1c0
enter a character
got: b
new alloc of 2 bytes at address 0x0x4000a1c0
enter a character
Дивіться також: Як зробити програми ARM з голих металів та запустити їх на QEMU?
повністю автоматизована установка блискавки Raspberry Pi за адресою: https://github.com/cirosantilli/raspberry-pi-bare-metal-blinker
Дивіться також: Як запустити програму C без ОС на Raspberry Pi?
Щоб "побачити" світлодіоди в QEMU, вам слід скомпілювати QEMU з джерела з прапором налагодження: /raspberrypi/56373/is-it-possible-to-get-the-state-of- the-leds-and-gpios-in-a-qemu-emulation-like-t
Далі слід спробувати світ привіт UART. Ви можете почати з прикладу blinker і замінити ядро на це: https://github.com/dwelch67/raspberrypi/tree/bce377230c2cdd8ff1e40919fdedbc2533ef5a00/uart01
Спочатку надішліть UART на роботу з Raspbian, як я пояснив за адресою: /raspberrypi/38/prepare-for-ssh-without-a-screen/54394#54394 Це буде виглядати приблизно так:
Не забудьте скористатися правильними штифтами, інакше ви можете записати ваш UART на USB-перетворювач, я це робив уже два рази за допомогою короткого замикання на землю та 5 В ...
Нарешті підключіться до серіалу від хоста за допомогою:
screen /dev/ttyUSB0 115200
Для Raspberry Pi ми використовуємо Micro SD карту замість USB-накопичувача, щоб містити наш виконуваний файл, для якого зазвичай потрібен адаптер для підключення до комп'ютера:
Не забудьте розблокувати адаптер SD, як показано на веб-сторінці : /ubuntu/213889/microsd-card-is-set-to-read-only-state-how-can-i-write-data -on-it / 814585 # 814585
https://github.com/dwelch67/raspberrypi виглядає як найпопулярніший на сьогоднішній день підручник із Raspberry Pi із голого металу.
Деякі відмінності від x86 включають:
IO робиться, записуючи на магічні адреси безпосередньо, немає in
і out
інструкцій.
Це називається IO пам'яті, відображеної в пам'яті .
для деяких справжніх апаратних засобів, таких як Raspberry Pi, ви можете самостійно додати прошивку (BIOS) до образу диска.
Це гарна річ, оскільки робить оновлення прошивки більш прозорим.
Ресурси