В ОС Линукс есть средства для проверки и наблюдения за работой кода в режиме ядра. В частности, есть Детектор Утечек Памяти в Ядре [1], который отслеживает операции выделения и освобождения памяти.
Для поиска и отладки утечек памяти в своём модуле ядра нужно сделать 3 шага:
- включить kmemleak;
- делаем тестовые прогоны и получаем отчёт об утечках. Если утечек нет, то завершаем поиск;
- находим место утечки и исправляем. Переходим к п. 2.
Включаем kmemleak
kmemleak, как очевидно из названия, это средство ядра Линукс-а по поиску утечек памяти. В обычной сборке ядра это средство отключено (так как оно подъедает много ресурсов) и прежде всего его нужно включить. Включается это средство при сборке ядра и на выходе получается ядро Линукса, которое подходит для отладки, и не очень хорошо подходит для всего остального. Для разрешения этих противоречий можно либо жонглировать ядрами, либо (очень хороший вариант) сделать отдельную виртуальную машину, на которой будет крутиться «отладочное» ядро.
И в нашем случае (отдельная виртуальная машина) задача включить kmemleak немного разрастается:
- установить ПО виртуализации и ОС;
- настроить новое отладочное ядро;
- собрать отладочное ядро.
Поехали!
Установить ПО виртуализации и ОС
Для виртуализации можно использовать различные решения. В моём случае хорошим вариантом является qemu. Про установку qemu можно найти статьи в интернете, например [2].
Установку ОС лучше всего начинать с установки обычной, не отладочной, версии. Такой шаг позволяет получить настройки ОС, которые потребуются позже. Обычная установка не представляет особых сложностей: скачиваем образ (в нашем случае — Debian) и с него ставим операционную систему. Для удобства работы с ОС рекомендуется поставить midnight commander и ssh сервер (пакеты mc и ssh).
Далее нужно загрузить исходный код ядра. Делается это через команду:
sudo apt install linux-source
Отмечу также, что также можно собрать ядро немного другой версии (если вдруг потребуется)
посмотреть версии ядра:
sudo apt search ^linux-source
загрузить явно указанную версию ядра (например 5.10):
sudo apt install linux-source-5.10
В результате в папке /usr/src появится файл linux-source-5.10.tar.xz или аналогичный. Содержимое архива распаковываем в удобную папку, например в ~/kernel/
Кроме исходных файлов ядра потребуется конфигурационный файл, в котором расписаны все настройки для сборки ядра. Создавать такой файл с нуля занятие совсем неблагодарное, поэтому берём готовый (который получился при установке обычной версии): в папке /boot находим файл config-zzz.zzz и копируем его в папку с исходным кодом (например, ~/kernel/linux-source-5.10) под именем .config . Имя, в принципе, можно выбрать любое, однако учтите, что это «любое» имя тогда придётся вводить вручную. Что же касается имени .config — то оно ставится по умолчанию.
Кроме исходных файлов потребуется ещё ряд других пакетов (это с учётом того, что gcc ставится вместе с исходным кодом ядра):
build-essential libncurses-dev libelf-dev libssl-dev rsync dwarves
Всё, исходный код готов к настройке.
Настроить новое отладочное ядро
Отладочное ядро, которое отслеживает утечки памяти, по сути отличается одним параметром: CONFIG_DEBUG_KMEMLEAK — его нужно активировать (установить в «y»). В принципе, это всё можно сделать вручную, однако ошибиться в этом случае можно на раз-два. Поэтому будем делать через настроечную утилиту. Настроечная утилита также поможет изменить другие настройки, которые скорее всего потребуется поменять.
В корневой папке исходного кода (например, ~/kernel/linux-source-5.10) делаем сборку и запуск утилиты:
make menuconfig
Будет собрана и запущена утилита настройки ядра:

Отметим, что сразу же загрузился файл .config. Если же хочется поработать с другим настроечным файлом, то есть кнопочки Load и Save.
Идём в раздел Kernel hacking:

Далее в раздел Memory Debugging:

В разделе Memory Debugging выбираем Kernel memory leak detector:

Выбранный раздел активируется и покажет несколько вложенных пунктов:

