大家好,我是你们的工具人老吴。
今天,和大家分享一下几个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——