调试linux内核(1): 环境准备和原理介绍
阅读原文时间:2023年08月09日阅读:1

现在流行的开源项目经历了长时间的开发, 积累了大量的代码, 想要一行一行地阅读代码去学习开源项目, 需要的时间成本是巨大的. 所以, 我们也需要用一种高效的方式去"阅读"代码. 计算机科学发展到现在, 产生了很多高效成熟的工具, 调试器就是其中之一(扯句题外话, 那些大牛程序员似乎就是喜欢琢磨怎么制造各种工具哈), 调试器能够帮程序员定位代码的bug, 理解代码的运行机制. 这篇文章总结了如何搭建一个调试linux内核源码的开发环境, 并简单介绍了一些调试器的实现原理.


调试内核和调试用户态程序有什么区别?

使用qemu调试内核, 原理是什么?

如何搭建调试linux内核源码的开发环境?

我们把讨论限定在使用c语言写的程序上, 一个程序想在linux运行, 大致会经过下面几个步骤:

  1. 你首先会编译源代码得到一个ELF文件, 这个文件中包含了用户程序的二进制指令和数据, 它具有可执行的权限;
  2. 接下来你在shell中运行这个ELF文件, shell会使用fork, exec等系统调用生成一个子进程去运行你的程序, 系统调用进入内核之后, 编译得到的ELF文件被用来构造这个子进程在内存中的初始状态, 内核还需要为这个进程返回用户态之后的寄存器设置合适的初始状态. 可以说, 一个进程就像一个状态机, 它的状态包括内存和寄存器的状态, 在这一步内核对这个状态机作了初始化;
  3. 子进程得到调度, 它从内核态返回用户态, 每执行一条指令, 伴随的是内存和寄存器状态的变化, 也就是状态机的状态转移;

以上是用户态进程的运行过程, 如果想调试一个用户态程序, 可以使用gdb工具, gdb之所以能够调试程序, 是借助了内核的帮助. 如果理解了进程是一个状态机这样的观点, 理解gdb调试程序的原理就比较容易了, 调试程序无非就是运行几条指令,观察或者修改一下内存和寄存器的状态. 内核提供了ptrace系统调用, 可以让一个进程去"跟踪(trace)"另一个进程的状态, 使用ptrace系统调用, gdb可以修改被调试进程的内存和寄存器, 比如修改原来内存中的某条指令为另一条特殊的指令, 就可以实现"打断点"的功能, 具体的细节会在其他文章中讨论, 手写一个简单的调试器体会一下ptrace系统调用.

调试用户态程序依赖内核提供的系统调用, 但是调试内核本身, 就不能再依赖它本身提供的系统调用了, 我们需要一种仿真器软件, 也就是接下来要介绍的qemu.

qemu是一种仿真器软件, 用软件的方式模拟了一整个硬件平台, 支持不同类型的CPU架构, 众多的IO设备. 使用qmeu, 可以运行完整的linux操作系统, 通过qemu的启动参数可以方便地添加修改硬件的配置, 比如CPU的数量, 增加硬盘, 网卡等设备. 对于调试内核来说, 我们需要把编译好的内核镜像传递给它, 依赖qemu提供的gdb调试服务支持, 改变启动参数, 可以让qemu启动内核的同时启动一个gdb server服务, 在特定的端口上等待gdb client的连接, 并执行gdb client的命令.

以上就是使用qemu调试内核的基本原理. 从操作系统的角度看, qemu也是一个普通的用户进程, 只不过它可以仿真一台真实的机器, 并且能够接受另外的gdb client进程的命令, 从而控制其中运行的linux内核的执行. 这个gdb client进程既可以和qemu在统一台机器上运行, 也可以通过网络连接到qemu的gdb server服务, 使用起来比较方便.

以下均在ubuntu系统上完成, 需要准备的内容包括:

  1. 交叉编译工具链

    • 这里使用arm或者riscv平台
    • 交叉编译工具用来编译linux内核源码和busybox源码
  2. linux内核源码

    • 可以使用5.4或者之后的版本
    • 编译得到内核镜像文件, 通过启动参数传递给qemu
  3. busybox源码

    • 可以使用1.36或者之后的版本
    • 编译之后用于制作根文件系统, 供内核使用
  4. 安装好的qemu

    • 启动linux内核
    • 等待gdb的连接
  5. 其他软件

    • gdb-multiarch
    • guestfs-tools
    • 其他

具体的调试环境准备可以参考下面的视频:


按照视频的步骤操作下来, 实现调试linux内核应该不会有什么问题. 但是qemu的一些启动参数, 传递给内核的启动参数, 以及这些参数为什么要这样设置, 是否可以改成其他的值, 可能还是会有疑惑的. 我的建议是, 先把环境搞起来, 等对qemu和内核源码熟悉一点了, 很多问题自然就明白了.

  1. 比如你想知道内核的启动参数有什么用? 就可以通过调试代码的方式, 找到内核解析启动参数的部分, 分析出它在做什么.
  2. 对于qmeu的问题, 最好是找官方文档, 这里放个链接, 每个参数的含义在文档中都可以找到.
  3. 文档中没说清楚的, 你可以把qemu的源码拿来看, 自己编译一下, 然后用gdb调试qemu, 并且在qemu启动时加上 -s -S参数, 这个时候, 你就成功实现了一个两层的gdb套娃, qemu在被调试, 同时qemu里的内核也在被调试, 如果你愿意, 你还可以在qemu虚拟机里面在起一个gdb去调试用户进程, 这样就得到了一个三层的gdb套娃.
  4. 三层gdb套娃不是为了炫技, 这能够帮你在更高的层次上理解这一整套软件栈时如何协同工作的. 比如你在qemu虚拟机里的进程访问了一个硬件设备, 会触发到qemu中的运行的linux内核的驱动代码, 驱动代码的执行, 又会触发到qemu对某个设备的模拟代码上, 有了三层gdb套娃, 你就有机会了解你想知道的任何实现细节, 从而帮助你解决遇到的各种问题.
  5. 以上是一些学习和工作经验的总结, 详细的讨论会在其他文章中展开, 这里先挖个坑.

总而言之, 源码之下没有秘密. 读文档, 读源码能够解决你的大部分问题.


建了个QQ群: 838923389. 有想法的老铁可以加一下, 一起交流linux内核的使用和学习经验. 后续也会在b站发一些技术视频, 老铁们觉得有需要可以先关注一下, 视频和文章肯定会给各位带来一些启发和帮助. 更多文章还可以参考我的csdn.