Blogs

如何制作堆分析器

Yossi Kreinin 2014年5月23日 1条评论

我们将看到如何制作堆分析器。此帖子的示例代码构成了Heapprof,巴黎人送28元用于使用Malloc /免费的程序的工作250线堆分析器。

它在Linux上的框中工作(在像GDB和Python这样的“真实”程序上)。主要观点虽然很容易港口,并修改以满足您的需求。代码,构建和测试脚本是在 GitHub. .

为什么滚动自己的堆分析器?

  • 这很简单!和乐趣,如果你是那种人。什么,不是足够的原因?好的,怎么样......
  • 您的平台在缺少OS的嵌入式系统中没有巴黎人送28元常见的情况,标准API输出数据等。
  • 您的实时程序在几个小时后覆盖堆。您想知道哪个缓冲区溢出。 Valgrind在设备上没有运行/太慢。自定义堆分析器如何在这里帮助?阅读!
  • 您希望从探查器所做的方式呈现统计数据。
  • 您只想将Malloc介绍一些时间来最小化减速。
  • ......

来自平台的堆分析器需求是多少

您无法在Portable,Standard C中编写堆分析器。您需要大多数平台的一些东西,但是C没有指定接口。您需要的是:

  • 拦截对Malloc,免费,Calloc和Realloc的呼叫
  • 在运行时获取当前的呼叫堆栈
  • 转储内存的内容(如核心转储)
  • 将指令地址匹配到源代码行

鉴于此,我们可以将每个分配的块与呼叫堆栈相关联。然后我们通过调用堆栈进行分配。最后,我们通过分配的内存的总量对呼叫堆栈进行排序。

为了说明这个想法,这是堆内存如何看起来的粗略草图(每行是巴黎人送28元存储器字 - 32/64b):

size (used by malloc/free)
user data (malloc returns this address - size is "invisible" to the user)
 ...... 
size (of the next block)
user data
 ...... 

这就是堆内存看起来像Heapprof的Malloc一样:

size (used by the underlying malloc/free)
"HeaP" (magic string; underlying malloc gives us this address)
user size (user's request - without heapprof's overhead)
caller 0 (the address malloc returns to)
caller 1 (the address caller 0 returns to)
 ...... 
"ProF" (magic string)
user data (our malloc returns this address to the user)
 ...... 
size
"HeaP"
user size
caller 0
caller 1
 ...... 
"ProF"
user data
 ...... 

读取包含此堆的核心转储的程序可以简单地查找包含在“堆”...“Prof”中的块。因此,它将找到所有实时块的大小 - 以及负责每个分配的呼叫堆栈。

虽然我们在它 - 为什么要在块的开头存储元数据而不是最后?

最常见的是,由于大的正数组索引,而不是负指标,缓冲区溢出。让我们说某人的数组溢出,搞砸了堆和倾倒核心。然后,如果我们在Heapprof下运行,我们将看到谁分配了块 就在之前 腐败点。这将大大缩小我们的搜索。

你可以告诉我从经验写作,你不能吗?。啊,安全关键的软件,零安全语言......是一种谋生的方式。无论如何,重点是,堆分析器包括巴黎人送28元“堆注释器”,它是巴黎人送28元掌握的调试工具。因为它更容易被附加到块的呼叫堆栈的堆。

那么我们如何完成所有 - 拦截Malloc,保存调用堆栈,转储内存并将返回地址匹配到源线?让我们巴黎人送28元接巴黎人送28元地回答这一点。

拦截Malloc和朋友

gcc -shared -fPIC -o heapprof.soheapprof.c ...
env LD_PRELOAD=heapprof.so program args ...

这一切都 - 添加到$ ld_pread巴黎人送28元共享对象重新定义Malloc,Calloc,免费和Realloc。 (小心 不是 重新定义任何其他东西 - 使用 静止的 to hide symbols.)

我们的重新定义Malloc将为呼叫堆栈分配给呼叫者的字节并提供几个。分配 - 怎么样?最简单的方法是调用原始的malloc(它与免费类似):

typedef void* (*malloc_func)(size_t);
static malloc_func g_malloc;
//at init time:
g_malloc = (malloc_func)dlsym(RTLD_NEXT, "malloc");
//upon malloc:
void* chunk = g_malloc(size+EXTRA);

