第十八章 调试

【学习时间:1小时 总结博客时间:1小时15分】

【学习内容:出现bug的原因、内核调试器gdb、使用Git进行二分查找】

内核级开发的调试工作远比用户级开发艰难,它带来的风险比用户级别更高。

一、准备开始

1. 准备工作需要:

  • 一个bug
  • 一个藏匿bug的内核版本
  • 相关内核代码的知识和运气

2. 在用户级程序中bug常常表现得清晰(执行foo就会让程序立即产生核心信息转储)但是内核中的bug表现得不像用户级程序中那么清晰。因为内核、用户以及硬件之间的交互很微妙。

3. 调试的主要思想是让bug重现,但是在内核中这并不是很容易做到的。因此,在跟踪bug的时候,掌握的信息越多越好。

二、内核中的bug

1. 内核bug产生的原因:

  • 错误代码,例如没有把正确的值存放在恰当的位置
  • 同步时发生的错误,例如共享变量锁定不当
  • 错误的管理硬件,例如给错误的控制寄存器发送错误的指令
  • ……

2. 内核bug发作的症状可能有:

  • 降低所有程序的运行性能
  • 毁坏数据
  • 使得系统处于死锁状态
  • ……

3. 从隐藏在源代码中的错误到展现在目击者面前的bug,往往是经历一系列连锁反应的事件才可能触发的。

4. 内核开发比起用户开发要多考虑一些独特的问题,如定时限制、竞争条件等,它们都是允许多个线程在内核中同时运行产生的结果。

三、通过打印来调试

内核提供的格式化打印函数printk()有一些特殊功能:

3.1 健壮性

健壮性——在任何时候、任何地方都能调用它,弹性极佳。可以在中断上下文和进程上下文中被调用;可以在任何持有锁时被调用;可以在多处理器上同时被调用,并且不必使用锁。

漏洞:在系统启动过程中,中断还没有初始化之前,在某些地方不能使用它。

解决方法:调试启动过程最开始的步骤时,可用early_printk()代替,功能与printk()完全相同,能更早工作。

3.2 日志等级

1. printk()和printf()在使用上最主要的区别就是前者可以指定一个日志级别,内核根据这个级别来判断是否在终端上打印消息。内核把级别比某个特定值低的所有消息显示在终端上。

2. KERN_ WARNING和KERN_ DEBUG都是简单的宏定义,加进printk()函数要打印的消息的开头。内核用这个指定的记录等级和当前终端的记录等级console_loglevel来决定是不是向终端上打印。

3. 如果没有特别特别指定,函数会选用默认的DEFAULT_ MESSAGE_ LOGLEVEL,在当前来看是KERN_ WARNING,即一个警告。最好还是给自己的消息指定一个记录等级。

4. 内核把最重要的记录等级KERN_EMERG定义为"<0>",将无关紧要的记录等级KERN _ DEBUG定义为"<7>"

5. 调试信息时两种赋予记录等级的方法:

  • 保持终端的默认记录等级不变,给所有调试信息KERN_CRIT或更低的等级。
  • 给所有调试信息KERN_DEBUG等级,调整终端的默认记录等级。

3.3 记录缓冲区

1. 内核消息都被记录在环形队列中,以队列方式进行读写;大小可以通过设置CONFIGLOGBUF_SHIFT进行调整。

2. 在单处理器上,该缓冲区大小默认为16KB,也就是说,超过的消息将覆盖旧消息。

3. 优势:

  • 健壮性:读写同步问题容易解决,在中断上下文中也可以方便的使用
  • 简单性:记录的维护更加方便

4. 劣势:可能会丢失消息。

3.4 syslogd和klogd

  syslogd和klogd是两个用户空间的守护进程。klogd从记录缓冲区中获取内核消息,再通过syslogd守护进程将他们保存在系统日志文件中。

1. klogd

  • 既可以从/proc/kmsg文件中,也可以通过syslog()系统调用读取这些消息
  • 默认情况下选择读取/proc方式实现
  • 两种情况klogd都会阻塞:知道有新的内核消息可供读出,唤醒之后默认处理是将消息传给syslogd
  • 启动klogd时可以通过-c标志来改变终端的记录等级

2. syslogd

  syslogd将它接收到的所有消息添加到一个文件中,默认是/var/log/messages。

3.5 从printf()到printk()的转换

四、oops

