Вольный перевод Oops! Debugging Kernel Panics

Небольшой обзор причин, приводящих к kernel panic, и утилит, которые помогут получить больше информации.

Работая в среде с ядром Linux, как часто вы видели kernel panic? Когда это случается, система находится в повреждённом состоянии пока вы её не перезагрузите. И даже после восстановления работоспособности остаётся вопрос: почему это произошло? Вы можете даже не догадываться, в чём причина. Тем не менее, ответ может быть найден, и это руководство постарается помочь вам найти первопричину kernel panic и устранить её.

Изображение 1. Паника ядра - kernel panic.
Изображение 1. Паника ядра — kernel panic.

Начнём с обзора утилит kexec и kdump. kexec позволяет загружать другое ядро из текущего (и работающего), а kdump — это механизм создания crash dump’ов ядра, основанный на kexec.

Установка необходимых пакетов

Первое и самое важное, ядро должно содержать эти компоненты, статически встроенные в его образ:

CONFIG_RELOCATABLE=y
CONFIG_KEXEC=y
CONFIG_CRASH_DUMP=y
CONFIG_DEBUG_INFO=y
CONFIG_MAGIC_SYSRQ=y
CONFIG_PROC_VMCORE=y

Проверить эти значения можно в конфигурационном файле ядра, для стандартной установки Debian/Ubuntu:

$ grep "CONFIG_ЗНАЧЕНИЕ" /boot/config-`uname -r`

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

# apt update && apt upgrade

Установите следующие пакеты (я сейчас использую Debian, но то же должно работать и для Ubuntu):

# apt install gcc make binutils linux-headers-`uname -r` kdump-tools crash `uname -r`-dbg

Замечание: имена пакетов могут отличаться в разных дистрибутивах

Во время установки вам будет предложено включить перехват перезагрузок посредством kexec (вы можете ответить на это как пожелаете, но я выбрал «No», см. Изображение 2)

Изображение 2. Меню конфигурации kexec
Изображение 2. Меню конфигурации kexec

И включить kdump во время загрузки системы, отвечайте «Yes» (Изображение 3).

Изображение 3. Меню конфигурации kdump
Изображение 3. Меню конфигурации kdump

Конфигурация kdump

Откройте файл /etc/default/kdump-tools, в самом верху вы должны увидеть таки строки:

USE_KDUMP=1
#KDUMP_SYSCTL="kernel.panic_on_oops=1"

В дальнейшем, мы напишим собственный модуль, который будет провоцировать состояние ядра OOPS. Для того, чтобы kdump собирал и сохранял состояние системы для анализа, необходимо ядру запаниковать на OOPS. Для этого нужно раскомментировать строчку, начинающуюся с KDUMP_SYSCTL:

USE_KDUMP=1
KDUMP_SYSCTL="kernel.panic_on_oops=1"

Для первоначального тестирование системы нужно включить SysRq, есть несколько способов сделать это, но я выберу тот, который требует перезагрузки системы для активации этой функциональности. Откройте файл /etc/sysctl.d/99-sysctl.conf и убедитесь, что следующая строчка (располагается ближе к концу файла) раскомментирована:

kernel.sysrq=1

Теперь откройте файл /etc/default/grub.d/kdump-tools.defaul, там будет одна строчка на подобии этой:

GRUB_CMDLINE_LINUX_DEFAULT="$GRUB_CMDLINE_LINUX_DEFAULT crashkernel=384M-:128M"

Замените crashkernel=384M-:128M на crashkernel=128M.

Обновите конфигурационный файл GRUB2, чтобы изменения вступили в силу:

# update-grub

После чего перезагрузите систему.

Проверка работы kdump

После перезагрузки, в dmesg будут похожие строки:

# dmesg |grep -i crash
[    0.000000] Command line: BOOT_IMAGE=/boot/vmlinuz-4.9.0-8-amd64 root=UUID=bd76b0fe-9d09-40a9-a0d8-a7533620f6fa ro quiet crashkernel=128M
[    0.000000] Reserving 128MB of memory at 720MB for crashkernel (System RAM: 4095MB)
[    0.000000] Kernel command line: BOOT_IMAGE=/boot/ vmlinuz-4.9.0-8-amd64 root=UUID=bd76b0fe-9d09-40a9-a0d8-a7533620f6fa ro quiet crashkernel=128M

