Linux内核调试方法

时间:2024-03-12 18:33:41

kdb:只能在汇编代码级进行调试;

  优点是不需要两台机器进行调试。

  gdb:在调试模块时缺少一些至关重要的功能,它可用来查看内核的运行情况,包括反汇编内核函数。

  kgdb:能很方便的在源码级对内核进行调试,缺点是kgdb只能进行远程调试,它需要一根串口线及两台机器来调试内核(也可以是在同一台主机上用vmware软件运行两个操作系统来调试)

printk() 是调试内核代码时最常用的一种技术。在内核代码中的特定位置加入printk() 调试调用,可以直接把所关心的信息打打印到屏幕上,从而可以观察程序的执行路径和所关心的变量、指针等信息。 Linux 内核调试器(Linux kernel debugger,kdb)是 Linux 内核的补丁,它提供了一种在系统能运行时对内核内存和数据结构进行检查的办法。Oops、KDB在文章掌握 Linux 调试技术有详细介绍,大家可以参考。 Kprobes 提供了一个强行进入任何内核例程,并从中断处理器无干扰地收集信息的接口。使用 Kprobes 可以轻松地收集处理器寄存器和全局数据结构等调试信息,而无需对Linux内核频繁编译和启动,具体使用方法,请参考使用 Kprobes 调试内核。

/proc文件系统

在 /proc 文件系统中,对虚拟文件的读写操作是一种与内核通信的手段,要查看内核回环缓冲区中的消息,可以使用 dmesg 工具(或者通过 /proc 本身使用 cat /proc/kmsg 命令)。清单 6 给出了 dmesg 显示的最后几条消息。

清单 6. 查看来自 LKM 的内核输出

[root@plato]# dmesg | tail -5
cs: IO port probe 0xa00-0xaff: clean.
eth0: Link is down
eth0: Link is up, running at 100Mbit half-duplex
my_module_init called.  Module is now loaded.
my_module_cleanup called.  Module is now unloaded.


可以在内核输出中看到这个模块的消息。现在让我们暂时离开这个简单的例子,来看几个可以用来开发有用 LKM 的内核 API。

调试工具

  使用调试器来一步步地跟踪代码,查看变量和计算机寄存器的值。在内核中使用交互式调试器是一个很复杂的问题。内核在它自己的地址空间中运行。许多用户空间下的调试器所提供的常用功能很难用于内核之中,比如断点和单步调试等。

目录

[隐藏]

内核bug跟踪

oops消息分析

(1)oops消息产生机制

oops(也称 panic),称程序运行崩溃,程序崩溃后会产生oops消息。应用程序或内核线程的崩溃都会产生oops消息,通常发生oops时,系统不会发生死机,而在终端或日志中打印oops信息。

当使用NULL指针或不正确的指针值时,通常会引发一个 oops 消息,这是因为当引用一个非法指针时,页面映射机制无法将虚拟地址映像到物理地址,处理器就会向操作系统发出一个"页面失效"的信号。内核无法"换页"到并不存在的地址上,系统就会产生一个"oops"。

oops 显示发生错误时处理器的状态,包括 CPU 寄存器的内容、页描述符表的位置,以及其一些难理解的信息。这些消息由失效处理函数(arch/*/kernel/traps.c)中的printk 语句产生。较为重要的信息就是指令指针(EIP),即出错指令的地址。

由于很难从十六进制数值中看出含义,可使用符号解析工具klogd。klogd 守护进程能在 oops 消息到达记录文件之前对它们解码。klogd在缺省情况下运行并进行符号解码。

通常Oops文本由klogd从内核缓冲区里读取并传给syslogd,由syslogd写到syslog文件中,该文件典型为/var/log/messages(依赖于/etc/syslog.conf)。如果klogd崩溃了,用户可"dmesg > file"从内核缓冲区中读取数据并保存下来。还可用"cat /proc/kmsg > file"读取数据,此时,需要用户中止传输,因为kmsg是一个"永不结束的文件"。

当保护错误发生时,klogd守护进程自动把内核日志信息中的重要地址翻译成它们相应的符号。klogd执行静态地址翻译和动态地址翻译。静态地址翻译使用System.map文件将符号地址翻译为符号。klogd守护进程在初始化时必须能找到system.map文件。

动态地址翻译通常对内核模块中的符号进行翻译。内核模块的内存从内核动态内存池里分配,内核模块中符号的位置在内核装载后才最终确定。

Linux内核提供了调用,允许程序决定装载哪些模块和它们在内存中位置。通过这些系统调用,klogd守护进程生成一张符号表用于调试发生在可装载模块中的保护错误。内核模块的装载或者卸载都会自动向klogd发送信号,klogd可将内核模块符号的地址动态翻译为符号字符串。

(2)产生oops的样例代码

使用空指针和缓冲区溢出是产生oops的两个最常见原因。下面两个函数faulty_write和faulty_read是一个内核模块中的写和读函数,分别演示了这两种情况。当内核调用这两个函数时,会产生oops消息。

函数faulty_write删除一个NULL指针的引用,由于0不是一个有效的指针值,内核将打印oops信息,并接着,杀死调用些函数的进程。
ssize_t faulty_write (struct file *filp, const char _ _user *buf, size_t count, loff_t *pos)
{
    /* make a simple fault by dereferencing a NULL pointer */
    *(int *)0 = 0;
    return 0;
}

函数faulty_write产生oops信息列出如下(注意 EIP 行和 stack 跟踪记录中已经解码的符号):
Unable to handle kernel NULL pointer dereference at virtual address \
   00000000 