在嵌入式系统上常见的静态相关的二进制文件中,只是将Malloc等添加到构建中通常足以覆盖标准功能。虽然呼叫原始函数很难或不可能。我会拉出巴黎人送28元开源的malloc - 就像道格lea的 dlmalloc. - 将函数重命名为real_malloc或其他任何版本,并从我自己的版本调用它们。

获取当前的呼叫堆栈

GNU C具有恰好工作的精彩回溯()函数。 n

void** p = (void**)chunk;

//fill the metadata
p[START_INDEX] = START_MAGIC;
backtrace(p+SIZE_INDEX, nframes+1); //+1 for &malloc
p[SIZE_INDEX] = (void*)size; // overwrite &malloc
p[END_INDEX] = END_MAGIC;

//give the user a pointer past the metadata
return (char*)p + EXTRA;

不幸的是,并非所有系统都有回溯 - 甚至不是所有GNU C端口(例如,MIPS,AFAIK没有回溯)。没有回溯,让自己呼叫堆栈仍然相对容易,尽管它可以得到一点无忧无虑。如果你关心,你可以读一群关于它的 这里 .

倾倒核心

如果有一件事是擅长,它倾倒核心:

int*p=0;*p=0;

分割错误(核心转储)

(有更加简洁的方式吗?int * p = * p想到,但它可能意外地 不是 如果P未初始化为合法指针,则崩溃。 *(int *)0 = 0?剃掉字符的任何其他建议?..)

如果这些野蛮意味着不适合你的目的怎么办? GDB允许您在函数中放置巴黎人送28元断点you_func,因此转储核心:

gdb program -ex "b some_func" -ex r -ex "gcore my.core" -ex q

您可以在同一过程中多次执行此操作,获取几个堆状态快照。

或者让我们说你是巴黎人送28元你想要的C模块的Python进程:

os.kill(os.getpid(), 11)

...... ...或者是kill -segv进程-ID等等。

在嵌入式系统上,您可以使用JTAG探测器或在某些通信通道等中使用JTAG探测器的任何方法转储内存等。它不会以调试器识别的格式为“真实”的核心转储。但正如我们在下一节看到的那样,它可能就足够了。

将返回地址与源代码匹配

现在我们的脱机堆统计分析仪,Heapprof.py,搜索“堆...专业版”的块元数据,并找到块大小和堆栈:

class Block:
 def __init__(self, metadata):
  # 'I',4 for 32b, 'Q',8 for 64b machines
  self.size = struct.unpack('I', metadata[0:4])[0]
  self.stack = struct.unpack('%d'%(len(metadata)/4 - 1)+'I', metadata[4:])

所以现在block.stack是巴黎人送28元返回地址列表,

{addr for block in blocks for addr in block.stack}

...... 是我们核心转储中的所有返回地址集。我们如何将它们与源线和函数名称匹配?

我们可以将地址与“AddR2Line -F-e程序”(C程序)管制:

from subprocess import *
addr2line = Popen('addr2line -f -e'.split()+[exe],
                  stdin=PIPE, stdout=PIPE)
for addr in addrs:
  addr2line.stdin.write('0x%x\n'%addr)
  addr2line.stdin.flush()
  func = addr2line.stdout.readline().strip()
  line = addr2line.stdout.readline().strip()

但是,这不适用于共享库的返回地址 - AddR2Line不知道他们加载了哪些地址。

工作是GDB的是什么 - 如果它给出了核心转储告诉它在加载共享库的位置:

gdb = Popen(['gdb',prog,core], stderr=STDOUT,
             stdin=PIPE, stdout=PIPE)
for addr in addrs:
  gdb.stdin.write(' 信息符号  0x%x\n'%addr)
  gdb.stdin.write(' 列表  *0x%x\n'%addr)
  gdb.stdin.write('printf "\\ndone\\n"\n')
  gdb.stdin.flush()
  s = ''
  while s != 'done':
    s = gdb.stdout.readline().strip()
    if 'is in' in s: line = s.split('is in ')[1]
    if 'in section' in s: func = s.split('(gdb) ')[1]

剧本看起来很丑陋,但结果是那个 信息符号0xwhy 告诉你函数名称(然后有些) 列表* 0xWhoration 告诉你源行号(然后有些)。

