Десять статей серії позаду. Bare-metal STM32, Buildroot на Luckfox, GStreamer стрім, NPU розпізнавання обличчя — все це userspace код, який звертався до /dev/ttyS1, /dev/video0, до драйверів які хтось колись написав. Я туди ще не заліз, руки чухаються. Час лізти.
Місяць 4 це зоряний час kernel space. Тут немає printf, є printk. Немає malloc, є kmalloc. Помилився з вказівником — не segfault, а kernel panic і reboot. Десь чув, що embedded middle від junior відрізняється саме здатністю писати свій драйвер, а не тільки використовувати чужі. Та байдуже, що говорять, як на мене це круто забацати свій драйвер, а ще краще зрозуміти, що воно таке і з чим його їдять
◆ Замість дисклеймера
Трохи предісторії. Linux я відкрив не минулого тижня, але й не дуже так і давно починав з 16 Ubuntu і остаточно пересів з 2019 року. На Orange pi — Arch Linux ще з доковідних часів. На полицях артефакти з тих самих часів — Raspbian Buster 2019, CentOS 7 ISO, ще одна стара малинка з підписом CentOS 6 і маркером на коробці. Колись я ці системи ставив, ламав, відновлював, переставляв. Не так щоб сильно навчився, але точно вже звик і на вінду вже не хочу. Весело пригадувати, як вивчав лінукс через Cygwin UNIX-подібну незалежну оболонку та набір інструментів, що емулює роботу Linux-оточення у Windows. Навіщо? Та для мене стало сюрпризом, що нода і пайтон трохи обрізані, а С++ там меж інший і різноманітні приклади рішень усі були на linux. Але зламало мене те, що я ніяк не міг налаштувати на вінді Docker.
Користуючись нагодою, хочу нагадати, що паралельно з цією серією веду інші, зокрема про Zephyr RTOS на Pi Pico (Частина 0 вже на DOU), поки на паузі, бо думаю вона стане логічним продовженням цієї де ми теж будемо кулупати RTOS. Ну, а тим хто, як і я в захваті від малінки, апельсинів і прочих фруктів до душі може припасти П’ять малинок, стійка і DietPi: як я будую домашній IoT-сервер
◆ Ґуля (граблі набили) № 0: де я хотів додати голос
Останнє що залишилось з Місяця 3 — відкрите архітектурне питання. Хотів додати голосове керування на Luckfox: моя wake-word модель «Катя» слухає, NPU розпізнає обличчя, STM32 крутить реле. Класична embedded магія.
Сів читати даташит RV1106. Потер очі. Знову прочитав. Подивився на свій Pico Pro і ще раз на pinout. Audio I²S піни на header не виведені. Внутрішній codec є — це я бачу у специфікації чіпа. А назовні не прокинули. Можливо Pico Pro позиціонується саме як IP-камера SoC: аудіо в дизайні плати не передбачено.
⚠ Тут варто зробити уточнення яке часто плутають.
I²C виведений нормально, він є на пінах, ним я вже користувався — BMP180, MPU6050 в попередніх статтях через нього спілкувалися з STM32. Але тут потрібен I²S — це зовсім інший інтерфейс, не плутати:
| I²C | I²S | |
|---|---|---|
| Розшифровка | Inter-Integrated Circuit | Inter-IC Sound |
| Для чого | Сенсори, EEPROM, RTC, OLED | Тільки аудіо |
| Лінії | SDA, SCL (2 проводи) | BCLK, LRCK, DATA (3+ проводи) |
| Швидкість | 100 kHz — 3.4 MHz | Десятки MHz, безперервний потік |
| Призначення | Команди + дрібні дані | Стрім PCM семплів |
I²C — для датчиків, типу BMP180, MPU6050. I²S — спеціально для цифрового аудіо. Передає неперервний потік PCM семплів (наприклад 16 000 чисел за секунду = 16 kHz audio). На таких швидкостях I²C просто не вистачить bandwidth, плюс у I²S інша архітектура — continuous streaming замість transaction-based.
Може виникнути логічна думка: «а є I²C мікрофон?» Технічно — є. Але для wake-word він не підходить. I²C-мікрофон передає дані запитами (request-response). Кожен раз треба слати команду «дай мені семпл», потім приймати відповідь. Для 16 000 семплів на секунду це 16 000 транзакцій I²C — bus буде задушено, ніякого іншого I²C девайса вже не приєднати. Окрім того jitter — між семплами непостійний, що псує якість для wake-word.
Що з чим на Luckfox Pico Pro
| Інтерфейс | Виведений на header? | Для чого |
|---|---|---|
| I²C | ✓ Так | Сенсори, OLED, RTC |
| SPI | ✓ Так | Дисплеї, флеш, ADC |
| UART (1, 3, 4) | ✓ Так | STM32, GPS, debug |
| GPIO | ✓ Так | Кнопки, LED, реле |
| PWM | ✓ Так | Сервоприводи, реле |
| I²S (audio) | ✗ Ні | На цій моделі плати не виведений |
| USB audio через USB-C | Технічно так | Єдиний шлях, але конфлікт з живленням |
Залишався один варіант — USB audio dongle. Але:
- USB-C порт на Luckfox один, і він мені потрібен для живлення і RNDIS network.
- USB камера з мікрофоном тягне 200-500 mA, на межі того що Luckfox може дати з власного host port.
- Тре пересборку ядра з
SND_USB_AUDIO+ alsa-utils у Buildroot.
Ось тут тре подумати. А чи це взагалі правильна архітектура? Якщо Luckfox — це наш «мозок з очима» (NPU + камера), то навіщо туди ще й «вуха» прикручувати? Echo Dot же не на одному SoC робить — там завжди увімкнений low-power MCU слухає wake-word, а основний процесор спить.
Архітектурне рішення: wake-word йде на STM32
Чим більше думав, тим більше мені це подобалось, ну прям модульно, ух красота:
- STM32 має I²S виведений на пінах — INMP441 мікрофон і MAX98357A DAC підключаються прямо.
- DMA на STM32 пише I²S семпли в RAM без участі CPU — стабільний 16 kHz без jitter, який буде на Linux scheduler.
- TFLite Micro компілюється під STM32 F4/F7/H7. Моя 50 KB модель Катя влізе з запасом (Стаття Як я навчив нейромережу чути своє ім’я: wake word з нуля на TensorFlow вже майже в редакції ).
- Bare-metal = real-time. Від звуку до спрацювання ~50 ms predictable. На Linux 100-300 ms з jitter.
- Енергія: STM32F4 у активному режимі ~50 mA, Luckfox ~300-500 mA. Для always-on wake-word різниця в 10 разів.
- STM32 слухає Катю → детектить → шле UART trigger на Luckfox, той прокидається з камерою та NPU.
Це і є справжня heterogeneous embedded архітектура — та що використовують Amazon Echo Dot, Apple HomePod, Google Nest. Завжди увімкнений low-power MCU з wake-word моделлю. SoC прокидається тільки коли wake-word спрацював.
Як само собою виходить, що в Місяць 3 Luckfox призначено стати мозком, а STM32 буде керувати м’язом. А тепер додаємо ще й вухо на STM32. Це щось неймовірне, м’язи, які чують і реагують.
⚠ Це окрема стаття, до якої я ще повернусь у Місяці 5 (FreeRTOS на STM32 + I²S + TFLite Micro). А зараз в нас інші важливі справи, дізнатись, що всередені самого Linux. Власне колупаємо, kernel modules.
◆ Залізо: шість малинок, п’ять простих + одна з плюсиком
У тумбочці 6 штук Pi 3: 5 звичайних 3B + одна 3B+. Дістав ту що вже використовую в серії де я будую домашній IoT-сервер. Перший boot:
|
1 2 3 |
<span class="hljs-variable">$</span> ssh pi <span class="hljs-variable">$</span> cat /proc/device-tree/model<span class="hljs-type"> Raspberry</span> Pi <span class="hljs-number">3</span><span class="hljs-type"> Model</span> B<span class="hljs-type"> Rev</span> <span class="hljs-number">1.2</span> |
Чекай, я ж думав це 3B+. А це звичайний 3B без плюсика. Малінка в корпусі, а /proc/device-tree/model не робив, от і маєш. Корисна команда, щоб не розбирати корпус і дізнатись модель, запам’ятовуємо.
Чим відрізняються:
| Pi 3B | Pi 3B+ | |
|---|---|---|
| CPU | Cortex-A53 @ 1.2 GHz | Cortex-A53 @ 1.4 GHz |
| Ethernet | 100 Mbps | Gigabit (через USB 2.0) |
| WiFi | 2.4 GHz | 2.4 + 5 GHz |
Для kernel модулів — той самий BCM2837, той самий GPIO chip, той самий код. Поточну 3B пускаю в експерименти (їх багато, не шкода якщо щось зламаю в ядрі), 3B+ резервую для Місяця 5 — там буде FreeRTOS + OpenCV, додаткові 200 MHz і двосмуговий WiFi не зайві.
◆ Перший boot: чиста картка з нуля
SD картка в ній вже з образом і він мені тре. Тому розпакую свіжу SanDisk 32 GB.
Записувати буду з Ubuntu. Найпростіший шлях — офіційний інструмент Raspberry Pi Imager:
|
1 2 3 |
sudo snap install rpi-imager <span class="hljs-comment"># або</span> sudo apt install rpi-imager |
Перед тим як встромляти картку — перевіряю які диски в системі зараз:
|
1 |
lsblk |
⚠ Запам’ятай вивід. Тепер встромляєш картку — і знову
lsblk. Нова строка з розміром ~29-30 GB (32 GB номінально завжди показуються трохи меншими) — це наша SD. Назву обов’язково запам’ятай, бо якщо переплутаєш з системним диском,ddабо Imager радо затре твою Ubuntu без зайвих запитань.
Запуск Imager
Запускаю rpi-imager, тиснемо три кнопки:
- CHOOSE DEVICE → Raspberry Pi 3
- CHOOSE OS → Raspberry Pi OS (other) → Raspberry Pi OS Lite (64-bit). Чому Lite — нам не треба GUI, kernel розробка вся в терміналі. Чому 64-bit — Pi 3B це Cortex-A53, 64-bit native, майбутнє за aarch64.
- CHOOSE STORAGE → наша SanDisk. Звіряємо розмір з тим що бачив у
lsblk.
Далі — Next → EDIT SETTINGS. Це ключовий момент, тут одразу налаштовуємо все що знадобиться при першому boot, щоб потім не возитись.
General вкладка:
- Hostname:
alex-pi - Username:
alex, пароль будь-який - Configure wireless LAN: вмикаю, вписую WiFi мережу. Country code: UA. Це резервний канал на випадок якщо з ethernet щось піде не так.
- Locale: timezone Europe/Kyiv, keyboard us
Services вкладка:
- Enable SSH → ✓
- Password authentication (ключі додам потім)
SAVE → Yes → Yes. Запис триває ~5 хвилин, Imager сам перевіряє контрольну суму.
◆ Перший boot і SSH у мережі
Картку в Pi, ethernet кабель у роутер, HDMI у монітор (на всяк випадок щоб бачити що відбувається), USB клавіатура, і в останню чергу — micro USB живлення. Pi стартує одразу як з’явилось живлення.
Через ~30 секунд на моніторі: alex-pi login:. Перший boot пройшов чисто.
Тепер як знайти Pi в мережі з ноута? Підказка — nmap по підмережі шукаючи MAC префікс Raspberry Pi Foundation (B8:27:EB):
|
1 2 3 4 5 6 |
ip route | grep <span class="hljs-keyword">default</span> <span class="hljs-comment"># default via 192.168.1.1 dev enp0s25 proto dhcp metric 100</span> <span class="hljs-comment"># отже наша мережа 192.168.1.0/24</span> sudo apt install nmap sudo nmap -sn <span class="hljs-number">192.168</span>.<span class="hljs-number">1.0</span>/<span class="hljs-number">24</span> | grep -B <span class="hljs-number">2</span> -i raspberry |
Вивід:
|
1 2 3 4 5 6 |
Nmap scan report for <span class="hljs-number">192.168</span>.<span class="hljs-number">1.109</span><span class="hljs-type"> Host</span> is up (<span class="hljs-number">0.0015</span>s latency).<span class="hljs-literal"> MAC</span> Address: B8:<span class="hljs-number">27</span><span class="hljs-literal">:EB</span>:D4:<span class="hljs-number">26</span>:<span class="hljs-number">95</span> <span class="hljs-type">(Raspberry</span> Pi<span class="hljs-type"> Foundation</span>)<span class="hljs-type"> Nmap</span> scan report for <span class="hljs-number">192.168</span>.<span class="hljs-number">1.114</span><span class="hljs-type"> Host</span> is up (<span class="hljs-number">0.0016</span>s latency).<span class="hljs-literal"> MAC</span> Address: B8:<span class="hljs-number">27</span><span class="hljs-literal">:EB</span>:D4:<span class="hljs-number">26</span>:<span class="hljs-number">95</span> <span class="hljs-type">(Raspberry</span> Pi<span class="hljs-type"> Foundation</span>) |
Два IP, але MAC однаковий — це одна і та сама Pi на двох інтерфейсах. Підключаюся:
|
1 |
ssh alex<span class="hljs-variable">@192</span><span class="hljs-number">.168</span><span class="hljs-number">.1</span><span class="hljs-number">.109</span> |
На питання fingerprint — yes, пароль той що задав у Imager.
Вже на Pi перевіряю інтерфейси:
|
1 2 3 4 |
<span class="hljs-variable">$</span> ip -br addr lo <span class="hljs-literal"> UNKNOWN</span> <span class="hljs-number">127.0</span>.<span class="hljs-number">0.1</span>/<span class="hljs-number">8</span> eth0 <span class="hljs-literal"> UP</span> <span class="hljs-number">192.168</span>.<span class="hljs-number">1.109</span>/<span class="hljs-number">24</span> wlan0 <span class="hljs-literal"> UP</span> <span class="hljs-number">192.168</span>.<span class="hljs-number">1.114</span>/<span class="hljs-number">24</span> |
OK, eth0 = .109, wlan0 = .114. Зафіксую ethernet IP як основний — для kernel розробки стабільність важлива (якщо щось зламаю в мережевому стеку через невдалий модуль, wifi першим помре).
На Ubuntu додаю alias у ~/.ssh/config щоб не запам’ятовувати IP:
|
1 2 3 |
Host pi <span class="hljs-type"> HostName</span> <span class="hljs-number">192.168</span>.<span class="hljs-number">1.109</span> <span class="hljs-type"> User</span> alex |
Тепер просто ssh pi. На роутері додатково прив’язую MAC B8:27:EB:D4:26:95 до цього IP в DHCP reservation, щоб ніколи не плавав.
◆ Перші команди на Pi
Системна інформація:
|
1 2 3 |
<span class="hljs-variable">$</span> uname -a<span class="hljs-type"> Linux</span> alex-pi <span class="hljs-number">6.12</span>.<span class="hljs-number">75</span>+rpt-rpi-v8 <span class="hljs-comment">#1 SMP PREEMPT Debian 1:6.12.75-1+rpt1</span> (<span class="hljs-number">2026</span>-<span class="hljs-number">03</span>-<span class="hljs-number">11</span>) aarch64<span class="hljs-literal"> GNU</span>/Linux |
Ядро 6.12.75 від березня 2026, aarch64. Свіже.
|
1 2 3 4 5 6 7 8 9 |
<span class="hljs-meta">$</span><span class="language-bash"> <span class="hljs-built_in">cat</span> /proc/device-tree/model</span> Raspberry Pi 3 Model B Rev 1.2 <span class="hljs-meta"> $</span><span class="language-bash"> <span class="hljs-built_in">df</span> -h /</span> Filesystem Size Used Avail Use% Mounted on /dev/mmcblk0p2 29G 3.0G 24G 11% / <span class="hljs-meta"> $</span><span class="language-bash"> vcgencmd measure_temp</span> temp=51.0'C |
29 GB rootfs (Pi сама розширила до повного розміру картки при першому boot), 3 GB зайнято — місця море. 51°C у спокої — нормально для Pi 3 без радіатора.
Запускаю оновлення:
|
1 2 |
sudo apt <span class="hljs-keyword">update</span> sudo apt upgrade <span class="hljs-operator">-</span>y |
⚠ Якщо у тебе тут оновиться ядро — обов’язково
sudo rebootпісля завершення і перепідключитись по SSH. Інакшеuname -rпоказуватиме одне ядро, а/lib/modulesз headers матиме інше — і потім всі kernel модулі будуть з помилкою «Invalid module format».
◆ Ґуля № 2: де поділись raspberrypi-kernel-headers
Класичний рецепт для kernel модулів на Pi казав так:
|
1 |
sudo apt install raspberrypi-kernel-headers build-essential |
Запускаю — і отримую:
|
1 |
Error: Unable to locate <span class="hljs-keyword">package</span> raspberrypi-kernel-headers |
Хм. На свіжих Raspberry Pi OS Bookworm пакет Headers окремо більше не існує. Headers тепер пакетуються разом з ядром, в одному пакеті linux-image-rpi-v8. Ставлячи ОС з образу — ти вже маєш все що потрібно.
Raspberry Pi OS Bookworm — це офіційна операційна система для одноплатних комп’ютерів Raspberry Pi, яка базується на випуску Debian 12 «Bookworm». Вона прийшла на зміну версії Bullseye (на базі Debian 11) та принесла значні архітектурні зміни, підвищену продуктивність та повну підтримку новітніх моделей, таких як Raspberry Pi 5
Перевіряємо чи це справді так:
|
1 2 3 4 5 6 7 8 |
$ <span class="hljs-built_in">ls</span> /lib/modules/$(<span class="hljs-built_in">uname</span> -r)/build/ | <span class="hljs-built_in">head</span> <span class="hljs-built_in">arch</span> include Makefile Module.symvers scripts tools vmlinux |
Бачимо Makefile, include, scripts — все необхідне для kbuild. Чудово, ставити нічого не треба, рухаємось далі.
⚠ Урок: туторіали 2-3-річної давності можуть пропонувати команди яких уже не існує. Завжди перевіряємо
ls /lib/modules/$(uname -r)/build/— якщо там є вміст, headers вже встановлені. Якщо порожньо або битий symlink — це окрема пригода зrpi-source, але у нашому випадку обійшлось. Істина проста. Як було раніше вже не буде))
Доставляю інші утиліти для розробки (без kernel-headers, який вже не потрібен):
|
1 |
sudo apt install build-essential git bc bison <span class="hljs-attribute">flex</span> libssl-dev |
Перевіряю що компілятор і make на місці:
|
1 2 3 |
gcc <span class="hljs-comment">--version</span> make <span class="hljs-comment">--version</span> git <span class="hljs-comment">--version</span> |
◆ Ґуля № 3: Pi пропала з мережі
Між сесіями робив паузу, повернувся за ноут — ssh pi:
|
1 2 |
<span class="hljs-variable">$</span> ssh pi ssh: connect to host <span class="hljs-number">192.168</span>.<span class="hljs-number">1.109</span> port <span class="hljs-number">22</span>:<span class="hljs-type"> No</span> route to host |
«No route to host» — це не «пароль не той» і не «ssh не запущений». Це нижчий рівень: ноут просто не знає куди слати пакети. Сама Pi або вимкнена, або відключена від мережі, або змінила IP. Перед тим як панікувати — короткий чек-ліст з 4 кроків:
Крок 1: чи Pi взагалі жива
|
1 |
ping <span class="hljs-operator">-</span><span class="hljs-built_in">c</span> <span class="hljs-number">3</span> <span class="hljs-number">192.168</span>.1.109 |
Якщо «Destination Host Unreachable» — Pi нема в мережі. Якщо «100% packet loss» без unreachable — Pi є але не відповідає.
Крок 2: перевір WiFi адресу як резерв
|
1 |
ping <span class="hljs-operator">-</span><span class="hljs-built_in">c</span> <span class="hljs-number">3</span> <span class="hljs-number">192.168</span>.1.114 |
У мене дві адреси — eth0 і wlan0. Якщо одна не пінгується, інша може.
Крок 3: чи ноут в тій самій мережі
|
1 2 |
ip route | grep <span class="hljs-keyword">default</span> <span class="hljs-meta"># default via 192.168.1.1 dev enp0s25 proto dhcp metric 100</span> |
Маршрут має йти через 192.168.1.x. Якщо там інша підмережа (наприклад 192.168.0.1 або 10.x.x.x) — ноут перепідключився до іншої мережі.
Крок 4: сканування — раптом IP змінився
|
1 |
sudo nmap -sn 192.168.1.0/24 | grep -B 2 -i raspberry |
Якщо Pi знайшлась на іншому IP — отже DHCP видав їй нову адресу після перезавантаження. Тре робити DHCP reservation у роутері, але не сьогодні.
У моєму випадку: подивився на плату — червоний LED живлення не горить. Просто був вимкнений блок живлення (хтось випадково смикнув подовжувач, не питайте). Підключив назад — Pi за 30 секунд знову у мережі, ssh pi працює як треба. У мене завжди так)), починаю з самого важкого, а тре з самого простого і очевидного, але ж тоді це буде інша пригода.
⚠ Урок сісадміна: перш ніж лізти у Wireshark або переустановлювати OS — глянь на саму плату. Чи горять світлодіоди? Чи в розетці кабель живлення? У 80% випадків відповідь там.
Користуючись нагодою. Ось класичний жарт про сисадміна та пошук несправностей: Сидить сисадмін, глибоко задумавшись, крутить крісло туди-сюди, нахиляється, заглядає під системний блок, знову крутиться. До нього підходить колега:— Щось серйозне сталося? Сервер «ліг»? База даних не відповідає?Сисадмін, не перестаючи крутити крісло:— Та ні, розумієш… У кріслі щось поскрипує, коли кручуся. Вже пів години не можу зрозуміти, чи це крісло змастити треба, чи в мене десь кулер на сервері так шумить.
◆ Частина 2: пишемо hello.ko
Все готове, рухаємось до самого цікавого. Створюю робочу структуру і одразу git репо щоб коміти не загубились:
|
1 2 3 |
<span class="hljs-built_in">mkdir</span> -p ~/kernel-modules/01-hello <span class="hljs-built_in">cd</span> ~/kernel-modules/01-hello git init |
hello.c — перший модуль
Файл hello.c — найпростіший kernel модуль з параметрами. Init друкує привіт, exit прощається, параметри name і times передаються при insmod:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
<span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string"><linux/init.h></span> <span class="hljs-comment">/* __init, __exit */</span></span> <span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string"><linux/module.h></span> <span class="hljs-comment">/* всі макроси для модуля */</span></span> <span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string"><linux/kernel.h></span> <span class="hljs-comment">/* pr_info, pr_err */</span></span> <span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string"><linux/moduleparam.h></span> <span class="hljs-comment">/* module_param */</span></span> <span class="hljs-comment">/* ─── параметри модуля ─── */</span> <span class="hljs-type">static</span> <span class="hljs-type">char</span> *name = <span class="hljs-string">"World"</span>; <span class="hljs-type">static</span> <span class="hljs-type">int</span> times = <span class="hljs-number">1</span>; <span class="hljs-built_in">module_param</span>(name, charp, <span class="hljs-number">0644</span>); <span class="hljs-built_in">MODULE_PARM_DESC</span>(name, <span class="hljs-string">"Кому передаємо привіт"</span>); <span class="hljs-built_in">module_param</span>(times, <span class="hljs-type">int</span>, <span class="hljs-number">0644</span>); <span class="hljs-built_in">MODULE_PARM_DESC</span>(times, <span class="hljs-string">"Скільки разів привітатись"</span>); <span class="hljs-comment">/* ─── викликається при insmod ─── */</span> <span class="hljs-function"><span class="hljs-type">static</span> <span class="hljs-type">int</span> __init <span class="hljs-title">hello_init</span><span class="hljs-params">(<span class="hljs-type">void</span>)</span> </span>{ <span class="hljs-type">int</span> i; <span class="hljs-built_in">pr_info</span>(<span class="hljs-string">"hello: модуль завантажено\n"</span>); <span class="hljs-keyword">for</span> (i = <span class="hljs-number">0</span>; i < times; i++) { <span class="hljs-built_in">pr_info</span>(<span class="hljs-string">"hello: Привіт, %s! (раз %d з %d)\n"</span>, name, i + <span class="hljs-number">1</span>, times); } <span class="hljs-comment">/* 0 = успіх. Будь-яке інше — помилка */</span> <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>; } <span class="hljs-comment">/* ─── викликається при rmmod ─── */</span> <span class="hljs-function"><span class="hljs-type">static</span> <span class="hljs-type">void</span> __exit <span class="hljs-title">hello_exit</span><span class="hljs-params">(<span class="hljs-type">void</span>)</span> </span>{ <span class="hljs-built_in">pr_info</span>(<span class="hljs-string">"hello: до побачення, %s! Модуль вивантажено\n"</span>, name); } <span class="hljs-built_in">module_init</span>(hello_init); <span class="hljs-built_in">module_exit</span>(hello_exit); <span class="hljs-built_in">MODULE_LICENSE</span>(<span class="hljs-string">"GPL"</span>); <span class="hljs-built_in">MODULE_AUTHOR</span>(<span class="hljs-string">"Alex"</span>); <span class="hljs-built_in">MODULE_DESCRIPTION</span>(<span class="hljs-string">"Перший kernel module: hello world з параметрами"</span>); <span class="hljs-built_in">MODULE_VERSION</span>(<span class="hljs-string">"0.1"</span>); |
Кілька цікавих деталей:
__initі__exit— це не для красоти, це секції в ELF файлі. Код з__initпісля успішного завантаження модуля звільняється з пам’яті ядра. Тобтоhello_initіснує тільки до моменту коли він повернув 0 — потім його пам’ять вивільняється. Мертвий код у kernel space не тримаємо.pr_info— обгортка надprintk(KERN_INFO ...). Має багато братів:pr_err,pr_warn,pr_debug. У серйозному коді додають#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt— тоді кожне повідомлення автоматично починається з імені модуля.module_param— третій параметр0644це права доступу до файлу/sys/module/hello/parameters/.0644= читати можна всім, записує тільки root. Якщо0000— параметр не буде видно у sysfs.MODULE_LICENSE("GPL")— обов’язково. Без цього ядро видасть «taints kernel» з warning’ом про missing key.
Makefile
⚠ Дуже важливо: відступи перед командами це таб, а НЕ пробіли. Якщо копіюєш через буфер обміну, перевір
cat -A Makefile— табуляція показується як^I. Якщо там пробіли — отримаєш*** missing separator. Stop.У свій час дуже з тим намучився, тому постійно на цьому наголошую.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<span class="hljs-comment"># Makefile для kernel модуля — використовує kbuild систему ядра</span> obj-m += hello.o KDIR := /lib/modules/<span class="hljs-variable">$(<span class="hljs-built_in">shell</span> uname -r)</span>/build PWD := <span class="hljs-variable">$(<span class="hljs-built_in">shell</span> pwd)</span> <span class="hljs-section">all:</span> <span class="hljs-variable">$(MAKE)</span> -C <span class="hljs-variable">$(KDIR)</span> M=<span class="hljs-variable">$(PWD)</span> modules <span class="hljs-section">clean:</span> <span class="hljs-variable">$(MAKE)</span> -C <span class="hljs-variable">$(KDIR)</span> M=<span class="hljs-variable">$(PWD)</span> clean <span class="hljs-section">reload:</span> -sudo rmmod hello 2>/dev/null sudo insmod hello.ko dmesg | tail -20 |
Як це працює: ми не компілюємо самі. Ми просимо kbuild систему ядра ($(MAKE) -C $(KDIR)) зайти в директорію headers і скомпілювати наш файл з усіма правильними прапорами, які використовуються для збірки ядра. M=$(PWD) каже kbuild «зовнішній модуль лежить отут». Це ключовий момент: всі kernel модулі компілюються через kbuild.
Збираємо
|
1 2 3 4 5 6 7 |
$ make make -C /lib/modules/6.18.33+rpt-rpi-v8/build M=/home/alex/kernel-modules/01-hello modules make[1]: Entering directory <span class="hljs-string">'/usr/src/linux-headers-6.18.33+rpt-rpi-v8'</span> CC [M] hello.o MODPOST Module.symvers CC [M] hello.mod.o LD [M] hello.ko |
⚠ Стривайте — ядро тут 6.18.33, а коли я перевіряв уперше було 6.12.75. Виявляється
apt upgradeтихенько оновив ядро до 6.18 під час встановлення git і build-essential. Headers оновились разом з ним. Якби міжsudo apt upgradeіmakeя не зробив reboot —unameпоказував би старе 6.12, а build linkувався б до 6.18 headers, і модуль не завантажився б. Це той самий випадок «Invalid module format» який лякає всіх початківців, ну чи не всіх.
Перевіряємо що вийшло:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<span class="hljs-variable">$</span> ls -la -rw-rw-r-- <span class="hljs-number">1</span> alex alex <span class="hljs-number">1994</span><span class="hljs-type"> Jun</span> <span class="hljs-number">10</span> <span class="hljs-number">08</span>:<span class="hljs-number">02</span> hello.c -rw-rw-r-- <span class="hljs-number">1</span> alex alex <span class="hljs-number">8112</span><span class="hljs-type"> Jun</span> <span class="hljs-number">10</span> <span class="hljs-number">08</span>:<span class="hljs-number">31</span> hello.ko -rw-rw-r-- <span class="hljs-number">1</span> alex alex <span class="hljs-number">761</span><span class="hljs-type"> Jun</span> <span class="hljs-number">10</span> <span class="hljs-number">08</span>:<span class="hljs-number">02</span><span class="hljs-type"> Makefile</span> <span class="hljs-keyword">...</span> <span class="hljs-variable">$</span> modinfo hello.ko filename: /home/alex/kernel-modules/<span class="hljs-number">01</span>-hello/hello.ko version: <span class="hljs-number">0.1</span> description: Перший kernel module: hello world з параметрами author: <span class="hljs-type"> Alex</span> license: <span class="hljs-literal"> GPL</span> name: hello vermagic: <span class="hljs-number">6.18</span>.<span class="hljs-number">33</span>+rpt-rpi-v8<span class="hljs-literal"> SMP</span> preempt mod_unload modversions aarch64 parm: name:Кому передаємо привіт (charp) parm: times:Скільки разів привітатись (int) |
vermagic — це той самий «штамп сумісності» який ядро перевіряє при завантаженні модуля. Якщо твій vermagic не співпадає з ядром що працює зараз — модуль не вантажиться.
Завантажуємо у ядро
|
1 2 3 4 5 6 7 8 |
$ sudo insmod hello.ko $ dmesg | tail <span class="hljs-number">-5</span> [<span class="hljs-meta">2271.782480</span>] hello: loading <span class="hljs-keyword">out</span>-of-tree module taints kernel. [<span class="hljs-meta">2271.784431</span>] hello: модуль завантажено [<span class="hljs-meta">2271.784448</span>] hello: Привіт, World! (раз <span class="hljs-number">1</span> з <span class="hljs-number">1</span>) $ lsmod | grep hello hello <span class="hljs-number">12288</span> <span class="hljs-number">0</span> |
Працює! Розбираємо:
taints kernel— це нормально і очікувано. Tainted kernel означає що в ядро завантажено код не з основного дерева (out-of-tree). Кожен такий модуль робить це, включаючи Nvidia, VirtualBox, VMware. Це попередження для мейнтейнерів: «якщо потім буде kernel panic, не плачтесь офіційним розробникам».hello 12288 0— ім’я, розмір у байтах (12K — мінімум для будь-якого kernel модуля на ARM64), reference count (0 = ніхто не використовує).
Вивантажуємо:
|
1 2 3 4 5 |
<span class="hljs-variable">$</span> sudo rmmod hello <span class="hljs-variable">$</span> dmesg | tail -<span class="hljs-number">3</span> [<span class="hljs-number">2271.784431</span>] hello: модуль завантажено [<span class="hljs-number">2271.784448</span>] hello: Привіт,<span class="hljs-type"> World</span>! (раз <span class="hljs-number">1</span> з <span class="hljs-number">1</span>) [<span class="hljs-number">2314.989473</span>] hello: до побачення,<span class="hljs-type"> World</span>! Модуль вивантажено |
Тепер з параметрами
|
1 2 3 4 5 6 7 8 |
<span class="hljs-variable">$</span> sudo insmod hello.ko name<span class="hljs-type">=Alex</span> times=<span class="hljs-number">5</span> <span class="hljs-variable">$</span> dmesg | tail -<span class="hljs-number">10</span> [<span class="hljs-number">2324.628422</span>] hello: модуль завантажено [<span class="hljs-number">2324.628446</span>] hello: Привіт,<span class="hljs-type"> Alex</span>! (раз <span class="hljs-number">1</span> з <span class="hljs-number">5</span>) [<span class="hljs-number">2324.628458</span>] hello: Привіт,<span class="hljs-type"> Alex</span>! (раз <span class="hljs-number">2</span> з <span class="hljs-number">5</span>) [<span class="hljs-number">2324.628466</span>] hello: Привіт,<span class="hljs-type"> Alex</span>! (раз <span class="hljs-number">3</span> з <span class="hljs-number">5</span>) [<span class="hljs-number">2324.628474</span>] hello: Привіт,<span class="hljs-type"> Alex</span>! (раз <span class="hljs-number">4</span> з <span class="hljs-number">5</span>) [<span class="hljs-number">2324.628482</span>] hello: Привіт,<span class="hljs-type"> Alex</span>! (раз <span class="hljs-number">5</span> з <span class="hljs-number">5</span>) |
П’ять привітів як замовляли. Тепер найцікавіший трюк — параметри видно через sysfs і можна змінювати на льоту:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<span class="hljs-meta">$</span><span class="language-bash"> sudo insmod hello.ko name=Alex <span class="hljs-built_in">times</span>=3</span> <span class="hljs-meta">$</span><span class="language-bash"> <span class="hljs-built_in">cat</span> /sys/module/hello/parameters/name</span> Alex <span class="hljs-meta">$</span><span class="language-bash"> <span class="hljs-built_in">cat</span> /sys/module/hello/parameters/times</span> 3 <span class="hljs-meta"> $</span><span class="language-bash"> <span class="hljs-built_in">echo</span> <span class="hljs-string">"Stm32"</span> | sudo <span class="hljs-built_in">tee</span> /sys/module/hello/parameters/name</span> Stm32 <span class="hljs-meta"> $</span><span class="language-bash"> sudo rmmod hello</span> <span class="hljs-meta">$</span><span class="language-bash"> dmesg | <span class="hljs-built_in">tail</span> -3</span> [2335.933427] hello: Привіт, Alex! (раз 3 з 3) [2336.082876] hello: до побачення, Stm32 ! Модуль вивантажено |
⚠ Бачите ту дивну розривку «до побачення, Stm32 ! Модуль вивантажено»? Це не баг ядра — це
echoдодало символ переносу рядка в значення параметра. Параметрnameтепер містить"Stm32\n", а коли модуль друкує його з форматом «до побачення, %s! Модуль вивантажено», новий рядок ріже повідомлення посередині. Якщо тре чистіше —printf "Stm32" | sudo teeбез переносу. Дрібниця, але наглядно: userspace tooling працює непомітно, і це може ламати твій kernel код.
Поки це робив згадав один бородатий анекдот, але мене він веселить до цих пір.
Зі слів юзера:
— Комп не працював. Прийшов адмін, підніс руки до неба, пробурмотів якісь заклинання, прокрутив мій стілець 3 рази навколо своєї осі, штурхнув комп’ютер — і сталося диво — він запрацював!!!
Зі слів адміна:
— Дзвінок. Біжу до юзера. Цей бовдур так крутився на новому стільці, що кабель живлення намотався на ніжку і висмикнувся з блока. Побачивши це, підводжу руки до неба, виголошую кілька триповерхових матюків, розмотую кабель, заштовхую комп ногою подалі під стіл і вмикаю.
◆ Експерименти: коли все йде не так
Тепер давайте навмисно ламати. Бо знайти баг в чужому коді легко, а в своєму найскладніше — і навчитись передбачати проблеми можна тільки борячись з багами. Писати код легко, важко вмовити його працювати стабільно.
Експеримент 1: відмова від завантаження через -EINVAL
Що буде якщо init поверне не 0? Дописуємо в hello_init:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
pr_err(<span class="hljs-string">"hello: відмовляюсь завантажуватись, тестую помилку\n"</span>); <span class="hljs-keyword">return</span> -EINVAL; <span class="hljs-variable">$ make</span> <span class="hljs-variable">$ sudo insmod hello</span>.ko insmod: <span class="hljs-built_in">ERROR</span>: could not insert module hello.ko: Invalid parameters <span class="hljs-variable">$ echo </span><span class="hljs-string">"exit code: $?"</span> <span class="hljs-keyword">exit</span> code: <span class="hljs-number">1</span> <span class="hljs-variable">$ dmesg </span>| tail -<span class="hljs-number">10</span> [<span class="hljs-number">24360.911041</span>] hello: модуль завантажено [<span class="hljs-number">24360.911064</span>] hello: Привіт, World! (раз <span class="hljs-number">1</span> з <span class="hljs-number">1</span>) [<span class="hljs-number">24360.911074</span>] hello: відмовляюсь завантажуватись, тестую помилку <span class="hljs-variable">$ lsmod </span>| grep hello (нічого — модуль НЕ завантажений) |
Що сталось:
- init функція повертає
int, де 0 = успіх, мінус errno = помилка. Це contract ядра. -EINVAL= −22.insmodпереклав це в стандартне повідомлення «Invalid parameters».- Хоча
pr_infoвивів привіти іpr_errвивело помилку — модуль не залишився в системі. Ядро відкотило ініціалізацію. - Реальний приклад: USB драйвер не знайшов потрібний chip → повертає
-ENODEV. Не сумісне залізо →-ENXIO. Параметр модуля недопустимий →-EINVAL.
Експеримент 2: rate limiting та ring buffer
А що якщо просто навалити купу повідомлень? Запускаю times=10000:
|
1 2 3 |
<span class="hljs-meta">$</span><span class="language-bash"> sudo insmod hello.ko name=Test <span class="hljs-built_in">times</span>=10000</span> <span class="hljs-meta">$</span><span class="language-bash"> dmesg | grep <span class="hljs-string">"hello: Привіт"</span> | <span class="hljs-built_in">wc</span> -l</span> 2045 |
Стоп. Просив 10000, отримав 2045. Куди поділись 8000 повідомлень? Збільшую буфер запиту:
|
1 2 |
<span class="hljs-meta">$</span><span class="language-bash"> dmesg --buffer-size=10485760 | grep <span class="hljs-string">"hello: Привіт"</span> | <span class="hljs-built_in">wc</span> -l</span> 1987 |
Все одно ~2000. Значить це не обмеження dmesg, це обмеження самого kernel ring buffer. Дивимось перший рядок який залишився:
|
1 2 |
<span class="hljs-meta">$</span><span class="language-bash"> dmesg --buffer-size=10485760 | grep <span class="hljs-string">"hello: Привіт"</span> | <span class="hljs-built_in">head</span> -1</span> [24379.679135] hello: Привіт, Test! (раз 8016 з 10000) |
Перші 8015 повідомлень буквально витиснуті новішими. Kernel ring buffer — це circular buffer фіксованого розміру (часто 256K-1M, задається при компіляції ядра через CONFIG_LOG_BUF_SHIFT). Коли він повний — старі повідомлення затираються.
- Цикл виконався весь — побачили «(раз 10000 з 10000)» в кінці.
- Але історія обрізана — лише ~2000 останніх повідомлень доступні.
⚠ Урок:
pr_infoв тісному циклі — це антипаттерн. У реальних драйверах для гарячого коду використовуютьpr_info_ratelimited()(явно обмежує частоту) абоdev_dbg()(виводиться тільки якщо ядро в debug режимі). Або просто не друкують у hot path взагалі.
Експеримент 3: справжній kernel oops
Тепер найкраще. Робимо навмисний NULL pointer dereference в init. Це безпечно: ядро спіймає помилку, вб’є процес insmod, але система продовжить працювати. Я це робив десятки разів.
Додаю параметр crash_test:
|
1 2 3 4 5 6 7 8 9 10 |
<span class="hljs-keyword">static</span> <span class="hljs-keyword">int</span> crash_test = <span class="hljs-number">0</span>; module_param(crash_test, <span class="hljs-keyword">int</span>, <span class="hljs-number">0644</span>); MODULE_PARM_DESC(crash_test, <span class="hljs-string">"Якщо 1 — навмисно ламаємось у init"</span>); <span class="hljs-comment">/* в hello_init, перед return 0: */</span> <span class="hljs-keyword">if</span> (crash_test) { <span class="hljs-keyword">int</span> *bad_pointer = <span class="hljs-literal">NULL</span>; pr_warn(<span class="hljs-string">"hello: зараз буде боляче — навмисний NULL deref\n"</span>); *bad_pointer = <span class="hljs-number">42</span>; <span class="hljs-comment">// BOOM</span> } |
Спочатку — без crash, переконатись що нічого не зламали:
|
1 2 3 4 5 6 7 |
<span class="hljs-variable">$</span> make <span class="hljs-variable">$</span> sudo insmod hello.ko name<span class="hljs-type">=Safe</span> <span class="hljs-variable">$</span> sudo rmmod hello <span class="hljs-variable">$</span> dmesg | tail -<span class="hljs-number">3</span> [<span class="hljs-number">24477.510478</span>] hello: модуль завантажено [<span class="hljs-number">24477.510500</span>] hello: Привіт,<span class="hljs-type"> Safe</span>! (раз <span class="hljs-number">1</span> з <span class="hljs-number">1</span>) [<span class="hljs-number">24477.643819</span>] hello: до побачення,<span class="hljs-type"> Safe</span>! Модуль вивантажено |
Працює як раніше. Тепер з crash_test=1:
|
1 2 |
<span class="hljs-meta">$</span><span class="language-bash"> sudo insmod hello.ko name=Boom crash_test=1</span> <span class="hljs-meta">$</span><span class="language-bash"> dmesg | <span class="hljs-built_in">tail</span> -50</span> |
І ось воно, шикарне:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
[<span class="hljs-number">24497.385684</span>] hello: модуль завантажено [<span class="hljs-number">24497.385714</span>] hello: Привіт,<span class="hljs-type"> Boom</span>! (раз <span class="hljs-number">1</span> з <span class="hljs-number">1</span>) [<span class="hljs-number">24497.385733</span>] hello: зараз буде боляче — навмисний<span class="hljs-literal"> NULL</span> deref [<span class="hljs-number">24497.385763</span>]<span class="hljs-type"> Unable</span> to handle kernel<span class="hljs-literal"> NULL</span> pointer dereference at virtual address <span class="hljs-number">0000000000000000</span> [<span class="hljs-number">24497.394196</span>]<span class="hljs-type"> Mem</span> abort info: [<span class="hljs-number">24497.396997</span>] <span class="hljs-literal"> ESR</span> = <span class="hljs-number">0</span>x0000000096000045 [<span class="hljs-number">24497.400755</span>] <span class="hljs-literal"> EC</span> = <span class="hljs-number">0</span>x25:<span class="hljs-literal"> DABT</span> (current<span class="hljs-literal"> EL</span>),<span class="hljs-literal"> IL</span> = <span class="hljs-number">32</span> bits [<span class="hljs-number">24497.412356</span>] <span class="hljs-literal"> FSC</span> = <span class="hljs-number">0</span>x05: level <span class="hljs-number">1</span> translation fault [<span class="hljs-number">24497.417305</span>]<span class="hljs-type"> Data</span> abort info: [<span class="hljs-number">24497.420195</span>] <span class="hljs-literal"> ISV</span> = <span class="hljs-number">0</span>,<span class="hljs-literal"> ISS</span> = <span class="hljs-number">0</span>x00000045,<span class="hljs-literal"> ISS2</span> = <span class="hljs-number">0</span>x00000000 [<span class="hljs-number">24497.425759</span>] <span class="hljs-literal"> CM</span> = <span class="hljs-number">0</span>,<span class="hljs-type"> WnR</span> = <span class="hljs-number">1</span>,<span class="hljs-type"> TnD</span> = <span class="hljs-number">0</span>,<span class="hljs-type"> TagAccess</span> = <span class="hljs-number">0</span> [<span class="hljs-number">24497.451571</span>]<span class="hljs-type"> Internal</span> error:<span class="hljs-type"> Oops</span>: <span class="hljs-number">0000000096000045</span> [<span class="hljs-comment">#1] SMP</span> [<span class="hljs-number">24497.457280</span>]<span class="hljs-type"> Modules</span> linked in: hello(O+) rfcomm cmac <span class="hljs-keyword">...</span> [last unloaded: hello(O)] [<span class="hljs-number">24497.524318</span>]<span class="hljs-literal"> CPU</span>: <span class="hljs-number">1</span><span class="hljs-literal"> UID</span>: <span class="hljs-number">0</span><span class="hljs-literal"> PID</span>: <span class="hljs-number">2409</span><span class="hljs-type"> Comm</span>: insmod <span class="hljs-type"> Tainted</span>: G C O <span class="hljs-number">6.18</span>.<span class="hljs-number">33</span>+rpt-rpi-v8 <span class="hljs-comment">#1 PREEMPT</span> [<span class="hljs-number">24497.540962</span>]<span class="hljs-type"> Hardware</span> name:<span class="hljs-type"> Raspberry</span> Pi <span class="hljs-number">3</span><span class="hljs-type"> Model</span> B<span class="hljs-type"> Rev</span> <span class="hljs-number">1.2</span> <span class="hljs-literal">(DT</span>) [<span class="hljs-number">24497.553910</span>] pc : hello_init+<span class="hljs-number">0</span>x78/<span class="hljs-number">0</span>xff8 [hello] [<span class="hljs-number">24497.558403</span>] lr : hello_init+<span class="hljs-number">0</span>x70/<span class="hljs-number">0</span>xff8 [hello] [<span class="hljs-number">24497.631250</span>] x2 : <span class="hljs-number">0000000000000000</span> x1 : <span class="hljs-number">000000000000002</span>a x0 : <span class="hljs-number">0000000000000000</span> [<span class="hljs-number">24497.638475</span>]<span class="hljs-type"> Call</span> trace: [<span class="hljs-number">24497.640942</span>] hello_init+<span class="hljs-number">0</span>x78/<span class="hljs-number">0</span>xff8 [hello] (P) [<span class="hljs-number">24497.645434</span>] do_one_initcall+<span class="hljs-number">0</span>x60/<span class="hljs-number">0</span>x2a0 [<span class="hljs-number">24497.649309</span>] do_init_module+<span class="hljs-number">0</span>x5c/<span class="hljs-number">0</span>x268 [<span class="hljs-number">24497.653096</span>] load_module+<span class="hljs-number">0</span>x197c/<span class="hljs-number">0</span>x1fb8 [<span class="hljs-number">24497.656885</span>] init_module_from_file+<span class="hljs-number">0</span>x90/<span class="hljs-number">0</span>xe0 [<span class="hljs-number">24497.661201</span>] __arm64_sys_finit_module+<span class="hljs-number">0</span>x1c4/<span class="hljs-number">0</span>x340 [<span class="hljs-number">24497.665957</span>] invoke_syscall+<span class="hljs-number">0</span>x4c/<span class="hljs-number">0</span>x100 [<span class="hljs-number">24497.669746</span>] el0_svc_common.constprop.<span class="hljs-number">0</span>+<span class="hljs-number">0</span>x48/<span class="hljs-number">0</span>xf0 [<span class="hljs-number">24497.674502</span>] do_el0_svc+<span class="hljs-number">0</span>x24/<span class="hljs-number">0</span>x38 [<span class="hljs-number">24497.677849</span>] el0_svc+<span class="hljs-number">0</span>x38/<span class="hljs-number">0</span>x120 [<span class="hljs-number">24497.681020</span>] el0t_64_sync_handler+<span class="hljs-number">0</span>xa0/<span class="hljs-number">0</span>xe8 [<span class="hljs-number">24497.685249</span>] el0t_64_sync+<span class="hljs-number">0</span>x198/<span class="hljs-number">0</span>x1a0 [<span class="hljs-number">24497.695119</span>] ---[ end trace <span class="hljs-number">0000000000000000</span> ]--- |
Це справжній kernel forensics артефакт — exactly те що embedded інженер бачить коли драйвер багнувся в продакшні. Уміння читати stack trace — окрема навичка. Швидкий розбір зверху вниз:
Unable to handle kernel NULL pointer dereference at virtual address 0x0— ARM64 MMU спіймало запис у адресу 0. Це найважливіший рядок, з нього починаємо читати будь-який oops.ESR = 0x96000045,FSC = level 1 translation fault,WnR = 1— це деталі: переклад віртуальної адреси в фізичну впав на верхньому рівні page table, операція була запис (WnR=1, не читання). Тобто ми писали в неіснуючу пам’ять.Modules linked in: hello(O+)— наш модуль зі статусомO(Out-of-tree) і+(init in inconsistent state).Hardware name: Raspberry Pi 3 Model B Rev 1.2— куди корисніше за просто «Linux» 🙂pc : hello_init+0x78 [hello]— Program Counter в момент крашу.+0x78від початку нашої функції. Якщо мати символи з налагоджувальною інформацією, можна знайти точний рядок в коді.x1 : 000000000000002a— регістрx1містить значення0x2a= 42 (саме його ми хотіли записати в NULL!).x0 : 0— нашbad_pointer.- Call trace знизу вгору — як ми сюди дістались: userspace викликав syscall (
el0_svc), ядро викликало__arm64_sys_finit_module(це syscall завантаження модуля), потімload_module→do_init_module→do_one_initcall→ і нарешті нашhello_init+0x78— БУМ.
⚠ Зверни увагу на «last unloaded: hello(O)» — це наш попередній чистий
rmmodпісля Safe тесту. Ядро пам’ятає недавно вивантажені модулі, бо їх символи можуть фігурувати в oops’ах якщо одразу післяrmmod. Корисна функція для debug’у.
Невеличкий рікошет: одна навичка — два шляхи
Той oops тригернув таракана в голові. Якщо в kernel модулі можна писати в довільну пам’ять (наприклад через bug — buffer overflow у драйвері), теоретично можна переписати функцію в kernel symbol table, підмінити syscall у sys_call_table, викликати потрібний код з повними kernel-привілеями. Можливо це саме те, що називається kernel exploitation і privilege escalation через kernel bug.
Тобто та сама базова навичка — написати kernel module — це одночасно фундамент для embedded driver development та перший крок у kernel security. Один і той самий insmod, два різні шляхи з нього. Хочу показати один невинний експеримент який ілюструє це наочно.
Робимо модуль який знаходить current task struct (себе самого, процес insmod) і друкує його UID. Потім намагається змінити свій UID на 0 через kernel API. Якщо це працює — побачимо що kernel code справді має повний доступ до credentials будь-якого процесу. Якщо не працює — побачимо як сучасне ядро себе захищає.
⚠ Це НЕ exploit. Цей модуль завантажується лише через
sudo insmod— тобто ми вже root перед запуском. Демонстрація лише ілюструє що означає «kernel space має повний доступ». Не використовується жодна CVE, жодного memory corruption, жодного обходу захисту. Просто два публічні виклики kernel API які кожен embedded інженер мусить знати.
Створив директорію 02-creds-demo, написав модуль, який в init робить три речі: читає current_cred(), виводить UID/EUID, потім викликає prepare_kernel_cred(NULL) і commit_creds() щоб отримати root credentials.
Перший запуск:
|
1 2 |
$ sudo insmod creds_demo.ko insmod: ERROR: could <span class="hljs-keyword">not</span> <span class="hljs-keyword">insert</span> <span class="hljs-keyword">module</span> creds_demo.ko: Cannot <span class="hljs-keyword">allocate</span> memory |
Cannot allocate memory? Подивимось dmesg:
|
1 2 3 4 5 6 7 8 |
[<span class="hljs-meta">25836.342661</span>] x2 : <span class="hljs-number">0000000000000000</span> x1 : ffffff8002e48000 x0 : <span class="hljs-number">0000000000000000</span> [<span class="hljs-meta">25836.345127</span>] Call trace: [<span class="hljs-meta">25836.349797</span>] prepare_kernel_cred+<span class="hljs-number">0x2a8</span>/<span class="hljs-number">0x300</span> (P) [<span class="hljs-meta">25836.354816</span>] creds_demo_init+<span class="hljs-number">0x90</span>/<span class="hljs-number">0xff8</span> [creds_demo] [<span class="hljs-meta">25836.358692</span>] do_one_initcall+<span class="hljs-number">0x60</span>/<span class="hljs-number">0x2a0</span> [<span class="hljs-meta">25836.362479</span>] do_init_module+<span class="hljs-number">0x5c</span>/<span class="hljs-number">0x268</span> [<span class="hljs-meta">25836.366268</span>] load_module+<span class="hljs-number">0x197c</span>/<span class="hljs-number">0x1fb8</span> [<span class="hljs-meta">25836.403085</span>] creds_demo: не вдалось prepare_kernel_cred |
⚠ Ого.
prepare_kernel_cred(NULL)повернув NULL — функція пройшла далеко (+0x2a8/0x300, тобто майже кінець реалізації) і явно вирішила відмовити. Це не баг — це захист. У Linux 5.18 (2022 рік)prepare_kernel_cred(NULL)було навмисно зламано, бо саме цей виклик був класичним інструментом kernel rootkit’ів — повертав чисті root credentials. Спільнота зважила на те що це більше шкодить ніж допомагає, і додала перевірку. Моє ядро 6.18.33 успадкувало цей захист.
Виправлення для legitimate use case — передавати task struct реального процесу замість NULL:
|
1 2 3 4 5 |
<span class="hljs-comment">/* було: */</span> new_cred = prepare_kernel_cred(<span class="hljs-literal">NULL</span>); <span class="hljs-comment">/* стало (додаємо #include <linux/sched/task.h>): */</span> new_cred = prepare_kernel_cred(&init_task); |
init_task — це PID 1, він завжди root. Тобто ми просимо ядро не «дай root з повітря», а «дай мені копію credentials реального процесу init». Це legitimate use, який використовується багатьма kernel threads. Запускаємо знову:
|
1 2 3 4 5 6 7 8 9 10 11 12 |
<span class="hljs-variable">$</span> sudo insmod creds_demo.ko <span class="hljs-variable">$</span> dmesg | tail -<span class="hljs-number">10</span> creds_demo: =====<span class="hljs-literal"> STAGE</span> <span class="hljs-number">1</span>: хто запустив insmod ===== creds_demo:<span class="hljs-literal"> PID</span>=<span class="hljs-number">2917</span>, name=insmod creds_demo:<span class="hljs-literal"> UID</span>=<span class="hljs-number">0</span>,<span class="hljs-literal"> EUID</span>=<span class="hljs-number">0</span> (це твій реальний sudo user) creds_demo: =====<span class="hljs-literal"> STAGE</span> <span class="hljs-number">2</span>: змінюємо<span class="hljs-literal"> UID</span> процесу на <span class="hljs-number">0</span> ===== creds_demo: тепер<span class="hljs-literal"> UID</span>=<span class="hljs-number">0</span>,<span class="hljs-literal"> EUID</span>=<span class="hljs-number">0</span> creds_demo: =====<span class="hljs-literal"> STAGE</span> <span class="hljs-number">3</span>: що це означало ===== creds_demo: процес insmod <span class="hljs-literal">(PID</span> <span class="hljs-number">2917</span>) тепер має<span class="hljs-literal"> UID</span> <span class="hljs-number">0</span> creds_demo: але це не exploit — sudo вже дав нам root, creds_demo: ми просто продемонстрували що kernel code creds_demo: має повний доступ до task credentials |
Працює. Це і є ілюстрація: моя 50-рядкова функція з обчисленнями над struct cred та полями task_struct має ту саму вагу, що й код будь-якого реального драйвера USB чи мережі. Жодного «security» рівня всередині ядра немає — є тільки контракти між модулями і ядром. Якщо твій код запустився всередині ядра — він всемогутній.
З цього випливають defensive lessons для embedded інженера: Module signing (CONFIG_MODULE_SIG_FORCE) щоб тільки підписані модулі могли завантажуватись. Kernel lockdown mode щоб обмежити навіть root. Secure Boot щоб обмежити що взагалі може стартонути. Для серйозного embedded продукту відкритий root + можливість insmod = catastrophe — ніби ти комусь дав незаповнений підписаний бланк з печаткою на своє їм’я.
Тут зупиняюсь — це окрема глибока тема, яку я люблю і вивчаю, але це не для цієї статті та і взагалі не для DOU. Для тих хто хоче далі: MITRE ATT&CK T1547.006 (Kernel Modules and Extensions), документація Linux kernel hardening, репозиторій linux-hardened. На DOU я колись писав огляд книги Packt про malware development — теж дотичне.
Це фінальна ілюстрація для розділу: одна навичка insmod hello.ko — і два світи. Один пише /dev/hcsr04 для робота, інший вивчає як цей сам механізм еволюціонує проти зловживань. Обидва шляхи варто знати.
◆ Що зробили і що далі
У цій статті ми розглянули фундамент для Місяця 4:
- Виявили що audio на Luckfox Pico Pro архітектурно не варто (I²S не виведений на header, Linux scheduler нестабільний для real-time). Wake-word йде на STM32 — це окрема стаття можливо для Місяця 5.
- Записали свіжу Raspberry Pi OS Lite 64-bit, ядро 6.12.75 (потім apt upgrade тихенько оновив до 6.18.33).
- Знайшли Pi у мережі через nmap, підключились по SSH, зафіксували IP, додали alias.
- Виявили що 3B+ виявилась 3B Rev 1.2 — без розбирання корпусу не побачиш.
- Headers вже стоять разом з ядром, окремого пакета
raspberrypi-kernel-headersбільше не існує. - Зібрали чек-ліст «Pi пропала з мережі» з 4 кроків.
- Написали і завантажили перший
hello.koз module params та sysfs runtime зміною. - Експериментували:
-EINVALвідмова, ring buffer переповнення, повний живий kernel oops з ARM64 stack trace.
Непогано розважились, як на мене
В наступній статті — переходимо від «hello world» до реальної взаємодії з залізом. Пишемо kernel module який блимає LED через GPIO ядра. Перша різниця між sysfs способом (echo > /sys/class/gpio/export, як ми звикли з userspace) і власним driver’ом. Зробимо те ж саме — але як справжні драйверщики.
До зустрічі — Hello, GPIO! Підпишись і клацни зірочку, у мене ще багато чого є написано в шухляді, окрім цієї серії.