Linux 内核调试

肺炎疫情憋在家里,总结一下很久之前的学习笔记,先说说系统开发工程都需要的内核调试方面。主要是结合以前的项目调试经验和笔记,欢迎大家补充,还记得以前从0 开始写一个bootloader,把一个linux 系统在三星s3c2410上跑起来,基本就是使用printk 来调试的。

 

Linux 内核调试

上面这一段就是一个典型的IC 原厂的工程师在最开始把linux移植到芯片上的bootargs,是bootloader 启动时传给kenerl的参数,因为一个芯片从来没有跑过linux,基本从0开始,根据经验,在没有仿真器的情况下,是完全可以把linux跑起来。源码级调试的需求在linux工程上是非常小的,linux主要是靠经验及printk去推测代码路径,只有极少数情况才会用到仿真器,比如硬件有什么故障,总线hang死了,CPU模式切换,内存错了等非常底层问题才可能用到仿真器。当然这些小概率的问题没仿真器是没法调试,因为代码跟不进去,不过这种情况通常是IC原厂的工程师会做的事情。

  1. early printk(early console) 实现及initcall

    在我们820a 平台上,这些文件是非常lowlevel的打印语句(uart driver驱动子系统起来之前),是通过汇编语言实现的非常简单的打印语句,这些语句可以在linux非常早期就可以使用,比如刚把linux 解压完,“uncompress linux”就可以打印出来了。

Linux 内核调试

Linux 内核调试

Kenel config如下:

Linux 内核调试

所以在调试linux早期阶段,会利用这种lowlevel的手段调用打印跟踪启动过程,之后linux就会调用到很多初始化init function。 初始化也有不同的level,其中很多也是驱动的初始化,Linux使用一个for循环把所有函数调用一段,他不是手工的去掉,他是把每个初始化函数的指针放在一个特殊的数据段里面,再利用一个for循环取出这个数据段的第一个函数取出来,一直调用到最后一个指针。这个过程可能有几百个initcall,这些initcall有些是linux自带的,有些是我们自己写的,所以这个过程中initclal可能会直接挂掉,需要有一个办法在开机时就打印出来,linux有一个方式是initccall debug,通常我们再bootarges 中传入一个initcall_debug,linux就会把每个初始化函数什么时候调用的,及返回是什么全部打印出来,如果某个calling打印之后就没有打印了,说明这个calling就卡住了。

Linux 内核调试

printk虽然非常简单,但是作为一个linux工程师不能一上来就直接一个printk,显得非常不专业,专业的linux工程师一般调用printk的两个变体,一个是dev_xxx,pr_xxx, 我们一般在驱动中调用dev_xxx(dev_info, dev_err 等),这个会自动给你打印增加设备的打印前缀。这样就一目了然,不会混乱,比如你是spi,就会打印spi***之类的。有时候我们仅仅增加一个模块(非设备驱动中),这时候我们可以使用pr_XXX(pr_info, pr_err...).这个时候我们可以在模块最前面定义一个pr_fmt, 这个整个模块的pr打印自动会添加签名pr_fmt定义的前缀

Linux 内核调试

Linux 内核调试

 

还有一种情况在工作中遇到比较多,就是在打印中加上自己的名字,尽量针对需要的打印的特点,功能,模块添加打印信息,加个名字显得很怪,搞不好还要遗臭万年,嘿嘿。

Linux 内核调试

Linux printk 比我们想象的要好用,printk 这个函数不像printf,因为printk不会引起睡眠,在软中断,中断,spinlock里面等,基本所有的上下文都可以调用。

当然,调得太多,也会让系统变慢,所以我们有时候需要基于linux本身的理解,防止影响系统,所以一个牛x的工程能够在正确的位置加printk。

Linux内部函数调用有时候是非常深的,我们有时候直接在最底层函数加一个dump_stack()或者使用warn_on(1) ,内核调到这里就会把调用栈打印出来。

  1. 源码级调试

   工程上通常去淘宝买个jtag 仿真器就可以了,插根网线或者usb就可以gdb 调试了。Arm官方的是DS-5,这个东西功能很强大可以结合eclipse 调试,但是对于我们来说,图形界面再强大,还是基于对linux代码的理解去调试。

通常可以利用qemu 或者仿真器连接板子,再使用gdb vmlinux 启动,使用target remote ip:port 连接目标板,这样就可以进行源码调试了。

Linux 内核调试

按下ctr+x 抬起来按下a,就分裂两个窗口,一个源码,一个命令,这样就进行源码级的调试了,

Linux 内核调试

  1. 内核模块源码级调试

   模块调试与kernel 有点不一样,因为模块是insmod之后才知道哪些代码段,数据段在哪里。 所以先进入模块的sections 获取代码段,数据段,再利用add-symbol-file 传递数据、代码段地址,就可以调试了。

Linux 内核调试

  1. 待机调试