Здесь можно оставить настройки без изменений. Таких настроек хватает, чтобы сохранить порядка 20 записей об утечках памяти. Если нужно больше, то можно увеличить размер пула.
ЕСЛИ ОШИБКА и вы не видите соответствующие пункты меню, например, как на следующем скрине:

то скорее всего вы пользуетесь неподходящим конфигурационным файлом. Попробуйте взять правильный файл, как описано выше.
Ещё немного настроек: если вы не собираетесь завязываться на проверку собранного ядра (а вы не собираетесь, т.к. это отладочное ядро для внутреннего применения), то лучше запретить соответствующий функционал. Заходим в Cryptographic API:

Идём в раздел сертификатов Certificates for signature checking:

и имя файла с сертификатом uefi-pem вычищаем до пустой строки:

После всех настроек сохраняем результат.
Также стоит отметить, что сборка ядра помимо данной инструкции может потребовать дополнительных действий, т.к. кодовая база обновляется и что-то может поменяться.
Собрать отладочное ядро
Запускаем сборку ядра:
make deb-pkg
И часов через несколько соберутся несколько файлов: само ядро, заголовочные файлы, отладочные символы и др. Все файлы собраны как .deb пакеты и устанавливаются как обычные пакеты. Поэтому просто устанавливаем новое ядро и заголовочные файлы:
sudo dpkg -i linux-image-zzzz.zzzz_zzzz.zzzz.deb
sudo dpkg -i linux-headers-zzzz.zzzz_zzzz.zzzz.deb
Установка нового ядра сразу же пропишет его в загрузчик grub. Нужно только перегрузиться и вуаля! — появился файл /sys/kernel/debug/kmemleak. А если не появился, то проверяем по шагам описанную выше последовательность действий.
Тестовый прогон
Для тестового прогона нам потребуется сделать несколько действий (которые лучше всего загнать в скрипт):
- собрать модуль в новом ядре с новыми заголовочными файлами;
- получаем ассемблерный код модуля:
objdump -DS yourmodule.ko > ~/disasm.txt - очищаем состояние kmemleak:
echo clear | sudo tee /sys/kernel/debug/kmemleak - запускаем модуль
- получаем адреса, по которым загрузился модуль:
cd /sys/module/yourmodule/sections
sudo cat .text .data .bss >> ~/sections.txt - тестируем функционал модуля, в котором предполагаем утечку памяти
- сканируем память на предмет утечек:
echo scan | sudo tee /sys/kernel/debug/kmemleak - сохраняем дамп скана:
sudo cat /sys/kernel/debug/kmemleak > ~/memleak.txt
Пройдёмся по этапам подробнее и уточним некоторые детали.
Модуль
Модуль необходимо пересобрать в текущем (отладочном) ядре, так как у вас это уже будет другое ядро, собранное со специфичными настройками.
В дальнейшем в процессе отладки утечек памяти вам потребуются две вещи:
- ассемблерный код модуля (его мы получаем через команду objdump);
- адреса секций, по которым модуль загрузился (содержимое файлов .text и др.). Адреса необходимо получать «текущие», на уже запущенном модуле. Секции могут быть разные, соответственно их имена нужно смотреть в зависимости от вашего модуля.
И ассемблерный код и адреса секций сохраняются в файлы. В командах для сохранения используется домашняя папка и, конечно, можно использовать другую удобную.
Дамп kmemleak
Kmemleak взаимодействует с пользователем через файл (очевидно с именем kmemleak), в который можно записывать команды (через tee, чтобы работать из-под пользователя) и вычитывать результат. По умолчанию средство отслеживания утечек включено сразу же, оно постоянно мониторит ядро и модули, записывает информацию в свой пул.
Чтобы отслеживать утечки памяти именно нашего модуля используем две команды:
clear — очистить пул от предыдущей информации — обычно делается перед запуском модуля. Как вариант, очистку можно делать перед предполагаемой утечкой;
scan — просканировать текущее состояние и записать в пул — делается после выполнения «плохих» действий с утечками.
Для получения текущего состояния пула вычитываем этот файл.
Итак, собрали всю информацию по утечкам, приступаем к разбору.
Находим место утечки
Разбор кода, который ведёт к утечке делается в ручном режиме, т.к. хотя ядро и может что-то знать про отладочную информацию модулей, kmemleak об этом явно не в курсе.
Из дампа получаем набор записей. Что-то наподобие следующего:
unreferenced object 0xffff941b880c5a00 (size 256):
comm "ln", pid 25594, jiffies 4297528255 (age 69.936s)
hex dump (first 32 bytes):
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
08 00 08 00 31 00 00 00 00 00 00 00 00 00 00 00 ....1...........
backtrace:
[<0000000084843d48>] 0xffffffffc0d3c090
[<00000000a65c3060>] 0xffffffffc0d3c276
[<00000000c7ed9d90>] 0xffffffffc0d3d653
[<000000006b97b67e>] 0xffffffffc0d3dd08
[<00000000201065fd>] vfs_symlink+0x122/0x1d0
[<000000005201bd7c>] do_symlinkat+0x11d/0x130
[<0000000012388a50>] do_syscall_64+0x30/0x80
[<0000000090c08e48>] entry_SYSCALL_64_after_hwframe+0x62/0xc7
Что мы здесь видим? Есть некоторый объект по адресу 0xffff941b880c5a00, размером 256 байт. Объект появился в результате команды «ln». Также приведены первые 32 байта этого объекта, возможно вы сможете быстро понять, что это за объект.
Ещё приведён стэк вызовов, которые привели к появлению «объекта». В данном случае мы видим вызовы в ядре do_syscall_64 и т.п. И, очевидно, верхние четыре записи относятся к нашему коду и нам придётся в них разобраться. В целом, всё не так сложно, но немного неудобно. Разберём на примере адреса 0xffffffffc0d3d653. Итак:
Берём адреса секций (из файла sections.txt). В моём случае это:
.text 0xffffffffc0d3a000
.data 0xffffffffc0d44240
.bss 0xffffffffc0d44800
Здесь мы видим, что адрес 0xffffffffc0d3d653 относится к первой секции (больше 0xffffffffc0d3a000 и меньше 0xffffffffc0d44240). В таком случае адрес секции будет смещением и если мы вычтем из адреса инструкции смещение, то получим адрес в ассемблерном коде:
0xffffffffc0d3d653 — 0xffffffffc0d3a000 = 0x3653
Находим наш код (из файла disasm.txt):
for (i = 0; i < fba; ++i) {
3636: 4d 85 ed test %r13,%r13
3639: 74 59 je 3694 <tagfs_get_fileino_by_name+0xd4>
363b: 45 31 f6 xor %r14d,%r14d
struct qstr res;
int cmp;
if (GetFileInfo(sr, i, mask, &res, NULL)) {
363e: 45 31 c0 xor %r8d,%r8d
3641: 48 8d 4d b8 lea -0x48(%rbp),%rcx
3645: 48 89 da mov %rbx,%rdx
3648: 4c 89 f6 mov %r14,%rsi
364b: 4c 89 e7 mov %r12,%rdi
364e: e8 00 00 00 00 callq 3653 <tagfs_get_fileino_by_name+0x93>
3653: 85 c0 test %eax,%eax
3655: 75 34 jne 368b <tagfs_get_fileino_by_name+0xcb>
continue;
}
cmp = compare_qstr(res, name);
3657: 48 8b 55 a8 mov -0x58(%rbp),%rdx
365b: 48 8b 7d b8 mov -0x48(%rbp),%rdi
Из кода получаем команду по адресу 0x3653: 3653: 85 c0 test %eax,%eax
Здесь может быть два вариант: либо сама команда выделяет память (не наш случай), либо в предыдущей команде есть вложенный вызов кода, который выделяет память. Второй вариант — наш случай и предыдущая команда:
callq 3653 <tagfs_get_fileino_by_name+0x93>
Команда является вызовом функции. Какой функции? И здесь листинг кода нам подсказывает:
GetFileInfo(sr, i, mask, &res, NULL)
Аналогично поступаем и с другими адресами (к счастью, обычно их бывает небольшое количество). В результате получаем цепочку кода, которая вызвала выделение памяти с утечкой.
Ну и далее всё стандартно: ide — bugfix — compile — etc. В общем, удачи!
Кислов Евгений.
Внешние ссылки: