您的需求有点类似于我目前正在处理的一个小项目,但我的目标不是制作内存管理器,我的目标是检测 dmalloc(以及调试模式长时间运行的应用程序它正在其中运行)具有定期停止执行和扫描内存以查找没有引用的堆分配的能力。有点像“愚蠢”的垃圾收集器,但不是以释放内存为目标;相反,目的是记录泄漏的分配以供以后分析(以及在分配时捕获的堆栈跟踪,我已经将其添加到 dmalloc)。请注意,作为通用内存管理器的垃圾收集器,这将是一个非常低效的过程,并且需要“很长时间”才能运行(我还没有完成,但如果每次运行它我都不会感到惊讶停止正常程序执行超过 10 秒),但出于我自己的目的,我不太关心性能,因为我每隔几个月才启用一次以测试我公司产品中的新内存泄漏。
无论如何,我假设您的内存管理器将是您应用程序中堆内存的唯一来源?并且您系统中的线程在完全共享内存环境中运行,其中没有线程有任何内存,包括堆栈空间和线程本地存储空间,这是其他线程无法看到的?如果是这样……
我相信只有四种类型的内存可以在其中找到指向堆分配的指针:
- 在每个线程的调用栈上
- 在堆分配本身内
- 在静态分配的可写内存中(.bss & .data/.sdata,但
不是 .rdata/.rodata)
- 在每个线程的线程本地存储空间中
您已经知道指向堆分配的指针可能出现在堆栈上。指向分配的指针也可以(代替)存储在堆对象本身中,甚至不存储在堆栈中。您的问题表明您可能希望将堆栈用作垃圾收集器搜索的“根”;我认为这意味着您希望能够跟随堆栈上的指针向外指向其他分配,在内存中从一个对象搜索到另一个对象,直到您遍历内存中的所有对象并找到指向所有分配的所有指针。 “根”指针也可能存在于静态分配的对象中,可以直接引用它,甚至没有指向堆栈上此类对象的指针,因此您不能假设所有分配都可以从您在堆。此外,不幸的是,对于 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 数据。
为此,您必须能够向线程管理器查询每个线程的线程本地存储空间,否则每个线程的启动部分必须将其线程本地存储添加到列表中应用程序的线程局部空间,并在线程退出时将其删除。
如果您仍然参与并想要这样做,那么现在您可能已经意识到这是一个比您最初想象的更大的项目。告诉我进展如何!