printing eip: c48370c3 *pde = 00000000 Oops: 0002 CPU: 0 EIP: 0010:[faulty:faulty_write+3/576] EFLAGS: 00010286 eax: ffffffea ebx: c2c55ae0 ecx: c48370c0 edx: c2c55b00 esi: 0804d038 edi: 0804d038 ebp: c2337f8c esp: c2337f8c ds: 0018 es: 0018 ss: 0018 Process cat (pid: 23413, stackpage=c2337000) Stack: 00000001 c01356e6 c2c55ae0 0804d038 00000001 c2c55b00 c2336000 \

          00000001 
     0804d038 bffffbd4 00000000 00000000 bffffbd4 c010b860 00000001 \ 
          0804d038 
     00000001 00000001 0804d038 bffffbd4 00000004 0000002b 0000002b \ 
          00000004 

Call Trace: [sys_write+214/256] [system_call+52/56]

Code: c7 05 00 00 00 00 00 00 00 00 31 c0 89 ec 5d c3 8d b6 00 00

上述oops消息中,字符串 3/576 表示处理器正处于函数的第3个字节上,函数整体长度为 576 个字节。 函数faulty_read拷贝一个字符串到本地变量,由于字符串比目的地数组长造成缓冲区溢出。当函数返回时,缓冲区溢出导致产生oops信息。因为返回指令引起指令指针找不到运行地址,这种错误很难发现和跟踪。
ssize_t faulty_read(struct file *filp, char _ _user *buf, size_t count, loff_t *pos)
{
    int ret;
    char stack_buf[4];
    /* Let\'s try a buffer overflow */
    memset(stack_buf, 0xff, 20);
    if (count > 4)
        count = 4;
    /* copy 4 bytes to the user */
    ret = copy_to_user(buf, stack_buf, count);
    if (!ret)
        return count;
    return ret;
}

函数faulty_read产生oops信息列出如下:
EIP: 0010:[<00000000>]

