В ОС Линукс есть средства для проверки и наблюдения за работой кода в режиме ядра. В частности, есть Детектор Утечек Памяти в Ядре [1], который отслеживает операции выделения и освобождения памяти.
Для поиска и отладки утечек памяти в своём модуле ядра делаем 3 пункта:
- включить kmemleak;
- делаем тестовые прогоны и получаем отчёт об утечках. Если утечек нет, то завершаем поиск;
- находим место утечки и исправляем. Переходим к п. 2.
Включаем kmemleak
kmemleak, как очевидно из названия, это средство ядра Линукс-а по поиску утечек памяти. В обычной сборке ядра это средство отключено (так как оно подъедает много ресурсов) и прежде всего его нужно включить. Включается это средство при сборке ядра и на выходе получается ядро Линукса, которое подходит для отладки, и не очень хорошо подходит для всего остального. Для разрешения этих противоречий можно либо жонглировать ядрами, либо (очень хороший вариант) сделать отдельную виртуальную машину, на которой будет крутиться «отладочное» ядро.
И в нашем случае (отдельная виртуальная машина) задача включить kmemleak немного разрастается:
- установить ПО виртуализации и ОС;
- настроить новое отладочное ядро;
- собрать отладочное ядро.
Поехали!
Установить ПО виртуализации и ОС
Для виртуализации можно использовать различные решения. В моём случае хорошим вариантом является 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. В общем, как обычно.
Внешние ссылки: