Лайфхак: перенаправления в- или из файла в bash не обязаны идти в самом конце команды. Допустим, я хочу отсортировать строки в таком файле:
при помощи команды sort. Забудем на минуточку, что конкретно команде sort файл(ы) также можно передавать как аргументы командной строки, и представим, что у нас есть только фильтр, преобразующий stdin в stdout:
... и тут мы вспоминаем, что сортировать нужно по второй колонке — той, что с буквой — для этого нужно просто указать опцию
3 B Some string
1 C Some other string
2 A One more test string
при помощи команды sort. Забудем на минуточку, что конкретно команде sort файл(ы) также можно передавать как аргументы командной строки, и представим, что у нас есть только фильтр, преобразующий stdin в stdout:
$ sort < /tmp/long-input-name
1 C Some other string
2 A One more test string
3 B Some string
... и тут мы вспоминаем, что сортировать нужно по второй колонке — той, что с буквой — для этого нужно просто указать опцию
-k 2. Так вот, оказывается, необязательно гнать курсор до начала всех перенаправлений, можно просто дописать опцию после:$ sort < /tmp/long-input-name -k 2
2 A One more test string
3 B Some string
1 C Some other string
С тех пор, как #маленький_но_гордый_сервер был проапгрейжен (да, я ещё не потерял надежду продолжить эту тему заметок), у меня остался лежать Orange Pi Zero 3. Захотелось сделать из него ютубосмотрелку, как-никак, TV boxes — это одно из предназначений его процессора — хотя правильнее, наверное, говорить System-on-Chip — Allwinner H618 (ссылка, если что, на фанатский сайт не на официальный сайт производителя).
Увы, поставив на него свежий OpenSUSE Tumbleweed (обычно я пользуюсь Убунтой, но раз уж для "сервера" настроил MicroOS, то и здесь решил использовать что-нибудь из OpenSUSE), я не обнаружил следов видеодрайверов, и в частности, модуля panfrost, который я так старательно банил в "серверном" варианте, потому что при его инициализации напрочь висла система. Думал, за прошедшее время модуль допилили, но оказалось, что теперь "модуль не виснет, он делает больно по-другому".
По запросу "opensuse panfrost" гугл выдал что-то вроде официальной документации дистрибутива, из которой следовало, что всё уже давно поддерживается, но мало в каких описаниях плат Mali GPU описан в device tree. Что же, идём в Armbian, там я когда-то видел сборку под Zero 3 с предположительно работающей графикой. Правда, сборка не особо официальная, и внутренний параноик как-то не особо хочет доверять ей свой пароль от гугла-ютуба. К счастью, из неё мне требуется только небольшой файлик
В общем, подложил я этот dtb вместо шедшего в комплекте с ядром, перезагрузился, и понял, что всё верно, теперь система знает про GPU... и вновь зависает при загрузке, блин. Похоже, тот патч для power domain driver (что бы это ни значило) всё ещё на вмержили. Некоторое количество времени я пытался нагуглить, как прямо на плате пропатчить и пересобрать один-единственный модуль, потом засомневался, а модуль ли это, или он намертво влинкован в ядро, плюнул и пошёл читать описание, а что же этот патч делает.
В описании патча, который я пытался применить, говорилось
Костыль получился простой: посмотрев в консоли U-Boot при помощи
... можно понять, что вклиниться в процесс загрузки можно, положив в корень FAT32-раздела файлпосле вместо скрипта пошёл бы загружаться efi), получился скрипт из двух строчек:
который нужно положить, например, в файл
Увы, поставив на него свежий OpenSUSE Tumbleweed (обычно я пользуюсь Убунтой, но раз уж для "сервера" настроил MicroOS, то и здесь решил использовать что-нибудь из OpenSUSE), я не обнаружил следов видеодрайверов, и в частности, модуля panfrost, который я так старательно банил в "серверном" варианте, потому что при его инициализации напрочь висла система. Думал, за прошедшее время модуль допилили, но оказалось, что теперь "модуль не виснет, он делает больно по-другому".
По запросу "opensuse panfrost" гугл выдал что-то вроде официальной документации дистрибутива, из которой следовало, что всё уже давно поддерживается, но мало в каких описаниях плат Mali GPU описан в device tree. Что же, идём в Armbian, там я когда-то видел сборку под Zero 3 с предположительно работающей графикой. Правда, сборка не особо официальная, и внутренний параноик как-то не особо хочет доверять ей свой пароль от гугла-ютуба. К счастью, из неё мне требуется только небольшой файлик
sun50i-h618-orangepi-zero3.dtb с описанием оборудования.В общем, подложил я этот dtb вместо шедшего в комплекте с ядром, перезагрузился, и понял, что всё верно, теперь система знает про GPU... и вновь зависает при загрузке, блин. Похоже, тот патч для power domain driver (что бы это ни значило) всё ещё на вмержили. Некоторое количество времени я пытался нагуглить, как прямо на плате пропатчить и пересобрать один-единственный модуль, потом засомневался, а модуль ли это, или он намертво влинкован в ядро, плюнул и пошёл читать описание, а что же этот патч делает.
В описании патча, который я пытался применить, говорилось
The Allwinner H616 features register 0x7010254 in the PRCM MMIO frame,
where bit 0 needs to be cleared to enable operation of the Mali GPU.
With this bit set (the reset default), any access to the Mali registers
hangs the bus and thus the whole system. The BSP code clears this bit
in U-Boot and their kernel never touches it again.
Костыль получился простой: посмотрев в консоли U-Boot при помощи
env print (вывод слегка переформатирован для читаемости), что происходит в окрестности момента загрузки EFI-бинарника GRUB 2...=> env print
...
boot_prefixes=/ /boot/
boot_scripts=boot.scr.uimg boot.scr
...
scan_dev_for_boot=echo Scanning ${devtype} ${devnum}:${distro_bootpart}...; \
for prefix in ${boot_prefixes}; do \
run scan_dev_for_extlinux; \
run scan_dev_for_scripts; \
done; \
run scan_dev_for_efi;
scan_dev_for_scripts=for script in ${boot_scripts}; do \
if test -e ${devtype} ${devnum}:${distro_bootpart} ${prefix}${script}; then # ...
... можно понять, что вклиниться в процесс загрузки можно, положив в корень FAT32-раздела файл
boot.scr. Фактически, мне нужно командой mw.b 0x7010254 0x00 1 включить GPU (disclaimer: здесь я заранее посмотрел при помощи md, что похоже, что по этому адресу всегда лежит значение 0x01, значит, со сброшенным младшим битом это будет просто 0x00, в общем же случае пришлось бы как-нибудь вычислять, наверное...) и возобновить процесс загрузки с минимальными изменениями. Поскольку закостылить требуется лишь вполне конкретную ситуацию (т.е. я знаю, какое железо используется, знаю где лежат загрузчики, знаю, что сразу же mw.b 0x7010254 0x00 1
run scan_dev_for_efi
который нужно положить, например, в файл
boot.cmd и обернуть правильным бинарным заголовком, чтобы U-Boot его узнал, в линуксовой консоли выполнивmkimage -A arm64 -O u-boot -T script -d boot.cmd boot.scr
Теперь ядро благополучно загрузилось, и при этом
PS: Оказывается, 1) ограничение на размер поста бывает не только при наличие картинок, 2) десктопный клиент сам разбивает длинное сообщение на части.
lsmod показал, что модуль panfrost был автоматически загружен... а вот графический сеанс на мониторе, подключенном по HDMI, так и не отобразился: видимо, некоторые из других драйверов, перечисленных в списке недоделок на linux-sunxi, действительно ещё не влиты в основное ядро. Но это, как говорится, уже совсем другая история... Зато теперь приветственная строчка "Have a lot of fun", печатаемая при текстовом логине в OpenSUSE заиграла новыми красками.PS: Оказывается, 1) ограничение на размер поста бывает не только при наличие картинок, 2) десктопный клиент сам разбивает длинное сообщение на части.
Кстати о том, как извлечь файл из preinstalled-образа диска с несколькими разделами.
Обычно в линуксе образ диска вроде iso-файла можно просто примонтировать командой
Но это образ одной файловой системы. А что, если на вход у нас есть дамп диска с таблицей разделов MBR или GPT, и нам нужен какой-то из этих разделов? Тут нам поможет знание о существовании loop devices, просто придётся вручную сделать два отдельных шага: настроить loop device и подмонтировать его. Дело в том, что при настройке можно попросить ядро представить, будто у него появился ещё один обычный диск (ну, там, флешку подключили), и попробовать поискать на нём разделы:
Да, здесь получилось не столь "зрелищно", поскольку раздел всего один. Хотя, как знать: раздел-то один, а без парсинга таблицы разделов не открыть:
Кстати,
Ну и после завершения работы с образом можно этот образ отцепить от loop device:
Обычно в линуксе образ диска вроде iso-файла можно просто примонтировать командой
mount так же, как обычное блочное устройство: просто вместо mount /dev/sda1 /mnt пишешь mount file.iso /mnt, и всё работает. При этом где-то в глубине команда mount настроит loop-устройство, превратив файл в block device, а далее подмонтирует уже block device (методом чайника, ага), но для пользователя это не столь важно.Но это образ одной файловой системы. А что, если на вход у нас есть дамп диска с таблицей разделов MBR или GPT, и нам нужен какой-то из этих разделов? Тут нам поможет знание о существовании loop devices, просто придётся вручную сделать два отдельных шага: настроить loop device и подмонтировать его. Дело в том, что при настройке можно попросить ядро представить, будто у него появился ещё один обычный диск (ну, там, флешку подключили), и попробовать поискать на нём разделы:
$ losetup --find --show --partscan /tmp/armbian.img
/dev/loop26
$ ls /dev/loop26*
/dev/loop26 /dev/loop26p1
$ mount /dev/loop26p1 /mnt
$ ls /mnt
bin bin.usr-is-merged boot dev etc # ...
Да, здесь получилось не столь "зрелищно", поскольку раздел всего один. Хотя, как знать: раздел-то один, а без парсинга таблицы разделов не открыть:
$ mount /tmp/armbian.img /mnt
mount: /mnt: wrong fs type, bad option, bad superblock on /dev/loop26, missing codepage or helper program, or other error.
dmesg(1) may have more information after failed mount system call.
$ file /tmp/armbian.img
/tmp/armbian.img: DOS/MBR boot sector; partition 1 : ID=0x83, start-CHS (0x0,130,3), end-CHS (0x2e9,131,26), startsector 8192, 11968512 sectors, extended partition table (last)
Кстати,
file точно так же расскажет о содержимом блочного устройства, нужно только правильно попросить:$ file /dev/loop26*
/dev/loop26: block special (7/26)
/dev/loop26p1: block special (259/5)
$ file --special-files /dev/loop26*
/dev/loop26: DOS/MBR boot sector; partition 1 : ID=0x83, start-CHS (0x0,130,3), end-CHS (0x2e9,131,26), startsector 8192, 11968512 sectors, extended partition table (last)
/dev/loop26p1: Linux rev 1.0 ext4 filesystem data, UUID=3b40068e-537c-4a9a-a1fe-6bff7c29918c, volume name "armbi_root" (extents) (64bit) (large files) (huge files)
Ну и после завершения работы с образом можно этот образ отцепить от loop device:
$ losetup --detach /dev/loop26
При этом понятно, откуда строчка "nan" — это NaN (Not-a-Number), а вот почему именно он появился при переходе от
P.S.: JS не мой любимый язык программирования, репостнул из-за синтаксиса :)
'b' + 'a' к 'b' + 'a' + + 'a' — вопрос... И да, без пробелов между + + — синтаксическая ошибка, "ты что, дурак? причём здесь инкремент?!?". P.S.: JS не мой любимый язык программирования, репостнул из-за синтаксиса :)
Кстати о JavaScript, который хоть и не мой любимый язык, но с которым я когда-то много взаимодействовал с необычной стороны — как с машинным языком, в который компилируется что-нибудь высокоуровневое типа C :) Знаете ли вы, что в какой-то момент было сформулировано компилируемое ahead-of-time подмножество JS, которое браузеры могли распознавать и более эффективно выполнять: https://en.wikipedia.org/wiki/Asm.js При этом браузеры могли игнорировать существование Asm.js или банально не знать о нём, и тогда код был просто несколько причудливым джаваскриптом. А потом пришёл бинарный WebAssembly, и необходимость в Asm.js, вроде бы, отпала.
Кстати о правильном толковании стандартов языка, на Stack Overflow даже есть тег [language-lawyer] с аннотацией "For questions about the intricacies of formal or authoritative specifications of programming languages", например
* What does the C++ standard say about the size of int, long?
* A positive lambda: '+[]{}' - What sorcery is this?
* What does the C++ standard say about the size of int, long?
* A positive lambda: '+[]{}' - What sorcery is this?
Stack Overflow
Highest scored 'language-lawyer' questions
Stack Overflow | The World’s Largest Online Community for Developers
Помимо StackOverflow есть целое семейство вопросо-ответных сайтов на разные тематики, называется StackExchange. Одним из таких сайтов является worldbuilding.stackexchange.com — сайт, где авторы художественных произведений спрашивают советы по более логичному обоснованию деталей сюжета.
Одним из таких вопросов был: зачем может быть полезна письменность обществу с развитой телепатией? Чтоб вы понимали, этот вопрос (а точнее самый популярный ответ на него) я с лёгкостью нашёл в поиске сайта по запросу "cache druids". В общем, вот: обсуждение друидов L2-кэша, вопросов отказоустойчивости и т.д.
P.S.: Кто-то, безусловно, уже мог видеть у меня эту ссылку, но сюда я её ещё не кидал.
Одним из таких вопросов был: зачем может быть полезна письменность обществу с развитой телепатией? Чтоб вы понимали, этот вопрос (а точнее самый популярный ответ на него) я с лёгкостью нашёл в поиске сайта по запросу "cache druids". В общем, вот: обсуждение друидов L2-кэша, вопросов отказоустойчивости и т.д.
P.S.: Кто-то, безусловно, уже мог видеть у меня эту ссылку, но сюда я её ещё не кидал.
Worldbuilding Stack Exchange
Does a hive mind race need a written language?
Imagine that human beings in a parallel world evolved with hive mind, so that they can communicate telepathically almost instantly at infinite range. To put thoughts into writing requires effort an...
Понадобилось мне запустить Thunderbird без доступа в интернет, чтобы проверить самодостаточность экспортированного mbox-файла с письмами. Наверное, можно было бы соорудить какую-нибудь изоляцию при помощи network namespace, но я решил, что чем разбираться с нюансами linux namespaces, а потом ещё и думать, как надёжно разделить профили Thunderbird "для экспорта" и "для теста", проще сразу поднять виртуальную машину. При этом хотелось работать прямо с Live DVD, чтобы найдя потом эту ВМ через пол-года, не пытаться судорожно вспомнить, а можно ли её удалить, или там что-то важное лежит: раз нет HDD, значит и ничего важного нет :) Впрочем, поскольку QEMU можно запускать прямо из терминала, то нет не то что HDD, нет даже какой-то перманентной сущности "виртуальная машина", которую нужно (или не нужно) удалять.
QEMU был выбран потому, что не хотелось возиться с установкой драйверов VirtualBox ни на хост, ни в гостевую систему. Хост не хотелось трогать просто потому, чтобы потом при каждом обновлении ядра не запускалась сборка модулей через dkms, а установка "дополнений гостевой ОС" внутри Live DVD без перезагрузки в принципе не выглядит простой задачей.
В общем, небольшая памятка (возможно, даже для себя через пол-года). Строка запуска QEMU с каталогом, расшаренным между хостом и гостем, выглядит примерно так:
Первая строчка (
Вторая строка описывает каталог, расшаренный при помощи некоего древнего протокола из операционной системы Plan 9, который, если верить документации QEMU, поддерживается аж со времён ядра
Что неудивительно, расшаренный каталог сам по себе в гостевой системе не подключится (потому что куда конкретно он должен подключиться?) — его нужно подмонтировать:
Здесь
Кстати, если хочется, чтобы внутри ВМ шустрее работала графика, можно добавить при запуске QEMU опции
Опция
В завершение не лишним будет упомянуть, что речь идёт об изоляции доверенной гостевой ОС для предотвращения случайного обращения куда не надо. Если же в гостевой системе предполагается наличие malware, то тут нужно отдельно читать best practices по надёжной изоляции — я даже советовать не берусь.
UPD: Ах да, так увлёкся документированием расшаривания каталога и тестированием разных эмулируемых видеоадаптеров, что до, собственно, проверки бекапа дело не дошло, поэтому о самом главном забыл:
QEMU был выбран потому, что не хотелось возиться с установкой драйверов VirtualBox ни на хост, ни в гостевую систему. Хост не хотелось трогать просто потому, чтобы потом при каждом обновлении ядра не запускалась сборка модулей через dkms, а установка "дополнений гостевой ОС" внутри Live DVD без перезагрузки в принципе не выглядит простой задачей.
В общем, небольшая памятка (возможно, даже для себя через пол-года). Строка запуска QEMU с каталогом, расшаренным между хостом и гостем, выглядит примерно так:
qemu-system-x86_64 \
-m 4096 -accel kvm \
-virtfs local,path=/tmp/share,mount_tag=share,security_model=none \
-net none \
-cdrom kubuntu-24.10-desktop-amd64.iso
Первая строчка (
-m 4096 -accel kvm) указывает выделить виртуалке 4ГБ оперативной памяти и использовать аппаратное ускорение виртуализации (KVM — штука стандартная, поэтому на типичном "десктопном линуксе" на хосте сторонние модули не нужны, разве что своего пользователя может потребоваться добавить в какую-нибудь группу и перезайти в систему).Вторая строка описывает каталог, расшаренный при помощи некоего древнего протокола из операционной системы Plan 9, который, если верить документации QEMU, поддерживается аж со времён ядра
2.6.36.rc4, то есть очень давно. Подробности можно почитать во всё той же документации, я же просто уточню, что path=/tmp/share указывает каталог на хосте, mount_tag=share задаёт метку, по которой эта шара будет монтироваться гостем, а security_model=none указывает не заморачиваться по поводу прав доступа к файлам.Что неудивительно, расшаренный каталог сам по себе в гостевой системе не подключится (потому что куда конкретно он должен подключиться?) — его нужно подмонтировать:
mount -t 9p -o trans=virtio,version=9p2000.L share /mnt
Здесь
/mnt — точка монтирования в гостевой системе, а share — тот самый mount tag.Кстати, если хочется, чтобы внутри ВМ шустрее работала графика, можно добавить при запуске QEMU опции
...
-display gtk,gl=on -vga none -device virtio-gpu-gl \
...
Опция
-display gtk,gl=on указывает включить OpenGL в графическом интерфейсе ВМ на хосте (т.е. самом окошке с виртуальным экраном и меню "Reboot"/"Power off"/...), -device virtio-gpu-gl добавляет в эмулируемую систему продвинутую виртуальную видеокарту, а -vga none извлекает видеокарту обычную, чтобы система не путалась. Правда, с "продвинутой видеокартой" изображение не появится, пока не запустится графический сеанс (ну или вообще никогда, если "гость" упадёт до этого этапа). Бонусом — гостевая система будет с пониманием относиться к тому, что монитор вдруг поменял нативное разрешение, когда на хосте меняют размер окна.В завершение не лишним будет упомянуть, что речь идёт об изоляции доверенной гостевой ОС для предотвращения случайного обращения куда не надо. Если же в гостевой системе предполагается наличие malware, то тут нужно отдельно читать best practices по надёжной изоляции — я даже советовать не берусь.
UPD: Ах да, так увлёкся документированием расшаривания каталога и тестированием разных эмулируемых видеоадаптеров, что до, собственно, проверки бекапа дело не дошло, поэтому о самом главном забыл:
-net none отключает сеть :) Дописал в команду запуска.Кстати, о запуске QEMU из командной строки. Как известно, в *nix длинную команду запуска чего-нибудь можно положить в файл, исполняемый как shell-скрипт. Этот файл можно будет редактировать в текстовом редакторе вместо встроенного редактора bash или другой оболочки, а для читаемости разбить команду на несколько строк, завершаемых символом
Всё бы хорошо, но потом начинает хотеться временно отключить часть аргументов. Казалось бы, есть же символ
Называется этот приём array variables. Хотя в shell-скриптах примерно все переменные — строки, но в bash некоторые переменные могут содержать последовательность логически разделённых строк. В документации написано, что поддерживаются даже ассоциативные массивы, но сейчас не о них. Если при присваивании переменной вместо
\ (как было сделано постом выше).Всё бы хорошо, но потом начинает хотеться временно отключить часть аргументов. Казалось бы, есть же символ
#, комментирующий до конца строки. Но нет: \ не продолжит "виртуальную строку", будучи закомментированным. Можно, конечно, в какой-то точке файла написать exit 0, а следом устроить склад временно выведенных из эксплуатации наборов аргументов — когда-то я так и делал. Но есть способ лучше! Правда, непохоже, чтобы минимальный /bin/sh, гарантированный стандартом, это поддерживал, но в bash (и наверняка zsh) такой приём жизнь облегчит.Называется этот приём array variables. Хотя в shell-скриптах примерно все переменные — строки, но в bash некоторые переменные могут содержать последовательность логически разделённых строк. В документации написано, что поддерживаются даже ассоциативные массивы, но сейчас не о них. Если при присваивании переменной вместо
name="some value" написать name=("value a" "value b"), то в переменной окажется сохранена последовательность строк, которую можно потом передать далее с сохранением разбиения на аргументы при помощи синтаксиса ${name[@]} (на самом деле, ${string_var} — это то же самое, что $string_var, так что из нового добавляется только [@]). Например:args=(
file
"multi word file name"
other_file
)
gzip "${args[@]}"
Уже не первый год, как во многих графических интерфейсах есть ночной цветовой режим — в смысле не тёмная тема, а светлая, но с пониженной цветовой температурой. Есть такая функция и в KDE. А недавно я обнаружил, что свежая Kubuntu 24.10 при прокрутке на значке "ночной" цветовой гаммы умеет ещё и менять яркость монитора. То есть не просто программно домножать компоненты R, G, B на коэффициент, меньший единицы, а буквально регулировать яркость подсветки монитора. Да, именно отдельностоящего монитора, подключенного по HDMI.
Впрочем, самой по себе возможности подобного управления я уже успел удивиться пару лет назад, когда решил загуглить, что же это за пункт меню под названием "DDC/CI" со значениями Enabled и Disabled. До чего, оказывается, техника дошла: когда-то давно пользовался здоровенным ЭЛТ монитором, у которого было чудо техники — USB-вход для управления настройками через специальный виндовый софт, а теперь вот случайно обнаруживаешь, что уже есть независимый от производителя стандарт (и I²C-шина в HDMI-разъёме), а из репозитория можно просто поставить консольную или графическую утилиту.
Впрочем, самой по себе возможности подобного управления я уже успел удивиться пару лет назад, когда решил загуглить, что же это за пункт меню под названием "DDC/CI" со значениями Enabled и Disabled. До чего, оказывается, техника дошла: когда-то давно пользовался здоровенным ЭЛТ монитором, у которого было чудо техники — USB-вход для управления настройками через специальный виндовый софт, а теперь вот случайно обнаруживаешь, что уже есть независимый от производителя стандарт (и I²C-шина в HDMI-разъёме), а из репозитория можно просто поставить консольную или графическую утилиту.
Пару лет назад я заказал на Алиэкспрессе отладочную плату с STM32 с намерением "ну надо же когда-нибудь приобщиться к программированию микроконтроллеров, а то чё я как лох-то?..". И вот, наконец-то решился: буду моргать светодиодом! Впрочем, не спешите с раздражением пролистывать очередной гайд по установке Arduino IDE (или что там обычно в таких случаях ставят) — сегодня будет занимательное (надеюсь) чтение technical reference manual на микроконтроллер и документации по системе команд Armv7. Ну и практика, конечно: ассемблер, gdb, все дела...
Целью сегодняшней "лабораторки" будет написание минимального примера, который можно запустить на железе, без использования готовых библиотек и прочей "магии" — буквально, получаем управление с первых тактов работы CPU, хотя и с некоторым читерством: я воспользуюсь возможностью отладки с компьютера, чтобы загрузить код прямиком в SRAM и передать на него управление.
Сразу уточню, что я не являюсь профессиональным писателем "загрузочного кода", равно как и программистом микроконтроллеров — у меня больше "компиляторостроительная" специализация — поэтому приведённый код и подход ни в коем случае не best practices, это скорее заметки вида "итак, я почти ничего не знаю про этот микроконтроллер, и я примерно понимаю, куда читать — давайте попробуем сделать хоть что-то".
#моргаем_светодиодом
Целью сегодняшней "лабораторки" будет написание минимального примера, который можно запустить на железе, без использования готовых библиотек и прочей "магии" — буквально, получаем управление с первых тактов работы CPU, хотя и с некоторым читерством: я воспользуюсь возможностью отладки с компьютера, чтобы загрузить код прямиком в SRAM и передать на него управление.
Сразу уточню, что я не являюсь профессиональным писателем "загрузочного кода", равно как и программистом микроконтроллеров — у меня больше "компиляторостроительная" специализация — поэтому приведённый код и подход ни в коем случае не best practices, это скорее заметки вида "итак, я почти ничего не знаю про этот микроконтроллер, и я примерно понимаю, куда читать — давайте попробуем сделать хоть что-то".
#моргаем_светодиодом
Для начала, с чем будем работать. В качестве отладочной платы используется одна из Black pill, в моём случае WeAct v3.0 с микроконтроллером STM32F401CDU6. Кстати, вот этот сайт, похоже, будет очень полезен как справочник: stm32-base.org — например, вот страница, описывающая мою плату (там, правда, указана микросхема ...CEU6).
Для отладки я буду использовать open hardware адаптер NanoDAP, о котором, когда-нибудь, наверное, напишу подробнее, а пока просто скажу, что из него торчит с одной стороны USB, а с другой — пины JTAG, SWD и UART. В случае STM32 я буду использовать интерфейс SWD.
При подаче на плату питания по USB или через SWD (только не одновременно двумя способами — думаю, последствия будут не ахти...), изначально в ней запускается то, что в неё прошил производитель. В моём случае это код, который... мигает светодиодом.Эксперимент завершён, расходимся.
#моргаем_светодиодом
Для отладки я буду использовать open hardware адаптер NanoDAP, о котором, когда-нибудь, наверное, напишу подробнее, а пока просто скажу, что из него торчит с одной стороны USB, а с другой — пины JTAG, SWD и UART. В случае STM32 я буду использовать интерфейс SWD.
При подаче на плату питания по USB или через SWD (только не одновременно двумя способами — думаю, последствия будут не ахти...), изначально в ней запускается то, что в неё прошил производитель. В моём случае это код, который... мигает светодиодом.
#моргаем_светодиодом
После того, как SWD-интерфейс физически присоединён, нужно понять, как через него подключиться отладчиком к микроконтроллеру. Для работы с JTAG- и SWD-адаптерами можно использовать OpenOCD: будучи правильно сконфигурирована, эта программа подсоединится к аппаратному адаптеру и откроет сетевой порт, на котором будет ожидать подключения от GDB, реализуя протокол gdb extended-remote.
Чтобы объяснить OpenOCD, с чем ей нужно работать, создадим файл
и запустим openocd:
Теперь можно запустить gdb (а точнее, придётся установить gdb-multiarch, поскольку ваш компьютер, скорее всего, тоже использует систему команд x86_64, а не 32-битный ARM) и подключиться к OpenOCD:
В примере выше
На 32-битном ARM инструкции могут иметь длину как 2, так и 4 байта, и как можно догадаться по примеру выше, мимо начала "второй с конца" инструкции я промахнулся. Попробуем ещё раз:
Вот, теперь больше похоже на правду.
#моргаем_светодиодом
Чтобы объяснить OpenOCD, с чем ей нужно работать, создадим файл
nanodap.cfg с таким содержимым:# тип адаптера, с которым будем работать
adapter driver cmsis-dap
# имеющаяся у меня реализация cmsis-dap представляется компьютеру
# как USB-устройство с VendorID 0xc251, ProductID 0xf001
cmsis_dap_vid_pid 0xc251 0xf001
transport select swd
source [find target/stm32f4x.cfg]
и запустим openocd:
$ sudo openocd -f nanodap.cfg
Open On-Chip Debugger 0.12.0
Licensed under GNU GPL v2
For bug reports, read
http://openocd.org/doc/doxygen/bugs.html
Info : Listening on port 6666 for tcl connections
Info : Listening on port 4444 for telnet connections
Info : CMSIS-DAP: SWD supported
Info : CMSIS-DAP: JTAG supported
Info : CMSIS-DAP: SWO-UART supported
Info : CMSIS-DAP: Atomic commands supported
Info : CMSIS-DAP: Test domain timer supported
Info : CMSIS-DAP: FW Version = 2.0.0
Info : CMSIS-DAP: Interface Initialised (SWD)
Info : SWCLK/TCK = 1 SWDIO/TMS = 1 TDI = 0 TDO = 0 nTRST = 0 nRESET = 1
Info : CMSIS-DAP: Interface ready
Info : clock speed 2000 kHz
Info : SWD DPIDR 0x2ba01477
Info : [stm32f4x.cpu] Cortex-M4 r0p1 processor detected
Info : [stm32f4x.cpu] target has 6 breakpoints, 4 watchpoints
Info : starting gdb server for stm32f4x.cpu on 3333
Info : Listening on port 3333 for gdb connections
Теперь можно запустить gdb (а точнее, придётся установить gdb-multiarch, поскольку ваш компьютер, скорее всего, тоже использует систему команд x86_64, а не 32-битный ARM) и подключиться к OpenOCD:
>>> target extended :3333
Remote debugging using :3333
warning: No executable has been specified and target does not support
determining executable automatically. Try using the "file" command.
0x0800343e in ?? ()
>>> x/10i $pc-8
0x8003436: mcr2 7, 5, pc, cr10, cr13, {7} @ <UNPREDICTABLE>
0x800343a: @ <UNDEFINED> instruction: 0xfacc42b8
=> 0x800343e: beq.n 0x80034aa
0x8003440: uxtb.w r10, r9
0x8003444: bl 0x80009d4
0x8003448: mov r7, r0
0x800344a: cmp.w r10, #1
0x800344e: bne.n 0x80034aa
0x8003450: ldrb.w r1, [r8]
0x8003454: ldrh.w r0, [r8, #2]
В примере выше
>>> — это приглашение gdb, а $pc — обозначение регистра, указывающего на текущую инструкцию: program counter. x/10i $pc - 8 означает "отобрази мне содержимое памяти (x), начиная с адреса, заданного выражением $pc - 8, при этом интерпретируй содержимое как инструкции (i) в количестве 10 штук".На 32-битном ARM инструкции могут иметь длину как 2, так и 4 байта, и как можно догадаться по примеру выше, мимо начала "второй с конца" инструкции я промахнулся. Попробуем ещё раз:
>>> x/10i $pc-6
0x8003438: bl 0x80009d4
0x800343c: cmp r0, r7
=> 0x800343e: beq.n 0x80034aa
0x8003440: uxtb.w r10, r9
0x8003444: bl 0x80009d4
0x8003448: mov r7, r0
0x800344a: cmp.w r10, #1
0x800344e: bne.n 0x80034aa
0x8003450: ldrb.w r1, [r8]
0x8003454: ldrh.w r0, [r8, #2]
Вот, теперь больше похоже на правду.
#моргаем_светодиодом
Как видно из карточки платы на stm32-base, красный светодиод намертво подключен к питанию, а вот синим мы может управлять через GPIO ножку C13.
Теперь нужно найти документ, описывающий, какие регистры управляют GPIO на данном конкретном микроконтроллере, и куда они отображены в памяти. В моём случае это RM0368 — 847-страничный reference manual, но целиком читать мы его не будем: интересующие нас сейчас части — это 2.3 Memory map, из которой мы узнаём, что регистры, управляющие GPIOC, отображаются, начиная с адреса 0x40020800, и дальше рекомендуется свериться с разделом 8.4.11 для подробностей. Поскольку я не знаком с использованием GPIO на этом микроконтроллере, от краткого содержания промотаю немного назад, на начало раздела 8.4 GPIO registers. Отсюда мы узнаём, что регистр GPIOC_ODR (output data register) находится по смещению 0x14 среди прочих GPIOC_* регистров, и в нём нас интересует 13-й бит, а верхнюю половину менять не следует.
Что ж, отладчик всё ещё остановлен где-то посреди оригинальной прошивки, которая мигает светодиодом. Значит, вывод GPIO уже правильно настроен и, наверное, можно помигать светодиодом в ручном режиме, записывая правильные значения в регистр. Итак, сейчас светодиод не горит, а в регистре лежит
Как видно, 13-й бит выставлен в 1, а старшие биты, которые не следует менять — все нули. Попробуем переключить: 13-й бит выставим в 0, а все остальные и так нули:
Хоба, зажглось!
А теперь потухло! Значит, 0 — светодиод горит, 1 — не горит. Здесь я неявно подразумеваю, что на 32-битной платформе тип
#моргаем_светодиодом
Теперь нужно найти документ, описывающий, какие регистры управляют GPIO на данном конкретном микроконтроллере, и куда они отображены в памяти. В моём случае это RM0368 — 847-страничный reference manual, но целиком читать мы его не будем: интересующие нас сейчас части — это 2.3 Memory map, из которой мы узнаём, что регистры, управляющие GPIOC, отображаются, начиная с адреса 0x40020800, и дальше рекомендуется свериться с разделом 8.4.11 для подробностей. Поскольку я не знаком с использованием GPIO на этом микроконтроллере, от краткого содержания промотаю немного назад, на начало раздела 8.4 GPIO registers. Отсюда мы узнаём, что регистр GPIOC_ODR (output data register) находится по смещению 0x14 среди прочих GPIOC_* регистров, и в нём нас интересует 13-й бит, а верхнюю половину менять не следует.
Что ж, отладчик всё ещё остановлен где-то посреди оригинальной прошивки, которая мигает светодиодом. Значит, вывод GPIO уже правильно настроен и, наверное, можно помигать светодиодом в ручном режиме, записывая правильные значения в регистр. Итак, сейчас светодиод не горит, а в регистре лежит
>>> x/x 0x40020814
0x40020814: 0x00002000
Как видно, 13-й бит выставлен в 1, а старшие биты, которые не следует менять — все нули. Попробуем переключить: 13-й бит выставим в 0, а все остальные и так нули:
>>> set *(int*)0x40020814 = 0
Хоба, зажглось!
>>> set *(int*)0x40020814 = 0x2000
А теперь потухло! Значит, 0 — светодиод горит, 1 — не горит. Здесь я неявно подразумеваю, что на 32-битной платформе тип
int будет считаться 32-битным, а short, наверное, 16-битным. К сожалению, никакого бинарника с содержательной отладочной информацией в gdb сейчас не загружено, поэтому он не знает об изысках вроде uint16_t.#моргаем_светодиодом
Ну и, наконец, напишем минимальную программу. Для её компиляции я буду использовать clang (пока что программа будет только на ассемблере, но пусть так...) и ld.lld для линковки. Во-первых, я лучше знаком с этим тулчейном, а главное, если у вас на компьютере есть clang, значит почти наверняка он умеет работать с кодом для Armv7 и кучи других популярных наборов инструкций, а в случае gcc всё-таки понадобилась бы armv7-специфичная сборка. Конечно, в реальном проекте всё равно будет нужна libc и прочие библиотеки и заголовочные файлы, поэтому в любом случае придётся искать правильный полноценный тулчейн вместе с sysroot, но у нас же сейчас цель — написать свой велосипед полностью с нуля, поэтому можно обойтись первым попавшимся clang-ом и эквивалентами binutils из LLVM для просмотра, что же у нас в итоге лежит внутри ELF-файла.
Для начала прикинем, что придётся писать в ассемблерном файле помимо самих инструкций. Для этого попросим
Часть этого кода можно с натяжкой считать "лишними деталями". Плюс то, что начинается с
#моргаем_светодиодом
Для начала прикинем, что придётся писать в ассемблерном файле помимо самих инструкций. Для этого попросим
clang сгенерировать ассемблерный код (-S) для Armv7-M (-target armv7m-unknown-none — так называемый target triple, например, на обычном десктопе он бы был x86_64-linux-gnu) и напечатать его в терминал (-o -) для максимально простой программы на C — настолько простой, что clang прочитает её прямо со стандартного ввода (-x c -). Более того, я эту программу напишу прямо в командной строке через перенаправление ввода (<<< ...):$ clang -target armv7m-unknown-none -S -x c - -o - <<< 'int f(void) { return 42; }'
clang: warning: unknown platform, assuming -mfloat-abi=soft
clang: warning: unknown platform, assuming -mfloat-abi=soft
clang: warning: unknown platform, assuming -mfloat-abi=soft
.text
.syntax unified
.eabi_attribute 67, "2.09" @ Tag_conformance
.cpu cortex-m3
.eabi_attribute 6, 10 @ Tag_CPU_arch
.eabi_attribute 7, 77 @ Tag_CPU_arch_profile
.eabi_attribute 8, 0 @ Tag_ARM_ISA_use
.eabi_attribute 9, 2 @ Tag_THUMB_ISA_use
.eabi_attribute 34, 1 @ Tag_CPU_unaligned_access
.eabi_attribute 17, 1 @ Tag_ABI_PCS_GOT_use
.eabi_attribute 20, 1 @ Tag_ABI_FP_denormal
.eabi_attribute 21, 0 @ Tag_ABI_FP_exceptions
.eabi_attribute 23, 3 @ Tag_ABI_FP_number_model
.eabi_attribute 24, 1 @ Tag_ABI_align_needed
.eabi_attribute 25, 1 @ Tag_ABI_align_preserved
.eabi_attribute 38, 1 @ Tag_ABI_FP_16bit_format
.eabi_attribute 18, 4 @ Tag_ABI_PCS_wchar_t
.eabi_attribute 26, 2 @ Tag_ABI_enum_size
.eabi_attribute 14, 0 @ Tag_ABI_PCS_R9_use
.file "-"
.globl f @ -- Begin function f
.p2align 2
.type f,%function
.code 16 @ @f
.thumb_func
f:
.fnstart
@ %bb.0:
movs r0, #42
bx lr
.Lfunc_end0:
.size f, .Lfunc_end0-f
.cantunwind
.fnend
@ -- End function
.ident "Ubuntu clang version 19.1.1 (1ubuntu1)"
.section ".note.GNU-stack","",%progbits
.addrsigЧасть этого кода можно с натяжкой считать "лишними деталями". Плюс то, что начинается с
@ — расставленные компилятором комментарии.#моргаем_светодиодом
Изначально я предполагал, что получу код, который можно загрузить сразу после сброса, и всё заработает, но похоже, что нужно читать подробности по Reset and clock control, а это уже отдельная тема. Поэтому пока напишу лишь про загрузку кода "на ходу" в уже проинициализированный контроллер. Как оказалось, для этого в gdb есть удобная команда load, которой можно передать ELF-файл, и gdb его вгрузит по указанным в файле виртуальным адресам.
Теперь нужно этот ELF как-то получить. А что для этого нужно? Содержимое секций — это в моём случае машинный код, тут, вроде, всё понятно: создаю entry.S, пишу туда ассемблерные инструкции, дописываю немного служебных директив вроде
Судя по тому, что указано в таблицах в разделе 2.4 Boot configuration, в каком бы мы режиме ни загрузились, embedded SRAM всегда начинается с адреса 0x20000000 — строчка
мы сначала превращаем
... и #моргаем_светодиодом ! Фух, на сегодня всё :)
Теперь нужно этот ELF как-то получить. А что для этого нужно? Содержимое секций — это в моём случае машинный код, тут, вроде, всё понятно: создаю entry.S, пишу туда ассемблерные инструкции, дописываю немного служебных директив вроде
.text и .p2align, и готово. В объектном файле, сгенерированном компилятором (здесь — entry.o) есть несколько секций: таблица строк с именем .strtab, секция с описанием релокаций, которые линкер должен применить к коду (но это ещё одна отдельная тема, а в финальном бинарнике в нашем случае их остаться всё равно не должно) и т. д. Также есть секция .text, содержащая машинный код, который "ну, допустим, по нулевому адресу". А откуда берутся адреса загрузки в финальном файле? Тут на помощь придёт другой тип "исходника": linker script. В первом приближении это описание, как взять пачку входных файлов и перекомпоновать секции оттуда, чтобы получить выходной файл. Поскольку мне хочется сделать максимально простой пример, скрипт получился такой:SECTIONS {
. = 0x20000000;
.text : { *(.text) }
/DISCARD/ : { INPUT_SECTION_FLAGS(SHF_ALLOC) *(*) }
}Судя по тому, что указано в таблицах в разделе 2.4 Boot configuration, в каком бы мы режиме ни загрузились, embedded SRAM всегда начинается с адреса 0x20000000 — строчка
. = 0x20000000 присваивает это значение "счетчику" текущего адреса выходного файла. Далее говорится собрать секции с именем .text из всех входных файлов (*(.text)) и сформировать из этого секцию .text результата. А всё остальное, что могло бы быть загружено в память (в отличие от таблицы строк и прочей "справочной" информации) — выкинуть (discard): плата, конечно, недорогая, так что градус паранойи поменьше, да и при записи во всякие однократно записываемые "фьюзы", наверняка, есть какая-нибудь защита от дурака, но всё-таки лучше не грузить что попало куда попало, если это не планировалось. При помощи командclang -target thumbv7m-unknown-none -c entry.S
ld.lld -o image.elf -T image.lds entry.o
мы сначала превращаем
entry.S в бинарник entry.o, а потом при помощи image.lds, приведённого выше, формируем image.elf. Собираем, загружаем с помощью gdb:>>> load image.elf
Loading section .text, size 0x32 lma 0x20000000
Start address 0x00000000, load size 50
Transfer rate: 400 bits in <1 sec, 50 bytes/write.
>>> set $pc=0x20000000
>>> continue
... и #моргаем_светодиодом ! Фух, на сегодня всё :)