一个真实的故事,一个工程师调手机的待机,一个电路板上有很多个硬件,这个硬件的驱动里面都有一个suspend,之前suspend的时候系统会hang住,这个工程师之前花了一个星期在所有的suspend函数加上printk打印调试,其实这样如此通用的需求,很多公司很多人都有,最后其实只要在bootargs,加入no_console suspend ,以及pm相关的debug打开,这样suspend的时候,linux自动把suspend的信息打开, 从这个例子之后,我明白了一个linux常识,linux是一个如此强大生态,怎么轮到我发明一份新的需求,也不会已如此野蛮,呆板的实现方式,很多牛人早就想到了,并实现了一套牛x的方式。

 

 

  1. OOPS & Panic

    其实oops已经完整告诉了内核出错信息,出错的原因,比如下面例子是访问了一个NULL指针,出错的PC指针位置是globalfifo_read + 0x50/0x200(flobalfifo_read 最开始一条指令偏移0x50的位置,flobalfifo_read 整个代码段加起来是0x200) ,之后会列出完整的backtrace。

Linux 内核调试

 

这时候就需要反汇编来调试,一般反汇编ko,或者对应的.o 文件就可以了,没必要反汇编整个内核,那样时间就非常久了。

工程上objdump比用addr2ine 要更全面和有助于调试

Linux 内核调试

打开反汇编结果,找到出错的globalfifo_read ,再找到偏移 0x394 +0x50 =0x3e4, 不仅找到了出错的c代码,也可以精确到汇编。

Linux 内核调试

 

 

Linux 内核调试

 

 

Oops 与 Panic不是一回事,他们打印是差不多的,Panic 之后内核一定是挂了,oops 不会崩溃。Oops 一般是在进程上下文,所以内核通常只是把进程挂掉,中断上下文的oops 会导致panic,当然如果我们设置了panic_on_oops 为1 ,就都会panic。工程上通常会把panic_on_oops 上打开,因为不确定oops到底改了哪些东西,后面搞不好会有莫名其妙的错误,使得调试更加复杂。

  1. grabserial

   这个工具会打印两个时间戳,第一个是接收到打印的时间,第二个是与上一个打印的时间差,这样对我们优化开机时间比较有帮助。

 

Linux 内核调试

  1. 内核的debug选项

   内核中有很多的debug选项,默认大部分是不打开的一般在调试某个功能的时候把相关选项打开,比如调试电源管理,需要把no console suspend打开。Linux是一个如此强大的生态,基本上你想到的所有的需求,linux都为你准备好了,不要自己去摸索创造。

比如在spinlock 或者中断上下文调用了sleep,崩溃的位置不一定在中断或者spinlock里面。所以很难去调试,内核里面有个DEBUG_ATOMIC_SLEEP 打开,内核中spinlock中只要调用了sleep相关的函数(mutex malloc),内核就会打印出来,很多错误都可以去google 查找到相关的问题。通常我们费尽脑汁想的事情,别人早就想过了。

  Linux 内核调试

对于内存的践踏,或者多次free,或者free之后访问等问题。

举一个kmalloc 内存泄漏的例子。曾经有个研究所有一个内存泄漏的问题,他们的平台是芯片厂商直接提供的BSP,他们在上面跑一个qt程序,发现跑个把月之后内存就耗尽了,一直查,各种怀疑人生,怀疑自我,不断替换qt版本,去掉应用程序的等,搞了一年没有搞定,最后要交货了没办法,找了咨询机构解决。咨询的工程师只花了3天解决。

  1. 首先看是不是内存泄漏,发现Free命令确实是随着时间推移,内存在减少
  2. 接着看看所有应用程序的uss 追踪一段时间,没有变化
  3. 再看看proc/meminfo 里面的slab区域,发现随着时间的推移,slab区域在变化,再进一步查看一下proc/slabinfo ,slabinfo会显示内核每个slab object分配情况,发现里面有个很小的kmalloc区域在慢慢变大,从而确定是内核的内存泄漏
  4. 把内核的kmemleak 去监控内存的泄漏,最后发现是芯片公司给他们的BSP 有个bug,里面有个指针是kmalloc 出来,指针指向的内容也是kmalloc出来的,芯片BSP工程师范了个错误,他只释放了指针指向的内容的内存,指针没有释放,所以导致每次就泄漏几个字节

所以linux工程师 懂linux是一个基础,否则就会话大量的时间解决不了问题,最后怀疑人生。比如这个,懂内存管理的话,很容易查到问题所在。

Double free:

Linux 内核调试

Linux 内核调试

 

 

 

Kmalloc内存越界:

Linux 内核调试

Linux 内核调试

 

Kmalloc内存泄漏(注意在启动参数中使能kmemleak=on,内核congfig只配置没使能)

Linux 内核调试

 

这个文件会把怀疑的内存泄漏点记录下来

Linux 内核调试

 

  1. Lockup 检测

   Linux 内核调试

  对于softlockup ,比如某个线程调用了spinlock,这时候cpu抢占就关闭了,linux会调用一个高优先级的实时线程,线程里面有个计数器每次加一,并且有个定时器判断计数器是否有没有加1 ,计数器来了之后中断中检测计数器一直没更新,就证明系统已经softlockup了,此时系统会打印出来,定位到backtrace。

Linux 内核调试

 对于hardlockup,比如某个cpu上连中断都锁住了,中断都无法进入。Linux里面必须使用NMI 非可屏蔽的中断,(内核编译的时候使能)。

所以调试的时候发现内核没有响应了,这时候就可以打开lockup调试

 

再多的工具和手段也代替不了解决问题的思维,只有不断的去实践,才能找到解决问题的“感觉”