所以谁需要GDB时addr2line?在嵌入式系统上,通常只有巴黎人送28元静态二进制文件,所以addr2line就足够了。另一方面,没有以标准格式出来的核心转储。也许你拥有的只是巴黎人送28元JTAG探测器,你将内存转储在巴黎人送28元大块中。所以你不能使用`GDB程序核心核心。

在这种情况下,如果您,Heapprof.py将正常工作 setenv heapprof_addr2line.。如果它是“真实”的核心转储或只是巴黎人送28元原始内存转储 - 搜索“堆... PROP”是否同样容易。只有GDB关心,而$ HeapProf_Addr2line避免使用GDB。

如果您使用专有编译器,那么也许它没有ADDR2LINE。 bummer。如果可执行文件格式是标准(ELF / COFF / IMORY),那么GDB的 信息符号 命令将工作(但是 列表 不会 - 除非矮化调试信息没有可用。)此外,专有的编译器对嵌入式系统中的业务不利。但这是咆哮的另巴黎人送28元时间。

聚类和排序

通过分配堆栈群集块,并按块大小的总和对堆栈进行排序:

stack2sizes = {}
for block in blocks:
  stack2sizes.setdefault(block.stack,list()).append(block.size)
total = sorted([(sum(sizes), stack) for stack, sizes in stack2sizes.iteritems()])

现在,我们可以使用上面从AddR2Line / GDB获得的符号信息简单地打印出排序的堆栈。

每个人都喜欢Malloc

为什么Heapprof会带我几个小时写入而不是只有巴黎人送28元 - 几个小时蔓延到几天内,所以在这里我正在编程后,在切换到兼职工作之后,专门用于编程更少?根本原因与我分开是巴黎人送28元完整的笨蛋?

问题是每个人mallocs。 Dlsym Mallocs。回溯Mallocs。 pthread - 我只需要哪些,因为那些人​​malloc - Mallocs。 she

所以发生了什么,你在Malloc里面。您想记录调用堆栈,以便您调用回溯。回溯呼叫malloc。应该避免无限递归,我们使用全局变量。现在,全局变量需要用互斥锁保护。我们必须在第一次调用malloc之前初始化 - 和 初始化Mallocs。我们也需要dlsym最初获得原件&malloc, but dlsym Mallocs。

所以我们需要能够没有 &Malloc,最初。所以我使用SBRK,我需要自由 不是 用&自由尝试和释放SBRK的东西的原因会误用。等等

一切都在 heapprof.c. 如果你想看看。我不认为这非常有趣;它确实制作了巴黎人送28元更难编写的堆分析器,但它仍然适合111个Sloc,所以真的是没有大的交易。由于全局变量防范回溯的Malloc调用,这是真正的愚蠢的事情。

我怀疑初始化时只有Mallocs,也许不是静态相关的二进制文件。因此,如果您正在移植到嵌入式系统,则毕竟不需要担心线程问题。我只是想为“普通案”写一些“强大”。

移植

如果要将其移植到您的平台上的HeapProf或Bits,则描述了您可能需要处理的问题 这里 。基本上,您可能需要调整我们上面讨论的特定于平台的事情,以及其他一些像对齐和endian等等。

结论

  • 堆分析器是巴黎人送28元非常简单的工具
  • 堆分析器在运行时用元数据注释堆块 - 这本身可以成为巴黎人送28元很好的调试工具
  • 我是巴黎人送28元绝望的笨蛋,拼命地需要节目少

[]
评论 MR_BANDIT. 2019年12月11日

Quote:Â  每个人都喜欢Malloc

嗯..没有。

由于在创建任何类变量时,我不会在关键系统上使用C ++,或者调用C ++库函数。我将放入巴黎人送28元假malloc()来捕捉这些案件。

如果我*做*需要“动态缓冲区”,我会进行分析以确定缓冲区的数量和大小并创建缓冲池。部分分析是:说我需要10个字节&20字节缓冲区。我会看看何时&我需要两个缓冲区大小多长。通常,我可以创建巴黎人送28元20字节缓冲池,只需将其用于这两种情况。

它还容易录制缓冲池来确定使用,Malloc无需自由,跟踪缓冲区如何在任务之间共享。

该方法对大型嵌入式系统相当小。

如果您真的需要使用malloc(),那种方法看起来应该有效......

要发布回复评论,请单击连接到每个注释的“回复”按钮。发布新的评论(不是回复评论),请在评论的顶部查看“写评论”选项卡。

注册将允许您参加所有相关网站的论坛,并为您提供所有PDF下载。

注册

我同意 使用条款 隐私政策 .

尝试我们偶尔但流行的时事通讯。非常容易取消订阅。
或登录