第十章 调试
10.1 调试概览
调试本质是一项复杂而艰巨的任务,它需要你投入多年的时间才能完全掌握。千万不要以为仅仅读完本章就能成为一名调试高手----你需要将这些技术付诸实际。
我们首先面临的(通常是)困难是准确找出产生错误的原因
Brain W.Kernighan: “调试代码的难度是首次编写这些代码的两倍,因此,如果你在编写代码的时候就已经发挥了全部的聪明才智,那么按照常理,你将无法凭借自己的智慧去调试这些代码。”
内存管理错误:错误的内存分配、没有释放内存、使用已经释放的内存等错误
a 段错误:程序试图访问超出其合法数据存储区域之外的内存时,会发生此类错误。有这些非法访问时,其虚拟内存子系统会负责处理底层坏页故障,并终结应用程序,并根据系统的配置来确定是否需要创建一个包含程序状态的核心转储文件(Coredump)
b 内存泄漏(Memory leap) 普通应用程序中的内存泄漏不会导致整个系统的崩溃。一旦应用程序崩溃或终止,它泄漏的内存就可以再次提供给其他应用程序。系统管理员可以设置一定的资源限制来限制每个程序可以使用的内存量。
在GDB中容易发现前一类的错误,而第二类错误则需要使用类似于Valgrind等工具来识别。
10.2 GDB
GDB简单介绍
用于跟踪和调试应用程序的最流行的调试工具是GDB,被大量GNU软件项目及众多与GNU没有关联但是希望能有一个高质量调试器的第三方软件所使用,并被许多第三方工具合并其并作为其调试功能的基础,并在GDB上建立各级的图形化抽象。
建立概念:任何调试器都有两个组成部分。GDB的底层处理单独进程或线程的启动和关闭、跟踪代码执行以及在运行代码中插入或删除断点。支持大量不同的平台和机制以在各种架构上实现这些操作。其次,提供一个带有一套标准命令的接口,而不管它在何种硬件之上运行。只需要学习一次核心命令就可以到处使用GDB。
最简单的调试用例:
#include <stdio.h> void print_hello() { printf("Hello World! \n"); } int main(int argc, char **argv) { print_hello(); return 0; }
如果想要使用GDB调试,需要用下面的语句进行编译:
gcc -o hello hello.c -g # -g 代表加入调试信息
常见调试命令:
使用gdb命令将hello程序装载到GDB中,如下所示:
dash@Rachel:~/code/chapter10$ gdb ./hello GNU gdb 6.8-debian Copyright (C) 2008 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "x86_64-linux-gnu"... (gdb)
GDB把hello ELF二进制文件装载到内存里,并为程序的运行设置了一个环境,然后将同时在程序的二进制文件和连接到程序的任何库文件中查找有用的符号,run:正常运行程序,help:打印出命令集,命令集的分类如下。当然,可以使用help all得到一个超级全的帮助,使用help running得到一个运行时命令的帮助:
(gdb) help List of classes of commands: aliases -- Aliases of other commands 别名--命令的别名 breakpoints -- Making program stop at certain points 断点--让程序在某个特定点停止 data -- Examining data 数据--检查数据 files -- Specifying and examining files 文件 internals -- Maintenance commands 内部 obscure -- Obscure features running -- Running the program stack -- Examining the stack status -- Status inquiries support -- Support facilities tracepoints -- Tracing of program execution without stopping the program user-defined -- User-defined commands Type "help" followed by a class name for a list of commands in that class. Type "help all" for the list of all commands. Type "help" followed by command name for full documentation. Type "apropos word" to search for commands related to "word". Command name abbreviations are allowed if unambiguous.
设置断点技巧:
break main,在main函数的入口设置一个断点。断点插入之后,可以运行到该特定点。到达该断点之后将由用户决定是继续运行还是执行另一个操作。可以使用step或next命令继续向前执行程序。step命令将继续执行程序直到源代码的新一行,而next则是向前执行一条机器指令。(一条语句可能是多条机器指令)。nexti命令在遇到函数调用时不会进入函数内部----这样在调试代码时就不需要担心函数调用了。
从技术上说,在main函数的入口插入一个断点并不会在程序最开始处停止程序的执行,因为运行在Linux系统中的所有基于C语言的程序都会在运行时利用GNU C函数库为调用main函数做好安排。因此在到达断点时,已有许多额外的库函数执行了大量设置工作。
print命令来查询程序中存储的数据。run命令传递给示例程序的可选参数。传递给run命令的参数将作为argv参数列表传递给被调用程序。
(gdb) break main Breakpoint 1 at 0x40052b: file hello.c, line 8. (gdb) run foo baz Starting program: /home/dash/code/chapter10/hello foo baz Breakpoint 1, main (argc=3, argv=0x7ffff2f5d6d8) at hello.c:8 8 print_hello(); (gdb) print argv[1] $1 = 0x7ffff2f5e998 "foo" (gdb) print argv[0] $2 = 0x7ffff2f5e978 "/home/dash/code/chapter10/hello" (gdb) print argv[2] $3 = 0x7ffff2f5e99c "baz"
额外的注意点:
(gdb) print argv[4] $4 = 0x7ffff2f5e9a0 "SHELL=/bin/bash" (gdb) print argv[5] $5 = 0x7ffff2f5e9b0 "TERM=xterm" (gdb) print argv[6] $6 = 0x7ffff2f5e9bb "XDG_SESSION_COOKIE=736d0f7473a1c0be71879bd74939d1c6-1231764727.821163-1233323124" (gdb) print argv[8] $7 = 0x7ffff2f5ea2d "SSH_TTY=/dev/pts/2" (gdb) print argv[3] $8 = 0x0
Backtrace:
(gdb) b main Breakpoint 1 at 0x40052b: file hello.c, line 8. (gdb) run Starting program: /home/dash/code/chapter10/hello Breakpoint 1, main (argc=1, argv=0x7fff7ea00198) at hello.c:8 8 print_hello(); (gdb) step print_hello () at hello.c:4 4 printf("Hello World! \n"); (gdb) bt #0 print_hello () at hello.c:4 #1 0x0000000000400535 in main (argc=1, argv=0x7fff7ea00198) at hello.c:8
第一个是当前堆栈帧,由print_hello函数使用,外层堆栈帧被全局函数main用于保存它在调用print_hello之前的局部变量。
一个有错误的示例:
稍后将补上。
ulimit:
Linux发行版中使用ulimit(用户限制)来控制核心转储文件的创建:
dash@Rachel:~/code/chapter10$ ulimit -a core file size (blocks, -c) 0 data seg size (kbytes, -d) unlimited scheduling priority (-e) 0 file size (blocks, -f) unlimited pending signals (-i) 2560 max locked memory (kbytes, -l) 32 max memory size (kbytes, -m) unlimited open files (-n) 1024 pipe size (512 bytes, -p) 8 POSIX message queues (bytes, -q) 819200 real-time priority (-r) 0 stack size (kbytes, -s) 8192 cpu time (seconds, -t) unlimited max user processes (-u) 2560 virtual memory (kbytes, -v) unlimited file locks (-x) unlimited
设置ulimit:
dash@Rachel:~/code/chapter10$ ulimit -c 1024 dash@Rachel:~/code/chapter10$ ulimit -a core file size (blocks, -c) 1024
在打开ulimit限制后,重新运行有错的文件将生成一个真正的核心转储,在特定的版本下,可能会包含运行原程序的进程号,可以在内核配置中启用这个可选的特征。GDB可以读取核心转储文件并基于该文件来开始一个调试会话。该文件由一个不再运行的程序产生,所以并不是所有的GDB命令都可以使用的,但是还是非常有用的,至少,允许以离线方式调试它们。但是程序控制流命令不能被使用-因为程序早已停止。
Valgrid工具
是一个运行时诊断工具,用于监视一个特定程序的活动并通知在代码中可能遇到的各种各样的内存管理问题。
valgrid ./Your_Program leak-check选项可以找到泄漏的内存来自何方。
图形化调试工具
DDD/Eclipse
ddd中,View->Data Window可以打开另一个窗口,改窗口中,各种类型的数据都可以可视化显示。插入预定义的表达式来定义你想要查看的数据即可。
10.3 内核调试
可以把内核看成是一个比较复杂的软件,所以出错很正常。
oops错误,将导致当前任务(进程)被杀死,该任务所使用的所有资源将处于不太一致的状态。应该尽可能快的重启一个产生oops错误的系统。
kernel panic,内核在系统无法继续安全运行的情况下将立即停止其正常运行并强制系统突然死亡。UNIX/UNIX内核在历史上使用panic函数强制系统崩溃。系统在Panic后组织了进一步的数据损坏。
kexec/kdump支持内核崩溃转储,可以使用这个机制来保存kernel panic时产生的崩溃输出,这比拍数码照片好多了。
注意看Oops消息,可以得到相关的消息。这个主要是看经验而定的。
10.4 UML
User Mode Linux(用户模式Linux),把Linux内核当成一个普通应用程序来运行,使用信号来模拟硬件中的时钟和中断,并不直接和真正的硬件通信。可以用UML来测试:跟踪内核执行并测试纯软件功能
$ make ARCH=um menuconfig
$ make ARCH=um
$ ./linux
可以下载一份伪Linux文件系统的拷贝用于UML。
gdb的应用:
gdb ./linux
(gdb) run debug
(gdb) break start_kernel
(gdb) continue
注意:针对内核的调试器,都不是标准的,例如KGDB等,而且Linus Torvalds本人不支持在内核中使用调试器。