В ОС Линукс есть средства для проверки и наблюдения за работой кода в режиме ядра. В частности, есть Детектор Утечек Памяти в Ядре [1], который отслеживает операции выделения и освобождения памяти.

Для поиска и отладки утечек памяти в своём модуле ядра делаем 3 пункта:

  1. включить kmemleak;
  2. делаем тестовые прогоны и получаем отчёт об утечках. Если утечек нет, то завершаем поиск;
  3. находим место утечки и исправляем. Переходим к п. 2.

Включаем kmemleak

kmemleak, как очевидно из названия, это средство ядра Линукс-а по поиску утечек памяти. В обычной сборке ядра это средство отключено (так как оно подъедает много ресурсов) и прежде всего его нужно включить. Включается это средство при сборке ядра и на выходе получается ядро Линукса, которое подходит для отладки, и не очень хорошо подходит для всего остального. Для разрешения этих противоречий можно либо жонглировать ядрами, либо (очень хороший вариант) сделать отдельную виртуальную машину, на которой будет крутиться «отладочное» ядро.

И в нашем случае (отдельная виртуальная машина) задача включить kmemleak немного разрастается:

  1. установить ПО виртуализации и ОС;
  2. настроить новое отладочное ядро;
  3. собрать отладочное ядро.

Поехали!

Установить ПО виртуализации и ОС

Для виртуализации можно использовать различные решения. В моём случае хорошим вариантом является qemu. Про установку qemu можно найти статьи в интернете, например [2].

С установкой ОС (в нашем случае — Debian) всё ещё проще: скачиваем образ и с него ставим операционную систему. Для удобства работы с ОС рекомендуется поставить midnight commander и ssh сервер (пакеты mc и ssh).

фывафываПо результатам этого отслеживания выводится содержимое, которое было выделено но не освобождено.

Для работы с такой диагностикой её прежде всего нужно «включить», т.к. в обычной сборке Линукс-а такой функционал выключен/отсутствует. Причина такого поведения очевидна: диагностика замедляет работу ОС.

содержит средства для поиска утечек памяти в модулях ядра. Ядро линукса позволяет делать отладку утечек памяти. Подробнее можно почитать здесь: Kernel Memory Leak Detector

Там всё сводится к тому, что нужно выставить флаг CONFIG_DEBUG_KMEMLEAK в параметрах Kernel hacking и всё случится: ядро начнёт каждые 10 минут чекать все операции выделения/освобождения памяти и выводить всё в файл /sys/kernel/debug/kmemleak вместе с колстэком.

Ок, хорошая функция. Однако нужно ядро с этим флагом. Если файловую систему ещё можно было разрабатывать/пробовать на текущем ядре, то с выставлением флагов всё это грустнее, поэтому ставим qemu.

Сначала установим qemu

Как обычно, инструкции в интернете в целом верны, однако всё не так. (И так как эта статья тоже инструкция в интернете, то она тоже неверна).

В целом, можно посмотреть на статью и сделать там согласно пункту debian 11.

Далее скачиваем debian как образ и создаём новую виртуальную машину, ставим туда ОС. Особых проблем там не видно.

для удобства работы ставим midnight-commander (пакет mc) и ssh сервер (пакет ssh).

Теперь пересобираем ядро:

Базовая статья по сборке ядра: здесь.

Сначала ставим исходники ядра:

посмотреть возможные ядра: sudo apt search ^linux-source

установить исходники (то, которое дефалтовое) ядра: sudo apt install linux-source

либо можно указать явро: sudo apt install linux-source-5.10

при установке исходников сразу ставится gcc и всё остальное.

ставим пакет build-essential и libncurses-dev. Ещё libelf-dev и libssl-dev также rsync. Ещё нужно поставить dwarves.

Итак, в результате в папке /usr/src появится файл linux-source-5.10.tar.xz

распаковываем архив в папку у пользователя. например в ~/kernel/

В папку с исходниками (~/kernel/linux-source-xxx/) копируем файл config-xxxx из папки /boot под именем .config . Это такой «хак»: если вы собираете ядро, которое близко к существующему, то лучше брать уже существующие настройки за основу, нежели делать всё с нуля. Соответственно, если в папке /boot несколько config-xxx файлов (что для только что установленной виртуалки совсем неактуально), то выбираем наиболее близкий по версии вариант. прим.: имя файла можно выбрать любое, только учтите, что это имя тогда придётся вводить вручную. Что же касается имени .config — то оно дефалтовое.

