Заметки бессистемного программиста
13 subscribers
70 photos
112 links
Download Telegram
Уже не первый год, как во многих графических интерфейсах есть ночной цветовой режим — в смысле не тёмная тема, а светлая, но с пониженной цветовой температурой. Есть такая функция и в 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: если что, я ни в коем случае не призываю подобное повторять. Тем более там, где разъёмы на десятки ампер действительно необходимы. Просто конкретно мне конкретно в этом случае такой способ показался допустимым (колпачка в комплекте не было, надеть термоусадку и не усадить её при пайке было проблематично, а сооружать комок хитро подсунутой изоленты не хотелось).
Захотел купить более удобное жало для паяльника, решил загуглить, что именно подразумевается под обозначением "900M-T-I" (запрос содержал конкретно эту строку без других слов), а гугл мне предложил помочь с математическими задачками (что бы под этим ни подразумевалось). Такой формат "быстрых ответов" я увидел впервые.
Из примера на cppreference:

class ThreadsafeCounter
{
mutable std::mutex m; // The "M&M rule": mutable and mutex go together
int data = 0;
// ...
}
🔥3
На Алиэкспрессе каталог каждого продавца может быть сгруппирован по категориям, и вот сразу у нескольких продавцов радиодеталей обнаружилась загадочная категория с названием "повел". В принципе, иногда даже "технические" аббревиатуры хорошо звучат, например, "мосфет", а уж "маркетинговый" термин тем более будет красивым. Хоть я и не планировал покупать загадочные по́велы, но интересно же, что так назвали — наверное, какую-нибудь мудрёную и современную микросхему, подающую питание на что-нибудь ещё более заковыристое и высокотехнологичное — ну, там, от power и electronics...

Почему-то в этой категории были просто какие-то светодиоды. Ну, думаю, может особый тип для каких-нибудь умных светодиодных лент... А потом до меня дошло: светодиод — это же грамматическая форма свинца: lead / led / LED. Ну а в названии категории потерялись точки над "ё".
2
Загадка: что может содержать файл с именем proton-pack.c в ядре Linux?
Ответ: противодействие уязвимостям типа Spectre 😀
Пояснение в комментарии в "шапке": "If there's something strange in your neighbourhood, who you gonna call?"
В копилку научного юмора и искусства в целом: Dance Your PhD 2025. Если обладатели главного приза кажутся слишком шумными, то вот, например, красивый спокойный клип про лазерное охлаждение атомов до сверхнизких температур что бы это ни значило :)
Обновил телефон (с Самсунга обратно на Моторолу и прекрасно себя чувствую), обновил ОС на телефоне. После обновления до Android 15 захотел поставить из F-Droid уже упоминавшийся на этом канале SatStat, да не тут то было: оказалось, что в Android 15 слишком старые приложения поставить нельзя (документация). Ну, то есть, не совсем так: во-первых, судя по всё той же странице документации, гайки начали закручивать ещё в Android 14. Во-вторых, поставить приложение всё же можно — с компьютера.

Итак, что я наблюдаю в F-Droid: плашка "Это приложение было разработано для старой версии Андроид..." (что пока ещё ничего не означает: у самого приложения F-Droid, через которое я смотрю каталог, в собственной карточке такая же плашка и ничего) и сказано, что совместимых с моим устройством версий нет. Кнопки "установить", соответственно, тоже. Проверяю предположение: из F-Droid на телефоне перекидываю через любой мессенджер ссылку на приложение себе же в браузер на компьютере. Из списка версий выбираю какую-нибудь посвежее, скачиваю APK-файл и пробую установить через ADB (добавлены переводы строк, чтобы не так сильно растягивать текст по горизонтали):
$ adb install /tmp/com.vonglasow.michael.satstat_30600.apk
Performing Streamed Install
adb: failed to install /tmp/com.vonglasow.michael.satstat_30600.apk:
Failure [INSTALL_FAILED_DEPRECATED_SDK_VERSION: App package must target
at least SDK version 24, but found 23]

Ага, то самое изменение из Android 15. По совету из всё той же документации добавляю опцию --bypass-low-target-sdk-block:
$ adb install --bypass-low-target-sdk-block /tmp/com.vonglasow.michael.satstat_30600.apk
Performing Streamed Install
Success

