LDD-第四章 调试技术
内核编程有自身独特的调试难题。由于内核是一个不与特定进程相关的功能集合,所以内核代码无法轻易放在调试器中执行,而且很难追踪。同样,要想重现内核代码中的错误也是相当困难的,因为这种错误可能导致整个系统崩溃,这样就破坏了可以用来追踪它们的现场。
内核中的调试支持
内核配置工具的“kernel hacking”菜单中:
CONFIG_DEBUG_KERNEL:
该选项仅仅使得其他调试选项可用。我们应该打开该选项,但它本身不会打开所有的调试功能。
CONFIG_DEBUG_SLAB:
这是一个非常重要的选项,它打开内核内存分配函数中的多个类型的检查;打开该检查后,就可以检测许多内存溢出及忘记初始化的错误。在将已分配内存返回给调用者之前,内核将把其中的每个字节设置为0xa5,而在释放后将其设置为0x6b。如果在自己的驱动程序的输出中,或者在oops信息中看到上述“毒剂”字符,则可以轻松判断问题所在。在打开该调试选项后,内核还会在每个已分配内存对象的前面和后面放置一些特殊的防护值;这样,当这些防护值发生变化时,内核就可以知道有些代码超出了内存的正常访问范围,并“大声抱怨”。同时,该选项还会检查更多隐蔽的错误。
CONFIG_DEBUG_PAGEDEBUG:
在释放时,全部内存页从内核地址空间移出。该选项将大大降低运行速度,但可以快速定位特定的内存损坏错误的所在位置。
CONFIG_DEBUG_SPINLOCK:
打开该选项,内核将捕获对未初始化自旋锁的操作,也会捕获诸如两次解开同一锁的操作等其他错误。
CONFIG_DEBUG_SPINLOCK_SLEEP:
该选项将检测拥有自旋锁时的休眠企图。实际上如果调用可能引起休眠的函数,这个选项也会生效,即使该函数可能不会导致真正的休眠。
CONFIG_INIT_DEBUG:
标记为__init(或者__initdata)的符号将会在系统初始化或者模块装载之后被丢弃。该选项可用来检查初始化完成后对用于初始化的内存空间的访问企图。
CONFIG_DEBUG_INFO:
该选项将使内核的构造包含完整的调试信息。如果打算利用gdb调试内核,将需要这些信息。如果计划使用gdb,还应该打开CONFIG_FRAME_POINTER选项。
CONFIG_MAGIC_SYSRQ:
SysRq经常被称为Magic System Request,它被定义为一系列按键组合。它可以在系统挂起,大多数服务已经无法响应的情况下,还能通过按键组合来完成一系列预先定义的系统操作。通过它,不但可以在保证磁盘数据安全的情况下重启一台挂起的服务器,避免数据丢失和重启后长时间的文件系统检查,还可以手机包括系统内存使用,CPU任务处理,进程运行状态等系统运行信息。
内核需要配置:CONFIG_MAGIC_SYSRQ=y。可以通过如下打开:sysctl -w kernel.sysrq=1或echo 1 > /proc/sys/kernel/sysrq打开。通过echo输出命令到/proc/sysrq-trigger来执行命令。
Sysrq默认会根据console_loglevel输出到本地终端。只要console_loglevel大于default_message_loglevel,Sysrq信息就会输出到本地控制台。常用命令:m –打印内存实用信息,p-打印当前cpu寄存器信息,t-打印进程列表,w-打印cpu信息,h-打印帮助。格式:
echo h >/proc/sysrq-trigger。
通过打印调试
八种可用的日志级别,按照降序:
KERN_EMERG:用于紧急事件消息,一般是系统崩溃之前的提示信息。
KERN_ALERT:用于需要立即采取动作的情况。
KERN_CRIT:临界状态,通常涉及严重的硬件或软件操作失败。
KERN_ERR:用于报告错误状态,设备驱动程序常使用来报告来自硬件的问题。
KERN_WARNING:对可能出现问题的情况进行警告,但这类情况通常不会对系统造成严重问题。
KERN_NOTICE:有必要进行提示的正常情形。许多与安全相关的状况用这个级别进行汇报。
KERN_INFO:提示性信息。很多驱动程序在启动时以这个级别来打印它们找到的硬件信息。
KERN_DEBUG:用于调试信息。
代表0~7八个整数,数值越小,优先级越高。
未指定优先级的printk语句采用的默认级别是DEFAULT_MESSAGE_LOGLEVEL(4)。
我们可以通过对文本文件/proc/sys/kernel/printk的访问来读取和修改控制台的日志级别。这个文件包含了四个整数:当前日志级别、未明确指定日志级别时的默认消息级别、最小允许的日志级别、引导时的默认日志级别。向该文件写入单个整数值,会改变当前日志级别。如:echo 8 > /proc/sys/kernel/printk可以使所有的内核消息打印出来。为了避免某些打印大量刷屏,可以使用 int printk_ratelimit(void),如下:
if (printk_ratelimit())
printk(“……”);
printk通过跟踪发送到控制台的消息数量工作,如果输出的速度超过一个阈值,printk_ratelimit返回0,从而避免发送重复消息。可以通过修改/proc/sys/kernel/printk_ratelimit(再重新打开消息前需要等待的秒数)以及/proc/sys/kernel/printk_ratelimit_burst(在进行限制前,可以接受的消息数)来控制printk_ratelimit的行为。
使用/proc文件系统
/proc文件系统是一种特殊的、由软件创建的文件系统,内核使用它向外界导出信息,用户读取其中的文件时,内核函数动态的生成文件内容。
/proc下常见的文件:
cmdline:系统启动时输入给内核命令行参数
cpuinfo:CPU的硬件信息 (型号, 家族, 缓存大小等)
devices:主设备号及设备组的列表,当前加载的各种设备(块设备/字符设备)
dma:使用的DMA通道
filesystems:当前内核支持的文件系统,当没有给 mount(1) 指明哪个文件系统的时候, mount(1) 就依靠该文件遍历不同的文件系统
interrupts :中断的使用及触发次数,调试中断时很有用
ioports I/O:当前在用的已注册 I/O 端口范围
kcore:该伪文件以 core 文件格式给出了系统的物理内存映象(比较有用),可以用 GDB 查探当前内核的任意数据结构。该文件的总长度是物理内存 (RAM) 的大小再加上 4KB
kmsg:可以用该文件取代系统调用 syslog(2) 来记录内核日志信息,对应dmesg命令
kallsym:内核符号表,该文件保存了内核输出的符号定义, modules(X)使用该文件动态地连接和捆绑可装载的模块
loadavg:负载均衡,平均负载数给出了在过去的 1、 5,、15 分钟里在运行队列里的任务数、总作业数以及正在运行的作业总数。
locks:内核锁 。
meminfo物理内存、交换空间等的信息,系统内存占用情况,对应df命令。
misc:杂项 。
modules:已经加载的模块列表,对应lsmod命令 。
mounts:已加载的文件系统的列表,对应mount命令,无参数。
partitions:系统识别的分区表 。
slabinfo:sla池信息。
stat:全面统计状态表,CPU内存的利用率等都是从这里提取数据。对应ps命令。
swaps:对换空间的利用情况。
version:指明了当前正在运行的内核版本。
通过监视调试
strace可以显示由用户空间程序所发出的所有系统调用,它不仅可以显示调用,还能显示调用参数以及符号形式表示的返回值。
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 以十六进制形式输出非标准字符串
-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以外的其他调用.有两个特殊的符号 all 和 none.
注意有些shell使用!来执行历史记录里的命令,所以要使用\\.
-e trace=set
只跟踪指定的系统 调用.例如:-e trace=open,close,read,write表示只跟踪这四个系统调用.默认的为set=all.
-e trace=file
只跟踪有关文件操作的系统调用.
-e trace=process
只跟踪有关进程控制的系统调用.
-e trace=network
跟踪与网络有关的所有系统调用.
-e strace=signal
跟踪所有与系统信号有关的 系统调用
-e trace=ipc
跟踪所有与进程通讯有关的系统调用
-e abbrev=set
设定 strace输出的系统调用的结果集.-v 等与 abbrev=none.默认为abbrev=all.
-e raw=set
将指 定的系统调用的参数以十六进制显示.
-e signal=set
指定跟踪的系统信号.默认为all.如 signal=!SIGIO(或者signal=!io),表示不跟踪SIGIO信号.
-e read=set
输出从指定文件中读出 的数据.例如:
-e read=3,5
-e write=set
输出写入到指定文件中的数据.
-o filename
将strace的输出写入文件filename
-p pid
跟踪指定的进程pid.
-s strsize
指定输出的字符串的最大长度.默认为32.文件名一直全部输出.
-u username
以username 的UID和GID执行被跟踪的命令
调试系统故障
oops消息
大部分错误都是因为对NULL指针取值或因为使用了其他不正确的指针值,这些错误通常会导致一个oops消息。由处理器使用的地址几乎都是虚拟地址,这些地址(除了内存管理子系统本身所使用的物理内存之外)通过一个复杂的被称为“页表”的结构被映射为物理地址。当引用一个非法指针时,分页机制无法将改地址映射到物理地址,此时处理器就会向操作系统发出一个“页面失效(page fault)”的信号。如果地址非法,内核就无法“换入(page in)”缺失页面,这时,如果处理器恰好处于超级用户模式,系统就会产生一个oops。
oops信息包含以下几部分内容
- 一段文本描述信息
比如类似“Unable to handle kernel NULL pointer dereference at virtual address 00000000”的信息 ,他说明了发生的是哪类错误。
- oops信息的序号
比如是第一次、第二次等。这些信息与下面类似,中括号内的数据表示序号。
Internal error:Oops:805 [#1]
- 内核中加载的模块名称,也可能没有,以下面字样开头。
Modules linked in:
- 发生错误的CPU的序号,对于单处理器系统,序号为0,比如:
CPU:0
Not tainted(2.6.22.6 #36)
- 发生错误是CPU的哥哥寄存器值。
- 当前进程的名字及进程PID,比如:
Process swapper(pid:1,stack limit = 0xc0480258)这并不是说发生错误的是这个进程,而是表示发生错误时,当前进程是它。错误可能发生在内核代码、驱动程序,也可能就是这个进程的错误。
- 栈信息。
- 栈回溯信息,可以从中看出函数调用关系,形式如下:
Backtrace:
[<c001a6f4>] (s3c2410fb_probe+0x0/0x560) from [<c01bf4e8>] (platform_drv_
probe+0x20/0x24)
...
- 出错指令附件的指令的机器码,比如(出错指令在小括号里)
Code: e24cb004 e24dd010 e59f34e0 e3a07000 (e5873000)
配置内核使oops信息的栈回溯信息更直观
Linux 2.6.22自身具备的调试功能,可以使得打印出的oops信息更直观。通过oops信息中的PC寄存器的值可以知道出错指令的地址,通过栈回溯信息可以知道出错时的函数调用关系,根据这两点可以很快定位错误信息。要让内核出错时能够打印栈回溯信息,编译内核时要增加“-fno-omit-frame-pointer”选项,这可以通过配置CONFIG_FRAME_POINTER来实现。查看内核目录下的配置文件.config,确保已经被定义,如果没有,执行make menuconfig重新配置内核。CONFIG_FRAME_POINTER有可能被其他配置项自动选上。从oops信息的pc寄存器值可以得知崩溃发生时的函数、出错指令。但错误有可能是它的调用者引入,所以还要找出函数的调用关系。由于内核配置了CONFIG_FRAME_POINTER,当出现oops信息时,会打印栈回溯信息。
一个程序包含代码段、数据段、BSS段、堆、栈;从可执行文件角度来讲,如果一个数据未被初始化那就不需要为其分配空间,所以.data和.bss一个重要的区别就是.bss并不占用可执行文件的大小,它只是记载需要多少空间来存储这些未初始化数据,而不分配实际空间。
- 一般情况下,一个可执行二进制程序在存储(没有调入内存运行)时拥有三个部分,分别是代码段(text)、数据段(data)、bss段。
可执行程序 = 代码段 + 数据段 + bss段
- 当程序被加载到内存单元时,则需要堆和栈。
正在运行的c程序 =代码段 + 数据段 + bss段 + 堆 +栈
- 在将应用程序加载到内存空间执行时,操作系统负责代码段、数据段、bss段的加载,并在内存为这些段分配空间,栈也由操作系统分配和管理,不需要程序员显式地管理,堆由程序员自己管理,即显式地申请和释放空间。
- 动态分配和静态分配的最大区别在于:1.直到Run-Time时,执行动态分配,而在compile-time时,就已经决定好分配多少Text+Data+BSS+Stack。2.通过malloc动态分配的内存,需要程序员手工调用free()释放内存,否则容易造成内存泄露,而静态分配的内存在进程执行结束后系统释放(Text、Data),但Stack段中的数据很短暂,函数立即被销毁。
代码段-text:
它是由编译器在编译链接时自动计算的
数据段-data:
data包含静态初始化的数据,所以有初始值的全局变量和static变量在data区。段的起始位置也是有链接文件确定,大小在编译链接时自动分配,和程序大小无关,但是和程序使用到的全局变量、常数常量相关。数据静态内存分配。
bss段-bss:
通常是指用来存放程序中未初始化的全局变量的一块内存区域,在程序载入时由内核清0 。它的初始值也是由用户自己定义的链接文件所确定,用户应该将它定义在可读写的RAM区内,源程序使用malloc分配的内存就是这一块。属于静态分配。
stack:
保存函数的局部变量(但不包含static声明的变量,static意味着在数据段中存放变量),参数以及返回值。栈另外一个重要的特征是,它的地址空间“向下减少”,即当栈上保存的数据越多,栈的地址就越低。栈(stack)的顶部在可读写的RAM区的最后。
heap: 堆(heap)保 存函数内部动态分配内存,是另外一种用来保存程序信息的数据结构,更准确的说是保存程序的动态变量。堆是“先进先出”(First In first Out,FIFO)数据结构。它只允许在堆的一端插入数据,在另一端移走数据。堆的地址空间“向上增加”,即当堆上保存的数据越多,堆的地址就越高
调试器和相关工具
在内核中使用交互式调试器是一个很复杂的问题。出于对系统所有进程的整体利益的考虑,内核在自己的地址空间运行。其结果是,许多用户空间下的调试器所提供的常用功能很难用于内核之中,如断点和单步调试。使用gdb、kdb、kgdb