Программирование голого железа Raspberry Pi
Я уже затрагивал тему программирования голого железа (bare metal). Заметку на эту тему опубликовал здесь же на форуме. Сейчас решил вернуться к этой теме. Но понял, что она слишком обширна и, наверное, не до конца отвечает тематике нашего форума. В общем, в этой теме буду по мере появления у меня настроения потихоньку публиковать материалы по теме, а также отвечать на вопросы, если таковые возникнут.
Начну с того, без чего невозможно стартануть, чтобы запустить свой код на голом железе. Необходимо понимать как организован процесс загрузки Raspberry Pi. В какой файл нужно поместить свой код, с какого адреса скомпилировать и т.д. Вот ссылка на заметку Как загружается Raspberry Pi. Не стал публиковать эту информацию на форуме, чтобы не засорять его непрофильной информацией.
Программа "Hello, World!"
Для тех, кто хотел бы сделать первый шаг и, возможно, немного поэкспериментировать, приведу последовательность действий, которые необходимо произвести, чтобы запустить простейшую программу на ассемблере на голом железе под эмулятором. Здесь я буду описывать процесс для Windows.
Итак, для начала скачайте и установить эмулятор QEMU. Последнюю сборку под Windows можно скачать отсюда: https://qemu.weilnetz.de/w64/
Также нам потребуется кросс-ассемблер FASMARM для Windows. Скачать его можно отсюда: https://arm.flatassembler.net/
Далее скопируйте текст программы в блокнот и сохраните в файл HelloWorld.s:
Код:
org 0x6001000
mov r0, 0x9000 ; Загружаем в регистр r0 адрес консоли 0x10009000, вначале младшую половину
movt r0, 0x1000 ; а потом старшую половину
adr r1,text ; Загружаем в регистр r1 адрес начала текста
mov r2,0 ; Обнуляем счётчик в регистре r2
loop:
ldrb r3,[r1,r2] ; Загружаем в регистр r3 код следующего символа надписи
str r3,[r0] ; Выводим в консоль код символа из регистра r3
add r2,1 ; Увеличиваем счётчик на единицу
cmp r3,10 ; Сравниваем код символа с кодом перевода строки
bne loop ; Переходим к следующему символу, если код символа не равен коду перевода
finish:
b finish ; В конце программы зацикливаем строку саму на себя
text:
db "Hello, World!", 10
Сам я лично вместо стандартного блокнота пользуюсь Notepad++ и подсветкой синтаксиса, которую сделал похожей на привычный мне PyCharm:
https://pic.maxiol.com/images2/16928...helloworld.png
Распакуйте архив с ассемблером. Оттуда понадобится только исполняемый (.exe) файл. И положите рядом с ним ваш HelloWorld.s. Теперь всё готово для компиляции! Открываем консоль, переходим в папку с ассемблером и нашим файлом, и подаём команду:
Код:
fasmarm HelloWorld.s
https://pic.maxiol.com/images2/16928...84.compile.png
На выходе получаем файл HelloWorld.bin объёмом 54 байта. Теперь осталось только его запустить под эмулятором. Для запуска подаём такую команду в консоли:
Код:
"c:\Program Files\qemu\qemu-system-arm.exe" -M vexpress-a9 -serial stdio -kernel HelloWorld.bin
И наслаждаемся результатом:
https://pic.maxiol.com/images2/16928....consolehw.png
Появились вопросы? Задавайте!
Запуск собственных программ под эмулятором
Ну вот теперь мы вплотную приблизились к эмулятору Raspberry Pi. На самом деле я мог бы сейчас одной фразой сказать, как скомпилировать программу, чтобы она запустилась на эмуляторе. Но, думаю, что так будет менее интересно. Судя по активности, данная тема заинтересовала лишь 2-3 человека, да и для них, скорее всего интересен не конечный результат, а повествование. Поэтому я расскажу, как я пришёл к конечному результату, шаг за шагом.
Для того, чтобы писать приложения под операционную систему особенно не требуется каких-то изощрений. Писать и отлаживать можно прямо на Raspberry Pi. Другое дело низкоуровневое программирование - bare metal. Здесь нужно либо поверять написанную программу на реальном железе, либо иметь эмулятор. Правда в итоге написанную программу всё равно придётся запускать на настоящем микрокомпьютере, так как ни один эмулятор не способен обеспечить 100%-ного качества эмуляции. Пик интереса к bare metal программированию пришёлся на 7-10 лет назад, спустя короткое время после выхода Raspberry Pi, когда этот микрокомпьютер утвердился в качестве некоего стандарта, и когда появилось достаточное количество информации о его схемотехнике. Но на тот момент вообще не существовало какого-либо эмулятора этого устройства. Да, уже тогда существовал QEMU, который неплохо эмулировал устройства, базирующие на архитектуре ARM, но среди них не было платформы Raspberry Pi. Разработчики игры PiFox, очевидно для удобства отладки, на базе QEMU сделали свой эмулятор, который худо-бедно мог эмулировать архитектуру Raspberry Pi. Доводилось мне читать упоминания и о других подобных начинаниях. Но в конечном итоге, спустя несколько лет в QEMU появилась официальная поддержка Raspberry Pi. И начали они с эмуляции Raspberry Pi 2. А на сегодня мы имеем возможность эмулировать Raspberry Pi 1, 2, 3 и Zero.
Когда я только начал изучать вопрос, то поиски приводили меня лишь к всевозможным инструкциям для запуска RaspbianOS на QEMU. Сам запуск осуществлялся на кастомном ядре и на архитектуре отличной от Raspberry Pi - Arm Versatile. Предлагались инструкции по самостоятельным конфигурированию и сборке ядра, также готовые ядра под разные версии RaspbianOS. Но в итоге в QEMU завезли нативную эмуляцию Raspberry Pi и появилась возможность использовать стандартное ядро для запуска RaspbianOS. Правда, ядро не может подхватываться из образа, его предварительно необходимо оттуда извлечь, положить рядом с загружаемым образом и прописать в ключ запуска.
Вот так примерно будет выглядеть довольно длинная строка запуска нативной эмуляции RaspbianOS:
Код:
qemu-system-aarch64 -M raspi3b -append "rw earlyprintk loglevel=8 console=ttyAMA0,115200 dwc_otg.lpm_enable=0 root=/dev/mmcblk0p2 rootdelay=1" -dtb ./dtbs/bcm2710-rpi-3-b-plus.dtb -sd 2020-08-20-raspios-buster-armhf-full.img -kernel kernel8.img -m 1G -smp 4 -serial stdio -usb -device usb-mouse -device usb-kbd
И действительно, всё загружается и работает. Только это никак не помогает понять как же загрузить свою собственную программу в эмулятор QEMU. Из всего этого стало лишь понятно, что нужен особым образом скомпилированный kernel. И что я только не делал, как только не экспериментировал, но моя программа отказывалась запускаться под эмулятором. Вот оно по всем законам должно работать, но работает! И, честно сказать, руки мои практически опустились. Но тут мои поиски привели меня вот к этому примеру. Этот пример написан на ассемблере, он в графическом виде выводит надпись "NO OS" на экран, а также выводит надпись "No OS installed" в консоль. И при этом данный пример должен был запускаться под эмулятором! (Тоже скачайте его себе для экспериментов, я в дальнейшем буду объяснять всё именно на его примере!)
И у меня получилось скомпилировать и запустить эту простую программу под эмулятором!
https://pic.maxiol.com/thumbs2/16937...8384.noos1.png https://pic.maxiol.com/thumbs2/16937...8384.noos2.png
Не скажу, что это было для меня просто, но у меня всё получилось. И вот тут пришло время немножко подробнее остановиться на средствах разработки.
FASMARM
В качестве кросс-ассемблера я остановил свой выбор на FASMARM. Я уже достоверно не помню как я на него вышел, но скорее всего потому что этот кросс-ассемблер фигурировал в примерах Питера Лемона. По своей сути FASMARM является аддоном для Flat Assembler и работает как под Windows, так и под Linux. Этот инструмент представляет из себя крохотный по сегодняшним меркам исполняемый файл. Он не требует инсталляции и не подтягивает какие-либо сторонние библиотеки.
Любопытно, что данный кросс-ассемблер сам написан на ассемблере. А что самое интересное - все последние версии FASMARM компилируются на Flat Assembler'е, то есть на самом себе.
Скачать сам ассемблер и его исходники можно с официальной странички: https://arm.flatassembler.net/
Использование его предельно простое:
А на выходе моментально получим готовый для запуска файл filename.bin.
Но определяющим при выборе ассемблера для меня стал всё-таки синтаксис. Дело в том, разные программы написанные на ассемблере ARM под разные компиляторы скорее всего потребуют доработок, а иногда весьма серьёзных из-за различий в синтаксисе. Например, FASMARM для обозначения комментариев использует привычную для меня точку с запятой ( ; ), а стандартные инструменты для этой же цели используют "собаку" ( @ ). Также весьма существенно отличается синтаксис макросов, обозначение чисел, наличие и отсутствие некоторых псевдокоманд. Нет смысла сейчас подробно останавливаться на этих различиях, а достаточно просто знать, что они есть.
Большую часть примеров, которые я находил в начале моего изучения, я смог скомпилировать на FASMARM либо вообще без переделок, либо с какими-то минимальными доработками. И вот как раз тот самый единственный найденный мной пример, который должен запускался под эмулятором, ни в какую не хотел переделываться под FASMARM. Да, к тому же ещё требовал линковки. Пришлось разбираться как компилировать другим инструментом. Оказалось, что там тоже нет ничего сложного.
Arm GNU Toolchain
Для начала нужно скачать инструментарий на сайте ARM. Инструментарий бесплатный, но с недавних пор, чтобы скачать его из РФ придётся воспользоваться VPN или Tor.
Качаем с этой страницы: https://developer.arm.com/downloads/...hain-downloads
Прямую ссылку на установочный файл не привожу, так как с выходом новых версий она меняется. Но вам нужно будет скачать примерно такой файл: arm-gnu-toolchain-12.3.rel1-mingw-w64-i686-arm-none-eabi.exe ,где:
w64-i686 - платформа компилятора Windows 64 бит
arm-none-eabi - целевая платформа ARM 32 бит
Скачиваем и устанавливаем.
Чтобы скомпилировать тот самый пример я подготовил вот такой bat-файл, основываясь на информации из Makefile:
Код:
@set file=boot-arm-raspi
@set toolchain="c:\Program Files (x86)\Arm GNU Toolchain arm-none-eabi\12.3 rel1\bin\arm-none-eabi"
python bmp2inc.py > bmp_no_os.inc
%toolchain%-as -o %file%.o -mcpu=arm1176jzf-s %file%.s
%toolchain%-ld -o %file%.elf -Ttext 0x8000 %file%.o
@if exist %file%.o del %file%.o
На выходе получаем boot-arm-raspi.elf, который можем запустить в эмуляторе следующей командой:
Код:
"c:\Program Files\qemu\qemu-system-arm.exe" -M raspi2b -serial stdio -kernel boot-arm-raspi.elf
Оказалось, что QEMU может обрабатывать elf-файлы, забирая оттуда всю необходимую для запуска информацию.
И всё бы хорошо, но этот вариант годится только для компилирования исходников с помощью Arm GNU Toolchain, а мне хотелось как и раньше пользоваться FASMARM. И я в итоге разобрался как прикрутить FASMARM к этой цепочке.
Прежде всего в исходном тексте своей программы в самом начале нужно добавить вот такие строки:
Код:
format ELF
section '.text' executable
public _start
org 0x8000
_start:
А для компиляции я создал вот такой батник:
Код:
@set file=filename
@set toolchain="c:\Program Files (x86)\Arm GNU Toolchain arm-none-eabi\12.3 rel1\bin\arm-none-eabi"
fasmarm %file%.s
%toolchain%-ld.exe -o %file%.elf -Ttext 0x8000 %file%.o
@if exist %file%.o del %file%.o
Ну и сразу здесь же напишу, что на выходе вы можете получить не только elf, но и бинарный файл, который можно записать на SD-карту и запустить на реальной Raspberry Pi. Для этого в конец цепочки компиляции нужно добавить ещё одну строку:
Код:
%toolchain%-objcopy %file%.elf -O binary %file%.img
Собственно, это и есть рабочий рецепт, которым я и начал пользоваться. И на этом можно было бы завершить данное сообщение. Но оказалось, что на деле для запуска скомпилированых программ под эмулятором можно обойтись вообще без всех этих плясок с Arm GNU Toolchain. И всё делается предельно просто!
В процессе изучения другого вопроса, я натолкнулся на ключ QEMU "-d in_asm", который показывает в консоли последние выполненные команды. Я сделал и скомпилироваал простую программу, состоящую из одной строки:
Я запустил её под эмулятором с указанным ключом. И что же я увидел:
https://pic.maxiol.com/images2/16939...4.deadloop.png
Данная команда работает по адресу 0x10000! Это очень странный адрес! 32-х битные приложения запускаются и работают с адреса 0x8000. Для старых ядер адрес запуска был нулевой (0x0000). (Этот адрес можно вернуть, если в системной файле Raspberry Pi config.txt прописать команду kernel_old = 1.) Есть ещё адрес 0x200000 - это адрес запуска 64-х битных бинарников. Но никогда и нигде мне не попадалось упоминание о 0x10000. Я попробовал скомпилировать и запустить первую попавшуюся программу с этого адреса. И она как ни в чём не бывало запустилась под QEMU! Всё оказалось до безобразия просто.
Для запуска под эмулятором вам нужно всего лишь в своём коде прописать org 0x10000! (Собственно, это и есть та самая фраза, которой можно было ограничиться в этом сообщении.)
Теперь вы можете брать примеры Питера Лемона, о которых говорилось выше, менять там ORG, компилировать, и запускать всё под QEMU.