Unable to handle kernel paging request at virtual address ffffffff printing eip: ffffffff Oops: 0000 [#5] SMP CPU: 0 EIP: 0060:[] Not tainted EFLAGS: 00010296 (2.6.6) EIP is at 0xffffffff eax: 0000000c ebx: ffffffff ecx: 00000000 edx: bfffda7c esi: cf434f00 edi: ffffffff ebp: 00002000 esp: c27fff78 ds: 007b es: 007b ss: 0068 Process head (pid: 2331, threadinfo=c27fe000 task=c3226150) Stack: ffffffff bfffda70 00002000 cf434f20 00000001 00000286 cf434f00 fffffff7 bfffda70 c27fe000 c0150612 cf434f00 bfffda70 00002000 cf434f20 00000000 00000003 00002000 c0103f8f 00000003 bfffda70 00002000 00002000 bfffda70 Call Trace: [] sys_read+0x42/0x70 [] syscall_call+0x7/0xb

Code: Bad EIP value.

在上述oops消息中,由于缓冲区溢出,仅能看到函数调用栈的一部分,看不见函数名vfs_read和faulty_read,并且代码(Code)处仅输出"bad EIP value.",列在栈上开始处的地址"ffffffff"表示内核栈已崩溃。

(3)oops信息分析

面对产生的oops信息,首先应查找源程序发生oops的位置,通过查看指令指令寄存器EIP的值,可以找到位置,如:EIP: 0010:[faulty:faulty_write+3/576]。

再查找函数调用栈(call stack)可以得到更多的信息。从函数调用栈可辨别出局部变量、全局变量和函数参数。例如:在函数faulty_read的oops信息的函数调用栈中,栈顶为ffffffff,栈顶值应为一个小于ffffffff的值,为此值,说明再找不回调用函数地址,说明有可能因缓冲区溢出等原因造成指针错误。

在x86构架上,用户空间的栈从0xc0000000以下开始,递归值bfffda70可能是用户空间的栈地址。实际上它就是传递给read系统调用的缓冲区地址,系统调用read进入内核时,将用户空间缓冲区的数据拷贝到内核空间缓冲区。

如果oops信息显示触发oops的地址为0xa5a5a5a5,则说明很可能是因为没有初始化动态内存引起的。

另外,如果想看到函数调用栈的符号,编译内核时,请打开CONFIG_KALLSYMS选项。

klogd 提供了许多信息来帮助分析。为了使 klogd 正确地工作,必须在 /boot 中提供符号表文件 System.map。如果符号表与当前内核不匹配,klogd 就会拒绝解析符号。

有时内核错误会将系统完全挂起。例如代码进入一个死循环,系统不会再响应任何动作。这时可通过在一些关键点上插入 schedule 调用可以防止死循环。

系统崩溃重启动

由于内核运行错误,在某些极端情况下,内核会运行崩溃,内核崩溃时会导致死机。为了解决此问题,内核引入了快速装载和重启动新内核机制。内核通过kdump在崩溃时触发启动新内核,存储旧内存映像以便于调试,让系统在新内核上运行 ,从而避免了死机,增强了系统的稳定性。

(1)工具kexec介绍

kexec是一套系统调用,允许用户从当前正执行的内核装载另一个内核。用户可用shell命令"yum install kexec-tools"安装kexec工具包,安装后,就可以使用kexec命令。

工具kexec直接启动进入一个新内核,它通过系统调用使用户能够从当前内核装载并启动进入另一个内核。在当前内核中,kexec执行BootLoader的功能。在标准系统启动和kexec启动之间的主要区别是:在kexec启动期间,依赖于硬件构架的固件或BIOS不会被执行来进行硬件初始化。这将大大降低重启动的时间。

为了让内核的kexec功能起作用,内核编译配置是应确认先择了"CONFIG_KEXEC=y",在配置后生成的.config文件中应可看到此条目。

工具kexec的使用分为两步,首先,用kexec将调试的内核装载进内存,接着,用kexec启动装载的内核。

装载内核的语法列出如下:

kexec -l kernel-image --append=command-line-options --initrd=initrd-image

上述命令中,参数kernel-image为装载内核的映射文件,该命令不支持压缩的内核映像文件bzImage,应使用非压缩的内核映射文件vmlinux;参数initrd-image为启动时使用initrd映射文件;参数command-line-options为命令行选项,应来自当前内核的命令行选项,可从文件"/proc/cmdline"中提取,该文件的内容列出如下:

^-^$ cat /proc/cmdline

ro root=/dev/VolGroup00/LogVol00 rhgb quiet

例如:用户想启动的内核映射为/boot/vmlinux,initrd为/boot/initrd,则kexec加载命令列出如下:

Kexec –l /boot/vmlinux –append=/dev/VolGroup00/LogVol00 initrd=/boot/initrd

还可以加上选项-p或--load-panic,表示装载新内核在系统内核崩溃使用。

在内核装载后,用下述命令启动装载的内核,并进行新的内核中运行:

kexec -e

当kexec将当前内核迁移到新内核上运行时,kexec拷贝新内核到预保留内存块,该保留位置如图1所示, 原系统内核给kexec装载内核预保留一块内存(在图中的阴影部分),用于装载新内核,其他内存区域在未装载新内核时,由原系统内核使用。

Linux kernel debug method 02.png

图1 kexec装载的内核所在预保留位置示意图

在x86构架的机器上,系统启动时需要使用第一个640KB物理内存,用于内核装载,kexec在重启动进入转储捕捉的内核之前备份此区域。相似地,PPC64构架的机器在启动里需要使用第一个32KB物理内核,并需要支持64K页,kexec备份第一个64KB内存。

(2)kdump介绍

kdump是基于kexec的崩溃转储机制(kexec-based Crash Dumping),无论内核内核需要转储时,如:系统崩溃时,kdump使用kexec快速启动进入转储捕捉的内核。在这里,原运行的内核称为系统内核或原内核,新装载运行的内核称为转储捕捉的内核或装载内核或新内核。

在重启动过程中,原内核的内存映像被保存下来,并且转储捕捉的内核(新装载的内核)可以访问转储的映像。用户可以使用命令cp和scp将内存映射拷贝到一个本地硬盘上的转储文件或通过网络拷贝到远程计算机上。

当前仅x86, x86_64, ppc64和ia64构架支持kdump和kexec。

当系统内核启动时,它保留小部分内存给转储(dump)捕捉的内核,确保了来自系统内核正进行的直接内存访问(Direct Memory Access:DMA)不会破坏转储捕捉的内核。命令kexec –p装载新内核到这个保留的内存。

在崩溃前,所有系统内核的核心映像编码为ELF格式,并存储在内核的保留区域。ELF头的开始物理地址通过参数elfcorehdr=boot传递到转储捕捉的内核。

通过使用转储捕捉的内核,用户可以下面两种方式访问内存映像或旧内存:

(1)通过/dev/oldmem设备接口,捕捉工具程序能读取设备文件并以原始流的格式写出内存,它是一个内存原始流的转储。分析和捕捉工具必须足够智能以判断查找正确信息的位置。

(2)通过/proc/vmcore,能以ELF格式文件输出转储信息,用户可以用GDB(GNU Debugger)和崩溃调试工具等分析工具调试转储文件。

(3)建立快速重启动机制和安装工具

1)安装工具kexec-tools

可以下载源代码编译安装工具kexec-tools。由于工具kexec-tools还依赖于一些其他的库,因此,最好的方法是使用命令"yum install kexec-tools"从网上下载安装并自动解决依赖关系。

2)编译系统和转储捕捉的内核

可编译独立的转储捕捉内核用于捕捉内核的转储,还可以使用原系统内核作为转储捕捉内核,在这种情况下,不需要再编译独立的转储捕捉内核,但仅支持重定位内核的构架才可以用作转储捕捉的内核,如:构架i386和ia64支持重定位内核。

对于系统和转储捕捉内核来说,为了打开kdump支持,内核需要设置一些特殊的配置选项,下面分别对系统内核和转储捕捉内核的配置选项进行说明:

系统内核的配置选项说明如下:

  • 在菜单条目"Processor type and features."中打开选项"kexec system call",使内核编译安装kexe系统调用。配置文件.config生成语句"CONFIG_KEXEC=y"。
  • 在菜单条目"Filesystem"->"Pseudo filesystems."中打开选项"sysfs file system support",使内核编译安装文件系统sysfs.配置文件.config生成语句"CONFIG_SYSFS=y"。
  • 在菜单条目"Kernel hacking."中打开选项"Compile the kernel with debug info ",使内核编译安装后支持调试信息输出,产生调试符号用于分析转储文件。配置文件.config生成语句"CONFIG_DEBUG_INFO=Y"。

转储捕捉内核配置选项(不依赖于处理器构架)说明如下:

  • 在菜单条目"Processor type and features"中打开选项"kernel crash dumps",配置文件.config生成语句" CONFIG_CRASH_DUMP=y"。
  • 在菜单条目"Filesystems"->"Pseudo filesystems"中打开选项"/proc/vmcore support",配置文件.config生成语句"CONFIG_PROC_VMCORE=y"。