Работает! Едва ли я бы хотел таким способом ставить что-то из Google Play — хотя бы потому, что не совсем понятно, откуда брать "эталонный" APK, который точно-точно без дополнительных троянов, да и при наличии альтернатив, возможно, стоит поискать какое-нибудь менее заброшенное приложение. Но к программам из репозитория* F-Droid лично у меня доверия больше, а правильный APK можно без лишних плясок с бубном скачать по удобной ссылке. В общем, опция удобная полезная, но использовать нужно на свой страх и риск: здесь никто за вас новому apk подпись не проверит.

* Почему важно уточнение именно о "репозитории" F-Droid — потому что существуют сторонние репозитории в том же формате, их так же можно добавлять в официальный клиент F-Droid, но это будут уже совершенно независимые, не контролируемые и не проверяемые авторами F-Droid источники ПО.

#android
Кстати, похоже, что в Андроиде есть разрешения, которые в принципе доступны и без рутования телефона, но выдаваться должны через ADB, а не обычный пользовательский интерфейс (тут, впрочем, нужно оговориться, что я совсем не Андроид-разработчик, и, например, не вполне уверен, что можно заявлять о неком стандартном интерфейсе управления правами, хотя в документации я и вижу достаточно чёткое разделение на normal, runtime a.k.a. dangerous permissions и ряд других типов).

Например, в репозитории F-Droid есть приложение LogFox, которое при первом запуске просит выдать права через ADB:
adb shell pm grant com.f0x1d.logfox android.permission.READ_LOGS

После чего при начале записи логов система переспросит уже на экране устройства, хотим ли мы позволить приложению читать "глобальные" логи. Если разрешили, можно видеть, например, такое:
libPowerHal: [setClusterFreq] system_server: sysfs_freq set cpu freq: 1750000 -1 1850000 -1

Или вот, например, SaverTuner: при первом запуске ждёт, пока ему не выдадут разрешение:
adb shell pm grant s1m.savertuner android.permission.WRITE_SECURE_SETTINGS

После чего позволяет выбирать один из нескольких профилей экономии, а внутри профиля — переключать большое количество параметров из серии "разрешить анимации", "отложить полное резервное копирование" и т.д.

Лично я для себя пока не нашёл "полезный в быту" пермишн и приложение, его использующее, но сам факт существования таких разрешений кажется мне полезным знанием.

#android
Первого апреля я уже хвастался колпачком для разъёма XT60, отлитым из термоклея. И вот недавно история получила не менее первоапрельское продолжение... Как я уже говорил, столь суровый разъём требовался, чтобы подключить одноплатник, питающийся от пяти вольт, без значительного падения напряжения на разъёме, потому что со штекерами-бочонками я к тому моменту уже намучился. Но кроме одноплатника линии 5 и 12 вольт шли ещё и на "метеостанцию" на ESPHome, роутер и ethernet-свич, поэтому мешанина проводов была та ещё. Я захотел хоть как-то это смотать и убрать в обычную электромонтажную распределительную коробку, а чтобы упростить себе перекоммутацию в стеснённых условиях, повесить эту коробку на край столешницы при помощи примерно таких крючков с Алиэкспресса.

Начитавшись отзывов, мол, "крючки хорошие, но мне пришли без липучек", осмотрел то, что прислали мне. С грустью отметил, что у меня тоже просто крючки, и стал лепить на двусторонний скотч. А они, заразы, ещё и как из вредности к скотчу не липнут... Ладно, к распределительной коробке с горем пополам прилепил на термоклей (который, разумеется, тоже отваливался, но я упорный, я через край чисто механически прихватил, а потом долго подрезал ножом, чтобы нигде не клинило...). Но не буду же я к столу ответную часть тоже на термоклей лепить... Тут я вспомнил про сантехнический скотч, решил сделать из него кольцо клеевой стороной наружу и хоть как-то уже прилепить ответную часть крючка.

Отклеил от мотка скотча небольшой участок, приложил туда крючок... а он отваливается!!! Да вы что, издеваетесь, что же это за крючки такие, что их даже чудо-скотч не берёт?!? И тут-то я пригляделся и заметил, что на скотче осталась прозрачная плёночка... 🤦‍♂️ В общем, всё нормально: честь сантехнического скотча восстановлена, а я усвоил урок, что если тебе кажется, что некая поверхность отваливается от двустороннего скотча, термоклея и прочих нормальных липучек, словно специально спроектирована, чтобы легко отваливаться от любого клея, то возможно, тебе не кажется. После снятия защитной плёнки "предустановленный" клеевой слой крючка благополучно прилип, куда следовало.