1. oops是内核告知用户有不幸发生的最常用的方式。内核很难自我修复,也不能将自己杀死(因为内核是整个系统的管理者,不能将自己杀死,也很难自行修复),只能发布oops。

2. 发布oops的过程

  • 向终端上输出错误消息
  • 输出寄存器中保存的信息
  • 输出可供跟踪的回溯线索

3. oops发生的时机:

  • 发生在中断上下文:内核无法继续,会陷入混乱,导致系统死机
  • 发生在idle进程或init进程(0号进程和1号进程),同上
  • 发生在其他进程运行时,内核会杀死该进程并尝试着继续执行

4. oops发生的可能原因:

  • 内存访问越界
  • 非法的指令
  • ……

5. oops中包含的重要信息对于所有体系结构都是相同的:寄存器上下文和回溯线索。

  • 回溯线索:显示了导致错误发生的函数调用链
  • 寄存器上下文信息可能同样有用,比如帮助冲进引发问题的现场

4.1 ksymoops

  回溯线索中的地址需要转化成有意义的符号名称才能使用,这需要调用ksymoops命令,并且还必须提供编译内核时产生的System.map。如果用的是模块,还需要一些模块信息。

调用方式:

kysmoop saved_oops.txt

4.2 kallsyms

  现在不需要使用sysmoops工具,因为用户使用时可能会发生很多问题。新版本中引入了kallsyms特性,可以通过定义CONFIG_KALLSYMS配置选项启用。

五、内核调试配置选项

  编译时为了方便调试和测试内核代码,内核提供了许多配置选项。这些选项都在内核配置编译器的内核开发菜单中,都依赖于CONFIG_ DEBUG_ KERNEL。

常用选项:

slab layer debugging slab层调试选项

high-memory debugging 高端内存调试选项

I/O mapping debugging I/O映射调试选项

spin-lock debugging 自旋锁调试选项

stack-overflow debugging 栈溢出检查选项

sleep-inside-spinlock checking 自旋锁内睡眠选项

……

六、引发bug并打印信息

1. 一些内核调用可以用来方便标记bug,提供断言并输出信息。最常用的两个是BUG()和BUG_ON()。当被调用时会引发oops,导致栈的回溯和错误信息的打印。

大部分体系把BUG()和BUG_ON()定义成某种非法操作,这样自然会产生需要的oops。可以把这些调用当做断言使用,想要断言某种情况不该发生:

if (bad_thing)
BUG();

或使用更好的形式:

BUG_ON(bad_thing);

2. BUILD_ BUG_ ON() 与BUG_ ON()作用相同,仅在编译时调用。

3. panic()可以引发更严重的错误,不但会打印错误信息,还会挂起整个系统。

4. dump_stack()只在终端上打印寄存器上下文和函数的跟踪线索。

七、神奇的系统请求键

这个功能可以通过定义CONFIG_ MAGIC _SYSRQ配置选项来启用。SysRq(系统请求)键在大多数键盘上都是标准键。

该功能被启用时,无论内核出于什么状态,都可以通过特殊的组合键和内核进行通信。

除了配置选项以外,还要通过一个sysctl用来标记该特性的开或关,启动命令如下:

echo 1 > /proc/sys/kernel/sysrq

支持Sysrq的几个命令:

八、内核调试器的传奇

8.1 gdb

1. 可以使用标准的GNU调试器对正在运行的内核进行查看。 针对内核启动调试器的方法与针对进程的方法大致相同:

gdb vmlinux /proc/kcore

其中vmlinx文件是未经压缩的内核映像,区别于zImage或bImage,它存放于源代码树的根目录上。

/proc/kcore作为一个参数选项,是作为core文件来用的,通过它能够访问到内核驻留的高端内存。只有超级用户才能读取此文件的数据可以使用gdb的所有命令来获取信息。如:

p global_variable //打印一个变量的值

disassemble function //反汇编一个函数

2. 如果编译内核的时候使用了-g参数(在内核的Makefile文件的CFLAGS变量中加入-g)gdb还可以提供更多的信息。

3. gdb的局限性:

  • 没有办法修改内核数据
  • 不能单步执行内核代码

8.2 kgdb

1. kgdb是一个补丁,可以让我们在远程主机上通过串口利用gdb的所有功能对内核进行调试。这需要两台计算机:第一台运行带有kgdb补丁的内核,第二台通过串行线使用gdb对第一台进行调试。