А в ядре будут включены такие параметры («1» значит включён):

# sysctl -a|grep kernel|grep -e panic_on_oops -e sysrq
kernel.panic_on_oops = 1
kernel.sysrq = 1

Сервис kdump должен быть запущен:

# systemctl status kdump-tools.service
 kdump-tools.service - Kernel crash dump capture service
   Loaded: loaded (/lib/systemd/system/kdump-tools.service;     enabled; vendor preset: enabled)
   Active: active (exited) since Tue 2019-02-26 08:13:34 CST;    1h 33min ago
  Process: 371 ExecStart=/etc/init.d/kdump-tools start   (code=exited, status=0/SUCCESS)
 Main PID: 371 (code=exited, status=0/SUCCESS)
    Tasks: 0 (limit: 4915)
   CGroup: /system.slice/kdump-tools.service

Feb 27 08:13:34 deb-panic systemd[1]: Starting Kernel crash dump capture service...
Feb 27 08:13:34 deb-panic kdump-tools[371]: Starting kdump-tools: loaded kdump kernel.
Feb 27 08:13:34 deb-panic kdump-tools[505]: /sbin/kexec -p --command-line="BOOT_IMAGE=/boot/vmlinuz-4.9.0-8-amd64 root=
Feb 27 08:13:34 deb-panic kdump-tools[506]: loaded kdump kernel
Feb 27 08:13:34 deb-panic systemd[1]: Started Kernel crash dump capture service.

А аварийное ядро должен быть загружено (в область памяти 128Мб, которую вы задали раньше):

$ cat /sys/kernel/kexec_crash_loaded
1

Так вы можете проверить свою конфигурацию kdump:

# kdump-config show
DUMP_MODE:        kdump
USE_KDUMP:        1
KDUMP_SYSCTL:     kernel.panic_on_oops=1
KDUMP_COREDIR:    /var/crash
crashkernel addr: 0x2d000000
   /var/lib/kdump/vmlinuz: symbolic link to /boot/vmlinuz-4.9.0-8-amd64
kdump initrd:
   /var/lib/kdump/initrd.img: symbolic link to /var/lib/kdump/initrd.img-4.9.0-8-amd64
current state:    ready to kdump

kexec command:
  /sbin/kexec -p --command-line="BOOT_IMAGE=/boot/vmlinuz-4.9.0-8-amd64 root=UUID=bd76b0fe-9d09-40a9-a0d8-a7533620f6fa ro quiet irqpoll nr_cpus=1 nousb systemd.unit=kdump-tools.service ata_piix.prefer_ms_hyperv=0" --initrd=/var/lib/kdump/initrd.img /var/lib/kdump/vmlinuz

Давайте его протестируем без реального его запуска:

# kdump-config test
USE_KDUMP:         1
KDUMP_SYSCTL:      kernel.panic_on_oops=1
KDUMP_COREDIR:     /var/crash
crashkernel addr:  0x2d000000
kdump kernel addr:
kdump kernel:
   /var/lib/kdump/vmlinuz: symbolic link to /boot/vmlinuz-4.9.0-8-amd64
kdump initrd:
   /var/lib/kdump/initrd.img: symbolic link to /var/lib/kdump/initrd.img-4.9.0-8-amd64
kexec command to be used:
  /sbin/kexec -p --command-line="BOOT_IMAGE=/boot/vmlinuz-4.9.0-8-amd64 root=UUID=bd76b0fe-9d09-40a9-a0d8-a7533620f6fa ro quiet irqpoll nr_cpus=1 nousb systemd.unit=kdump-tools.service ata_piix.prefer_ms_hyperv=0" --initrd=/var/lib/kdump/initrd.img /var/lib/kdump/vmlinuz

Момент истины

Теперь, когда kdump настроен, необходимо его протестировать, и лучший способ — это спровоцировать падение ядра с помощью SysRq. Если ваше ядро собрано с поддержкой SysRq, положить ядро так же просто, как напечатать в консоле:

# echo "c" | tee -a /proc/sysrq-trigger

