第十八章 调试

【学习时间: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. Shell脚本应用(for、while循环语句和case分支语句)

    1.for:读取不同的变量值,逐个执行同一组命令,直到取值完毕退出,变量值以空格分隔 语法: for  变量值  in  取值列表 do 命令序列 done 2.while:重复测试某个条件,成立则执 ...

  2. 关于UIPageViewController那些事

    一.前言 这些天有新生问及UIPageViewController这个视图控制器,自己原来没有用过,所以就看了一下相关的知识,就写了下来,分享一下经验. 主要的关于这个控制器的内容就从例子中去解说了. ...

  3. Django复习之ORM

    QuerySet数据类型:                        1.可切片,可迭代      [obj,....]                    2.惰性查询:            ...

  4. Graphviz安装及简单使用

    Graphviz Windows环境安装: 1.官网下载 官网下载地址 2.配置环境变量 将graphviz安装目录下的bin文件夹添加到Path环境变量中: 3.验证是否安装并配置成功 进入wind ...

  5. dispatchTouchEvent

    View /**  * Pass the touch screen motion event down to the target view, or this  * view if it is the ...

  6. Ngnix中的fastcgi參数性能优化和解释

    版权声明:本文为博主原创文章,未经博主同意不得转载. https://blog.csdn.net/luozhonghua2014/article/details/37737823 优化性能參数设置,在 ...

  7. yii2 修改验证码小部件样式

    <?= $form->field($model, 'verifyCode',['labelOptions' => ['class' => 'yanzhengma','style ...

  8. vue - 状态管理器 Vuex

    状态管理 vuex是一个专门为vue.js设计的集中式状态管理架构.状态?我把它理解为在data中的属性需要共享给其他vue组件使用的部分,就叫做状态.简单的说就是data中需要共用的属性.

  9. JSON数据解析(自写)

    自写的JSON解析数据 void setup() { Serial.begin(115200); char chArray[50] = "some characters"; Str ...

  10. 使用verdaccio 搭建npm私有仓库

    使用verdaccio 搭建npm私有仓库 1. 为什么要搭建私有的npm仓库?    随着公司的业务越来越复杂,项目迭代速度也越来越快,那么项目间的常用的业务代码共享变得非常之有必要.但是对于公司的 ...