转储捕捉内核配置选项(依赖于处理器构架i386和x86_64)说明如下:

  • 在处理器构架i386上,在菜单条目"Processor type and features"中打开高端内存支持,配置文件.config生成语句"CONFIG_HIGHMEM64G=y"或"CONFIG_HIGHMEM4G"。
  • 在处理器构架i386和x86_64上,在菜单条目"rocessor type and features"中关闭对称多处理器支持,配置文件.config生成语句"CONFIG_SMP=n"。如果配置文件中的设置为"CONFIG_SMP=y",则可在装载转储捕捉内核的内核命令行上指定"maxcpus=1"。
  • 如果想构建和使用可重定位内核,在菜单条目"rocessor type and featuresIf"中打开选项"Build a relocatable kernel",配置文件.config生成语句"CONFIG_RELOCATABLE=y"。
  • 在菜单"Processor type and features"下的条目"Physical address where the kernel is loaded"设置合适的值用于内核装载的物理地址。它仅在打开了"kernel crash dumps"时出现。合适的值依赖于内核是否可重定位。

如果设置了值"CONFIG_PHYSICAL_START=0x100000",则表示使用可重定位内核。它将编译内核在物理地址1MB处,内核是可重定位的,因此,内核可从任何物理地址运行。Kexec BootLoader将装载内核到用于转储捕捉内核的内核保留区域。

否则,将使用启动参数"crashkernel=Y@X"指定第二个内核保留内核区域的开始地址,其中,Y表示内存区域的大小,X表示保留给转储捕捉内核的内存区域的开始地址,通过X为16MB (0x1000000),因此用户可设置"CONFIG_PHYSICAL_START=0x1000000"。

在配置完内核后,编译和安装内核及内核模块。

3)扩展的crashkernel语法

在系统内核的启动命令行选项中,通常语法"crashkernel=size[@offset]"对于大多数据配置已够用了,但有时候保留的内存依赖于系统RAM。此时可通过扩展的crashkernel命令行对内存进行 限制避免从机器上移去一部分内核后造成系统不可启动。扩展的crashkernel语法列出如下:

crashkernel=<range1>:<size1>[,<range2>:<size2>,...][@offset]

其中,range=start-[end]。

例如:crashkernel=512M-2G:64M,2G-:128M,含义为:如果内存小于512M,不设置保留内存,如果内存为512M到2G之间,设置保留内存区域为64M,如果内存大于128M,设置保留内存区域为128M。

4)启动进入系统内核

必要时更新BootLoader。然后用参数"crashkernel=Y@X"启动系统内核,如:crashkernel=64M@16M,表示告诉系统内核保留从物理地址0x01000000 (16MB)开始的64MB大小给转储捕捉内核使用。通常x86和x86_64平台设置"crashkernel=64M@16M",ppc64平台设置"crashkernel=128M@32M"。

5)装载转储捕捉内核

在启动进入系统内核后,需要装载转储捕捉内核。根据处理器构架和映射文件的类型(可否重定位),可以选择装载不压缩的vmlinux或压缩的bzImage/vmlinuz内核映像。选择方法说明如下:

对于i386和x86_64平台:

  • 如果内核不是可重定位的,使用vmlinux。
  • 如果内核是可重定位的,使用bzImage/vmlinuz。

对于ppc64平台:

  • 使用vmlinux。

对于ia64平台:

  • 使用vmlinux或vmlinuz.gz。
如果用户使用不压缩的vmlinux映像,那么使用下面的命令装载转储捕捉内核:
kexec -p <dump-capture-kernel-vmlinux-image> \
   --initrd=<initrd-for-dump-capture-kernel> --args-linux \
   --append="root=<root-dev> <arch-specific-options>"

如果用户使用压缩的bzImage/vmlinuz映像,那么使用下面的命令装载转储捕捉内核:
kexec -p <dump-capture-kernel-bzImage>\
  --initrd=<initrd-for-dump-capture-kernel> \
   --append="root=<root-dev> <arch-specific-options>"

注意:参数--args-linux在ia64平台中不用指定。

下面是在装载转储捕捉内核时使用的构架特定命令行选项:

  • 对于i386, x86_64和ia64平台,选项为"1 irqpoll maxcpus=1 reset_devices"。
  • 对于ppc64平台,选项为"1 maxcpus=1 noirqdistrib reset_devices"。

在装载转储捕捉内核时需要注意的事项说明如下:

  • 缺省设置下,ELF头以ELF64格式存储,以支持多于4GB内核的系统,在i386上,kexec自动检查物理RAM尺寸是否超过4GB限制,如果没有超过,使用ELF32。因此,在非PAE系统上ELF头总是使用ELF32格式。
  • 选项--elf32-core-headers可用于强制产生ELF32头,这是必要的,因为在32位系统上,GDB当前不能打开带有ELF64头的vmcore文件。
  • 在转储捕捉内核中,启动参数irqpoll减少了由于共享中断引起的驱动程序初始化失败。
  • 用户必须以命令mount输出的根设备名的格式指定<root-dev>。
  • 启动参数"1"将转储捕捉内核启动进入不支持网络的单用户模式。如果用户想使用网络,需要设置为3。
  • 通常不必让转储捕捉内核以SMP方式运行。因此,通常编译一个单CPU转储捕捉内核或装载转储捕捉内核时指定选项"maxcpus=1"。

6)内核崩溃时触发内核启动

