核心 , linux

调试Linux内核

这是一些有用的配置选项和调试Linux内核的基本方法的一些汇编。有很好的文档 核心 .org 以及其他地方,但是当我学习这些东西时,我感到几乎没有什么地方可以得到高水平的概述。我们将在另一篇文章中介绍更高级的概要分析和跟踪方法。

假设我们已经知道如何编译内核,使用 菜单配置 以及什么是内核选项。

基本内核配置

 

首先,我们要激活一些选项,以使它们在运行的内核中可用。他们中的一些人会付出巨大的努力并赢得比赛’不会出现在生产系统中。其他人只会增加内核大小。您可以大致了解以下内容 Kconfig.debug,而且您可能会发现这个 内核符号参考 便利。

DEBUG_KERNEL

此选项专门用于取消隐藏menuconfig中的调试选项。

调试信息
生成的内核映像将包含调试信息,从而产生更大的内核映像。这会将调试符号添加到内核和模块(gcc -g),如果您打算在内核上使用kernel crashdump或二进制对象工具(例如crash,kgdb,LKCD,gdb等),则需要此符号。
DEBUG_BUGVERBOSE
当程序由于异常而崩溃时,或者内核检测到内部错误时,内核可以打印一条不太简短的消息,说明问题所在。跟踪问题时,此调试信息对开发人员和内核黑客很有用,但对其他人而言却毫无意义。这对于调试总是有帮助的,但对生产系统毫无用处。
BUG()紧急事件将输出BUG调用的文件名和行号以及EIP和oops跟踪。这有助于调试,但会花费大约70-100K的内存。
CONFIG_FRAME_POINTER

此选项将代码插入到已编译的可执行文件中,该文件将帧信息保存在寄存器中或堆栈上的不同位置,这使调试器(例如gdb)可以在调试内核时更准确地构造堆栈回溯跟踪。

CONFIG_KALLSYMS

这将为我们提供来自内核oopes的堆栈跟踪中的更多信息。内核将打印出符号崩溃信息和符号堆栈回溯。由于所有符号都必须加载到内核映像中,因此这在某种程度上增加了内核的大小。

如果内核崩溃,此选项将使我们能够获得包含函数名称而不只是地址的输出,因此可以更轻松地发现正在发生的事情。这是取自Ubuntu Wiki的示例

