【问题标题】:C++ Stack Walking on WindowsC++ 栈在 Windows 上行走
【发布时间】:2012-11-30 16:01:49
【问题描述】:

我正在使用非常 .NET 风格的方法为 C++ 构建内存管理器。在这样做时,我需要知道哪些对象被认为是可达的;如果可到达对象具有相关对象的句柄,则该对象被认为是可到达的。所以这就提出了哪些对象是我们搜索的根的问题?答案是这些“eve”对象在堆栈上,无论是以托管对象句柄的形式,还是本身具有托管对象句柄的作用域本地对象实例的形式。

我已经阅读了这方面的一些文章,还查看了 MSDN 上有关 Win32 API 中 StackWalk 方法的实现细节。

一如既往地非常感谢任何帮助。并且请不要建议不要制作内存管理器,或者建议诸如智能指针之类的替代方案。我完全明白我在做什么。谢谢!

【问题讨论】:

  • 您需要精确还是近似?因为除非您自定义编译器,否则精确可能是不可能的。
  • 记住操作系统持有的其他线程、对象(带有回调)的堆栈...
  • 在遍历堆栈时,您也不知道遇到的变量的类型。它们可以是 INT,可以是指针,也可以是更大的类或结构的一部分……
  • 同一个堆栈帧的同一个内存单元可能意味着不同的事情,具体取决于您所在的块(空间重用);它不是由函数(入口点)定义的。
  • @JanDvorak:我认为总结所有这些问题的简单方法是说“没有编译器的帮助就不可能正确完成”。 (这并不意味着即使在编译器的帮助下也很有可能做到这一点,但这是另一个问题......):P

标签: c++ winapi memory-management stack callstack


【解决方案1】:

您的需求有点类似于我目前正在处理的一个小项目,但我的目标不是制作内存管理器,我的目标是检测 dmalloc(以及调试模式长时间运行的应用程序它正在其中运行)具有定期停止执行和扫描内存以查找没有引用的堆分配的能力。有点像“愚蠢”的垃圾收集器,但不是以释放内存为目标;相反,目的是记录泄漏的分配以供以后分析(以及在分配时捕获的堆栈跟踪,我已经将其添加到 dmalloc)。请注意,作为通用内存管理器的垃圾收集器,这将是一个非常低效的过程,并且需要“很长时间”才能运行(我还没有完成,但如果每次运行它我都不会感到惊讶停止正常程序执行超过 10 秒),但出于我自己的目的,我不太关心性能,因为我每隔几个月才启用一次以测试我公司产品中的新内存泄漏。

无论如何,我假设您的内存管理器将是您应用程序中堆内存的唯一来源?并且您系统中的线程在完全共享内存环境中运行,其中没有线程有任何内存,包括堆栈空间和线程本地存储空间,这是其他线程无法看到的?如果是这样……

我相信只有四种类型的内存可以在其中找到指向堆分配的指针:

  1. 在每个线程的调用栈上
  2. 在堆分配本身内
  3. 在静态分配的可写内存中(.bss & .data/.sdata,但 不是 .rdata/.rodata)
  4. 在每个线程的线程本地存储空间中

您已经知道指向堆分配的指针可能出现在堆栈上。指向分配的指针也可以(代替)存储在堆对象本身中,甚至不存储在堆栈中。您的问题表明您可能希望将堆栈用作垃圾收集器搜索的“根”;我认为这意味着您希望能够跟随堆栈上的指针向外指向其他分配,在内存中从一个对象搜索到另一个对象,直到您遍历内存中的所有对象并找到指向所有分配的所有指针。 “根”指针也可能存在于静态分配的对象中,可以直接引用它,甚至没有指向堆栈上此类对象的指针,因此您不能假设所有分配都可以从您在堆。此外,不幸的是,对于 C++,除非您能够知道每个分配的结构(如果没有编译器的帮助,您不会知道),您将不得不假设任何位置都可能是一个指针。因此,您必须扫描这四种内存类别中的每一种,寻找指向所有现有分配的潜在指针,如果您在内存中找到与分配地址匹配的值,则用“可能仍在使用”标志标记每一个,不管它是否真的是一个指针。当您扫描内存时,在每个字节位置(或在每个字节位置可被 sizeof(void*) 整除,如果您知道您的平台不能有未对齐地址的指针),您必须搜索分配列表查看该值是否在您的分配列表中。