在装载转储捕捉内核后,如果系统发生崩溃(Kernel Panic),系统将重启动进入转储捕捉内核。重启动的触发点在函数die(), die_nmi()和sysrq处理例程(按ALT-SysRq-c组合键)。

下面条件将执行一个崩溃触发点:

  • 如果检测到硬件锁住,并且配置了"NMI watchdog",系统将调用函数die_nmi()启动进入转储捕捉内核。
  • 如果调用了函数die(),并且该线程的pid为0或1,或者在中断上下文中调用die(),或者设置了panic_on_oops并调用了die(),系统将启动进入转储捕捉内核。
  • 在powerpc系统,当一个软复位产生时,所有的CPU调用die(),并且系统将启动进入转储捕捉内核。
  • 为了测试目的,用户可以使用"ALT-SysRq-c","echo c > /proc/sysrq-trigger"触发一个崩溃,或者写一个内核模块强制内核崩溃。

7)写出转储文件

在转储捕捉内核启动后,可用下面的命令写出转储文件:

cp /proc/vmcore <dump-file>

用户还可以将转储内存作为设备/dev/oldmem以线性原始流视图进行访问,使用下面的命令创建该设备:

mknod /dev/oldmem c 1 12

使用命令dd拷贝转储内存的特定部分,拷贝整个内存的命令列出如下:

dd if=/dev/oldmem of=oldmem.001

8)转储文件分析

在分析转储映像之前,用户应重启动进入一个稳定的内核。用户可以用GDB对拷贝出的转储进行有限分析。编译vmlinux时应加上-g选项,才能生成调试用的符号,然后,用下面的命令调试vmlinux:

gdb vmlinux <dump-file>

SysRq魔术组合键打印内核信息

SysRq"魔术组合键"是一组按键,由键盘上的"Alt+SysRq+[CommandKey]"三个键组成,其中CommandKey为可选的按键。SysRq魔术组合键根据组合键的不同,可提供控制内核或打印内核信息的功能。SysRq魔术组合键的功能说明如表1所示。

表1 SysRq组合键的功能说明
键名 功能说明
b 在没有同步或卸载硬盘的情况下立即启动。
c 为了获取崩溃转储执行kexe重启动。
d 显示被持的所有锁。
e 发送信号SIGTERM给所有进程,除了init外。
f 将调用oom_kill杀死内存热进程。
g 在平台ppc和sh上被kgdb使用。
h 显示帮助信息。
i 发送信号SIGKILL给所有的进程,除了init外。
k 安全访问密钥(Secure Access Key,SAK)杀死在当前虚拟终端上的所有程序。
m 转储当前的内存信息到控制台。
n 用于设置实时任务为可调整nice的。
o 将关闭系统(如果配置为支持)。
p 打印当前寄存器和标识到控制台。
q 将转储所有正运行定时器的列表。
r 关闭键盘Raw模式并设置为XLATE模式。
s 尝试同步所有挂接的文件系统。
t 将转储当前的任务列表和它们的信息到控制台。
u 尝试以仅读的方式重挂接所有已挂接的文件系统。
v 转储Voyager SMP处理器信息到控制台。
w 转储的所有非可中断(已阻塞)状态的任务。
x 在平台ppc/powerpc上被xmon(X监视器)接口使用。
0~9 设备控制台日志级别,控制将打印到控制台的内核信息。例如:0仅打印紧急信息,如:PANIC和OOPS信息。

默认SysRq组合键是关闭的。可用下面的命令打开此功能:

# echo 1 > /proc/sys/kernel/sysrq

关闭此功能的命令列出如下:

# echo 0 > /proc/sys/kernel/sysrq

如果想让此功能总是起作用,可在/etc/sysctl.conf文件中设置kernel.sysrq值为1。 系统重新启动以后,此功能将会自动打开。

打开SysRq组合键功能后,有终端访问权限的用户就可以自用它打印内核信息了。

注意:SysRq组合键在X windows上是无法使用的。必须先要切换到文本虚拟终端下。如果在图形界面,可以按Ctrl+Alt+F1切换到虚拟终端。在串口终端上,需要先在终端上发送Break信号,然后在5秒内输入sysrq组合键。如果用户有root权限,可把commandkey字符写入到/proc/sysrq-trigger文件,触发一个内核信息打印,打印的信息存放在/var/log/messages中。下面是一个命令样例:
^-^$ echo \'t\' > sysrq-trigger
^-^vim /var/log/messages
Oct 29 17:51:43 njllinux kernel: SysRq : Show State
Oct 29 17:51:43 njllinux kernel:  task                        PC stack   pid father
Oct 29 17:51:43 njllinux kernel: init          S ffffffff812b76a0     0     1      0
Oct 29 17:51:43 njllinux kernel: ffff81013fa97998 0000000000000082 0000000000000000 ffff81013fa9795c
Oct 29 17:51:43 njllinux kernel: 000000003fa97978 ffffffff81583700 ffffffff81583700 ffff81013fa98000
Oct 29 17:51:43 njllinux kernel: ffffffff813cc5b0 ffff81013fa98350 000000003c352a50 ffff81013fa98350
Oct 29 17:51:43 njllinux kernel: Call Trace:
Oct 29 17:51:43 njllinux kernel: 000300000004 ffff8101333cb090
Oct 29 17:51:43 njllinux kernel: Call Trace:
Oct 29 17:51:43 njllinux kernel: [<ffffffff81040c2e>] sys_pause+0x19/0x22
Oct 29 17:51:43 njllinux kernel: [<ffffffff8100c291>] tracesys+0xd0/0xd5
Oct 29 17:51:43 njllinux kernel:
Oct 29 17:51:43 njllinux kernel: lighttpd      S ffffffff812b76a0     0  3365      1
Oct 29 17:51:43 njllinux kernel: ffff810132d49b18 0000000000000082 0000000000000000 ffff810132d49adc
Oct 29 17:51:43 njllinux kernel: ffff81013fb2d148 ffffffff81583700 ffffffff81583700 ffff8101354896a0
Oct 29 17:51:43 njllinux kernel: ffffffff813cc5b0 ffff8101354899f0 0000000032d49ac8 ffff8101354899f0
Oct 29 17:51:43 njllinux kernel: Call Trace:
Oct 29 17:51:43 njllinux kernel: [<ffffffff81040722>] ? __mod_timer+0xbb/0xcd
Oct 29 17:51:43 njllinux kernel: [<ffffffff8129b2ee>] schedule_timeout+0x8d/0xb4
Oct 29 17:51:43 njllinux kernel: [<ffffffff81040100>] ? process_timeout+0x0/0xb
Oct 29 17:51:43 njllinux kernel: [<ffffffff8129b2e9>] ? schedule_timeout+0x88/0xb4
Oct 29 17:51:43 njllinux kernel: [<ffffffff810b9498>] do_sys_poll+0x2a8/0x370
……