BUG: unable to handle  核心  NULL pointer dereference at virtual address 00000008
  printing eip:
 c022a7b5
 *pde = 00000000
 Oops: 0000 [#1]
 SMP 
 Modules linked in: thinkpad_acpi ppdev speedstep_lib cpufreq_conservative cpufreq_userspace cpufreq_ondemand cpufreq_stats cpufreq_powersave freq_table video bay dock ac sbs button container battery lp irtty_sir sir_dev pcmcia parport_pc parport snd_cs46xx gameport snd_ac97_codec ac97_bus snd_pcm_oss snd_mixer_oss nsc_ircc snd_pcm snd_seq_dummy irda crc_ccitt snd_seq_oss psmouse i2c_piix4 snd_seq_midi snd_rawmidi snd_seq_midi_event serio_raw pcspkr snd_seq i2c_core snd_timer snd_seq_device snd soundcore snd_page_alloc shpchp pci_hotplug intel_agp yenta_socket rsrc_nonstatic pcmcia_core agpgart evdev ext3 jbd mbcache sg sr_mod cdrom sd_mod uhci_hcd usbcore ata_piix ata_generic libata scsi_mod e100 mii thermal processor fan fuse apparmor commoncap
 CPU:    0
 EIP:    0060:[<c022a7b5>]    Not tainted VLI
 EFLAGS: 00010202   (2.6.22-12-generic #1)
 EIP is at acpi_ns_internalize_name+0xd/0x83
 eax: 00000008   ebx: 00000000   ecx: 00000000   edx: c7879e54
 esi: d0b980c0   edi: c7879e54   ebp: c7879e70   esp: c7879de8
 ds: 007b   es: 007b   fs: 00d8  gs: 0033  ss: 0068
 Process modprobe (pid: 4467, ti=c7878000 task=ce5c94c0 task.ti=c7878000)
 Stack: 00000000 00000000 d0b97e60 00008080 c01c4390 d0b97e60 00000000 00000000 
        d0b980c0 00000000 c7879e70 c022a85c d0b97e60 c795d030 c7c604e0 c01c44ef 
        00000004 d0b97e60 c7acea18 c01c3884 00008080 00000004 00000004 00000080 
 Call Trace:
  [<c01c4390>] __sysfs_new_dirent+0x20/0x50
  [<c022a85c>] acpi_ns_get_node+0x31/0x93
  [<c01c44ef>] sysfs_make_dirent+0x2f/0x50
  [<c01c3884>] sysfs_add_file+0x74/0x90
  [<d0b910b7>] drv_acpi_handle_init+0x37/0x90 [thinkpad_acpi]
  [<c0231aef>] acpi_ut_release_mutex+0x5b/0x63
  [<c0233ac0>] acpi_method_notify_enable+0x15/0x34
  [<d0b5ba32>] cmos_init+0x52/0x70 [thinkpad_acpi]
  [<d0b5c21f>] thinkpad_acpi_module_init+0x27f/0x69a [thinkpad_acpi]
  [<c014a811>] sys_init_module+0x151/0x1a00
  [<c01fb8cf>] prio_tree_insert+0x1f/0x250
  [<c01041d2>] sysenter_past_esp+0x6b/0xa9
  =======================
 Code: c7 44 24 14 01 00 00 00 8b 54 24 14 8d 04 96 e9 f2 fe ff ff 83 c4 18 89 d0 5b 5e 5f 5d c3 55 57 89 d7 56 53 83 ec 1c 85 c0 74 67 <80> 38 00 74 62 85 d2 74 5e 89 04 24 89 e0 e8 b5 fb ff ff 8b 4c 
 EIP: [<c022a7b5>] acpi_ns_internalize_name+0xd/0x83 SS:ESP 0068:c7879de8

我们可以手动检查符号到地址映射 / proc / kallsyms。此输出在 纳米 格式 .

# head -20 / proc / kallsyms
0000000000000000 A irq_stack_union
0000000000000000 A __per_cpu_start
0000000000004000 A cpu_debug_store
0000000000005000 A cpu_tss_rw
0000000000008000 A gdt_page
0000000000009000 A exception_stacks
000000000000e000 A entry_stack_storage
000000000000f000 A cpu_llc_id
000000000000f020 A cpu_llc_shared_map
000000000000f060 A cpu_core_map
000000000000f0a0 A cpu_sibling_map
000000000000f0e0 A cpu_info
000000000000f1d0 A cpu_number
000000000000f1d8 A this_cpu_off
000000000000f1e0 A x86_cpu_to_acpiid
000000000000f1e4 A x86_cpu_to_apicid
000000000000f1e6 A x86_bios_cpu_apicid
000000000000f1e8 A sched_core_priority
000000000000f200 A cpu_loops_per_jiffy
000000000000f220 A pmc_prev_left

同样,很容易从内核代码访问此信息。我们可以使用 kallsyms_lookup_name()  从 kallsyms.c.

CONFIG_KALLSYMS_ALL

通常,kallsyms仅包含用于更好的OOPS消息和回溯的功能的符号(即,来自text和inittext部分的符号)。对于大多数情况,这已足够。而且只有在极少数情况下(例如,使用调试器时),才需要所有符号(例如,数据部分中的变量名称等)。

此选项可确保以增加的内核大小为代价(取决于内核配置,它可能是300KiB或类似的东西)将所有符号都加载到内核映像(即所有部分的符号)中。

CONFIG_IKCONFIG

此选项启用完整的Linux内核 .config 文件内容要保存在内核中。它提供了有关正在运行的内核或磁盘上的内核中使用哪些内核选项的文档。可以使用脚本scripts / extract-ikconfig从内核映像文件中提取此信息,并将其用作重建当前内核或构建另一个内核的输入。

CONFIG_IKCONFIG_PROC

启用访问 .config 通过 /proc/config.gz 在运行的内核中。

$ zcat /proc/config.gz | head -20
#
# Automatically generated file; DO NOT EDIT.
# Linux/x86 4.16.4-1 Kernel Configuration
#
CONFIG_64BIT=y
CONFIG_X86_64=y
CONFIG_X86=y
CONFIG_INSTRUCTION_DECODER=y
CONFIG_OUTPUT_FORMAT="elf64-x86-64"
CONFIG_ARCH_DEFCONFIG=" 拱 /x86/configs/x86_64_defconfig"
CONFIG_LOCKDEP_SUPPORT=y
CONFIG_STACKTRACE_SUPPORT=y
CONFIG_MMU=y
CONFIG_ARCH_MMAP_RND_BITS_MIN=28
CONFIG_ARCH_MMAP_RND_BITS_MAX=32
CONFIG_ARCH_MMAP_RND_COMPAT_BITS_MIN=8
CONFIG_ARCH_MMAP_RND_COMPAT_BITS_MAX=16
CONFIG_NEED_DMA_MAP_STATE=y
CONFIG_NEED_SG_DMA_LENGTH=y
CONFIG_GENERIC_ISA_DMA=y
命令行

我们可以检查使用了哪些参数来调用正在运行的内核 / proc / 命令行

$ cat / proc /  命令行 
8250.nr_uarts=0 bcm2708_fb.fbwidth=656 bcm2708_fb.fbheight=416 bcm2708_fb.fbswap=1 vc_mem.mem_base=0x3ec00000 vc_mem.mem_size=0x40000000 dwc_otg.lpm_enable=0 console=ttyS0,115200 console=tty1 root=PARTUUID=9f51643f-02 rootfstype=ext4 elevator=deadline fsck.repair=yes rootwait

这些通常由引导加载程序(GRUB或uboot)提供,但是有时它们被锁定,因此在构建内核时,我们可以使用CONFIG_CMDLINE和CONFIG_CMDLINE_OVERRIDE解决此问题。

  • CONFIG_CMDLINE。在此处输入应编译到内核映像中并在引导时使用的参数。如果引导加载程序在引导时提供了命令行,则在系统引导时,它将被附加到此字符串以形成完整的内核命令行。
  • CONFIG_CMDLINE_OVERRIDE。让内核忽略引导加载程序命令行,而仅使用内置命令行。这用于解决损坏的引导加载程序。

分析正在运行的模块

版画

也许最基本的调试方法是阅读 版画 输出。有多个日志记录级别,默认情况下并非所有日志记录级别都是活动的。价值源于 紧急情况 0级到 调试 级别为8,默认值为 警告 这是第4级。

我们可以使用以下命令激活最大详细日志记录:

echo 8 > /proc/sys/kernel/printk

我们可以使用以下命令检查当前的日志记录级别

$ cat /proc/sys/kernel/printk
7       4       1       7
current default minimum boot-time-default

我们只能看到 调试 消息(级别8),如果我们使用CONFIG_DEBUG编译内核。请注意,如果生成的日志过多,则会严重影响系统性能,尤其是当我们通过慢速UART发送内核日志时。对于这种情况,最好使用 版画 _ratelimited()  要么 版画 _once() 如果我们试图介绍 版画 经常被调用的地方(例如网络数据包堆栈)的语句。

例如,

 版画 _ratelimited(KERN_WARNING "this line will be limited. Iteration %d\n", count);

会导致许多迭代被一条消息抑制

[6886.360547] __ratelimit: 190 callbacks suppressed

一个例子 版画 _once()  可能

 版画 _once(KERN_INFO "this will only be printed once\n");

看到 版画 .h 本文 更多细节。

动态调试

使用 pr_debug() 全球范围内将导致大量的日志记录,因此不是很实用。为了使调试级别更易于管理, 动态调试 被介绍了。

我们可以使用CONFIG_DYNAMIC_DEBUG激活它。我们可以看到 版画 .h dynamic_pr_debug()  在这种情况下将接管。

/* If you are writing a driver, please use dev_dbg instead */
#if defined(CONFIG_DYNAMIC_DEBUG)
#include <linux/dynamic_debug.h>

/* dynamic_pr_debug() uses pr_fmt() internally so we don't need it here */
#define pr_debug(fmt, ...) \
dynamic_pr_debug(fmt, ##__VA_ARGS__)
#elif defined(DEBUG)
#define pr_debug(fmt, ...) \
printk(KERN_DEBUG pr_fmt(fmt), ##__VA_ARGS__)
#else
#define pr_debug(fmt, ...) \
no_printk(KERN_DEBUG pr_fmt(fmt), ##__VA_ARGS__)
#endif

为了控制我们要打印的内容,文件将出现在 dynamic_debug /控制。 现在,我们可以完全控制要记录的printk行。我们可以隐藏( -p  ) 要么 show ( + p  )在文件级别,功能级别甚至行级别记录消息。

例子

echo file tpm_nsc.c line 346-373  + p  > /sys/kernel/dynamic_debug/control
echo file tpm_nsc.c function init_nsc  + p  > /sys/kernel/dynamic_debug/control

我们还可以在模块加载时使用 dyndbg 选项,例如

 # modprobe snd_hda_intel  dyndbg =+p
CONFIG_DEBUG_FS

如果选择了此选项,则可以像这样挂载debugfs

mount -t  调试文件  none /sys/kernel/debug

调试文件 是一个虚拟文件系统,内核模块可以在其中提供调试信息,有时甚至可以通过在其中定义的挂钩接收特定的配置选项。 调试文件 .h.

例如,如果我们正在编译 交换 使用DEBUG_FS,我们可以检查不同的状态参数,例如

# cat /sys/kernel/debug/zswap/written_back_pages
公斤数

这真的很强大。我们可以用好旧的 gdb 分析我们的内核。我们可以设置断点,查看内存内容以及所有其他内容。我们会需要 gdb 或一些 跨gdb 如果我们使用的是其他架构,例如嵌入式ARM板。

在我们的内核中,我们将要激活

  • CONFIG_KGDB。包括kgdb的内核内挂钩,Linux内核源代码级调试器
  • CONFIG_KGDB_SERIAL_CONSOLE。与kgdb共享一个串行控制台。 Sysrq-g必须首先用于插入。
  • CONFIG_MAGIC_SYSRQ。这将激活 魔术SysRq密钥.

首先我们配置 公斤 (通过控制台使用kgdb)以及UART的tty和波特率。

echo "ttyS0,115200" > /sys/module/kgdboc/parameters/kgdboc

接下来,我们告诉内核进入调试模式。这将冻结以等待gdb连接。

echo g > /proc/sysrq-trigger

我们可能还想激活 公斤数据库 为了发送 版画 对gdb会话的语句。

echo 1 > /sys/module/kgdb/parameters/kgdb_use_con

最后,我们将(跨)gdb实例连接到QEMU实例或真实板。首先,我们运行调试器,将其传递给正在运行的内核,并使用调试符号(CONFIG_DEBUG_INFO)进行编译。然后,我们配置串行端口并附加 目标遥控器  命令,就像我们使用时一样 gdb server .

./toolchain-mips_34kc_gcc-5.2.0_musl-1.1.11/bin/mips-openwrt-linux-gdb ./build_dir/target-mips_34kc_musl-1.1.11/linux-ar71xx_generic/linux-4.1.13/vmlinux
set serial baud 115200
target remote /dev/ttyUSB0

我们还可以让内核在早期启动时等待gdb连接,但是随后我们必须进行配置  公斤 在内核命令行中,并使用 公斤数 wait 选项。在此示例中,我们还将停用内核 地址空间布局随机化 因此内存位置不会混乱。

console=ttyS0,115200  公斤 =ttyS0,11520 nokaslr   公斤数 wait 

内核将被冻结,直到我们与gdb连接为止。如果需要,这将允许我们从启动过程的早期进行调试。

数据库

数据库 是一个Shell接口,可让我们通过键盘或串行控制台连接并检查内存,寄存器和日志。它为N’t as powerful as 公斤数 在检查源代码之后的执行方面。有关更多详细信息,请参见 内核文档,但我建议您坚持 公斤数 .

CONFIG_PROC_KCORE

核心提供虚拟 精灵 可在以下位置访问实时内核的核心文件 / proc /核心。可以使用gdb和其他ELF工具读取。使用此机制无法对内核内存进行任何修改。

这非常酷,因为我们可以在调用时分析内存的图片 gdb 。我们需要使用调试符号(CONFIG_DEBUG_INFO)编译的正在运行的内核的副本。

不像 公斤数 ,这是对内存内容的有限模拟,我们无法做一些花哨的事情,例如控件执行。我们仍然可以访问内存内容。例如,如果您的带符号内核是 虚拟机 我们将调用 gdb 就像我们运行它来分析任何可执行文件的核心转储一样,但是我们的核心转储将是 / proc /核心.

#  gdb   虚拟机  / proc /核心
(gdb) print jiffies
$1 = 16467921357
(gdb)

我们可以用

(gdb) core-file / proc /核心

您可以在此阅读更详细的示例 文章 .

作者: 纳乔帕克

谦虚地分享我认为有用的东西 [ 的github 码头工人 hub ]

1 评论

  1. I’m正在致力于开发使用恩智浦i.mx6UL处理器的定制嵌入式linux板。一世’运行了u-boot,我的LCD上有一个图像(800×480并行LCD),并且已经构建了内核和设备树,并且内核似乎已加载,但是我’无论做什么我都会被困在同一地点:我’通过串行端口控制台输出进行监视,内核加载各种驱动程序后,它停止在“密钥类型dns_resolver已注册”. I’m用SD卡运行,从我所能知道的所有信息来看,sd卡工作正常(再次,u-boot正在运行,并且我能够在主机系统中对该卡进行读写操作)。

    我真的不知道内核是否已挂起或正在等待某件事,或者操作系统是否已完全启动,但是’出于某种原因在我的LCD上显示,也许不再使用串行端口作为显示shell提示的正确控制台端口。一世’米茫然。我有两个想法:1.)我可以修改内核源代码以进行类似连续切换gpio的操作,只是为了表明内核不是’吨hunh; 2.)将字符串输出添加到控制台以告诉我它是什么’目前正在研究中;或3.)使用您在本演示文稿中提到的一种或多种调试方法,如果是,那么您首先推荐哪种方法?

    在此先感谢您的帮助。

发表评论

您的电子邮件地址不会被公开。 必需的地方已做标记 *