Runnable примеры
Давайте создадим и запустим несколько крошечных программ с открытым железом, которые работают без ОС на:
Мы также максимально опробуем их на эмуляторе 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 на экране «Startup Interrupt Menu» и затем перехожу к:
Загрузочный сектор
На 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
в восьмеричном == 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. Но обратите внимание, что это не самый надежный метод.
main.S
.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
: говорит ГАЗ выводить 16-битный код
cli
: отключить программные прерывания. Это может заставить процессор начать работать снова послеhlt
int $0x10
: вызывает ли BIOS. Это то, что печатает символы один за другим.
Важные флаги ссылок:
--oformat binary
: выводить исходный двоичный код сборки, не помещайте его в файл ELF, как в случае с обычными исполняемыми файлами пользовательского пространства.
Чтобы лучше понять часть сценария компоновщика, ознакомьтесь с этапом перемещения ссылок: что делают компоновщики?
Cooler x86 голые метал программы
Вот несколько более сложных установок из чистого металла, которые я достиг:
Используйте C вместо сборки
Резюме: используйте GRUB multiboot, который решит множество досадных проблем, о которых вы никогда не задумывались. Смотрите раздел ниже.
Основная сложность на x86 состоит в том, что BIOS загружает только 512 байт с диска в память, и вы, вероятно, взорвете эти 512 байт при использовании C!
Чтобы решить эту проблему, мы можем использовать двухэтапный загрузчик . Это делает дальнейшие вызовы BIOS, которые загружают больше байтов с диска в память. Вот минимальный пример сборки на этапе 2 с нуля с использованием вызовов BIOS int 0x13 :
В качестве альтернативы:
- если он нужен только для работы в 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");
};
}
entry.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, которое реализует большую часть функциональности стандартной библиотеки C через POSIX .
Несколько возможностей, без перехода на полноценную ОС, такую как Linux, включают:
Напиши свое. В конце концов, это всего лишь куча заголовков и файлов C, верно? Правильно??
Newlib
Подробный пример по адресу: /electronics/223929/c-standard-libraries-on-bare-metal/223931
Newlib реализует для вас все скучные, не связанные с ОС вещи, например memcmp
, memcpy
и т. Д.
Затем он предоставляет некоторые заглушки для реализации системных вызовов, которые вам нужны сами.
Например, мы можем реализовать exit()
на ARM через semihosting с:
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 examples, чтобы иметь возможность легко запускать все примеры на реальном оборудовании, не перегружая USB миллион раз.
QEMU результат:
T430:
Если вы подготовите свою ОС как мультизагрузочный файл, GRUB сможет найти его в обычной файловой системе.
Это то, что делает большинство дистрибутивов, помещая образы ОС под /boot
.
Мультизагрузочные файлы - это в основном файл ELF со специальным заголовком. Они указаны GRUB по адресу: https://www.gnu.org/software/grub/manual/multiboot/multiboot.html.
Вы можете превратить мультизагрузочный файл в загрузочный диск с помощью grub-mkrescue
.
Прошивка
По правде говоря, ваш загрузочный сектор не является первым программным обеспечением, которое работает на процессоре системы.
На самом деле сначала запускается так называемая прошивка , то есть программное обеспечение:
- сделано изготовителями оборудования
- как правило, закрытый источник, но, вероятно, на основе C
- хранится в постоянной памяти, и поэтому его трудно / невозможно изменить без согласия поставщика.
Хорошо известные прошивки включают в себя:
- 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 :
Сложная часть
Когда вы включаете компьютер, микросхемы, которые составляют чипсет (северный мост, южный мост и SuperIO), еще не инициализированы должным образом. Несмотря на то, что ПЗУ BIOS удалено от ЦП настолько, насколько это возможно, это доступно ЦП, поскольку оно должно быть, иначе ЦП не будет иметь никаких инструкций для выполнения. Это не означает, что ПЗУ BIOS полностью сопоставлено, обычно нет. Но достаточно для отображения процесса загрузки. Любые другие устройства, просто забудьте об этом.
Когда вы запускаете Coreboot в QEMU, вы можете экспериментировать с более высокими уровнями Coreboot и с полезными нагрузками, но QEMU предлагает небольшую возможность экспериментировать с кодом запуска низкого уровня. Во-первых, RAM просто работает с самого начала.
Начальное состояние после BIOS
Как и многие вещи в оборудовании, стандартизация слаба, и одна из вещей, на которые вы не должны полагаться, это начальное состояние регистров, когда ваш код начинает работать после BIOS.
Поэтому сделайте себе одолжение и используйте некоторый код инициализации, например, следующий: https://stackoverflow.com/a/32509555/895245
Регистры любят %ds
и %es
имеют важные побочные эффекты, поэтому вы должны обнулять их, даже если вы не используете их явно.
Обратите внимание, что некоторые эмуляторы лучше, чем реальное оборудование, и дают вам хорошее начальное состояние. Затем, когда вы работаете на реальном оборудовании, все ломается.
Эль Торито
Формат, который можно записать на компакт-диски: https://en.wikipedia.org/wiki/El_Torito_%28CD-ROM_standard%29
Также возможно создать гибридное изображение, которое работает на ISO или USB. Это может быть сделано с grub-mkrescue
( например ), а также выполняется ядром Linux на make isoimage
использование isohybrid
.
РУКА
В ARM общие идеи совпадают.
Не существует широко доступной полу-стандартизированной предустановленной прошивки, такой как BIOS, которую мы могли бы использовать для ввода-вывода, поэтому два простейших типа ввода-вывода, которые мы можем сделать:
- сериал, который широко доступен на девбордах
- мигать светодиодом
Я загрузил:
несколько простых примеров 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- что светодиоды-и-GPIOs-в-QEMU эмуляции, как-т
Затем вы должны попробовать привет мир 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 -он-он / 814585 # 814585
https://github.com/dwelch67/raspberrypi выглядит как самое популярное на сегодняшний день учебное пособие по Raspberry Pi, посвященное голому металлу.
Некоторые отличия от x86:
IO делается путем написания магических адресов напрямую, там нет in
и out
инструкций.
Это называется IO с отображением памяти .
для некоторого реального оборудования, такого как Raspberry Pi, вы можете самостоятельно добавить прошивку (BIOS) в образ диска.
Это хорошо, поскольку делает обновление прошивки более прозрачным.
Ресурсы