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

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

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

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

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

好,下面开始正文。

最重要的是:思路

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

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

1、理解问题;

2、重现问题;

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

4、尝试修复问题;

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

bug一般分为这几类:

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

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

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

4、Resourceleak,偶尔会遇到;

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

调试工具的类别:

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

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

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

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

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


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

最常用的方法:打印

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

1、printk()

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

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


2、pr_*()

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

例如:

pr_info("BootingCPU%d\n",cpu);

内核会打印:

[202.350064]BootingCPU1


3、dev_*()

同样是一个函数族:

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

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

例如:

dev_info(pdev-dev,"inprobe\n");

内核会打印:

[25.878382]:inprobe


关于pr_debug()anddev_dbg()

要使用这两个api,需要在对应的代码里1]SMPARM
[23.710316]Moduleslinkedin:
[23.713394]CPU:1PID:177Comm:
[1.864781]Hardwarename: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_+0x20/0x28)
[1.897517][c0240cec](warn_slowpath_)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]---[tracedc4e090f55ad2de8]---

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

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


Pstore

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

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

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

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


用法:

1、配置内核:

CONFIG_PSTORE
CONFIG_PSTORE_RAM

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

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

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

$mount-tpstorepstore/sys/fs/pstore/

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

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

内核文档:


devmem2

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

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

用法:

$apt-getinstalldevmem2

1、查看寄存器TMR_IRQ_EN_REG:

$devmem20x0x01C20C00
/dev/memopened.
Memorymappedataddress0xb6f38000.
Valueataddress0x0(0xb6f38000):0xEA000016

2、修改TMR_IRQ_EN_REG:

13
pc:__memcpy+0x48/0x180
lr:lkdtm_WRITE_KERN+0x4c/0x90

下面开始调试。

1、查看调用栈:

(gdb)bt
Calltrace:
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)frame1
#10xffffff801056584cinlkdtm_WRITE_KERN()at/drivers/misc/lkdtm/:116
116
memcpy(ptr,(unsignedchar*)do_nothing,size);

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

3、查看相关代码:

(gdb)list
112size=(unsignedlong)do_overwritten-(unsignedlong)do_nothing;
[]
116memcpy(ptr,(unsignedchar*)do_nothing,size);

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

4、打印变量值:

(gdb)printsize
$3=709551584

(gdb)printdo_overwritten-do_nothing
$4=-32

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


Ftrace

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

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

用法:

$mount-ttracefsnone/sys/kernel/tracing
$cd/sys/kernel/tracing/
$catavailable_tracers
hwlatblkmmiotracefunction_graphwakeup_dlwakeup_rtwakeupfunctionnop

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

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

echo0tracing_on
echofunction_graphcurrent_tracer
echo*spi*set_ftrace_filter
echo*dma*set_ftrace_filter
echo*spin*set_ftrace_notrace
echo1tracing_on
./spidev_test
echo0tracing_on
cattrace

得到的信息:

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

相关参考:


Kdump

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

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

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

用法:

1、设置启动参数:

crashkernel=64M

2、运行kexec:

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

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

以后如果发生了kernelpanic,dump-capturekernel会被加载并运行。

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

$cp/proc/vmcoredump-file

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

$arm-linux-gdbpath/to/vmlinux-cpath/to//vmcore
$crashpath/to/vmlinuxpath/to/vmcore

内核文档:


总结

预防为主,调试为辅。

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

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

Anyway,whatweknowisadrop,whatwedon'tknowisanocean.

祝周末愉快。

——The——