内核开发比用户空间开发更难的一个因素就是内核调试艰难。内核错误往往会导致系统宕机,很难保留出错时的现场。调试内核的关键在于你的对内核的深刻理解。
嵌入式进阶教程分门别类整理好了,看的时候十分方便,由于内容较多,这里就截取一部分图吧。
在调试一个bug之前,我们所要做的准备工作有:
有一个被确认的bug,包含这个bug的内核版本号,需要分析出这个bug在哪一个版本被引入,这个对于解决问题有极大的帮助。可以采用二分查找法来逐步锁定bug引入版本号。
对内核代码理解越深刻越好,同时还需要一点点运气,该bug可以复现。如果能够找到规律,那么离找到问题的原因就不远了;最小化系统。把可能产生bug的因素逐一排除掉。
内核中的bug 内核中的bug也是多种多样的。它们的产生有无数的原因,同时表象也变化多端。从隐藏在源代码中的错误到展现在目击者面前的bug,其发作往往是一系列连锁反应的事件才可能触发的。虽然内核调试有一定的困难,但是通过你的努力和理解,说不定你会喜欢上这样的挑战。
内核调试配置选项 学习编写驱动程序要构建安装自己的内核(标准主线内核)。最重要的原因之一是:内核开发者已经建立了多项用于调试的功能。但是由于这些功能会造成额外的输出,并导致能量下降,因此发行版厂商通常会禁止发行版内核中的调试功能。
内核配置
为了实现内核调试,在内核配置上增加了几项:
kernel hacking ---> [*] magic sysrq key [*] kernel debugging [*] debug slab memory allocations [*]
spinlock and rw-lock debugging: basic checks [*]
spinlock debugging: sleep-inside-spinlock checking [*]
compile the kernel with debug info device drivers ---> generic driver options ---> [*]
driver core verbose debug messages general setup ---> [*]
configure standard kernel features (for small systems) ---> [*]
load all symbols for debugging/ksymoops 调试原子操作 从内核2.5开发,为了检查各类由原子操作引发的问题,内核提供了极佳的工具。 内核提供了一个原子操作计数器,它可以配置成,一旦在原子操作过程中,经常进入睡眠或者做了一些可能引起睡眠的操作,就打印警告信息并提供追踪线索。 所以,包括在使用锁的时候调用schedule(),正使用锁的时候以阻塞方式请求分配内存等,各种潜在的bug都能够被探测到。
下面这些选项可以最大限度地利用该特性:
config_preempt = y config_debug_kernel = y config_kllsyms = y config_spinlock_sleep = y 引发bug并打印信息 bug()和bug_on()
一些内核调用可以用来方便标记bug,提供断言并输出信息。最常用的两个是bug()和bug_on()。定义在中:
#ifndef have_arch_bug #define bug() do { printk(bug: failure at %s:%d/%s()! , __file__, __line__, __function__); panic(bug!); /* 引发更严重的错误,不但打印错误消息,而且整个系统业会挂起 */ } while (0) #endif #ifndef have_arch_bug_on #define bug_on(condition) do { if (unlikely(condition)) bug(); }while(0) #endif 当调用这两个宏的时候,它们会引发oops,导致栈的回溯和错误消息的打印。
※ 可以把这两个调用当作断言使用,如:bug_on(bad_thing);
warn(x) 和 warn_on(x)
而warn_on则是调用dump_stack,打印堆栈信息,不会oops。定义在中:
#ifndef __warn_taint#ifndef __assembly__extern void warn_slowpath_fmt( const char *file, const int line, const char *fmt, ...) __attribute__((format(printf, 3, 4)));extern void warn_slowpath_fmt_taint(const char *file, const int line, unsigned taint, const char *fmt, ...) __attribute__((format(printf, 4, 5)));extern void warn_slowpath_null(const char *file, const int line);
#define want_warn_on_slowpath
#endif
#define __warn() warn_slowpath_null(__file__, __line__)
#define __warn_printf(arg...) warn_slowpath_fmt(__file__, __line__, arg)
#define __warn_printf_taint(taint, arg...) warn_slowpath_fmt_taint(__file__, __line__, taint, arg)
#else
#define __warn() __warn_taint(taint_warn)
#define __warn_printf(arg...) do { printk(arg); __warn();
} while (0)
#define __warn_printf_taint(taint, arg...) do { printk(arg); __warn_taint(taint);
} while (0)
#endif#ifndef warn_on#define warn_on(condition) ({
int __ret_warn_on = !!(condition); if (unlikely(__ret_warn_on)) __warn(); unlikely(__ret_warn_on); })
#endif
#ifndef warn#define warn(condition, format...) ({
int __ret_warn_on = !!(condition); if (unlikely(__ret_warn_on)) __warn_printf(format); unlikely(__ret_warn_on); })
#endif
dump_stack()
有些时候,只需要在终端上打印一下栈的回溯信息来帮助你调试。这时可以使用dump_stack()。这个函数只是在终端上打印寄存器上下文和函数的跟踪线索。
if (!debug_check) { printk(kern_debug “provide some information…/n”); dump_stack(); }
printk() 内核提供的格式化打印函数。
printk函数的健壮性
健壮性是printk最容易被接受的一个特质,几乎在任何地方,任何时候内核都可以调用它(中断上下文、进程上下文、持有锁时、多处理器处理时等)。
printk函数脆弱之处
在系统启动过程中,终端初始化之前,在某些地方是不能调用的。如果真的需要调试系统启动过程最开始的地方,有以下方法可以使用:
使用串口调试,将调试信息输出到其他终端设备。
使用early_printk(),该函数在系统启动初期就有打印能力。但它只支持部分硬件体系。
log等级
printk和printf一个主要的区别就是前者可以指定一个log等级。内核根据这个等级来判断是否在终端上打印消息。内核把比指定等级高的所有消息显示在终端。 可以使用下面的方式指定一个log级别:
printk(kern_crit “hello, world! ”); 注意,第一个参数并不一个真正的参数,因为其中没有用于分隔级别(kern_crit)和格式字符的逗号(,)。kern_crit本身只是一个普通的字符串(事实上,它表示的是字符串 ;表 1 列出了完整的日志级别清单)。
作为预处理程序的一部分,c 会自动地使用一个名为 字符串串联 的功能将这两个字符串组合在一起。组合的结果是将日志级别和用户指定的格式字符串包含在一个字符串中。
内核使用这个指定log级别与当前终端log等级console_loglevel来决定是不是向终端打印。下面是可使用的log等级:
#define kern_emerg /* system is unusable */#define kern_alert /* action must be taken immediately */ #define kern_crit /* critical conditions */#define kern_err /* error conditions */#define kern_warning /* warning conditions */#define kern_notice /* normal but significant condition */#define kern_info /* informational */#define kern_debug /* debug-level messages */#define kern_default /* use the default kernel loglevel */
注意,如果调用者未将日志级别提供给 printk,那么系统就会使用默认值 kern_warning (表示只有kern_warning 级别以上的日志消息会被记录)。由于默认值存在变化,所以在使用时最好指定log级别。有log级别的一个好处就是我们可以选择性的输出log。
比如平时我们只需要打印kern_warning级别以上的关键性log,但是调试的时候,我们可以选择打印kern_debug等以上的详细log。而这些都不需要我们修改代码,只需要通过命令修改默认日志输出级别:
mtj@ubuntu :~$ cat /proc/sys/kernel/printk4 4 1 7mtj@ubuntu :~$ cat /proc/sys/kernel/printk_delay0mtj@ubuntu :~$ cat /proc/sys/kernel/printk_ratelimit5mtj@ubuntu :~$ cat /proc/sys/kernel/printk_ratelimit_burst10
第一项定义了 printk api 当前使用的日志级别。这些日志级别表示了控制台的日志级别、默认消息日志级别、最小控制台日志级别和默认控制台日志级别。printk_delay 值表示的是 printk 消息之间的延迟毫秒数(用于提高某些场景的可读性)。
注意,这里它的值为 0,而它是不可以通过的 /proc 设置的。
printk_ratelimit 定义了消息之间允许的最小时间间隔(当前定义为每 5 秒内的某个内核消息数)。消息数量是由 printk_ratelimit_burst 定义的(当前定义为 10)。
如果您拥有一个非正式内核而又使用有带宽限制的控制台设备(如通过串口), 那么这非常有用。注意,在内核中,速度限制是由调用者控制的,而不是在printk 中实现的。如果一个 printk 如果用户要求进行速度限制,那么该用户就需要调用printk_ratelimit 函数。
记录缓冲区
内核消息都被保存在一个log_buf_len大小的环形队列中。
关于log_buf_len定义:
#define __log_buf_len (1 << config_log_buf_shift)
※ 变量config_log_buf_shift在内核编译时由配置文件定义,对于i386平台,其值定义如下(在
linux26/arch/i386/defconfig中):
config_log_buf_shift=18
记录缓冲区操作:
① 消息被读出到用户空间时,此消息就会从环形队列中删除。
② 当消息缓冲区满时,如果再有printk()调用时,新消息将覆盖队列中的老消息。
③ 在读写环形队列时,同步问题很容易得到解决。
※ 这个纪录缓冲区之所以称为环形,是因为它的读写都是按照环形队列的方式进行操作的。
syslogd/klogd
在标准的linux系统上,用户空间的守护进程klogd从纪录缓冲区中获取内核消息,再通过syslogd守护进程把这些消息保存在系统日志文件中。klogd进程既可以从/proc/kmsg文件中,也可以通过syslog()系统调用读取这些消息。默认情况下,它选择读取/proc方式实现。klogd守护进程在消息缓冲区有新的消息之前,一直处于阻塞状态。一旦有新的内核消息,klogd被唤醒,读出内核消息并进行处理。默认情况下,处理例程就是把内核消息传给syslogd守护进程。syslogd守护进程一般把接收到的消息写入/var/log/messages文件中。不过,还是可以通过/etc/syslog.conf文件来进行配置,可以选择其他的输出文件。
dmesg
dmesg 命令也可用于打印和控制内核缓冲区。这个命令使用 klogctl 系统调用来读取内核环缓冲区,并将它转发到标准输出(stdout)。这个命令也可以用来清除内核环缓冲区(使用 -c 选项),设置控制台日志级别(-n 选项),以及定义用于读取内核日志消息的缓冲区大小(-s 选项)。注意,如果没有指定缓冲区大小,那么 dmesg 会使用 klogctl 的syslog_action_size_buffer 操作确定缓冲区大小。
注意
a) 虽然printk很健壮,但是看了源码你就知道,这个函数的效率很低:做字符拷贝时一次只拷贝一个字节,且去调用console输出可能还产生中断。所以如果你的驱动在功能调试完成以后做性能测试或者发布的时候千万记得尽量减少printk输出,做到仅在出错时输出少量信息。否则往console输出无用信息影响性能。
b) printk的临时缓存printk_buf只有1k,所有一次printk函数只能记录 /dynamic_debug/control
参考:
1 内核日志及printk结构浅析 -- tekkaman ninja
2 内核日志:api 及实现
3 printk实现分析
4 dynamic-debug-howto.txt
内存调试工具 memwatch
memwatch 由 johan lindh 编写,是一个开放源代码 c 语言内存错误检测工具,您可以自己下载它。只要在代码中添加一个头文件并在 gcc 语句中定义了 memwatch 之后,您就可以跟踪程序中的内存泄漏和错误了。memwatch 支持ansic,它提供结果日志纪录,能检测双重释放(double-free)、错误释放(erroneous free)、没有释放的内存(unfreedmemory)、溢出和下溢等等。
清单 1. 内存样本(test1.c)
#include #include #include memwatch.hint main(void){ char *ptr1; char *ptr2; ptr1 = malloc(512); ptr2 = malloc(512); ptr2 = ptr1; free(ptr2); free(ptr1);}
清单 1 中的代码将分配两个 512 字节的内存块,然后指向第一个内存块的指针被设定为指向第二个内存块。结果,第二个内存块的地址丢失,从而产生了内存泄漏。
现在我们编译清单 1 的 memwatch.c。
下面是一个 makefile 示例:test1gcc -dmemwatch -dmw_stdio test1.c memwatchc -o test1 当您运行 test1 程序后,它会生成一个关于泄漏的内存的报告。清单 2 展示了示例 memwatch.log 输出文件。
清单 2. test1 memwatch.log 文件
memwatch 2.67 copyright (c) 1992-1999 johan lindh...double-free: test1.c(15), 0x80517b4 was freed from test1.c(14)...unfreed: test1.c(11), 512 bytes at 0x80519e4{fe fe fe fe fe fe fe fe fe fe fe fe ..............}memory usage statistics (global): n)umber of allocations made: 2 l)argest memory usage : 1024 t)otal of all alloc() calls: 1024 u)nfreed bytes totals : 512
memwatch 为您显示真正导致问题出现的信息。如果您释放一个已经释放过的指针,它会告诉您。对于没有释放的内存也一样。日志结尾部分显示统计信息,包括泄露了多少内存,使用了多少内存,以及总共分配了多少内存。
yamd
yamd 软件包由 nate eldredge 编写,可以查找 c++ 和 c++ 动态的、与内存分配有关的问题。在撰写本文时,yamd 的最新版本为 0.32。请下载 yamd-0.32.tar.gz。执行 make 命令来构建程序;然后执行 make install 命令安装程序并设置工具。
一旦您下载了 yamd 之后,请在 test1.c 上使用它。请删除 #include memwatch.h 并对 makefile 进行如下小小的修改:
使用 yamd 的 test1
gcc -g test1.c -o test1
清单 3 展示了来自 test1 上的 yamd 的输出。
清单 3. 使用 yamd 的 test1 输出
yamd version 0.32executable: /usr/src/test/yamd-0.32/test1...info: normal allocation of this blockaddress 0x40025e00, size 512...info: normal allocation of this blockaddress 0x40028e00, size 512...info: normal deallocation of this blockaddress 0x40025e00, size 512...error: multiple freeing atfree of pointer already freedaddress 0x40025e00, size 512...warning: memory leakaddress 0x40028e00, size 512warning: total memory leaks:1 unfreed allocations totaling 512 bytes*** finished at tue ... 1015 2002allocated a grand total of 1024 bytes 2 allocationsaverage of 512 bytes per allocationmax bytes allocated at one time: 102424 k alloced internally / 12 k mapped now / 8 k maxvirtual program size is 1416 kend.
yamd 显示我们已经释放了内存,而且存在内存泄漏。让我们在清单 4 中另一个样本程序上试试 yamd。
清单 4. 内存代码(test2.c)
#include #include int main(void){ char *ptr1; char *ptr2; char *chptr; int i = 1; ptr1 = malloc(512); ptr2 = malloc(512); chptr = (char *)malloc(512); for (i; i eip; c01588fc code; c01588fc 00000000 :code; c01588fc <===== 0: 8b 2d 00 00 00 00 mov 0x0,%ebp 6: 55 push %ebp
接下来,您要确定 jfs_mount 中的哪一行代码引起了这个问题。oops 消息告诉我们问题是由位于偏移地址 3c 的指令引起的。做这件事的办法之一就是对 jfs_mount.o 文件使用 objdump 使用程序,然后查看偏移地址 3c。objdump 用来反汇编模块函数,看看您的 c 源代码会产生什么汇编指令。清单 8 显示了使用 objdump 后您将看到的内容,接着,我们查看jfs_mount 的 c 代码,可以看到空值是第 109 行引起的。偏移地址 3c 之所以这么重要,是因为 oops 消息将该处标识为引起问题的位置。
清单 8. jfs_mount 汇编程序清单
109 printk(%d ,*ptr);objdump jfs_mount.ojfs_mount.o: file format elf32-i386disassembly of section .text:00000000 : 0:55 push %ebp ... 2c: e8 cf 03 00 00 call 400 31: 89 c3 mov %eax,%ebx 33: 58 pop %eax 34: 85 db test %ebx,%ebx 36: 0f 85 55 02 00 00 jne 291 0x291> 3c: 8b 2d 00 00 00 00 mov 0x0,%ebp < /proc/sysrq-trigger 这造成内核崩溃,如配置有效,系统将重启进入 kdump 内核,当系统进程进入到启动 kdump 服务的点时,vmcore 将会拷贝到你在 kdump 配置文件中设置的位置。rhel 的缺省目录是 : /var/crash;sles 的缺省目录是 : /var/log/dump。然后系统重启进入到正常的内核。一旦回复到正常的内核,就可以在上述的目录下发现 vmcore 文件,即内存转储文件。可以使用之前安装的 kernel-debuginfo 中的 crash 工具来进行分析(crash 的更多详细用法将在本系列后面的文章中有介绍)。
# crash /usr/lib/debug/lib/modules/2.6.17-1.2621.el5/vmlinux /var/crash/2006-08-23-15:34/vmcore crash> bt 载入“转储捕获”内核
需要引导系统内核时,可使用如下步骤和命令载入“转储捕获”内核:
kexec -p --initrd=for-dump-capture-kernel> --args-linux --append=root= init 1 irqpoll 装载转储捕捉内核的注意事项:
转储捕捉内核应当是一个 vmlinux 格式的映像(即是一个未压缩的 elf 映像文件),而不能是 bzimage 格式;
默认情况下,elf 文件头采用 elf64 格式存储以支持那些拥有超过 4gb 内存的系统。但是可以指定“--elf32-core-headers”标志以强制使用 elf32 格式的 elf 文件头。这个标志是有必要注意的,一个重要的原因就是:当前版本的 gdb 不能在一个 32 位系统上打开一个使用 elf64 格式的 vmcore 文件。elf32 格式的文件头不能使用在一个“没有物理地址扩展”(non-pae)的系统上(即:少于 4gb 内存的系统);
一个“irqpoll”的启动参数可以减低由于在“转储捕获内核”中使用了“共享中断”技术而导致出现驱动初始化失败这种情况发生的概率 ;
必须指定 ,指定的格式是和要使用根设备的名字。具体可以查看 mount 命令的输出;“init 1”这个命令将启动“转储捕捉内核”到一个没有网络支持的单用户模式。如果你希望有网络支持,那么使用“init 3”。
后记
kdump 是一个强大的、灵活的内核转储机制,能够在生产内核上下文中执行捕获内核是非常有价值的。本文仅介绍在 rhel6.2 和 sles11 中如何配置 kdump。
新唐科技W77L032A主板简介
Luxtera和意法半导体实现量产硅光电解决方案
什么是零点和极点?时域上系统稳定性和S域的稳定性有什么关系?
微波、射频、电磁领域相关的学术期刊 -国内期刊
互联网的驱动力源自于哪里
Linux内核调试方法
开关电源EMI输入滤波器确定fcn的方法
简洁易作的胆功放,Vacuum Tube Amplifier
智能模组为何是车载终端的核心
一加5怎么样?一加评测:价格2999元起骁龙835双摄拍照6GB/8GB大内存一加5正式发布!
超强悍的UDOO X86开发板在贸泽开售
bonding技术优势和技术的应用
洗衣机行业进入“健康赛道” “超微净泡”独领风骚
华为自研地图App 未来或将登录车载导航
三星延后液晶面板停产计划
介绍一款专用的电机设计和分析工具
Surface Laptop值得买吗?Surface Laptop拆解:没有升级配置的余地
直流接触器过电流故障的原因分析
智能跳绳蓝牙芯片MS1656解决方案
上海将于本月14日举行2019年全国大学生5G物联网创意竞赛决赛