暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

我偶尔会用到的调试方法 | Linux 内核

嵌入式Linux 2021-07-27
1607

文章转自我朋友的公众号,以下为内容正文

====

大家好,我是你们的工具人老吴。

今天,和大家分享一下几个 Linux 内核的调试小技巧。

当你遇到一个 bug,你调试了 1 年半载都解决不了,这其实一件好事。

因为它会时刻提醒你平时写代码时要谨慎、要多看书、多去认识一些更资深的人,别问我为什么会有这样的感受,因为是亲身经历~

掌握一个调试工具是需要学习成本的,这里只是列举我自己会用到的工具,如果有某个你觉得特别牛逼的工具而我没提到的话,请原谅我。

好,下面开始正文。

最重要的是:思路

调试 bug 时不要急着做实验,先梳理一下思路。

一般可以总结成如下步骤:

1、理解问题;

2、重现问题;

3、定位问题,找到相关的代码;

4、尝试修复问题;

5、如果失败,回到第 1 步;

bug 一般分为这几类:

1、Crash,最常遇到的,可能是因为我是做设备驱动开发的缘故;

2、Lockup,比较少,这类问题预防比事后调试更重要;

3、Logic/implementation error,这个也比较容易遇到,一般是运行不报错,但是运行的结果不符合预期;

4、Resource leak,偶尔会遇到;

5、Performance,偶尔会遇到,对于做驱动开发的话,一般是先考虑功能,当性能达不到要求时,再考虑优化性能。

调试工具的类别:

1、很多人不知道,调试最重要的工具是:我们的大脑。换句话说,也就是我们对内核个子系统、驱动开发的理解;

2、Logs and dump analysis。内核很贴心,许多异常发生时都会有一堆的 Kernel Panic 的信息,经常能让我们直接定位到引起异常的代码;

3、Tracing/profiling。这类工具一般能让我们理解程序的运行流程,不仅适合用来调试问题,也适合用来学习和理解内核的各种功能实现。

4、Interactive debugging。主要就是 gdb,我个人用得很少。

5、Debugging frameworks。许多的调试工具经过不断地发展和完善后,就慢慢地形成了一整套的调试框架,例如 Ftrace、SystemTap。


下面是几个我常用的调试技巧 工具。

最常用的方法:打印

点击查看大图

关于打印的工具,主要是这 3 种:

1、printk()

最原始的打印 api,可以用但是主流观点已经不推荐使用了。

与之相关的是启动参数 loglevel,它决定了可以被打印出来的信息的最低优先级。


2、pr_*()

推荐用 pr_*() 来代替 printk(),这是一个函数族:

pr_emerg(), pr_alert(), pr_crit(), pr_err(), pr_warning(), pr_notice(), pr_info(), pr_cont(), pr_debug()

例如:

pr_info("Booting CPU %d\n", cpu);

内核会打印:

[ 202.350064] Booting CPU 1


3、dev_*()

同样是一个函数族:

dev_emerg(), dev_alert(), dev_crit(), dev_err(), dev_warn(), dev_notice(), dev_info(), dev_dbg()

它们的最大特点是需要传入一个 struct device 的参数,并且会打印出这个 device 的名字,一边是在驱动相关的代码里使用。

例如:

dev_info(&pdev->dev, "in probe\n");

内核会打印:

[ 25.878382] serial 48024000.serial: in probe


关于 pr_debug() and dev_dbg()

要使用这两个 api,需要在对应的代码里 #deinfe DEBUG。

当内核使能了 CONFIG_DYNAMIC_DEBUG,我们就可以通过 sys/kernel/debug/dynamic_debug/control 动态地是否要打印 log,以及打印哪些 log。

使用方法,大致如下:

