原文地址:https://techtalk.intersec.com/2013/12/memory-part-5-debugging-tools/
我们花了4篇文章介绍了什么是内存,如何处理内存,内存会给你带来什么问题。最好的开发人员也会写出有bug的代码。通常可接受的估算是每千行代码的bug数,这肯定是一个相当大的数字。因此,即使你熟练的掌握了我们的文章讲述的各种概念,你仍然可能写一些跟内存有关的bug。
内存相关的bug特别难定位和修复。我们以下面这个程序为例:
#include <stdio.h> #define MAX_LINE_SIZE 32 static const char *build_message(const char *name) { char message[MAX_LINE_SIZE]; sprintf(message, "hello %s!\n", name); return message; } int main(int argc, char *argv[]) { fputs(build_message(argc > 1 ? argv[1] : "world"), stdout); return 0; }
这个程序的行为是未定义的,有bug,并且可能会崩溃。build_message函数返回了执行它的栈上内存的指针。因为栈的工作机制,这块内存非常可能被后面调用的其他函数改写,很可能是fputs。因此,如果fputs内部使用了足够多的栈内存来改写消息,输出会被破坏(程序甚至可能会崩溃)。其他情况,程序可能会打印正常的消息。更进一步,程序可能会导致buffer溢出,因为使用了不安全的sprintf函数,这个函数没有对输入做任何限制。
因此,程序的行为取决于命令行输入的消息的大小,即fputs实现中MAX_LINE_SIZE的值。这种bug让人恼火的原因是,它的结果不是那么明显:在简单的消息情况下,它“工作”得很好,并且只会在收到正好让问题暴露出来的参数时,错误才会出现。这就是为什么开发人员要用一些工具来帮助验证(或调试)内存管理才能安心。
本文将会介绍几个免费的工具,我们认为一个C(或C++)开发人员的工具库里应该必备。
第一个是调试器。在Linux平台很可能是gdb。大多数开发人员知道gdb的基本用法:查看程序栈(bt, up, down, frame <id> ...),添加一个断点(break <function|line>, continue ...),单步执行(step, next, fin ...),查看内存(print <expr>, call <func>, x/<FMT> <addr>, ...)等等。当程序因为段错误崩溃时,调试器是大多数开发人员会选择的工具。调试器会捕获信号,并允许查看在那个时刻的程序状态。大多数的段错误都很明显(未初始化指针,空指针解引用...),只需要用调试器花一点功夫。
但是很少被人知道的是,调试器可以放置一个监控点:添加一个动态的断点,每次在一个表达式的结果改变的时候中断程序。这在检测内存被破坏的原因时及其有用:在内存的内容被破坏的位置放一个监控点,程序将会每次在那块内容变化时中断。这对程序的性能影响很小,只要你不监控太多的内存地址,这些监控点由硬件直接管理。
让我们回头看介绍中提到的那个例子:我们用fputs打印第一个指针参数指向的内容,但实际被打印的字符串并不是我们在build_message里面写出来的。看一下这一小部分调试会话:
(gdb) break build_message Breakpoint 1 at 0x400598: file blah.c, line 7. (gdb) run Starting program: /home/fruneau/blah warning: no loadable sections found in added symbol-file system-supplied DSO at 0x7ffff7ffa000 warning: Could not load shared library symbols for linux-vdso.so.1. Do you need "set solib-search-path" or "set sysroot"? Breakpoint 1, build_message (name=0x4006bf "world") at blah.c:7 7 sprintf(message, "hello %s!\n", name); (gdb) n 8 return message; (gdb) p message $1 = "hello world!\n\000\000\000\001\000\000\000\000\000\000\000m\006@\000\000\000\000"
(gdb) watch $1[0] Hardware watchpoint 2: $1[0] (gdb) c Continuing.
Hardware watchpoint 2: $1[0] Old value = 104 'h' New value = 32 ' ' 0x00007ffff7def1fc in ?? () from /lib64/ld-linux-x86-64.so.2 (gdb) bt #0 0x00007ffff7def1fc in ?? () from /lib64/ld-linux-x86-64.so.2 #1 0x00000000004005ff in main (argc=1, argv=0x7fffffffe258) at blah.c:13
valgrind是C/C++开发人员的瑞士军刀。它提供了各种工具,例如内存检查器(memcheck),内存分析器(massif),缓存分析器(cachegrind),CPU分析器(callgrind),还有一些线程检查器(helgrind, DRD, tsan)等等。
valgrind是一个基础的虚拟机,它监控了每一次跟操作系统和虚拟硬件的交互。为了实现这个功能,它执行一个未修改的可执行文件,并用带监控的版本包装每一个CPU指令和系统调用。它提供了相当多的可配置项:你可以定义你的虚拟机的预期行为:核心数,缓存大小,系统调用的行为(有些系统调用在不同版本的内核上行为不一样)。主要的缺点是,由于指令不是直接执行,valgrind带来很大的额外负担,导致性能下降5到50倍(取决于你选择的工具和选项)。
运行valgrind很容易。它不需要修改你的程序或构建系统。最基本的用法:valgrind --tool=<toolname> <yourprogram and arguments>。
memcheck
memcheck是valgrind的默认工具。他是一个内存检查器,它追踪每一次内存访问和分配,并查找这些管理错误:
为了做到这些,memcheck做的第一件事情就是维护一个已分配内存的注册表。每次一块新的内存被分配,memcheck记住返回的指针,并开始追踪它。
(未完)