SO2 实验 01——介绍

实验目标

  • 介绍操作系统 2 实验的规则和目标
  • 介绍实验文档
  • 介绍 Linux 内核及相关资源
  • 创建简单的模块
  • 描述内核模块编译的过程
  • 展示模块如何与内核一起使用
  • 简单的内核调试方法

关于本实验

操作系统 2 实验是内核编程和驱动程序开发实验。本实验的目标是:

  • 加深课程中介绍的概念
  • 展示内核编程接口(内核 API)
  • 获取在独立的环境中记录、开发和调试的技能
  • 获取驱动程序开发的知识和技能

每个实验将呈现一组特定问题的概念、应用和命令。实验将以演示开始(每个实验都会有一组幻灯片)(15 分钟),其余时间将用于实验练习(80 分钟)。

为了获得最佳的实验效果,我们建议你阅读相关幻灯片。要完全理解实验,我们建议你查阅实验技术支持材料。如果需要深入学习,你可以使用辅助文档。

文档

与用户空间编程相比,内核开发是一个困难的过程。内核的 API 和用户空间不同,内核子系统的复杂性也更高,因此需要额外的准备工作。相关的文档比较零散,有时候需要查阅多个来源才能对某个方面有较全面的了解。

Linux 内核的主要优势是可以访问源代码和其开放式开发系统。因此,互联网上存在大量的内核相关文档。

以下是与 Linux 内核相关的一些链接:

这些链接并不全面。使用 互联网内核源代码 是必不可少的。

内核模块概述

虽然单体内核的运行速度比微内核更快,但它在模块化和可扩展性方面存在不足。在现代的单体内核设计中,这个问题已经通过使用内核模块得到了解决。内核模块(或称为可加载内核模块)是一种包含代码的目标文件(object file),它可以在运行时根据需要加载,以扩展内核的功能;而当这些模块不再需要时,也可以将其卸载。大部分设备驱动程序都是以内核模块的形式实现的。

要想开发 Linux 设备驱动程序,建议下载内核源代码,配置和编译它们,然后将编译后的版本安装在测试/开发工具机上。

内核模块示例

以下是一个非常简单的内核模块示例。当加载到内核中时,它会生成消息 "Hi"。当卸载这个内核模块时,将生成消息 "Bye"

#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>

MODULE_DESCRIPTION("My kernel module");
MODULE_AUTHOR("Me");
MODULE_LICENSE("GPL");

static int dummy_init(void)
{
        pr_debug("Hi\n");
        return 0;
}

static void dummy_exit(void)
{
        pr_debug("Bye\n");
}

module_init(dummy_init);
module_exit(dummy_exit);

生成的消息不会显示在控制台上,而是保存在专门预留的内存区域中,由日志守护程序(syslog)负责将其提取出来。要显示内核消息,你可以使用 dmesg 命令或检查日志文件。

# cat /var/log/syslog | tail -2
Feb 20 13:57:38 asgard kernel: Hi
Feb 20 13:57:43 asgard kernel: Bye

# dmesg | tail -2
Hi
Bye

编译内核模块

编译内核模块与编译用户程序不同。首先,需要使用不同的头文件。此外,模块不应链接到库。最后,模块必须使用与加载模块的内核相同的选项进行编译。出于这些原因,有一种标准的编译方法(kbuild)。该方法需要使用两个文件: Makefile 文件和 Kbuild 文件。

以下是 Makefile 文件的示例:

KDIR = /lib/modules/`uname -r`/build

kbuild:
        make -C $(KDIR) M=`pwd`

clean:
        make -C $(KDIR) M=`pwd` clean

以下是用于编译模块的 Kbuild 文件示例:

EXTRA_CFLAGS = -Wall -g

obj-m        = modul.o

