C ++堆栈在Windows上行走

时间:2012-12-12 20:52:40

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

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

我已经阅读了一些关于此的文章,并在MSDN上检查了有关Win32 API中StackWalk方法的实现细节。

一如既往,我们非常感谢任何帮助。请不要建议不要做内存管理器,或建议智能指针等替代品。我完全理解我在做什么。谢谢!

1 个答案:

答案 0 :(得分:3)

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

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

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

  1. 在每个线程的callstack上
  2. 在堆分配中自己
  3. 在静态分配的可写内存中(.bss& .data / .sdata,但是 不是.rdata / .rodata)
  4. 每个线程的线程本地存储空间
  5. 您已经意识到堆栈上可能会出现指向堆分配的指针。分配的指针也可以(可能而不是)存储在堆对象本身中,甚至不存储在堆栈中。您的问题表明您可能希望将堆栈用作垃圾收集器搜索的“根”;我认为这意味着你希望能够将堆栈上的指针向外追踪到其他分配,通过内存从一个对象搜索到另一个对象,直到你遍历内存中的所有对象并找到所有分配的指针。 "根"指针也可以存在于静态分配的对象中,这些对象可以直接引用,甚至没有指向堆栈上这样的对象的指针,因此您不能假设所有分配都可以从"指针"你在堆栈中找到了。另外,遗憾的是使用C ++,除非你能够知道每个分配的结构(你不会没有编译器的帮助),你必须假设任何位置都可能是一个指针。因此,您必须扫描这四类内存中的每一类,寻找指向所有现有分配的潜在指针,如果在内存中找到与分配地址匹配的值,则用“可能仍在使用”标记标记每个分配,是否它实际上是一个指针。当您扫描内存时,在每个字节位置(或在每个字节位置均匀地被sizeof(void *)整除),如果您知道您的平台不能在未对齐的地址处有指针,那么您将必须搜索您的分配列表查看该值是否在您的分配列表中。

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

    现在,我将讨论您必须扫描的三类记忆中的每一类:

    • 每个帖子的callstack

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

    • 在堆分配中自己

    堆对象可以互相引用,并且您可以拥有一个泄漏对象的网络,这些对象都作为一个组相互引用,它们会被泄露。你不想看到他们彼此的指针&把它们视为"在使用中",所以你必须小心处理这些......并持续。完成所有其他类别后,您可以折叠/拆分void*分配地址的平面数组,制作一个单独的列表"考虑使用中"分配和"尚未验证"分配。扫描"考虑使用中"分配仍然在"尚未验证的"中寻找分配的潜在指针。名单。如你所发现的那样,将它们移出"尚未验证的"列表到"结束时考虑使用"列表,以便您最终扫描这些。

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

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

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

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


    如果你还在船上并希望这样做,那么现在你可能已经意识到这是一个比你最初想象的更大的项目。让我知道它是怎么回事!