命令strace

命令strace 显示程序调用的所有系统调用。使用 strace 工具,用户可以清楚地看到这些调用过程及其使用的参数,了解它们与操作系统之间的底层交互。当系统调用失败时,错误的符号值(如 ENOMEM)和对应的字符串(如Out of memory)都能被显示出来。

starce 的另一个用处是解决和动态库相关的问题。当对一个可执行文件运行ldd时,它会告诉你程序使用的动态库和找到动态库的位置

strace命令行选项说明如表1。常用的选项为-t, -T, -e, -o等。

表1 命令strace的命令行选项说明
选项 说明
-c 统计每个系统调用执行的时间、次数和出错的次数等。
-d 输出一些strace自身的调试信息到标准输出。
-f 跟踪当前进程由系统调用fork产生的子进程。
-ff 如果使用选项-o filename,则将跟踪结果输出到相应的filename.pid中,pid是各进程的进程号。
-F 尝试跟踪vfork调用.在-f时,vfork不被跟踪。
-h 输出简要的帮助信息。
-i 在系统调用的时候打印指令指针。
-q 禁止输出关于粘附和脱离的信息,发生在输出重定向到文件且直接而不是粘附运行命令时。
-r 依赖于每个系统调用的入口打印相对时间戳。
-t 在输出中的每一行前加上时间信息。
-tt 在输出中的每一行前加上时间信息,包括毫秒。
-ttt 毫秒级输出,以秒表示时间。
-T 显示系统调用所花费的时间。
-v 输出所有的系统调用的信息。一些关于环境变量,状态,输入输出等调用由于使用频繁,默认不输出。
-V 输出strace的版本信息。
-x 以十六进制形式输出非ASCII标准字符串。
-xx 所有字符串以十六进制形式输出。
-a column 以特定的列数对齐返回值,缺省值为40。
-e expr 指定一个表达式,用来控制如何跟踪.格式如下:
[qualifier=][!]value1[,value2]...
qualifier只能是 trace,abbrev,verbose,raw,signal,read,write其中之一。value是用来限定的符号或数字。默认的qualifier是 trace。感叹号是否定符号。
-eopen 等价于 -e trace=open,表示只跟踪open调用。而-etrace!=open表示跟踪除了open以外的其他调用。
-e trace=set 只跟踪指定的系统调用。例如:-e trace=open,close,rean,write表示只跟踪这四个系统调用。默认的为set=all。
-e trace=file 只跟踪文件名作为参数的系统调用,一般为文件操作。
-e trace=process 只跟踪有关进程控制的系统调用。
-e trace=network 只跟踪与网络有关的所有系统调用。
-e strace=signal 跟踪所有与系统信号有关的系统调用。
-e trace=ipc 跟踪所有与进程间通信有关的系统调用。
-o filename 将strace的输出写入文件filename。
-p pid 跟踪指定的进程pid。
-s strsize 指定最大字符串打印长度,默认值为32。
-u username 以username的UID和GID执行命令。
例如:命令strace pwd的输出部分列出如下:
execve("/bin/pwd", ["pwd"], [/* 39 vars */]) = 0
uname({sys="Linux", node="sammy", ...}) = 0
brk(0)                                  = 0x804c000
old_mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x4001...
	fstat64(3, {st_mode=S_IFREG|0644, st_size=115031, ...}) = 0
old_mmap(NULL, 115031, PROT_READ, MAP_PRIVATE, 3, 0) = 0x40017000
close(3)                                = 0
open("/lib/tls/libc.so.6", O_RDONLY)    = 3
read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0\360U\1"..., 1024) = 1024
fstat64(3, {st_mode=S_IFREG|0755, st_size=1547996, ...}) = 0

用函数printk打印内核信息

Linux内核用函数printk打印调试信息,该函数的用法与C库打印函数printf格式类似,但在内核使用。用户可在内核代码中的某位置加入函数printk,直接把所关心的信息打打印到屏幕上或日志文件中。

函数printk根据日志级别(loglevel)对调试信息进行分类。日志级别用宏定义,展开为一个字符串,在编译时由预处理器将它和消息文本拼接成一个字符串,因此函数printk中的日志级别和格式字符串间不能有逗号。