$ mount -t debugfs none /sys/kernel/debug/
cd /sys/kernel/debug/dynamic_debug/
echo “file xxx.c +p” > control
echo “file svcsock.c line 1603 +p” > control
echo “file drivers/usb/core/* +p” > control
echo “file xxx.c -p” > control

具体地,可以参考:

https://training.ti.com/sites/default/files/docs/Kernel-Debug-Series-Part4-dynamic-debug.pdf


分析 Kernel Panic 的 信息

举个例子,下面是一次 Kernel Panic:

$ cat /sys/class/gpio/gpio504/value
[23.688107] Unable to handle kernel NULL pointer dereference at virtual address 00000000
[23.696431] pgd = (ptrval)
[23.699167] [00000000] *pgd=28bd4831, *pte=00000000, *ppte=00000000
[23.705596] Internal error: Oops: 17 [#1] SMP ARM
[23.710316] Modules linked in:
[23.713394] CPU: 1 PID: 177 Comm: cat Not tainted 4.19.17 #8
[23.719060] Hardware name: Freescale i.MX6 Quad/DualLite (Device Tree)
[23.725606] PC is at mcp23sxx_spi_read+0x34/0x84
[23.730241] LR is at _regmap_raw_read+0xfc/0x384
[23.734866] pc : [<c0539c44>]
lr : [<c067d894>]
psr: 60040013
[23.741142] sp : d8c6da48 ip : 00000009 fp : d8c6da6c
[23.746375] r10: 00000040 r9 : d8a94000 r8 : d8c6db30
[23.751608] r7 : c12ed9d4 r6 : 00000001 r5 : c0539c10 r4 : c1208988
[23.758145] r3 : d8789f41 r2 : 2afb07c1 r1 : d8789f40 r0 : 00000000
[...] // 省略


关键信息:

  • PC is at mcp23sxx_spi_read+0x34
  • pc : [<c0539c44>]

PC 是当前执行的指令的地址。

接下来,我们可以借助 addr2line, 定位到具体是哪一行代码引起了panic:

$ arm-linux-addr2line -f -e vmlinux 0xc0539c44
mcp23sxx_spi_read
/home/sprado/elce/linux/drivers/pinctrl/pinctrl-mcp23s08.c:357

另外,还可以用 gdb 来定位代码:

$ arm-linux-gdb vmlinux
(gdb) list *(mcp23sxx_spi_read+0x34)
0xc0539c44 is in mcp23sxx_spi_read (drivers/pinctrl/pinctrl-mcp23s08.c:357)


earlyprintk

earlyprintk 一般用来处理一些发生在启动初期时的异常。

最常见的现象就是系统打印完 Starting Kernel... 后就 hang 住了。

用法:

1、配置内核:

CONFIG_EARLY_PRINTK
CONFIG_DEBUG_LL

2、设置启动参数,类似:

root=/dev/mmcblk0p2 rootwait rw earlyprintk console=ttyS0,115200


WARN_ON()

这个函数可以打印出当前的函数调用栈。

我一般会在高度可疑的地方使用它。

举个例子:

static int sun6i_spi_probe(struct platform_device *pdev)
{
 struct spi_master *master;
 struct sun6i_spi *sspi;
 [...]

// 用于调试
WARN_ON(1);
    master = spi_alloc_master(&pdev->dev, sizeof(struct sun6i_spi));
    [...]


当运行到 WARN_ON(1) 时,内核会打印:

[    1.847018] WARNING: CPU: 1 PID: 1 at drivers/spi/spi-sun6i.c:549 sun6i_spi_probe+0x20/0x3ac
[    1.855454] Modules linked in:
[    1.858525] CPU: 1 PID: 1 Comm: swapper/0 Not tainted 4.14.111 #196
[    1.864781] Hardware name: sun8i
[    1.868032] [<c02287fc>] (unwind_backtrace) from [<c0225398>] (show_stack+0x10/0x14)
[    1.875776] [<c0225398>] (show_stack) from [<c0a1ba3c>] (dump_stack+0x94/0xa8)
[    1.882997] [<c0a1ba3c>] (dump_stack) from [<c0240c24>] (__warn+0xe8/0x100)
[    1.889953] [<c0240c24>] (__warn) from [<c0240cec>] (warn_slowpath_null+0x20/0x28)
[    1.897517] [<c0240cec>] (warn_slowpath_null) from [<c06a03c0>] (sun6i_spi_probe+0x20/0x3ac)
[    1.905953] [<c06a03c0>] (sun6i_spi_probe) from [<c0617980>] (platform_drv_probe+0x4c/0xb0)
[    1.914299] [<c0617980>] (platform_drv_probe) from [<c06160dc>] (driver_probe_device+0x234/0x2f0)
[    1.923162] [<c06160dc>] (driver_probe_device) from [<c0616244>] (__driver_attach+0xac/0xb0)
[    1.931592] [<c0616244>] (__driver_attach) from [<c06144ec>] (bus_for_each_dev+0x68/0x9c)
[    1.939762] [<c06144ec>] (bus_for_each_dev) from [<c0615654>] (bus_add_driver+0x198/0x210)
[    1.948020] [<c0615654>] (bus_add_driver) from [<c0616aec>] (driver_register+0x78/0xf8)
[    1.956017] [<c0616aec>] (driver_register) from [<c0201a70>] (do_one_initcall+0x40/0x16c)
[    1.964193] [<c0201a70>] (do_one_initcall) from [<c1000e6c>] (kernel_init_freeable+0x1c8/0x264)
[    1.972884] [<c1000e6c>] (kernel_init_freeable) from [<c0a2ef4c>] (kernel_init+0x8/0x114)
[    1.981054] [<c0a2ef4c>] (kernel_init) from [<c0222058>] (ret_from_fork+0x14/0x3c)
[    1.988686] ---[ end trace dc4e090f55ad2de8 ]---

我们可以很清晰地看到 sun6i_spi_probe() 被调用的流程。

这个方法跑起来很简单,但是每次使用都得编译和更新内核,非常不方便,只适合轻度使用。


Pstore

如果发生 Kernel panic 时,我们并没有连接串口终端,那么这一次的崩溃信息就丢失了。

Pstore (persistent storage) 就可以用来处理这种情况。

当发生 Kernel painic 时,Pstore 会自动保存 oops 和 panic 的 log,并且在软重启后仍可以查看 log 信息。

默认情况下,log 是存储在 RAM 的某个保留区域中,但也可以使用存储设备,例如闪存。


用法:

1、配置内核:

CONFIG_PSTORE
CONFIG_PSTORE_RAM

2、配置 dts,为 Pstore 预留一块内存,类似:

reserved-memory {
#address-cells = <1>;
#size-cells = <1>;
ranges;
ramoops: ramoops@0b000000 {
compatible = "ramoops";
reg = <0x20000000 0x200000>; * 2MB */
record-size = <0x4000>; * 16kB */
console-size = <0x4000>; * 16kB */
};
};