далее в папке с исходниками ~/kernel/linux-source-xxx запускаем конфигуратор ядра: make menuconfig

И тут есть такая штука: если вы создали конфигурацию с нуля или неправильную, то там вообще может не быть пункта про memoryleak. Так что лучше используем готовый конфигурационный файл за основу.

Выведется текстовая консолька (есть скринчик), в которой переходим в Kernel hacking и Memory debugging и далее «включаем» пункт Kernel memory leak detector.

Ещё зайдём в Cryptographic API , там длинный список всякого, прокрутим его до конца и зайдём в Certificates for signature checking. и там имя файла с сертификатом uefi-pem вычищаем до пустой строки.

Всё сохраняем.

Запускаем сборку ядра: make deb-pkg

И часов через несколько соберутся несколько файлов.

потом делаем dpkg -i linux-image…..

также устанавливаем linux-headers

В общем, установка пакета собирает загрузочный образ и прописывает grub для его загрузки.

Перезагружаемся. Получаем файл /sys/kernel/debug/kmemleak

Теперь собственно отладка:

Делается всё через скрипт:

echo clear | sudo tee /sys/kernel/debug/kmemleak

… что-то делаем

echo scan | sudo tee /sys/kernel/debug/kmemleak

sudo cat /sys/kernel/debug/kmemleak

Дальше начинается небольшой цирк

В общем, хотя и модуль ядра содержит все отладочные символы, информацию и т.д., но 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

И, конечно, строка типа 0xffffffffc0d3d653 означает ваш код — ищите сами.

В принципе, всё не так сложно, но немного неудобно. Приступим:

Получаем диассемблер:

objdump -DS tagvfs.ko > disasm.txt

Получается нормальный дизассемблированный код с вставками сишного кода:

struct qstr alloc_qstr_from_qstr(const struct qstr s) {
     190:	e8 00 00 00 00       	callq  195 <alloc_qstr_from_qstr+0x5>
     195:	55                   	push   %rbp
     196:	49 89 f8             	mov    %rdi,%r8
     199:	48 89 f7             	mov    %rsi,%rdi
  return alloc_qstr_from_str(s.name, s.len);
     19c:	4c 89 c6             	mov    %r8,%rsi
     19f:	48 c1 ee 20          	shr    $0x20,%rsi
struct qstr alloc_qstr_from_qstr(const struct qstr s) {
     1a3:	48 89 e5             	mov    %rsp,%rbp
}
     1a6:	5d                   	pop    %rbp
  return alloc_qstr_from_str(s.name, s.len);
     1a7:	e9 b4 fe ff ff       	jmpq   60 <alloc_qstr_from_str>
     1ac:	0f 1f 40 00          	nopl   0x0(%rax)

Далее на ЗАПУЩЕННОМ модуле находим адреса секций:

????

Получается что-то наподобие:

.text .data .bss
0xffffffffc0d3a000
0xffffffffc0d44240
0xffffffffc0d44800

Здесь мы видим, что адрес 0xffffffffc0d3d653 относится к первой секции (больше 0xffffffffc0d3a000 и меньше 0xffffffffc0d44240). Соответственно адрес секции будет смещением и если мы вычтем 0xffffffffc0d3d653 — 0xffffffffc0d3a000 = 0x3653 Это и есть смещение в коде. Находим его в дизассемблере:

 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
 

Так как указатель в стэке указывает на следующую команду, то смотрим на предыдущую, видим callq 3653 <tagfs_get_fileino_by_name+0x93> что есть вызов функции. Что за функция — написано выше — в нашем случае GetFileInfo(sr, i, mask, &res, NULL).

Далее выпотрошив все адреса, (на самом деле, их обычно бывает небольшое количество) получаем цепочку кода, которая вызвала выделение памяти, которая потом не освободилась.

Ну и далее ide — bugfix — compile — etc. В общем, как обычно.

Внешние ссылки:

  1. Kernel Memory Leak Detector
  2. virt-manager в Debian 10 и 11, ставим вместо VirtualBox