Заметки бессистемного программиста
13 subscribers
70 photos
112 links
Download Telegram
Forwarded from Типичный программист
Опишите свой любимый язык программирования, не называя его.

#обсуждение
😁2
При этом понятно, откуда строчка "nan" — это NaN (Not-a-Number), а вот почему именно он появился при переходе от '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?
Помимо StackOverflow есть целое семейство вопросо-ответных сайтов на разные тематики, называется StackExchange. Одним из таких сайтов является worldbuilding.stackexchange.com — сайт, где авторы художественных произведений спрашивают советы по более логичному обоснованию деталей сюжета.

Одним из таких вопросов был: зачем может быть полезна письменность обществу с развитой телепатией? Чтоб вы понимали, этот вопрос (а точнее самый популярный ответ на него) я с лёгкостью нашёл в поиске сайта по запросу "cache druids". В общем, вот: обсуждение друидов L2-кэша, вопросов отказоустойчивости и т.д.

P.S.: Кто-то, безусловно, уже мог видеть у меня эту ссылку, но сюда я её ещё не кидал.
Понадобилось мне запустить Thunderbird без доступа в интернет, чтобы проверить самодостаточность экспортированного mbox-файла с письмами. Наверное, можно было бы соорудить какую-нибудь изоляцию при помощи network namespace, но я решил, что чем разбираться с нюансами linux namespaces, а потом ещё и думать, как надёжно разделить профили Thunderbird "для экспорта" и "для теста", проще сразу поднять виртуальную машину. При этом хотелось работать прямо с Live DVD, чтобы найдя потом эту ВМ через пол-года, не пытаться судорожно вспомнить, а можно ли её удалить, или там что-то важное лежит: раз нет HDD, значит и ничего важного нет :) Впрочем, поскольку QEMU можно запускать прямо из терминала, то нет не то что HDD, нет даже какой-то перманентной сущности "виртуальная машина", которую нужно (или не нужно) удалять.

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 или другой оболочки, а для читаемости разбить команду на несколько строк, завершаемых символом \ (как было сделано постом выше).

Всё бы хорошо, но потом начинает хотеться временно отключить часть аргументов. Казалось бы, есть же символ #, комментирующий до конца строки. Но нет: \ не продолжит "виртуальную строку", будучи закомментированным. Можно, конечно, в какой-то точке файла написать 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-разъёме), а из репозитория можно просто поставить консольную или графическую утилиту.
Пару лет назад я заказал на Алиэкспрессе отладочную плату с STM32 с намерением "ну надо же когда-нибудь приобщиться к программированию микроконтроллеров, а то чё я как лох-то?..". И вот, наконец-то решился: буду моргать светодиодом! Впрочем, не спешите с раздражением пролистывать очередной гайд по установке Arduino IDE (или что там обычно в таких случаях ставят) — сегодня будет занимательное (надеюсь) чтение technical reference manual на микроконтроллер и документации по системе команд Armv7. Ну и практика, конечно: ассемблер, gdb, все дела...

Целью сегодняшней "лабораторки" будет написание минимального примера, который можно запустить на железе, без использования готовых библиотек и прочей "магии" — буквально, получаем управление с первых тактов работы 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 (только не одновременно двумя способами — думаю, последствия будут не ахти...), изначально в ней запускается то, что в неё прошил производитель. В моём случае это код, который... мигает светодиодом. Эксперимент завершён, расходимся.

#моргаем_светодиодом
После того, как SWD-интерфейс физически присоединён, нужно понять, как через него подключиться отладчиком к микроконтроллеру. Для работы с JTAG- и SWD-адаптерами можно использовать OpenOCD: будучи правильно сконфигурирована, эта программа подсоединится к аппаратному адаптеру и откроет сетевой порт, на котором будет ожидать подключения от GDB, реализуя протокол gdb extended-remote.

Чтобы объяснить 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 уже правильно настроен и, наверное, можно помигать светодиодом в ручном режиме, записывая правильные значения в регистр. Итак, сейчас светодиод не горит, а в регистре лежит
>>> 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, пишу туда ассемблерные инструкции, дописываю немного служебных директив вроде .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

... и #моргаем_светодиодом ! Фух, на сегодня всё :)
Совсем уже спамеры обленились!
😁3
Между прочим, 31 марта (то есть за день до 1 апреля) празднуется международный день бэкапа!
Раз уж сегодня День Дурака, похвастаюсь своими недавним достижением из рубрики "я сделяль": колпачок для разъёма XT60, отлитый из термоклея. По моему, идея повторить призматическую форму разъёма, изготовив опалубку из малярного скотча — это достаточно, ну эээ... празднично. Не то, чтобы такой разъём был необходим, чтобы запитать Orange Pi 5 (это как раз тот самый #маленький_но_гордый_сервер), но просто я уже задолбался экспериментировать со способами уменьшить падение напряжения (нет, на OPi5 всё нормально с питанием через Type-C, просто захотелось избавиться от мешанины кабелей).

Disclaimer: если что, я ни в коем случае не призываю подобное повторять. Тем более там, где разъёмы на десятки ампер действительно необходимы. Просто конкретно мне конкретно в этом случае такой способ показался допустимым (колпачка в комплекте не было, надеть термоусадку и не усадить её при пайке было проблематично, а сооружать комок хитро подсунутой изоленты не хотелось).