由于您确信自己知道自己在做什么,因此您的内存管理器可能会在一个平衡的树结构(可能是红黑树或安德森树)中跟踪这些分配,从而为您提供 O(log n) 插入& 查找这些分配,但是导航这些树的比例常数会真正杀死你的垃圾收集器的性能。在进行垃圾收集扫描之前,您需要将树的分配指针按顺序(即使用中序遍历升序或降序)复制到一个平坦的连续缓冲区(即“数组”)中。我建议每个分配地址的void* 数组和一个单独的位数组(不是bool 数组),每个分配一个位,初始化为全零,如果你找到一个分配的对应位设置为 1潜在的参考。这仍然会在您扫描垃圾收集时为您提供 O(log n) 查找(使用二进制搜索),但您的查找具有更易于管理的比例常数;此外,这种更紧凑的数据结构往往比平衡树具有更好的缓存命中性能。

现在我将讨论您必须扫描的三类内存:

  • 每个线程的调用栈

为此,您必须能够向线程管理器查询每个线程堆栈的顶部和底部。如果您只能获取每个线程的当前堆栈指针,那么您可以使用“回溯”API 来获取该堆栈上的函数返回地址列表。从那里,您可以向后扫描每个堆栈的基数(您不知道),按顺序勾选每个返回地址,直到到达最后一个返回地址,然后您就可以找到堆栈基数(或足够接近) .对于“当前线程”,请确保不包含任何与您的内存管理器相关的堆栈帧;即,备份一些堆栈帧并忽略与您的垃圾收集器相关的那些,否则您可能会在垃圾收集器的局部变量中找到泄漏分配的地址并将它们误认为

  • 在堆分配本身内

堆对象可以相互引用,并且您可以拥有一个泄漏对象网络,这些对象都相互引用,但是作为一个组,它们被泄漏了。您不想看到它们彼此的指针并将它们视为“正在使用”,因此您必须小心处理这些......并且最后。完成所有其他类别后,您可以折叠/拆分 void* 分配地址的平面数组,制作单独的“考虑使用中”分配和“尚未验证”分配列表。扫描“考虑使用中”的分配,寻找指向仍在“尚未验证”列表中的分配的潜在指针。当您找到任何内容时,将它们从“尚未验证”列表移到“考虑使用中”列表的末尾,以便最终也扫描它们。

  • 在静态分配的可写内存中(.bss & .data/.sdata,但不是 .rdata/.rodata)

为此,您需要从链接器获取符号到每个部分的开始和结束(或长度)。如果此类符号尚不存在,或者您无法从平台 API 获取该信息,则需要获取链接器命令脚本(链接器脚本)并对其进行修改以将全局符号添加和初始化到起始地址和结束每个部分的地址(或长度)。 .bss 部分包含未初始化的全局、文件范围和类静态数据成员。 .data/.sdata 部分包含非常量预初始化全局、文件范围和类静态数据成员。您无需担心 .rdata/.rodata 部分,因为您的程序不会将堆分配地址写入静态 const 数据。

  • 在每个线程的线程本地存储空间中

为此,您必须能够向线程管理器查询每个线程的线程本地存储空间,否则每个线程的启动部分必须将其线程本地存储添加到列表中应用程序的线程局部空间,并在线程退出时将其删除。


如果您仍然参与并想要这样做,那么现在您可能已经意识到这是一个比您最初想象的更大的项目。告诉我进展如何!

【讨论】:

  • 如果您正在收集(而不是泄漏检测),那么您必须注意内部指针和存储在应用程序内存空间外部位置的指针。例如,作为参数传递给已发布消息的指针在消息队列中时对应用程序不可见,但您不希望在处理消息之前收集它。
  • @RaymondChen - 哎哟!没想到多重派生派生类指针不指向分配的基地址。这肯定会让活动变得混乱。现在我们又回到了垃圾收集器需要编译器支持的问题上。
  • 我不得不说这个回复的质量和信息的广度非常好;谢谢。随着时间的推移,我在两个想法中摇摇欲坠:“考虑到脆弱性,这太难了,真的不值得麻烦。” “自从我上次尝试以来,我已经学到了很多,所以让我们再试一次吧。”您的回复证实,要构建一个真正强大、高效和全面的内存管理器,我可能需要做更多阅读。再次感谢。点赞!
  • 我还应该提到,当我们发现我们神秘的约当我们进行第 4,294,967,296 次分配时,1GB“泄漏”,或者在我们的例子中,分配异常......在 dmalloc 本身内引起。之后,dmalloc 开始无法通过假设sa_seen_c / 2 计数器永远不会超过_dmalloc_iter_c 的完整性检查。当 _dmalloc_iter_c 回零时,在翻转之前进行的每个分配的 free() 都会触发健全性检查失败日志消息,从而填充内存中的日志缓冲区......很多。
  • @WilliamCustode - 哇......这个网页描述了一个显然已经实现的版本,正是我们在这里讨论的内容:hpl.hp.com/personal/Hans_Boehm/gc......他们声称它至少具有合理的性能.
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-10-25
  • 2011-01-29
  • 2015-01-07
  • 1970-01-01
  • 2015-10-16
相关资源
最近更新 更多