Что должно произойти? Вы увидите панику/падение ядра, похожу на Изображение 1. После падения, ядро, загруженное kexec, соберёт всю информаци о состоянии системы: памяти, процессора, dmesg, загруженные модули и много другое. Эти данные будут доступны для анализа в какой-то подпапке /var/chash/. После сбора данных, система автоматически перезагрузится обратно в рабочее состояние.

Что дальше?

Теперь у вас есть аварийные файлы, расположенные в папке /var/crash:

$ cd /var/crash/
$ ls
201902271006  kexec_cmd
$ cd 201902271006/

Кроме того, перед тем как открыть аварийный файл, нужно установить исходный код ядра:

# apt source linux-image-`uname -r`

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

# crash dump.201902271006 /usr/lib/debug/vmlinux-4.9.0-8-amd64

Когда всё загрузится, обзор kernel panic отобразится на экране:

      KERNEL: /usr/lib/debug/vmlinux-4.9.0-8-amd64
    DUMPFILE: dump.201902261006  [PARTIAL DUMP]
        CPUS: 4
        DATE: Tue Feb 26 10:07:21 2019
      UPTIME: 00:04:09
LOAD AVERAGE: 0.00, 0.00, 0.00
       TASKS: 100
    NODENAME: deb-panic
     RELEASE: 4.9.0-8-amd64
     VERSION: #1 SMP Debian 4.9.144-3 (2019-02-02)
     MACHINE: x86_64  (2592 Mhz)
      MEMORY: 4 GB
       PANIC: "sysrq: SysRq : Trigger a crash"
         PID: 563
     COMMAND: "tee"
        TASK: ffff88e69628c080 [THREAD_INFO: ffff88e69628c080]
         CPU: 2
       STATE: TASK_RUNNING (SYSRQ)

Заметьте причину паники ядра: sysrq: SysRq : Trigger a crash. Кроме этого, программу, которая привела к падению: tee. Ничего удивительного, ведь вы сами сделали это 🙂

Если вы запустите трассировку стека функций, которые привели к падению, то увидите что-то такое (код работал на 2 ядре процессора):

crash> bt
PID: 563    TASK: ffff88e69628c080  CPU: 2   COMMAND: "tee"
 #0 [ffffa67440b23ba0] machine_kexec at ffffffffa0c53f68
 #1 [ffffa67440b23bf8] __crash_kexec at ffffffffa0d086d1
 #2 [ffffa67440b23cb8] crash_kexec at ffffffffa0d08738
 #3 [ffffa67440b23cd0] oops_end at ffffffffa0c298b3
 #4 [ffffa67440b23cf0] no_context at ffffffffa0c619b1
 #5 [ffffa67440b23d50] __do_page_fault at ffffffffa0c62476
 #6 [ffffa67440b23dc0] page_fault at ffffffffa121a618
    [exception RIP: sysrq_handle_crash+18]
    RIP: ffffffffa102be62  RSP: ffffa67440b23e78  RFLAGS: 00010282
    RAX: ffffffffa102be50  RBX: 0000000000000063  RCX: 0000000000000000
    RDX: 0000000000000000  RSI: ffff88e69fd10648  RDI: 0000000000000063
    RBP: ffffffffa18bf320   R8: 0000000000000001   R9: 0000000000007eb8
    R10: 0000000000000001  R11: 0000000000000001  R12: 0000000000000004
    R13: 0000000000000000  R14: 0000000000000000  R15: 0000000000000000
    ORIG_RAX: ffffffffffffffff  CS: 0010  SS: 0018
 #7 [ffffa67440b23e78] __handle_sysrq at ffffffffa102c597
 #8 [ffffa67440b23ea0] write_sysrq_trigger at ffffffffa102c9db
 #9 [ffffa67440b23eb0] proc_reg_write at ffffffffa0e7ac00