下面两个 printk 的例子,一个是调试信息,一个是临界信息:
printk(KERN_DEBUG "Here I am: %s:%i\n", _ _FILE_ _, _ _LINE_ _); 
printk(KERN_CRIT "I\'m trashed; giving up on %p\n", ptr);

样例:在用户空间或内核中开启及关闭打印调试消息 用户还可以在内核或用户空间应用程序定义统一的函数打印调试信息,可在Makefile文件中打开或关闭调试函数。定义方法列出如下:
/*debug_on_off.h*/
#undef PDEBUG             /* undef it, just in case */ 
#ifdef SCULL_DEBUG 
#ifdef _ _KERNEL_ _ 
    /* This one if debugging is on, and kernel space */ 
#define PDEBUG(fmt,args...) printk(KERN_DEBUG "scull: " fmt, ## args)
#else 
    /* This one for user space */ 
#define PDEBUG(fmt, args...) fprintf(stderr, fmt, ## args) 
#endif 
#else 
#define PDEBUG(fmt, args...) /* not debugging: nothing */ 
#endif

在文件Makefile加上下面几行:
# Comment/uncomment the following line to disable/enable debugging 
DEBUG = y 
 
# Add your debugging flag (or not) to CFLAGS 
ifeq ($(DEBUG),y) 
 DEBFLAGS = -O -g -DSCULL_DEBUG # "-O" 
else 
 DEBFLAGS = -O2 
endif 
 
CFLAGS += $(DEBFLAGS)

更改makefile中的DEBUG值,需要调试信息时,DEBUG = y,不需要时,DEBUG赋其它值。再用make编译即可。

内核探测kprobe

kprobe(内核探测,kernel probe)是一个动态地收集调试和性能信息的工具,如:收集寄存器和全局数据结构等调试信息,无需对Linux内核频繁编译和启动。用户可以在任何内核代码地址进行陷阱,指定调试断点触发时的处理例程。工作机制是:用户指定一个探测点,并把用户定义的处理函数关联到该探测点,当内核执行到该探测点时,相应的关联函数被执行,然后继续执行正常的代码路径。

kprobe允许用户编写内核模块添加调试信息到内核。当在远程机器上调试有bug的程序而日志/var/log/messages不能看出错误时,kprobe显得非常有用。用户可以编译一个内核模块,并将内核模块插入到调试的内核中,就可以输出所需要的调试信息了。

内核探测分为kprobe, jprobe和kretprobe(也称return probe,返回探测)三种。kprobe可插入内核中任何指令处;jprobe插入内核函数入口,方便于访问函数的参数;return probe用于探测指定函数的返回值。

内核模块的初始化函数init安装(或注册)了多个探测函数,内核模块的退出函数exit将注销它们。注册函数(如:register_kprobe())指定了探测器插入的地方、探测点触发的处理例程。

(1)配置支持kprobe的内核

配置内核时确信在.config文件中设置了CONFIG_KPROBES、CONFIG_MODULES、CONFIG_MODULE_UNLOAD、CONFIG_KALLSYMS_ALL和CONFIG_DEBUG_INFO。

配置了CONFIG_KALLSYMS_ALL,kprobe可用函数kallsyms_lookup_name从地址解析代码。配置了CONFIG_DEBUG_INFO后,可以用命令"objdump -d -l vmlinux"查看源到对象的代码映射。

调试文件系统debugfs含有kprobe的调试接口,可以查看注册的kprobe列表,还可以关闭/打开kprobe。

查看系统注册probe的方法列出如下:

#cat /debug/kprobes/list
c015d71a  k  vfs_read+0x0
c011a316  j  do_fork+0x0
c03dedc5  r  tcp_v4_rcv+0x0

第一列表示探测点插入的内核地址,第二列表示内核探测的类型,k表示kprobe,r表示kretprobe,j表示jprobe,第三列指定探测点的"符号+偏移"。如果被探测的函数属于一个模块,模块名也被指定。

打开和关闭kprobe的方法列出如下:

#echo ‘1’ /debug/kprobes/enabled
#echo ‘0’ /debug/kprobes/enabled

(2)kprobe样例

Linux内核源代码在目录samples/kpobges下提供了各种kprobe类型的探测处理例程编写样例,分别对应文件kprobe_example.c、jprobe_example.c和kretprobe_example.c,用户稍加修改就可以变成自己的内核探测模块。下面仅说明kprobe类型的探测例程。

样例kprobe_example是kprobe类型的探测例程内核模块,显示了在函数do_fork被调用时如何使用kprobe转储栈和选择的寄存器。当内核函数do_fork被调用创建一个新进程时,在控制台和/var/log/messages中将显示函数printk打印的跟踪数据。样例kprobe_example列出如下(在samples/kprobe_example.c中):

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/kprobes.h>
 
/* 对于每个探测,用户需要分配一个kprobe对象*/
static struct kprobe kp = {
	.symbol_name	= "do_fork",
};
 
/* 在被探测指令执行前,将调用预处理例程 pre_handler,用户需要定义该例程的操作*/
static int handler_pre(struct kprobe *p, struct pt_regs *regs)
{
#ifdef CONFIG_X86
	printk(KERN_INFO "pre_handler: p->addr = 0x%p, ip = %lx,"
			" flags = 0x%lx\n",
		p->addr, regs->ip, regs->flags);  /*打印地址、指令和标识*/
#endif
#ifdef CONFIG_PPC
	printk(KERN_INFO "pre_handler: p->addr = 0x%p, nip = 0x%lx,"
			" msr = 0x%lx\n",
		p->addr, regs->nip, regs->msr);
#endif
 
	/* 在这里可以调用内核接口函数dump_stack打印出栈的内容*/
	return 0;
}
 