3、假设刚发生了一次 Panic,并且已经软重启:

$ mount -t pstore pstore /sys/fs/pstore/

$ ls /sys/fs/pstore/
dmesg-ramoops-0
dmesg-ramoops-1

通过上面这两个文件就可以看到内核的崩溃信息了。

内核文档:

Documentation/admin-guide/ramoops.rst


devmem2

这是一个命令行工具,它可以在用户空间去读写内存。

大多数情况,我是用它来读写寄存器,简单粗暴。

用法:

$ apt-get install devmem2

1、查看寄存器 TMR_IRQ_EN_REG:

$ devmem2 0x0x01C20C00
/dev/mem opened.
Memory mapped at address 0xb6f38000.
Value at address 0x0 (0xb6f38000): 0xEA000016

2、修改 TMR_IRQ_EN_REG:

# devmem2 0x0x01C20C00 w 0xEA000018
/dev/mem opened.
Memory mapped at address 0xb6fe8000.
Value at address 0x0 (0xb6fe8000): 0xEA000016
Written 0xEA000018; readback 0xEA000018


GDB

如果你想完全控制内核的运行,例如单步执行、查看变量等,可以用 GDB。

点击查看大图

这里采用的是 C/S 架构,在板子上运行 server (kgdb),在 PC 机上运行 client (gdb),通讯的方式可以是串口,或者网络,我一般是用串口。


如何配置:

1、配置内核:

CONFIG_KGDB
CONFIG_KGDB_SERIAL_CONSOLE
CONFIG_KGDB_KDB

2、设置启动参数:kgdoc

console=ttyS0,115200 kgdboc=ttyS0,115200 earlyprintk root=/dev/mmcblk0p2 oops=panic panic=0

ttyS0 是板子的调试串口。

要使用 kgdb,必须为其设置一个 I/O driver,我一般使用 kgdb over serial console (简称 kgdboc)

oops=panic panic=0 很重要。

另外,也通过在启动后设置 kgdboc:

echo ttyS0 > /sys/module/kgdboc/parameters/kgdboc

3、让内核进入 debug 模式:

echo g > /proc/sysrq-trigger
[ 1958.025927] sysrq: SysRq : DEBUG
[ 1958.029191] KGDB: Entering KGDB

4、让 PC 机连接板子

$ arm-linux-gdb vmlinux

(gdb) set serial baud 115200
(gdb) target remote /dev/ttyUSB0
Remote debugging using /dev/ttyUSB0
0xc02c3540 in kgdb_breakpoint ()


举个例子:

配置好之后,通过 gdb 调试内核跟通过 gdb 调试应用的操作是一样的。

这里我举一个小例子。

首先,人为让内核 Crash:

echo WRITE_KERN > /sys/kernel/debug/provoke-crash/DIRECT
Entering kdb (current=0xffffffc0de55f040, pid 1470) on processor 4 Oops: (null)
due to oops @ 0xffffff80108bfa48
CPU: 4 PID: 1470 Comm: bash Not tainted 5.3.0-rc2+ #13
pc : __memcpy+0x48/0x180
lr : lkdtm_WRITE_KERN+0x4c/0x90
...

下面开始调试。

1、查看调用栈:

(gdb) bt
Call trace:
dump_backtrace+0x0/0x138
show_stack+0x20/0x2c
kdb_show_stack+0x60/0x84
...
do_mem_abort+0x4c/0xb4
el1_da+0x20/0x94
__memcpy+0x48/0x180
lkdtm_do_action+0x24/0x44
direct_entry+0x130/0x178

2、查看栈帧的内容:

(gdb) frame 1
#1 0xffffff801056584c in lkdtm_WRITE_KERN () at .../drivers/misc/lkdtm/perms.c:116
116
memcpy(ptr, (unsigned char *)do_nothing, size);

基本可以确定是使用 memcpy() 时导致 Crash。

3、查看相关代码:

(gdb) list
112  size = (unsigned long)do_overwritten - (unsigned long)do_nothing;
[...]
116  memcpy(ptr, (unsigned char *)do_nothing, size);

需要核查一下 ptr、do_nothing、size,这 3 个参数是否合法。

4、打印变量值:

(gdb) print size
$3 = 18446744073709551584

(gdb) print do_overwritten - do_nothing
$4 = -32

最后发现 18446744073709551584 其实就是 (unsigned long) 的 -32。memcpy 的数据大小是 -32,导致了内核崩溃。


Ftrace

Ftrace 的作用是帮助开发人员了解 Linux 内核的运行时行为,以便进行故障调试或性能分析。

最早 Ftrace 是一个 function tracer,仅能够记录内核的函数调用流程。如今 ftrace 已经成为一个 framework,采用 plugin 的方式支持开发人员添加更多种类的 trace 功能。

用法:

$ mount -t tracefs none /sys/kernel/tracing
cd /sys/kernel/tracing/
$ cat available_tracers
hwlat blk mmiotrace function_graph wakeup_dl wakeup_rt wakeup function nop

跟踪器 tracer 表示的是要跟踪的目标。

假设我们抓一次 spi 传输的过程:

echo 0 > tracing_on
echo function_graph > current_tracer
echo *spi* > set_ftrace_filter
echo *dma* >> set_ftrace_filter
echo *spin* >> set_ftrace_notrace
echo 1 > tracing_on
./spidev_test
echo 0 > tracing_on
cat trace

得到的信息:

1) + 41.292 us   |  spidev_open();
 1)               |  spidev_ioctl() {
 1)               |    spi_setup() {
 1)   0.417 us    |      __spi_validate_bits_per_word.isra.0();
 1)               |      sunxi_spi_setup() {
 1)   0.834 us    |        sunxi_spi_check_cs();
 1)   0.875 us    |        spi_set_cs();
 1)   0.625 us    |        sunxi_spi_cs_control();
 1) + 17.125 us   |      }
 1)   0.833 us    |      spi_set_cs();
 1) + 30.458 us   |    }
 1) ! 699.875 us  |  }
     [...]

相关参考:

https://blog.csdn.net/Guet_Kite/article/details/101791125


Kdump

这个工具我没有用过,但是它似乎很强大,所以我觉得应该简单介绍一下。

kdump 是一种基于 kexec 系统调用 的内核崩溃转储机制。

当系统崩溃时,kdump 使用 kexec 启动进入到第二个内核 (dump-capture kernel),从而获得 coredump 信息。

用法:

1、设置启动参数:

crashkernel=64M

2、运行 kexec:

$ kexec --type zImage -p /boot/zImage \
   --initrd=<initrd-for-dump-capture-kernel> \
   --dtb=<dtb-for-dump-capture-kernel> \
   --command-line="XXX"

运行完 kexec 后,dump-capture kernel 就被加载进内存了。

以后如果发生了 kernel panic,dump-capture kernel 会被加载并运行。

我们可以在 dump-capture kernel 下,获得 coredump 文件:

$ cp /proc/vmcore <dump-file>

然后就可以在 PC 上使用 gdb/crash 来调试分析了:

$ arm-linux-gdb path/to/vmlinux -c path/to//vmcore
$ crash path/to/vmlinux path/to/vmcore

内核文档:

Documentation/kdump/kdump.txt


总结

预防为主,调试为辅。

软件开发没有银弹,同样的,bug 调试也没有银弹。但是多熟悉一些调试工具,是有好处的。

当然还有很多调试工具、技巧是我不知道了,欢迎大家分享给我。

Anyway, what we know is a drop, what we don't know is an ocean.

祝周末愉快。

—— The End ——



感谢完成阅读,我是喜欢打篮球的写代码的篮球球痴,这个是我的公众号,感谢你关注并支持。我从大学开始接触电子和嵌入式软件知识,至今,已经毕业工作了9年,我喜欢嵌入式,也愿意从事这个行业。不管是从技术还是职场经验,都积累了足够多的经验,目前在一个非常优秀的团队中做开发工作。


很高兴认识每一个对技术努力,对人用心的朋友。



关注公众号,后台回复「1024」获取学习资料网盘链接。

欢迎点赞,关注,转发,在看,您的每一次鼓励,我都将铭记于心~

嵌入式Linux

微信扫描二维码,关注我的公众号

文章转载自嵌入式Linux,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论