正如你所见,在示例中调用 make 命令对 Makefile 文件进行编译时,会在内核源代码目录 (/lib/modules/`uname -r`/build) 中执行 make 命令,并引用当前目录 (M = `pwd)。这个过程最终会读取当前目录中的 Kbuild 文件,并按照其中的指示编译模块。

注解

对于实验,我们将根据虚拟机的规格,配置不同的 KDIR

KDIR = /home/student/src/linux
[...]

Kbuild 文件包含了一系列指令,这些指令用于编译内核模块。其中一个最基本的指令例子是 obj-m = module.o。遵循这个指令,系统会基于 module.o 文件开始构建一个内核模块(也称为 ko,即内核对象)。module.o 文件是基于 module.cmodule.S 生成的。所有这些文件都应该存放在包含 Kbuild 的同一目录下。

下面是一个使用多个子模块的 Kbuild 文件示例:

EXTRA_CFLAGS = -Wall -g

obj-m        = supermodule.o
supermodule-y = module-a.o module-b.o

对于上面的示例,编译步骤如下:

  • 编译 module-a.cmodule-b.c 源文件,生成 module-a.o 和 module-b.o 对象文件(object)
  • 然后将 module-a.omodule-b.o 链接到 supermodule.o
  • 基于 supermodule.o 创建 supermodule.ko 模块

Kbuild 中目标(target)的后缀决定了它们的用途,如下所示:

  • M(modules)标示目标为可加载内核模块
  • Y(yes)标示目标是编译对象文件然后将其链接到模块($(模块名称)-y)或内核(obj-y
  • 其他任何目标后缀都将被 Kbuild 忽略,不会被编译

注解

借助这些后缀,开发者可以通过运行 make menuconfig 命令或直接编辑 .config 文件来轻松配置内核。该文件设置了一系列变量,这些变量决定了在构建过程中哪些特性会被添加到内核中。例如,当使用 make menuconfig 命令添加 BTRFS 支持时,.config 文件中会增加 CONFIG_BTRFS_FS = y 这一行。BTRFS 的 kbuild 包含了一行 obj-$(CONFIG_BTRFS_FS):= btrfs.o,如果设置了相应的变量,这行代码会变成 obj-y:= btrfs.o。这将导致系统编译 btrfs.o 对象,并将其链接到内核中。如果没有设置该变量,这行代码则会变成 obj:= btrfs.o 并被忽略,结果是内核构建时不会包含 BTRFS 支持。

要了解更多详细信息,请参阅内核源代码中的 Documentation/kbuild/makefiles.txtDocumentation/kbuild/modules.txt 文件。

加载/卸载内核模块

要加载内核模块,请使用 insmod 程序。该程序接收用于编译和链接模块的 *.ko 文件的路径作为参数。要从内核中卸载模块请使用 rmmod 命令,该命令接收模块名称作为参数。

$ insmod module.ko
$ rmmod module.ko

加载内核模块时,将执行 module_init 宏参数指定的例程。同样,当卸载模块时,将执行 module_exit 宏参数指定的例程。

下面是一个完整的编译、加载和卸载内核模块的示例:

faust:~/lab-01/modul-lin# ls
Kbuild  Makefile  modul.c

faust:~/lab-01/modul-lin# make
make -C /lib/modules/`uname -r`/build M=`pwd`
make[1]: Entering directory `/usr/src/linux-2.6.28.4'
  LD      /root/lab-01/modul-lin/built-in.o
  CC [M]  /root/lab-01/modul-lin/modul.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC      /root/lab-01/modul-lin/modul.mod.o
  LD [M]  /root/lab-01/modul-lin/modul.ko
make[1]: Leaving directory `/usr/src/linux-2.6.28.4'

faust:~/lab-01/modul-lin# ls
built-in.o  Kbuild  Makefile  modul.c  Module.markers
modules.order  Module.symvers  modul.ko  modul.mod.c
modul.mod.o  modul.o

faust:~/lab-01/modul-lin# insmod modul.ko

faust:~/lab-01/modul-lin# dmesg | tail -1
Hi

faust:~/lab-01/modul-lin# rmmod modul

faust:~/lab-01/modul-lin# dmesg | tail -2
Hi
Bye

可以使用 lsmod 命令或查看 /proc/modules/sys/module 目录来获取有关加载到内核中的模块的信息。

内核模块调试

与调试常规程序相比,调试内核模块要复杂得多。首先,内核模块中的错误可能导致整个系统阻塞。因此,故障排查的速度会大大降低。为了避免重新启动,建议使用虚拟机(qemu、virtualbox 或者 vmware)。

当插入包含错误的模块到内核中时,它最终会生成一个 内核 oops 。内核 oops 是内核检测到的无效操作,只可能由内核生成。对于稳定的内核版本,这几乎可以肯定意味着模块含有错误。在 oops 出现后,内核仍将继续工作。

出现内核 oops 时,保存生成的消息非常重要。如上所述,内核生成的消息保存在日志中,并可使用 dmesg 命令显示。为了确保不丢失任何内核消息,建议直接从控制台插入/测试内核,或定期检查内核消息。值得注意的是,oops 不止可能是由于编程错误,也有可能是硬件错误引起的。

如果发生致命错误,系统无法恢复到稳定状态,将造成 内核错误(kernel panic)

以下是一个包含错误并会造成 oops 的内核模块示例:

/*
 * 造成 oops 的内核模块
 */

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>

MODULE_DESCRIPTION ("Oops");
MODULE_LICENSE ("GPL");
MODULE_AUTHOR ("PSO");

#define OP_READ         0
#define OP_WRITE        1
#define OP_OOPS         OP_WRITE

static int my_oops_init (void)
{
        int *a;

        a = (int *) 0x00001234;
#if OP_OOPS == OP_WRITE
        *a = 3;
#elif OP_OOPS == OP_READ
        printk (KERN_ALERT "value = %d\n", *a);
#else
#error "Unknown op for oops!"
#endif

        return 0;
}

static void my_oops_exit (void)
{
}

module_init (my_oops_init);
module_exit (my_oops_exit);

将此模块插入内核将造成 oops:

faust:~/lab-01/modul-oops# insmod oops.ko
[...]

faust:~/lab-01/modul-oops# dmesg | tail -32
BUG: unable to handle kernel paging request at 00001234
IP: [<c89d4005>] my_oops_init+0x5/0x20 [oops]
  *de = 00000000
Oops: 0002 [#1] PREEMPT DEBUG_PAGEALLOC
last sysfs file: /sys/devices/virtual/net/lo/operstate
Modules linked in: oops(+) netconsole ide_cd_mod pcnet32 crc32 cdrom [last unloaded: modul]

Pid: 4157, comm: insmod Not tainted (2.6.28.4 #2) VMware Virtual Platform
EIP: 0060:[<c89d4005>] EFLAGS: 00010246 CPU: 0
EIP is at my_oops_init+0x5/0x20 [oops]
EAX: 00000000 EBX: fffffffc ECX: c89d4300 EDX: 00000001
ESI: c89d4000 EDI: 00000000 EBP: c5799e24 ESP: c5799e24
 DS: 007b ES: 007b FS: 0000 GS: 0033 SS: 0068
Process insmod (pid: 4157, ti=c5799000 task=c665c780 task.ti=c5799000)
Stack:
 c5799f8c c010102d c72b51d8 0000000c c5799e58 c01708e4 00000124 00000000
 c89d4300 c5799e58 c724f448 00000001 c89d4300 c5799e60 c0170981 c5799f8c
 c014b698 00000000 00000000 c5799f78 c5799f20 00000500 c665cb00 c89d4300
Call Trace:
 [<c010102d>] ? _stext+0x2d/0x170
 [<c01708e4>] ? __vunmap+0xa4/0xf0
 [<c0170981>] ? vfree+0x21/0x30
 [<c014b698>] ? load_module+0x19b8/0x1a40
 [<c035e965>] ? __mutex_unlock_slowpath+0xd5/0x140
 [<c0140da6>] ? trace_hardirqs_on_caller+0x106/0x150
 [<c014b7aa>] ? sys_init_module+0x8a/0x1b0
 [<c0140da6>] ? trace_hardirqs_on_caller+0x106/0x150
 [<c0240a08>] ? trace_hardirqs_on_thunk+0xc/0x10
 [<c0103407>] ? sysenter_do_call+0x12/0x43
Code: <c7> 05 34 12 00 00 03 00 00 00 5d c3 eb 0d 90 90 90 90 90 90 90 90
EIP: [<c89d4005>] my_oops_init+0x5/0x20 [oops] SS:ESP 0068:c5799e24
---[ end trace 2981ce73ae801363 ]---

虽然相对晦涩,但内核在出现 oops 时提供了有关错误的宝贵信息。第一行:

BUG: unable to handle kernel paging request at 00001234
EIP: [<c89d4005>] my_oops_init + 0x5 / 0x20 [oops]

告诉我们错误的原因和造成错误的指令的地址。在我们的例子中,这是对内存的无效访问。

接下来的一行是:

Oops: 0002 [# 1] PREEMPT DEBUG_PAGEALLOC

告诉我们这是第一个 oops(#1)。在这个上下文中,这很重要,因为一个 oops 可能会导致其他 oops。通常只有第一个 oops 是相关的。此外,oops 代码(0002)提供了有关错误类型的信息(参见 arch/x86/include/asm/trap_pf.h ):

  • 第 0 位 == 0 表示找不到页面,1 表示保护故障
  • 第 1 位 == 0 表示读取,1 表示写入
  • 第 2 位 == 0 表示内核模式,1 表示用户模式

在这种情况下,我们有一个写入访问导致了 oops(第 1 位为 1)。

下面是寄存器的转储(dump)。它解码了指令指针 (EIP) 的值,并指出错误出现在 my_oops_init 函数中,偏移为 5 个字节(EIP: [<c89d4005>] my_oops_init+0x5)。该消息还显示了堆栈内容和到目前为止的调用回溯。

如果发生了无效的读取调用 (#define OP_OOPS OP_READ),消息将是相同的,但是 oops 代码将不同,现在将是 0000:

faust:~/lab-01/modul-oops# dmesg | tail -33
BUG: unable to handle kernel paging request at 00001234
IP: [<c89c3016>] my_oops_init+0x6/0x20 [oops]
  *de = 00000000
Oops: 0000 [#1] PREEMPT DEBUG_PAGEALLOC
last sysfs file: /sys/devices/virtual/net/lo/operstate
Modules linked in: oops(+) netconsole pcnet32 crc32 ide_cd_mod cdrom

Pid: 2754, comm: insmod Not tainted (2.6.28.4 #2) VMware Virtual Platform
EIP: 0060:[<c89c3016>] EFLAGS: 00010292 CPU: 0
EIP is at my_oops_init+0x6/0x20 [oops]
EAX: 00000000 EBX: fffffffc ECX: c89c3380 EDX: 00000001
ESI: c89c3010 EDI: 00000000 EBP: c57cbe24 ESP: c57cbe1c
 DS: 007b ES: 007b FS: 0000 GS: 0033 SS: 0068
Process insmod (pid: 2754, ti=c57cb000 task=c66ec780 task.ti=c57cb000)
Stack:
 c57cbe34 00000282 c57cbf8c c010102d c57b9280 0000000c c57cbe58 c01708e4
 00000124 00000000 c89c3380 c57cbe58 c5db1d38 00000001 c89c3380 c57cbe60
 c0170981 c57cbf8c c014b698 00000000 00000000 c57cbf78 c57cbf20 00000580
Call Trace:
 [<c010102d>] ? _stext+0x2d/0x170
 [<c01708e4>] ? __vunmap+0xa4/0xf0
 [<c0170981>] ? vfree+0x21/0x30
 [<c014b698>] ? load_module+0x19b8/0x1a40
 [<c035d083>] ? printk+0x0/0x1a
 [<c035e965>] ? __mutex_unlock_slowpath+0xd5/0x140
 [<c0140da6>] ? trace_hardirqs_on_caller+0x106/0x150
 [<c014b7aa>] ? sys_init_module+0x8a/0x1b0
 [<c0140da6>] ? trace_hardirqs_on_caller+0x106/0x150
 [<c0240a08>] ? trace_hardirqs_on_thunk+0xc/0x10
 [<c0103407>] ? sysenter_do_call+0x12/0x43
Code: <a1> 34 12 00 00 c7 04 24 54 30 9c c8 89 44 24 04 e8 58 a0 99 f7 31
EIP: [<c89c3016>] my_oops_init+0x6/0x20 [oops] SS:ESP 0068:c57cbe1c
---[ end trace 45eeb3d6ea8ff1ed ]---

objdump

可以使用 objdump 程序找到造成 oops 的指令(instruction)的详细信息。常用的选项有 -d 用于反汇编代码, -S 用于将 C 代码与汇编语言代码交错显示。然而,为了进行高效的解码,我们需要找到内核模块被加载到的地址。这可以在 /proc/modules 中找到。

以下是在上述模块上使用 objdump 的示例,以确定造成 oops 的指令:

faust:~/lab-01/modul-oops# cat /proc/modules
oops 1280 1 - Loading 0xc89d4000
netconsole 8352 0 - Live 0xc89ad000
pcnet32 33412 0 - Live 0xc895a000
ide_cd_mod 34952 0 - Live 0xc8903000
crc32 4224 1 pcnet32, Live 0xc888a000
cdrom 34848 1 ide_cd_mod, Live 0xc886d000

faust:~/lab-01/modul-oops# objdump -dS --adjust-vma=0xc89d4000 oops.ko

oops.ko:     file format elf32-i386


Disassembly of section .text:

c89d4000 <init_module>:
#define OP_READ         0
#define OP_WRITE        1
#define OP_OOPS         OP_WRITE

static int my_oops_init (void)
{
c89d4000:       55                      push   %ebp
#else
#error "Unknown op for oops!"
#endif

        return 0;
}
c89d4001:       31 c0                   xor    %eax,%eax
#define OP_READ         0
#define OP_WRITE        1
#define OP_OOPS         OP_WRITE

static int my_oops_init (void)
{
c89d4003:       89 e5                   mov    %esp,%ebp
        int *a;

        a = (int *) 0x00001234;
#if OP_OOPS == OP_WRITE
        *a = 3;
c89d4005:       c7 05 34 12 00 00 03    movl   $0x3,0x1234
c89d400c:       00 00 00
#else
#error "Unknown op for oops!"
#endif

        return 0;
}
c89d400f:       5d                      pop    %ebp
c89d4010:       c3                      ret
c89d4011:       eb 0d                   jmp    c89c3020 <cleanup_module>
c89d4013:       90                      nop
c89d4014:       90                      nop
c89d4015:       90                      nop
c89d4016:       90                      nop
c89d4017:       90                      nop
c89d4018:       90                      nop
c89d4019:       90                      nop
c89d401a:       90                      nop
c89d401b:       90                      nop
c89d401c:       90                      nop
c89d401d:       90                      nop
c89d401e:       90                      nop
c89d401f:       90                      nop

c89d4020 <cleanup_module>:

static void my_oops_exit (void)
{
c89d4020:       55                      push   %ebp
c89d4021:       89 e5                   mov    %esp,%ebp
}
c89d4023:       5d                      pop    %ebp
c89d4024:       c3                      ret
c89d4025:       90                      nop
c89d4026:       90                      nop
c89d4027:       90                      nop

请注意,生成 oops 的指令(先前确定为 c89d4005)是:

C89d4005: c7 05 34 12 00 00 03 movl $ 0x3,0x1234

这正是预期的结果——将值 3 存储在地址 0x0001234 上。

/proc/modules 用于查找加载的内核模块的地址。--adjust-vma 选项允许你相对于 0xc89d4000 位置显示指令。-l 选项将显示源代码中每行的编号,源代码与汇编语言代码交错显示。

addr2line

寻找造成 oops 的代码的一种更简单的方法是使用 addr2line 实用程序:

faust:~/lab-01/modul-oops# addr2line -e oops.o 0x5
/root/lab-01/modul-oops/oops.c:23

其中 0x5 是生成 oops 的程序计数器的值(EIP = c89d4005),减去根据 /proc/modules 的信息得出的模块的基地址(0xc89d4000)。

minicom

Minicom (或其他等效程序,例如 picocom 以及 screen) 是一种用于与串行端口(serial port)连接和交互的程序。串行端口是在开发阶段分析内核消息(kernel message)或与嵌入式系统进行交互的基本方法。有两种常见的连接方式:

  • 使用串行端口,设备路径为 /dev/ttyS0
  • 使用串行 USB 端口(FTDI),在这种情况下,设备路径为 /dev/ttyUSB

对于实验中使用的虚拟机,在虚拟机启动后,我们需要使用的设备路径将显示在屏幕上:

char device redirected to /dev/pts/20 (label virtiocon0)

使用 Minicom:

# 使用 COM1 连接,速率为 115,200 字符/秒
minicom -b 115200 -D /dev/ttyS0

# 使用 USB 串行端口连接
minicom -D /dev/ttyUSB0

# 连接到虚拟机的串行端口
minicom -D /dev/pts/20

netconsole

Netconsole 是允许通过网络记录内核调试消息的程序。当磁盘日志系统不起作用、串行端口不可用或终端不响应命令时,这非常有用。Netconsole 是内核模块。

要想正常工作,它需要以下参数:

  • 调试站点的端口、IP 地址和源接口名称
  • 将调试消息发送到的机器的端口、MAC 地址和 IP 地址

这些参数可以在将模块插入内核时进行配置,甚至在模块插入后也可以配置,如果模块在编译时配置了 CONFIG_NETCONSOLE_DYNAMIC 选项。

插入 netconsole 内核模块时的示例配置如下:

alice:~# modprobe netconsole netconsole=6666@192.168.191.130/eth0,[email protected]/00:50:56:c0:00:08

因此,在具有地址 192.168.191.130 的站点上,调试消息将被发送到 eth0 接口,源端口为 6666。消息将被发送到 192.168.191.1,使用 MAC 地址 00:50:56:c0:00:08,至端口 6000 上。

可以在目标站点上使用 netcat 显示消息:

bob:~ # nc -l -p 6000 -u

或者,目标站点可以配置 syslogd 来拦截这些消息。更多信息可以在 Documentation/networking/netconsole.txt 中找到。

Printk 调试

最古老且最有用的两种调试辅助工具是你的大脑和 Printf

在调试过程中,通常会使用一种原始但非常有效的方法:printk 调试。尽管也可以使用调试器,但通常并不是非常有用:简单的错误(未初始化的变量、内存管理问题等)可以通过控制消息和内核解码的 oops 消息轻松找到。

对于更复杂的错误,即使是调试器也无法提供太多帮助,除非你对操作系统的结构非常熟悉。在调试内核模块时,其中存在许多未知因素:多个上下文(我们同时运行多个进程和线程)、中断以及虚拟内存等等。

你可以使用 printk 将内核消息显示到用户空间。它类似于 printf 的功能;唯一的区别是传输的消息可以用 "<n>" 字符串为前缀,其中 n 表示错误级别(日志级别),取值范围为 07 。除了 "<n>",级别也可以用符号常量编码:

KERN_EMERG——n = 0
KERN_ALERT——n = 1
KERN_CRIT——n = 2
KERN_ERR——n = 3
KERN_WARNING——n = 4
KERN_NOTICE——n = 5
KERN_INFO——n = 6
KERN_DEBUG——n = 7

所有日志级别的定义都可以在 linux/kern_levels.h 中找到。基本上,系统凭借这些日志级别将消息发送到各种输出:控制台、位于 /var/log 中的日志文件等等。

注解

要在用户空间显示 printk 消息,printk 日志级别必须比 console_loglevel 变量的优先级高。可以从 /proc/sys/kernel/printk 配置默认的控制台日志级别。

例如,以下命令:

echo 8 > /proc/sys/kernel/printk

将使所有内核日志消息在控制台上显示。也就是说,日志级别必须严格小于 console_loglevel 变量。例如,如果 console_loglevel 的值为 5``(指定于 :code:`KERN_NOTICE`),只有比 ``5 更严格的日志级别的消息(即 KERN_EMERGKERN_ALERTKERN_CRITKERN_ERR 以及 KERN_WARNING)将显示。

想要快速查看执行内核代码的效果的话,控制台重定向的消息可能对你很有帮助,但如果内核遇到不可修复的错误并且系统冻结,则不再那么有用。在这种情况下,必须查看系统的日志,因为它们保留从一次系统启动到下一次系统重新启动之间的信息。这些日志文件位于 /var/log 中,是由内核运行期间的 syslogdklogd 填充的文本文件。syslogdklogd 从挂载在 /proc 中的虚拟文件系统中获取信息。原则上,打开 syslogdklogd 后,来自内核的所有消息都将发送到 /var/log/kern.log

调试的更简单的方法是使用 /var/log/debug 文件。它只包含具有 KERN_DEBUG 日志级别的内核的 printk 消息。

给定一个生产内核(production kernel)(类似于我们可能正在运行的内核),其只包含发布代码,我们的模块是少数几个带有以 KERN_DEBUG 为前缀的消息的模块之一。通过查找与我们的模块的调试会话对应的消息,我们可以轻松浏览 /var/log/debug 中的信息。

一个示例如下:

# 清除先前信息的调试文件(或可能是备份文件)
$ echo "新调试会话" > /var/log/debug
# 运行测试
# 如果没有导致内核崩溃的关键错误,检查输出
# 如果发生关键错误且机器只能通过重新启动来响应,请重新启动系统并检查 /var/log/debug。

消息的格式显然必须包含所有相关信息,以便检测错误,但插入代码 printk 以提供详细信息可能会花费与编写代码解决问题一样多的时间。通常在使用 printk 显示的调试消息的完整性与将这些消息插入文本中所需的时间之间需要有权衡。

一种非常简单、插入 printk 更省时并使我们能够分析测试指令流的方法是使用预定义的常量 __FILE____LINE____func__

  • __FILE__ 会被编译器替换为当前正在编译的源文件的名称。
  • __LINE__ 会被编译器替换为当前源文件中当前指令所在的行号。
  • __func__ / __FUNCTION__ 会被编译器替换为当前指令所在的函数的名称。

注解

__FILE____LINE__ 是 ANSI C 规范的一部分,__func__ 是 C99 规范的一部分;__FUNCTION__ 是 GNU 的一个 C 扩展,不具有可移植性;然而,由于我们编写的代码是针对 Linux 内核的,所以可以毫无问题地使用它们。

可以在这种情况下使用以下宏定义:

#define PRINT_DEBUG \
       printk (KERN_DEBUG "[% s]: FUNC:% s: LINE:% d \ n", __FILE__,
               __FUNCTION__, __LINE__)

然后,在每个想要查看执行是否“到达”的位置,插入 PRINT_DEBUG;这是一种简单快捷的方法,通过仔细分析输出可以得出结果。

dmesg 命令用于查看在控制台上不显示,需要使用 printk 来打印的消息。

要删除日志文件中的所有先前消息,请运行:

cat /dev/null > /var/log/debug

要删除 dmesg 命令显示的消息,请运行:

dmesg -c

动态调试

动态调试( dyndbg )技术可以动态地激活/停用调试。与 printk 不同,它提供了更高级的 printk 选项,可以用于仅显示我们想要的消息;其对于复杂模块或故障排除子系统非常有用。这显著减少了显示的消息数量,只留下与调试上下文相关的消息。要启用 dyndbg ,内核必须编译时启用 CONFIG_DYNAMIC_DEBUG 选项。一旦配置了这个选项,就可以每次调用时动态启用 pr_debug()dev_dbg()print_hex_dump_debug()print_hex_dump_bytes()

debugfs 中的 /sys/kernel/debug/dynamic_debug/control 文件可以用于过滤消息或查看现有过滤器。

mount -t debugfs none /debug

Debugfs 是个简单的文件系统,用作内核空间接口和用户空间接口,以配置不同的调试选项。任何调试工具都可以在 debugfs 中创建和使用自己的文件/文件夹。

例如,要显示 dyndbg 中的现有过滤器,可以使用以下命令:

cat /debug/dynamic_debug/control

要启用 svcsock.c 文件中第 1603 行的调试消息:

echo 'file svcsock.c line 1603 +p' > /debug/dynamic_debug/control

/debug/dynamic_debug/control 文件不是普通文件。它显示了过滤器的 dyndbg 设置。使用 echo 在其中写入会更改这些设置(实际上不会进行写入)。请注意,该文件包含了 dyndbg 调试消息的设置。不要在该文件中进行日志记录。

Dyndbg 选项

  • func ——只显示与过滤器中定义的函数名称相同的函数的调试消息。

    echo 'func svc_tcp_accept +p' > /debug/dynamic_debug/control
    
  • file ——要显示调试消息的文件名。可以只是源文件名,也可以是绝对路径或内核树路径。

    file svcsock.c
    file kernel/freezer.c
    file /usr/src/packages/BUILD/sgi-enhancednfs-1.4/default/net/sunrpc/svcsock.c
    
  • module ——显示模块名称。

    module sunrpc
    
  • format ——只显示显示格式包含指定字符串的消息。

    format "nfsd: SETATTR"
    
  • line - 显示调试调用的行号或行号范围。

    # 在 svcsock.c 文件的第 1603 行到第 1605 行之间触发调试消息
    $ echo 'file svcsock.c line 1603-1605 +p' > /sys/kernel/debug/dynamic_debug/control
    # 从文件开头到第 1605 行启用调试消息
    $ echo 'file svcsock.c line -1605 +p' > /sys/kernel/debug/dynamic_debug/control
    

除了上述选项外,还可以使用操作符 +-= 来添加、删除或设置一系列标志:

  • p 激活 pr_debug()。
  • f 在打印的消息中包含函数名。
  • l 在打印的消息中包含行号。
  • m 在打印的消息中包含模块名称。
  • t 如果不是从中断上下文调用,则包括线程 ID。
  • _ 不设置标志。

KDB:内核调试器

内核调试器已被证明在开发和调试过程中非常有用。其主要优势之一是可以进行实时调试。这使得我们能够实时监视对内存的访问,甚至在调试过程中修改内存。内核调试器从版本 2.6.26-rc1 开始,已集成到主线内核中。KDB 不是一个 源代码调试器,但在进行完整分析时,可以与 gdb 和符号文件并行使用——请参见 GDB调试部分

要使用 KDB,你有以下选项:

  • 非 USB 键盘 + VGA 文本控制台
  • 串口控制台
  • USB EHCI 调试端口

在实验中,我们将使用连接到主机的串口接口。以下命令将在串口上激活 GDB:

echo hvc0 > /sys/module/kgdboc/parameters/kgdboc

KDB 是一种 停止模式调试器,这意味着在其活动期间,所有其他进程都将停止。可以使用 SysRq 命令强制内核在执行过程中进入 KDB

echo g > /proc/sysrq-trigger

或者在连接到串口的终端中使用键盘组合键 Ctrl+O g (例如使用 minicom)。

KDB 具有各种命令来控制和定义被调试系统的上下文:

  • lsmod, ps, kill, dmesg, env, bt(backtrace,回溯)
  • 转储跟踪日志
  • 硬件断点
  • 修改内存

要获取有关可用命令的更详细描述,可以在 KDB shell 中使用 help 命令。在下一个示例中,你可以看到一个简单的 KDB 使用示例,它设置了一个硬件断点来监视 mVar 变量的更改。

# 触发 KDB
echo g > /proc/sysrq-trigger
# 或者如果我们连接到串口,使用以下命令
Ctrl-O g
# 在对 mVar 变量进行写访问时设置断点
kdb> bph mVar dataw
# 从KDB返回
kdb> go

注解

如果你想学习如何轻松浏览 Linux 源代码和调试内核代码,请阅读 了解更多 部分。

练习

备注

注解

  • 通常,开发内核模块的步骤如下:
    • 编辑模块源代码(在物理机上);
    • 编译模块(在物理机上);
    • 生成用于虚拟机的最小镜像;该镜像包含内核、你的模块、busybox 以及测试程序;
    • 使用 QEMU 启动虚拟机;
    • 在虚拟机中运行测试。
  • 当使用 cscope 时,请使用文件 ~/src/linux。如果没有文件 cscope.out,可以使用命令 make ARCH=x86 cscope 来生成它。
  • 你可以在 推荐配置 找到有关虚拟机的更多详细信息。

重要

在解决练习之前, 请 仔细 阅读所有要点。

重要

我们强烈建议你使用 这个仓库 中的配置。

要解决练习问题,你需要执行以下步骤:

  • 用模板来准备骨架
  • 构建模块
  • 启动虚拟机并在虚拟机中测试模块。

当前实验名称为 kernel_modules。请参阅任务名称的练习。

骨架代码是从位于 tools/labs/templates 的完整源代码示例中生成的。要解决任务,首先要为所有实验生成骨架代码:

tools/labs $ make clean
tools/labs $ LABS=<实验名称> make skels

你还可以使用以下命令为单个任务生成骨架代码:

tools/labs $ LABS=<实验名称>/<任务名称> make skels

生成骨架驱动程序后,构建源代码:

tools/labs $ make build

然后,启动虚拟机:

tools/labs $ make console

模块将放置在 /home/root/skels/kernel_modules/<任务名称> 目录中。

重新构建模块时,无需停止虚拟机!本地 skels 目录与虚拟机共享。

请查看 练习 部分以获取更详细的信息。

警告

在开始练习或生成骨架之前,请在 Linux 仓库中运行 git pull 命令,以确保你拥有最新版本的练习。

如果你有本地更改,pull 命令将失败。使用 git status 检查本地更改。如果要保留更改,在 pull 之前运行 git stash,之后运行 git stash pop。要放弃更改,请运行 git reset --hard master

如果你在 git pull 之前已经生成了骨架,你需要再次生成骨架。

1. 内核模块

为了使用内核模块,我们将按照 上述 步骤进行操作。

tools/labs 目录下运行以下命令生成 1-2-test-mod 任务的骨架,然后构建模块。

$ LABS=kernel_modules make skels
$ make build

这些命令将构建当前实验骨架中的所有模块。

警告

在解决练习 3 之前,编译 3-error-mod 时会出现编译错误。为了避免此问题,请删除 skels/kernel_modules/3-error-mod/ 目录,并从 skels/Kbuild 中删除相应的行。

使用 make console 启动虚拟机,并执行以下任务:

  • 加载内核模块。
  • 列出内核模块并检查当前模块是否存在。
  • 卸载内核模块。
  • 使用 dmesg 命令查看加载/卸载内核模块时显示的消息。

注解

请阅读 加载/卸载内核模块 部分。在卸载内核模块时,只需指定模块名称(不包括扩展名)。

2. Printk

观察虚拟机控制台。为什么消息直接显示在虚拟机控制台上?

配置系统,使消息不直接显示在串行控制台上,只能使用 dmesg 命令来查看。

提示

一种方法是通过将所需级别写入 /proc/sys/kernel/printk 来设置控制台日志级别。使用的值应小于模块源代码中用于打印消息的级别。

重新加载/卸载该模块。消息不应该打印到虚拟机控制台上,但是在运行 dmesg 命令时应该可见。

3. 错误

生成名为 3-error-mod 的任务的框架。编译源代码并得到相应的内核模块。

为什么会出现编译错误? 提示: 这个模块与前一个模块有什么不同?

修改该模块以解决这些错误的原因,然后编译和测试该模块。

4. 子模块

查看 4-multi-mod/ 目录中的 C 源代码文件 mod1.cmod2.c。模块 2 仅包含模块 1 使用的函数的定义。

修改 Kbuild 文件,从这两个 C 源文件创建 multi_mod.ko 模块。

提示

阅读实验室中的 编译内核模块 部分。

编译、复制、启动虚拟机、加载和卸载内核模块。确保消息在控制台上正确显示。

5. 内核 oops

进入任务目录 5-oops-mod 并检查 C 源代码文件。注意问题将在哪里发生。在 Kbuild 文件中添加编译标记 -g

提示

阅读实验中的 编译内核模块 部分。

编译相应的模块并将其加载到内核中。识别 oops 出现的内存地址。

提示

阅读实验中的 `调试`_ 部分。要识别地址,请遵循 oops 消息并提取指令指针 (EIP) 寄存器的值。

确定是哪条指令触发了 oops。

提示

使用 proc/modules 信息获取内核模块的加载地址。在物理机上使用 objdump 和/或 addr2line。Objdump 需要编译时开启调试支持!请阅读实验中的 objdumpaddr2line 部分。

尝试卸载内核模块。请注意,该操作无法成功,因为自 oops 发生以来,内核模块内部仍然存在对内核的引用;在释放这些引用之前(在 oops 的情况下几乎不可能),模块无法卸载。

6. 模块参数

进入任务目录 6-cmd-mod 并检查 C 源代码文件 cmd_mod.c。编译并复制相关的模块,然后加载内核模块以查看 printk 消息。然后从内核中卸载该模块。

在不修改源代码的情况下,加载内核模块以显示消息 Early bird gets tired

提示

可以通过向模块传递参数来更改 str 变量。在 这里 找到更多相关信息。

7. 进程信息

检查名为 7-list-proc 的任务的框架。添加代码来显示当前进程的进程 ID( PID )和可执行文件名。

按照标记为 TODO 的命令进行操作。在加载和卸载模块时,必须显示这些信息。

注解

  • 在Linux内核中,进程由 struct task_struct 描述。使用 LXRcscope 来查找 struct task_struct 的定义。
  • 要找到包含可执行文件名的结构字段,请查找“executable”的注释。
  • 内核中给定时间运行的当前进程的结构指针由 current 变量(类型为 struct task_struct*)给出。

提示

要使用 current,你需要包含定义 struct task_struct 的头文件,即 linux/sched.h

编译、复制、启动虚拟机并加载模块。卸载内核模块。

重复加载/卸载操作。注意显示的进程 PID 是不同的。这是因为在加载模块时,从可执行文件 /sbin/insmod 创建了一个进程,而在卸载模块时,从可执行文件 /sbin/rmmod 创建了一个进程。

了解更多

以下部分包含帮助你适应 Linux 内核代码和调试技术的有用信息。

源代码导航

cscope

Cscope 是一个用于高效导航 C 源代码的工具。要使用它,你必须从现有的源代码生成 cscope 数据库。在 Linux 树中,执行命令 make ARCH=x86 cscope 就足够了。尽管不是必须通过 ARCH 变量指定架构,但建议这样做;否则,一些依赖于特定架构的函数会在数据库中出现多次。

你可以使用命令 make ARCH=x86 COMPILED_SOURCE=1 cscope 来构建 cscope 数据库。这样,cscope 数据库中只会包含在编译过程中使用过的符号(symbol),从而在搜索符号时可以获得更好的性能。

Cscope 也可以作为独立工具使用,但与编辑器结合使用会更加有用。要在 vim 中使用 cscope,你需要安装这两个软件包,并在文件 .vimrc 中添加以下几行(实验中的机器已经进行了配置):

if has("cscope")
        " Look for a 'cscope.out' file starting from the current directory,
        " going up to the root directory.
        let s:dirs = split(getcwd(), "/")
        while s:dirs != []
                let s:path = "/" . join(s:dirs, "/")
                if (filereadable(s:path . "/cscope.out"))
                        execute "cs add " . s:path . "/cscope.out " . s:path . " -v"
                        break
                endif
                let s:dirs = s:dirs[:-2]
        endwhile

        set csto=0  " Use cscope first, then ctags
        set cst     " Only search cscope
        set csverb  " Make cs verbose

        nmap `<C-\>`s :cs find s `<C-R>`=expand("`<cword>`")`<CR>``<CR>`
        nmap `<C-\>`g :cs find g `<C-R>`=expand("`<cword>`")`<CR>``<CR>`
        nmap `<C-\>`c :cs find c `<C-R>`=expand("`<cword>`")`<CR>``<CR>`
        nmap `<C-\>`t :cs find t `<C-R>`=expand("`<cword>`")`<CR>``<CR>`
        nmap `<C-\>`e :cs find e `<C-R>`=expand("`<cword>`")`<CR>``<CR>`
        nmap `<C-\>`f :cs find f `<C-R>`=expand("`<cfile>`")`<CR>``<CR>`
        nmap `<C-\>`i :cs find i ^`<C-R>`=expand("`<cfile>`")`<CR>`$`<CR>`
        nmap `<C-\>`d :cs find d `<C-R>`=expand("`<cword>`")`<CR>``<CR>`
        nmap <F6> :cnext <CR>
        nmap <F5> :cprev <CR>

        " Open a quickfix window for the following queries.
        set cscopequickfix=s-,c-,d-,i-,t-,e-,g-
endif

该脚本会在当前目录或父目录中搜索名为 cscope.out 的文件。如果 vim 找到该文件,你可以使用快捷键 Ctrl + ]Ctrl+\ g (按下 control-\ 然后按 g) 直接跳转到光标所在单词的定义(函数、变量、结构等)。类似地,你可以使用 Ctrl+\ s 前往光标所在单词的使用位置。

你可以从以下网址获取启用了 cscope 的 .vimrc 文件(还包含其他好用的东西):https://github.com/ddvlad/cfg/blob/master/_vimrc。以下指南基于该文件,同时也展示了具有相同效果的基本 vim 命令。

如果有多个结果(通常会有),你可以使用 F6F5:ccnext:cprev)在它们之间切换。你还可以使用命令 :copen 打开一个新的面板来显示结果。要关闭面板,可以使用 :cclose 命令。

要返回到先前的位置,可以使用 Ctrl+o (是字母 o,不是零)。该命令可以多次使用,即使 cscope 更改了你当前正在编辑的文件也有效。

要在 vim 启动时直接跳转到符号定义,可以使用 vim -t <symbol_name> (例如 vim -t task_struct)。如果你已经启动了 vim 并想按名称搜索符号,可以使用 cs find g <symbol_name> (例如 cs find g task_struct)。

如果你找到了多个结果,并且用 :copen 命令打开了一个显示所有匹配项的面板,如果你想在面板中找到某种结构类型的符号,建议你用 / ——斜杠命令在面板中搜索字符 { (左花括号)。

重要

你可以使用命令 :cs help 获取所有 cscope 命令的摘要。

若要了解更多信息,请使用 vim 内置的帮助命令::h cscope:h copen

如果你使用 emacs,请安装 xcscope-el 包,并在 ~/.emacs 文件中添加以下行。

(require ‘xcscope)
(cscope-setup)

这些命令将自动为 C 和 C++ 模式激活 cscope。C-s s 是按键绑定前缀,C-s s s 用于搜索符号(如果光标位置在单词上,调用它时将使用该位置的单词)。有关详细信息,请查看 https://github.com/dkogan/xcscope.el

clangd

Clangd 是一个语言服务器,提供了一些用于浏览 C 和 C++ 代码的工具。语言服务器协议 利用语义全项目分析,实现了诸如跳转到定义、查找引用、悬停提示、代码补全等功能。

Clangd 需要一个编译数据库来理解内核源代码。可以通过以下方式生成编译数据库:

make defconfig
make
scripts/clang-tools/gen_compile_commands.py

LSP 客户端:

Kscope

如果想要更简单的界面的话,可以尝试 Kscope。Kscope 是一个使用 QT 的 cscope 前端。它轻便、快速、易用。它支持使用正则表达式、调用图等方式进行搜索。Kscope 已经停止维护了。

还有一个适用于 Qt4 和 KDE 4 的 移植版本 ,其保留了与文本编辑器 Kate 的集成,并且比 SourceForge 上的最新版本更易于使用。

LXR Cross-Reference

LXR(LXR Cross-Reference)是一种工具,允许使用 Web 界面来索引和引用程序源代码中的符号。Web 界面显示了符号在文件中定义或使用的位置的链接。LXR 的开发网站是 http://sourceforge.net/projects/lxr。类似的工具有 OpenGrokGonzui

尽管 LXR 最初是用于 Linux 内核源代码的,但也用于 MozillaApache HTTP 服务器FreeBSD 的源代码。

有许多网站使用 LXR 来进行 Linux 内核源代码的交叉引用,主要网站是 开发原址,然而该网站已不再运作。你可以使用 https://elixir.bootlin.com/

LXR 允许在任意文本或文件名上搜索标识符(符号)。它提供的主要特点和优势是可以轻松地找到任何全局标识符的声明。这样,它便于快速访问函数声明、变量、宏定义,以及轻松地浏览代码。此外,它还能够检测当变量或函数发生变化时,哪些代码区域会受到影响,这对于开发和调试阶段是一个真正的优势。

SourceWeb

SourceWeb 是一个用于 C 和 C++ 的源代码索引器。它使用 Clang 编译器提供的 框架 来索引代码。

cscope 和 SourceWeb 之间的主要区别在于,SourceWeb 在某种程度上是一个编译器插件。SourceWeb 不会索引所有的代码,而只会索引实际被编译器编译的代码。这样的话,一些问题就没有了,例如在多个位置定义的函数变体中的的哪个被使用的歧义。这也意味着索引需要更多的时间,因为编译后的文件必须再次通过索引器生成引用。

使用示例:

make oldconfig
sw-btrace make -j4
sw-btrace-to-compile-db
sw-clang-indexer --index-project
sourceweb index

sw-btrace 是一个添加 libsw-btrace.so 库到 LD_PRELOAD 的脚本。这样,该库将被 make 启动的每个进程(基本上是编译器)加载, 注册用于启动进程的命令,并生成一个名为 btrace.log 的文件。然后,sw-btrace-to-compile-db 使用该文件将其转换为 clang 定义的格式: JSON Compilation Database 。 然后上述步骤生成的 JSON 编译数据库由索引器使用,索引器通过已编译的源文件再进行一次遍历,生成 GUI 使用的索引。

建议:不要对正在使用的源代码进行索引,而是使用其副本,因为 SourceWeb 目前没有单独重新生成单个文件的索引的功能,你将不得不重新生成完整的索引。

内核调试

与调试程序相比,调试内核更加困难,因为操作系统没有提供支持。这就是为什么通常使用两台通过串行接口相互连接的计算机进行此过程。

gdb(Linux)

在 Linux 上,一种更简单但也具有许多缺点的调试方法是使用 gdb 进行本地调试,其中涉及到未压缩的内核镜像 (vmlinux) 和文件:/proc/kcore (实时内核镜像)。这种方法通常用于检查内核并在其运行时检测特定的不一致性。特别是如果内核是使用 -g 选项编译的(该选项会保留调试信息)这种方法就非常有用。但是,这种方法无法使用一些常用的调试技术,例如数据修改的断点。

注解

因为 /proc 是一个虚拟文件系统,/proc/kcore 在磁盘上并不存在。当程序尝试访问 /proc/kcore 时,内核会即时生成它。它用于调试目的。

根据 man proc 的说明:

/proc/kcore
此文件代表系统的物理内存,并以 ELF 核心文件格式存储。借助这个伪文件(pseudo-file)和未剥离(unstripped)的内核(/usr/src/linux/vmlinux)二进制文件,可以使用 GDB 来检查任何内核数据结构的当前状态。

未压缩的内核镜像提供关于其中所包含的数据结构和符号的信息。

student@eg106$ cd ~/src/linux
student@eg106$ file vmlinux
vmlinux: ELF 32-bit LSB executable, Intel 80386, ...
student@eg106$ nm vmlinux | grep sys_call_table
c02e535c R sys_call_table
student@eg106$ cat System.map | grep sys_call_table
c02e535c R sys_call_table

nm 程序用于显示对象或可执行文件中的符号。在我们的例子中,vmlinux 是一个 ELF 文件。或者,我们可以使用文件 System.map 来查看内核中的符号信息。

然后,我们使用 gdb 来使用未压缩的内核镜像检查这些符号。一个简单的 gdb 会话如下所示:

student@eg106$ cd ~/src/linux
stduent@eg106$ gdb --quiet vmlinux
Using host libthread_db library "/lib/tls/libthread_db.so.1".
(gdb) x/x 0xc02e535c
0xc02e535c `<sys_call_table>`:    0xc011bc58
(gdb) x/16 0xc02e535c
0xc02e535c `<sys_call_table>`:    0xc011bc58      0xc011482a      0xc01013d3     0xc014363d
0xc02e536c `<sys_call_table+16>`: 0xc014369f      0xc0142d4e      0xc0142de5     0xc011548b
0xc02e537c `<sys_call_table+32>`: 0xc0142d7d      0xc01507a1      0xc015042c     0xc0101431
0xc02e538c `<sys_call_table+48>`: 0xc014249e      0xc0115c6c      0xc014fee7     0xc0142725
(gdb) x/x sys_call_table
0xc011bc58 `<sys_restart_syscall>`:       0xffe000ba
(gdb) x/x &sys_call_table
0xc02e535c `<sys_call_table>`:    0xc011bc58
(gdb) x/16 &sys_call_table
0xc02e535c `<sys_call_table>`:    0xc011bc58      0xc011482a      0xc01013d3     0xc014363d
0xc02e536c `<sys_call_table+16>`: 0xc014369f      0xc0142d4e      0xc0142de5     0xc011548b
0xc02e537c `<sys_call_table+32>`: 0xc0142d7d      0xc01507a1      0xc015042c     0xc0101431
0xc02e538c `<sys_call_table+48>`: 0xc014249e      0xc0115c6c      0xc014fee7     0xc0142725
(gdb) x/x sys_fork
0xc01013d3 `<sys_fork>`:  0x3824548b
(gdb) disass sys_fork
Dump of assembler code for function sys_fork:
0xc01013d3 `<sys_fork+0>`:        mov    0x38(%esp),%edx
0xc01013d7 `<sys_fork+4>`:        mov    $0x11,%eax
0xc01013dc `<sys_fork+9>`:        push   $0x0
0xc01013de `<sys_fork+11>`:       push   $0x0
0xc01013e0 `<sys_fork+13>`:       push   $0x0
0xc01013e2 `<sys_fork+15>`:       lea    0x10(%esp),%ecx
0xc01013e6 `<sys_fork+19>`:       call   0xc0111aab `<do_fork>`
0xc01013eb `<sys_fork+24>`:       add    $0xc,%esp
0xc01013ee `<sys_fork+27>`:       ret
End of assembler dump.

可以注意到未压缩的内核映像被用作 gdb 的参数。在编译后,可以在内核源代码的根目录中找到该映像。

使用 gdb 进行调试的几个命令如下:

  • x (examine)——用于显示指定地址的内存区域的内容(该地址可以是物理地址的值、符号或符号的地址)。它可以接受以下参数(以 / 开头):要显示数据的格式(x 表示十六进制,d 表示十进制,等等)、要显示的内存单元(memory unit)数量以及单个内存单元的大小。
  • disassemble ——用于反汇编函数。
  • p (print)——用于评估并显示表达式的值。可以通过参数指定要显示数据的格式(/x 表示十六进制,/d 表示十进制,等等)。

对内核映像的分析是一种静态分析方法。如果我们想进行动态分析(分析内核的运行情况,而不仅仅是静态映像),我们可以使用 /proc/kcore;这是内核的动态映像(存储在内存中)。

student@eg106$ gdb ~/src/linux/vmlinux /proc/kcore
Core was generated by `root=/dev/hda3 ro'.
#0  0x00000000 in ?? ()
(gdb) p sys_call_table
$1 = -1072579496
(gdb) p /x sys_call_table
$2 = 0xc011bc58
(gdb) p /x &sys_call_table
$3 = 0xc02e535c
(gdb) x/16 &sys_call_table
0xc02e535c `<sys_call_table>`:    0xc011bc58      0xc011482a      0xc01013d3     0xc014363d
0xc02e536c `<sys_call_table+16>`: 0xc014369f      0xc0142d4e      0xc0142de5     0xc011548b
0xc02e537c `<sys_call_table+32>`: 0xc0142d7d      0xc01507a1      0xc015042c     0xc0101431
0xc02e538c `<sys_call_table+48>`: 0xc014249e      0xc0115c6c      0xc014fee7     0xc0142725

使用内核的动态镜像有助于检测 rootkit

获取堆栈跟踪

有时,你需要获取有关执行路径到达某个特定点的信息。你可以使用 cscope 或 LXR 来确定这些信息,但某些函数从许多执行路径调用,这使得这种方法变得困难。

在这些情况下,使用函数 dump_stack() 获取堆栈跟踪非常有用。