/* 在被探测指令执行后,kprobe调用后处理例程post_handler */
static void handler_post(struct kprobe *p, struct pt_regs *regs,
				unsigned long flags)
{
#ifdef CONFIG_X86
	printk(KERN_INFO "post_handler: p->addr = 0x%p, flags = 0x%lx\n",
		p->addr, regs->flags);
#endif
#ifdef CONFIG_PPC
	printk(KERN_INFO "post_handler: p->addr = 0x%p, msr = 0x%lx\n",
		p->addr, regs->msr);
#endif
}
 
/*在pre-handler或post-handler中的任何指令或者kprobe单步执行的被探测指令产生了例外时,会调用fault_handler*/
static int handler_fault(struct kprobe *p, struct pt_regs *regs, int trapnr)
{
	printk(KERN_INFO "fault_handler: p->addr = 0x%p, trap #%dn",
		p->addr, trapnr);
	/* 不处理错误时应该返回*/
	return 0;
}
 
/*初始化内核模块*/
static int __init kprobe_init(void)
{
	int ret;
	kp.pre_handler = handler_pre;
	kp.post_handler = handler_post;
	kp.fault_handler = handler_fault;
 
	ret = register_kprobe(&kp);  /*注册kprobe*/
	if (ret < 0) {
		printk(KERN_INFO "register_kprobe failed, returned %d\n", ret);
		return ret;
	}
	printk(KERN_INFO "Planted kprobe at %p\n", kp.addr);
	return 0;
}
 
static void __exit kprobe_exit(void)
{
	unregister_kprobe(&kp);
	printk(KERN_INFO "kprobe at %p unregistered\n", kp.addr);
}
 
module_init(kprobe_init)
module_exit(kprobe_exit)
MODULE_LICENSE("GPL");

Systemtap调试

(1)Systemtap原理

Systemtap是一个基于kprobe调试内核的开源软件。调试者只需要写一些脚本,通过Systemtap提供的命令行接口对正在运行的内核进行诊断调试,不需要修改或插入调试代码、重新编译内核、安装内核和重启动等工作,使内核调试变得简单容易。Systemtap调试过程与在gdb调试器中用断点命令行调试类似。

Systemtap用类似于awk语言的脚本语言编写调试脚本,该脚本命名事件并给这些事件指定处理例程。只要指定的事件发生,Linux内核将运行对应的处理例程。

有几种类型的事件,如:进入或退出一个函数,一个定时器超时或整个systemtap会话开始或停止。处理例程是一系列脚本语言语句指定事件发生时所做的工作,包括从事件上下文提取数据,存储它们进入内部变量或打印结果。

Systemtap的运行过程如图2所示,用户调试时用Systemtap编写调试脚本,Systemtap的翻译模块(translator)将脚本经语法分析(parse)、功能处理(elaborate)和翻译后生成C语言调试程序,然后,运行C编译器编译(build)创建调试内核模块。再接着将该内核模块装载入内核,通过kprobe机制,内核的hook激活所有的探测事件。当任何处理器上有这些事件发生时,对应的处理例程被触发工作,kprobe机制在内核获取的调试数据通过文件系统relayfs传回Systemtap,输出调试数据probe.out。在调试结束时,会话停止,内核断开hook连接,并卸载内核模块。整个操作过程由单个命令行程序strap驱动控制。

Linux kernel debug method 03.png

图2 Systemtap运行过程

(2)stap程序

stap程序是Systemtap工具的前端,它接受用systemtap脚本语言编写的探测指令,翻译这些指令到C语言代码,编译C代码产生并装载内核模块到正运行的Linux内核,执行请求的跟踪或探测函数。用户可在一个命名文件中提供脚本或从命令行中提供调试语句。

命令stap的用法列出如下:

stap [ OPTIONS ] FILENAME [ ARGUMENTS ]

stap [ OPTIONS ] - [ ARGUMENTS ]

stap [ OPTIONS ] -e SCRIPT [ ARGUMENTS ]

stap [ OPTIONS ] -l PROBE [ ARGUMENTS ]

选项[ OPTIONS ]说明如下:

-h 显示帮助信息。

-V 显示版本信息。

-k 在所有操作完成后,保留临时目录。对于检查产生的C代码或重使用编译的内核对象来说,这是有用的。

-u 非优化编译模式。.

-w 关闭警告信息。

-b 让内核到用户数据传输使用bulk模式。

-t 收集时间信息:探测执行的次数、每个探测花费的平均时间量。

-sNUM 内核到用户数据传输使用NUM MB 的缓冲区。当多个处理器工作在bulk模式时,这是单个处理器的缓冲区大小。

-p NUM Systemtap在通过NUM个步骤后停止。步骤数为1-5: parse, elaborate, translate, compile, run。

-I DIR 添加tapset库(用于翻译C代码的函数集)搜索目录。

-D NAME=VALUE 添加C语言宏定义给内核模块Makefile,用于重写有限的参数。

-R DIR 在给定的目录查找Systemtap运行源代码。

-r RELEASE 为给定的内核发布版本RELEASE而不是当前运行内核编译内核模块。

-m MODULE 给编译产生的内核模块命名MODULE,替代缺省下的随机命名。产生的内核模块被拷贝到当前目录。

-o FILE 发送标准输出到命名文件FILE。在bulk模式,每个CPU的文件名将用"FILE_CPU序号"表示。

-c CMD 开始探测,运行CMD,当CMD完成时退出。

-x PID 设置target()