#10 [ffffa67440b23ec8] vfs_write at ffffffffa0e0b3b0
#11 [ffffa67440b23ef8] sys_write at ffffffffa0e0c7f2
#12 [ffffa67440b23f38] do_syscall_64 at ffffffffa0c03b7d
#13 [ffffa67440b23f50] entry_SYSCALL_64_after_swapgs at ffffffffa121924e
    RIP: 00007f3952463970  RSP: 00007ffc7f3a4e58  RFLAGS: 00000246
    RAX: ffffffffffffffda  RBX: 0000000000000002  RCX: 00007f3952463970
    RDX: 0000000000000002  RSI: 00007ffc7f3a4f60  RDI: 0000000000000003
    RBP: 00007ffc7f3a4f60   R8: 00005648f508b610   R9: 00007f3952944480
    R10: 0000000000000839  R11: 0000000000000246  R12: 0000000000000002
    R13: 0000000000000001  R14: 00005648f508b530  R15: 0000000000000002
    ORIG_RAX: 0000000000000001  CS: 0033  SS: 002b

В этой трассировке вы можете узнать символьный адрес хранящегося в указателе возврата инструкций (Return Instruction Pointer (RIP)): ffffffffa102be62. Давайте посмотрим на символ по этому адресу:

crash> sym ffffffffa102be62
ffffffffa102be62 (t) sysrq_handle_crash+18 ./debian/build/build_amd64_none_amd64/./drivers/tty/sysrq.c: 144

Подождите! Получается, что исключение было вызвано на 144 строке в файле drivers/tty/sysrq.c, внутри функции sysrq_handle_crash. Хмм… Интересно, что же происходит в этом файле ядра (вот зачем вы установили пакет исходного кода ядра недавно). Перейдём в папку /usr/src и распакуем пакет исходго кода ядра:

$ cd /usr/src
$ ls
linux_4.9.144-3.debian.tar.xz  linux_4.9.144.orig.tar.xz linux-headers-4.9.0-8-common
linux_4.9.144-3.dsc            linux-headers-4.9.0-8-amd64 linux-kbuild-4.9
# tar xJf linux_4.9.144.orig.tar.xz
$ vim linux-4.9.144/drivers/tty/sysrq.c

Найдите функцию sysrq_handle_crash:

static void sysrq_handle_crash(int key)
{
    char *killer = NULL;

    /* we need to release the RCU read lock here,
     * otherwise we get an annoying
     * 'BUG: sleeping function called from invalid context'
     * complaint from the kernel before the panic.
     */
    rcu_read_unlock();
    panic_on_oops = 1;      /* force panic */
    wmb();
    *killer = 1;
}

И что важнее, взгляните на 144 строку:

*killer = 1;

Это строка, которая привела к ошибке из-за отсутствия страницы на строке #6 в трассировке стека падения ядра Linux:

#6 [ffffa67440b23dc0] page_fault at ffffffffa121a618

Итак, теперь вы знаете основы отладки плохого кода ядра, но что случится, если вы захотите отладить свой собственный модули ядра Linux (к примеру, драйверы)? Я написал простой модуль ядра Linux, который приводит к похожему падению ядра при загрузке. Сохраните его в файл test-module.c где-нибудь в вашей домашней папке.

#include <linux/init.h>
#include <linux/module.h>
#include <linux/version.h>

static int test_module_init(void)
{
        int *p = 1;
printk("%d\n", *p);
        return 0;
}
static void test_module_exit(void)
{
        return;
}

module_init(test_module_init);
module_exit(test_module_exit);

Вам нужен Makefile для компиляции модуля ядра (сохраните его в той же папке):

obj-m += test-module.o

all:
    $(MAKE) -C/lib/modules/$(shell uname -r)/build M=$(PWD)

Запустите команду make для компиляции модуля, но не удаляйте никаких артефактов сборки (они понадобятся вам далее):

$ make

Замечание: вы можете увидеть предупреждения компилятора при сборке. Проигнорируйте их. Эти предупреждения указывают именно на то, что и приведёт к падению ядра в дальнейшем.

Теперь будьте осторожны, когда вы загрузите .ko файл система упадёт, поэтому убедитесь что всё сохранили и синхронизировали данные на диске:

# sync && insmod test-module.ko

Как и раньше, произойдёт паника ядра, а kexec сохранит всю информацию где-то в какой-нибудь подпапке /var/crash, после чего система перезагрузится. После загрузки системы в рабочем состоянии, найдите папку с аварийными файлами и перейдите в неё:

$ cd /var/crash/201902271035/

Кроме того, скопируйте в неё объектный файл test-module из своей домашней дериктории:

# cp ~/test-module.o /var/crash/201902271035/

Загрузите аварийный файл для отладки:

# crash dump.201902271035 /usr/lib/debug/vmlinux-4.9.0-8-amd64

Обзор падения будет выглядить как-то так:

      KERNEL: /usr/lib/debug/vmlinux-4.9.0-8-amd64
    DUMPFILE: dump.201902261035  [PARTIAL DUMP]
        CPUS: 4
        DATE: Tue Feb 26 10:37:47 2019
      UPTIME: 00:11:16
LOAD AVERAGE: 0.24, 0.06, 0.02
       TASKS: 102
    NODENAME: deb-panic
     RELEASE: 4.9.0-8-amd64
     VERSION: #1 SMP Debian 4.9.144-3 (2019-02-02)
     MACHINE: x86_64  (2592 Mhz)
      MEMORY: 4 GB
       PANIC: "BUG: unable to handle kernel NULL pointer
 ↪dereference at 0000000000000001"
         PID: 1493
     COMMAND: "insmod"
        TASK: ffff893c5a5a5080 [THREAD_INFO: ffff893c5a5a5080]
         CPU: 3
       STATE: TASK_RUNNING (PANIC)

Причина падения ядра описана как: BUG: unable to handle kernel NULL pointer dereference at 0000000000000001. Команда пользователя, которая привела к падению, была insmod.

В трассировке стека функций падения можно найти исключение из-за отсутсвие страницы по адресу ffffffffc05ed005:

crash> bt
PID: 1493   TASK: ffff893c5a5a5080  CPU: 3  COMMAND: "insmod"
 #0 [ffff9dcd013b79f0] machine_kexec at ffffffffa3a53f68
 #1 [ffff9dcd013b7a48] __crash_kexec at ffffffffa3b086d1
 #2 [ffff9dcd013b7b08] crash_kexec at ffffffffa3b08738
 #3 [ffff9dcd013b7b20] oops_end at ffffffffa3a298b3
 #4 [ffff9dcd013b7b40] no_context at ffffffffa3a619b1
 #5 [ffff9dcd013b7ba0] __do_page_fault at ffffffffa3a62476
 #6 [ffff9dcd013b7c10] page_fault at ffffffffa401a618
    [exception RIP: init_module+5]
    RIP: ffffffffc05ed005  RSP: ffff9dcd013b7cc8  RFLAGS: 00010246
    RAX: 0000000000000000  RBX: 0000000000000000  RCX: 0000000000000000
    RDX: 0000000080000000  RSI: ffff893c5a5a5ac0  RDI: ffffffffc05ed000
    RBP: ffffffffc05ed000   R8: 0000000000020098   R9: 0000000000000006
    R10: 0000000000000000  R11: ffff893c5a4d8100  R12: ffff893c5880d460
    R13: ffff893c56500e80  R14: ffffffffc05ef000  R15: ffffffffc05ef050
    ORIG_RAX: ffffffffffffffff  CS: 0010  SS: 0018
 #7 [ffff9dcd013b7cc8] do_one_initcall at ffffffffa3a0218e
 #8 [ffff9dcd013b7d38] do_init_module at ffffffffa3b81531
 #9 [ffff9dcd013b7d58] load_module at ffffffffa3b04aaa
#10 [ffff9dcd013b7e90] SYSC_finit_module at ffffffffa3b051f6
#11 [ffff9dcd013b7f38] do_syscall_64 at ffffffffa3a03b7d
#12 [ffff9dcd013b7f50] entry_SYSCALL_64_after_swapgs at ffffffffa401924e
    RIP: 00007f124662c469  RSP: 00007fffc4ca04a8  RFLAGS: 00000246
    RAX: ffffffffffffffda  RBX: 0000564213d111f0  RCX: 00007f124662c469
    RDX: 0000000000000000  RSI: 00005642129d3638  RDI: 0000000000000003
    RBP: 00005642129d3638   R8: 0000000000000000   R9: 00007f12468e3ea0
    R10: 0000000000000003  R11: 0000000000000246  R12: 0000000000000000
    R13: 0000564213d10130  R14: 0000000000000000  R15: 0000000000000000
    ORIG_RAX: 0000000000000139  CS: 0033  SS: 002b

Давайте посмотри на символ по адресу ffffffffc05ed005:

crash> sym ffffffffc05ed005
ffffffffc05ed005 (t) init_module+5 [test-module]

Хммм. Причина падения случилась где-то в коде инициализации драйвера ядра test-module. Но что случилось со всеми теми деталями, которые были в предыдущем анализе? Так как этот код не часть отладочного образа ядра, нужно найти способ загрузить его в текущий анализ падения. Вот почему выше был скопирован объектный файл модуля в текущую директорию. Сейчас пришло время загрузить этот объектный файл:

crash> mod -s test ./test.o
     MODULE       NAME                   SIZE  OBJECT FILE
ffffffffc05ef000  test                  16384  ./test-module.o

Вернёмся назад и посмотрим символ по адресу ещё раз:

crash> sym ffffffffc05ed005
ffffffffc05ed005 (T) init_module+5 [test-module] /home/ozi/test-module.c: 8

Теперь можно вернуться к исходному коду файла test-module.c и посмотреть на 8 строчку:

$ sed -n 8p test-module.c
        printk("%d\n", *p);

Так и есть, ошибка загрузки страницы произошла, когда вы пытались разыменовать плохой указатель. Помните предупреждения компилятора? Это касалось как раз этой проблемы, и эта же проблема привела к панике ядра. Возможно, мы сможем написать код получше в будущем =)

Что ещё вы можете сделать здесь?

Файл падения ядра содержит много данных о вашей системе в момент ошибки. Вы можете посмотреть список доступных команд введя help:

crash> help

*            files        mach         repeat       timer
alias        foreach      mod          runq         tree
ascii        fuser        mount        search       union
bt           gdb          net          set          vm
btop         help         p            sig          vtop
dev          ipcs         ps           struct       waitq
dis          irq          pte          swap         whatis
eval         kmem         ptob         sym          wr
exit         list         ptov         sys          q
extend       log          rd           task

К примеру, можно посмотреть обзор использования памяти:

crash> kmem -i
                 PAGES        TOTAL      PERCENTAGE
    TOTAL MEM   979869       3.7 GB         ----
         FREE   835519       3.2 GB   85% of TOTAL MEM
         USED   144350     563.9 MB   14% of TOTAL MEM
       SHARED     8374      32.7 MB    0% of TOTAL MEM
      BUFFERS     3849        15 MB    0% of TOTAL MEM
       CACHED        0            0    0% of TOTAL MEM
         SLAB     5911      23.1 MB    0% of TOTAL MEM

   TOTAL SWAP  1047807         4 GB         ----
    SWAP USED        0            0    0% of TOTAL SWAP
    SWAP FREE  1047807         4 GB  100% of TOTAL SWAP

 COMMIT LIMIT  1537741       5.9 GB         ----
    COMMITTED    16370      63.9 MB    1% of TOTAL LIMIT

Или же посмотреть актуальный лог dmesg при падении:

crash> log

[    0.000000] Linux version 4.9.0-8-amd64 (debian-kernel@lists.debian.org) (gcc version 6.3.0 20170516 (Debian 6.3.0-18+deb9u1) ) #1 SMP Debian 4.9.144-3 (2019-02-02)
[    0.000000] Command line: BOOT_IMAGE=/boot/vmlinuz-4.9.0-8-amd64 root=UUID=bd76b0fe-9d09-40a9-a0d8-a7533620f6fa ro quiet crashkernel=128M
[    0.000000] x86/fpu: Supporting XSAVE feature 0x001: 'x87 floating point registers'
[    0.000000] x86/fpu: Supporting XSAVE feature 0x002: 'SSE registers'
[    0.000000] x86/fpu: Supporting XSAVE feature 0x004: 'AVX registers'
[    0.000000] x86/fpu: xstate_offset[2]:  576, xstate_sizes[2]: 256

[ .... ]

Используя утилиту crash, можно ещё глубже детализировать области памяти и их содержимое, узнать что обрабатывалось на каждом ядре процессора и многое другое. Если вы хотите узнать больше об этих функция, просто введите help имя_комманды:

crash> help swap

Заключение

Это очень поверхностное введение в отладку ядра, оно охватывает лишь основы. Но, надеюсь, оно станет для вас отправной точкой для диагностики ошибок ядра в продакшен, тестовых и рабочих окружениях.

Навигация по записям