2. 通过kgdb,gdb的所有功能都能使用:

  • 读取和修改变量值
  • 设置断点
  • 设置关注变量
  • 单步执行
  • 某些版本的gdb甚至允许执行函数

九、探测系统

9.1 使用uid作为选择条件

1. 一般情况下,加入特性时,只要保留原有的算法而把新算法加入到其他位置上,基本就能保证安全。可以把用户id(UID)作为选择条件来实现这种功能,通过某种选择条件,安排到底执行哪种算法:

if(current->uid != 7777)
{
/*老算法*/
else
{
/*新算法*/
}

9.2 使用条件变量

如果代码与进程无关,或者希望有一个针对所有情况都能使用的机制来控制某个特性,可以使用条件变量。这种方式比使用UID更简单,只需要创建一个全局变量作为一个条件选择开关:如果该变量为0,就使用某一个分支上的代码;否则,选择另外一个分支。

可以通过某种接口提供对这个变量的操控,也可以直接通过调试器进行操控。

9.3 使用统计量

这种方法常用于使用者需要掌握某个特定事件的发生规律的时候。 通过创建统计量并提供某种机制访问其统计结果。

注意:这种方法不是SMP安全的,理想的办法是通过原子操作进行实现。

9.4 重复频率限制

当系统的调试信息过多的时候,有两种技巧可以防止这类问题发生:

  • 重复频率限制:就是限制调试信息,最多几秒打印一次,可以根据自己的需要调节频率。例如printk()函数的调节频率,可以用printk_ratelimit()函数限制
  • 发生次数限制:要调试信息至多输出几次,超过次数限制后就不能再输出。这种方法可以用来确认在特定情况下某段代码的确被执行了

注意:

  • 用到的变量都应该是静态的,并且限制在函数的局部范围以内,这样才能保证变量的值在经历多次函数调用后仍然能够保留下来
  • 这些例子的代码都不是SMP安全或抢占安全的,只需要用原子操作改造一下

十、用二分查找法找出罪恶的变更

在问题内核和良好内核之间使用二分法,能很容易地对引发bug的代码进行定位。

十一、使用Git进行二分搜索

Git源码管理工具提供了一个有用的二分搜索机制,如果使用Git来控制Linux源码树的副本,则Git将自动运行二分搜索进程。此外,Git会在修订版本中进行二分搜索,可以具体找到哪次提交的代码引发了bug。

git bisect start   //告知git要进行二分搜索
git bisect bad <revision> //已知出现问题的最早内核版本
git bisect bad //当前版本就是引发bug的最初版本的情况下使用这条命令
git bisect good <revision> //最新的可正常运行的内核版本

之后Git就会利用二分搜索法在Linux源码树中,自动检测正常的版本内核和有bug的内核版本之间哪个版本有隐患,然后再编译、运行以及测试正被检测的版本。

如果版本运行正常:
git bisect good 如果版本运行异常:
git bisect bad

对于每一个命令,Git将在每一个版本的基础上反复二分搜索源码树,并且返回所查的下一个内核版本,直到不能再进行二分搜索位置,最终Git会打印出有问题的版本号。

指定Git仅仅在与错误相关的目录列表中去二分搜索提交的补丁:
git bisect start - arch/x86

总结

  通过对本章的学习,我了解到调试过程其实是一种寻求实现与目标偏差的行为,从内核内置的调试架构到调试程序,从记录日志到用git二分法查找。此时夯实基础,为以后的学习积累经验。

《Linux内核设计与实现》第十八章学习笔记的更多相关文章

  1. Linux内核设计与实现 第十八章

    1. 内核调试的难点 重现bug困难 调试风险比较大 定位bug的初始版本困难 2. 内核调试的工具和方法 2.1 输出 LOG 输出LOG不光是内核调试, 即使是在用户态程序的调试中, 也是经常使用 ...

  2. Linux内核设计与实现第十周读书笔记

    第十七章 设备与模块 关于设备驱动与设备管理,我们讨论四种内核成分. 设备类型 模块 内核对象 sysfs 17.1设备类型 在Linux以及所有Unix系统中,设备被分为以下三种类型: 块设备,块设 ...

  3. 《Linux内核设计与实现》Chapter 18 读书笔记

    <Linux内核设计与实现>Chapter 18 读书笔记 一.准备开始 一个bug 一个藏匿bug的内核版本 知道这个bug最早出现在哪个内核版本中. 相关内核代码的知识和运气 想要成功 ...

  4. 《Linux内核设计与实现》Chapter 3 读书笔记

    <Linux内核设计与实现>Chapter 3 读书笔记 进程管理是所有操作系统的心脏所在. 一.进程 1.进程就是处于执行期的程序以及它所包含的资源的总称. 2.线程是在进程中活动的对象 ...

  5. 《Linux内核设计与实现》第四周读书笔记——第五章

    <Linux内核设计与实现>第四周读书笔记--第五章 20135301张忻 估算学习时间:共1.5小时 读书:1.0 代码:0 作业:0 博客:0.5 实际学习时间:共2.0小时 读书:1 ...

  6. 《Linux内核设计与实现》Chapter 1 读书笔记

    <Linux内核设计与实现>Chapter 1 读书笔记 一.Unix的特点 Unix从Multics中产生,是一个强大.健壮和稳定的操作系统. 特点 1.很简洁 2.在Unix系统中,所 ...

  7. 《Linux内核设计与实现》Chapter 2 读书笔记

    <Linux内核设计与实现>Chapter 2 读书笔记 一.获取内核源码 1.使用Git 我们曾经在以前的学习中使用过Git方法 $ git clone git://git.kernel ...

  8. 《Linux内核设计与实现》Chapter 5 读书笔记

    <Linux内核设计与实现>Chapter 5 读书笔记 在现代操作系统中,内核提供了用户进程与内核进行交互的一组接口,这些接口的作用是: 使应用程序受限地访问硬件设备 提供创建新进程与已 ...

  9. LINUX内核设计与实现第三周读书笔记

    LINUX内核设计与实现第三周读书笔记 第一章 LINUX内核简介 1.1 Unix的历史 1969年的夏天,贝尔实验室的程序员们在一台PDR-7型机上实现了Unix这个全新的操作系统. 1973年, ...

  10. 《Linux内核设计与实现》第一二章笔记

    第一章 linux内核简介 每个处理器在任何时间点上的活动必然概括为下列三者: 运行于用户空间,执行用户进程 运行于内核空间,处于进程上下文,代表某个特定的进程执行 运行于内核空间,处于中断上下文,与 ...

随机推荐

  1. January 12th, 2018 Week 02nd Friday

    Nothing behind me, everything ahead of me, as is ever so on the road. 我的身后空空荡荡,整个世界都在前方,这就是在路上. That ...

  2. ajax 数据类型结构

  3. js常见错误类型

    (1)SyntaxError SyntaxError是解析代码时发生的语法错误 // 变量名错误 var 1a; // 缺少括号 console.log 'hello'); (2)ReferenceE ...

  4. Ubuntu18.04安装Tensorflow+cuda+cuDNN

    本文写的比较简单,期间遇到的一些小麻烦,自己不认为成为阻碍,所以没有详细写. 如有疑问可以联系QQ:2922530320 Pycharm Pycharm使用Anaconda Pycharm 在新建项目 ...

  5. 修改CentOS 7.2系统的主机名

    之前使用网上的大部分说法,修改了两个配置文件: /etc/hosts /etc/sysconfig/network 然后,并没有什么卵用. 后来,搜阿里云配置,看到这个办法: 使用“经典网络”类型的E ...

  6. 【opatch打补丁】oracle10.2.0.5.0升级10.2.0.5.9 for linux

    https://wenku.baidu.com/view/c38702b56edb6f1afe001f59.html    这篇文章也不错,可参考 任务:oracle 10.2.0.5.0 打补丁升级 ...

  7. python在图片上写汉字

    1.python opencv的putText只能画英文上去 2.借鉴这个https://blog.csdn.net/dcrmg/article/details/79108491 使用pil 首先,你 ...

  8. PAT A1033 To Fill or Not to Fill (25 分)——贪心

    With highways available, driving a car from Hangzhou to any other city is easy. But since the tank c ...

  9. JVM有哪些分区?(解释详细 通俗易懂)

    JVM的分区可以分为两种:线程私有的内存区和线程共享的内存区 一.JVM中线程私有的内存区: 1.程序计数器:当前线程所执行的字节码行号计数指示器,是线程私有的,即每个线程都有自己的程序计数器,需要注 ...

  10. 一个简单的javascript节流器实现

    节流器 javascript的节流器主要用于延缓某些动作的执行,比如ajax请求,如果input框注册了input事件,那么当用户输入时就会持续的触发这个事件,如果回调函数中持续的通过ajax调用后台 ...