[翻译]内存 - 第五部分:调试工具

原文地址:https://techtalk.intersec.com/2013/12/memory-part-5-debugging-tools/

介绍

我们花了4篇文章介绍了什么是内存,如何处理内存,内存会给你带来什么问题。最好的开发人员也会写出有bug的代码。通常可接受的估算是每千行代码的bug数,这肯定是一个相当大的数字。因此,即使你熟练的掌握了我们的文章讲述的各种概念,你仍然可能写一些跟内存有关的bug。

内存相关的bug特别难定位和修复。我们以下面这个程序为例:

#include 
 
#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;
}



这个程序期望从参数获得一个消息,并打印“Hello <消息>!”(默认消息是"world")。

这个程序的行为是未定义的,有bug,并且可能会崩溃。build_message函数返回了执行它的栈上内存的指针。因为栈的工作机制,这块内存非常可能被后面调用的其他函数改写,很可能是fputs。因此,如果fputs内部使用了足够多的栈内存来改写消息,输出会被破坏(程序甚至可能会崩溃)。其他情况,程序可能会打印正常的消息。更进一步,程序可能会导致buffer溢出,因为使用了不安全的sprintf函数,这个函数没有对输入做任何限制。

因此,程序的行为取决于命令行输入的消息的大小,即fputs实现中MAX_LINE_SIZE的值。这种bug让人恼火的原因是,它的结果不是那么明显:在简单的消息情况下,它“工作”得很好,并且只会在收到正好让问题暴露出来的参数时,错误才会出现。这就是为什么开发人员要用一些工具来帮助验证(或调试)内存管理才能安心。

本文将会介绍几个免费的工具,我们认为一个C(或C++)开发人员的工具库里应该必备。

调试器

第一个是调试器。在Linux平台很可能是gdb。大多数开发人员知道gdb的基本用法:查看程序栈(bt, up, down, frame ...),添加一个断点(break , continue ...),单步执行(step, next, fin ...),查看内存(print , call , x/ , ...)等等。当程序因为段错误崩溃时,调试器是大多数开发人员会选择的工具。调试器会捕获信号,并允许查看在那个时刻的程序状态。大多数的段错误都很明显(未初始化指针,空指针解引用...),只需要用调试器花一点功夫。

但是很少被人知道的是,调试器可以放置一个监控点:添加一个动态的断点,每次在一个表达式的结果改变的时候中断程序。这在检测内存被破坏的原因时及其有用:在内存的内容被破坏的位置放一个监控点,程序将会每次在那块内容变化时中断。这对程序的性能影响很小,只要你不监控太多的内存地址,这些监控点由硬件直接管理。

让我们回头看介绍中提到的那个例子:我们用fputs打印第一个指针参数指向的内容,但实际被打印的字符串并不是我们在build_message里面写出来的。看一下这一小部分调试会话:

  • 首先我们在构造消息的地方设置了断点,并且检查sprintf正确的构造了消息。

(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.



  • 监控点中断了程序的执行。调试器打印出旧的和新的值,我们可以容易的查看程序。快速的查看一下堆栈,我们发现处于一段动态链接的代码中(很可能处于fputs符号的解析中)。

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

valgrind是C/C++开发人员的瑞士军刀。它提供了各种工具,例如内存检查器(memcheck),内存分析器(massif),缓存分析器(cachegrind),CPU分析器(callgrind),还有一些线程检查器(helgrind, DRD, tsan)等等。

valgrind是一个基础的虚拟机,它监控了每一次跟操作系统和虚拟硬件的交互。为了实现这个功能,它执行一个未修改的可执行文件,并用带监控的版本包装每一个CPU指令和系统调用。它提供了相当多的可配置项:你可以定义你的虚拟机的预期行为:核心数,缓存大小,系统调用的行为(有些系统调用在不同版本的内核上行为不一样)。主要的缺点是,由于指令不是直接执行,valgrind带来很大的额外负担,导致性能下降5到50倍(取决于你选择的工具和选项)。

运行valgrind很容易。它不需要修改你的程序或构建系统。最基本的用法:valgrind --tool=

memcheck

memcheck是valgrind的默认工具。他是一个内存检查器,它追踪每一次内存访问和分配,并查找这些管理错误:

  • 访问未分配的内存
  • 程序行为依赖未初始化的内存
  • 内存泄漏

为了做到这些,memcheck做的第一件事情就是维护一个已分配内存的注册表。每次一块新的内存被分配,memcheck记住返回的指针,并开始追踪它。

(未完)

转载于:https://my.oschina.net/tz8101/blog/632361

你可能感兴趣的:([翻译]内存 - 第五部分:调试工具)