从进程内部获取对进程堆元数据的访问

Gaining access to heap metadata of a process from within itself

虽然我可以编写合理的 C 代码,但我的专长主要是 Java 如果这个问题没有意义,我深表歉意。

我正在编写一些代码来帮助我进行堆分析。我通过使用 LLVM 进行检测来做到这一点。我正在寻找的是一种从自身内部访问进程的堆元数据的方法。这样的事情可能吗?我知道有关堆的信息存储在许多 malloc_state 结构中(例如 main_arena)。如果我可以访问 main_arena,我可以开始枚举不同的竞技场、堆、容器等。据我了解,这些变量都是静态定义的,因此无法访问。

但是有什么方法可以获取这些信息吗?例如,我可以使用 /proc/$pid/mem 以某种方式泄露信息吗?

获得此信息后,我想基本上获取有关所有不同空闲列表的信息。所以我想要,对于每个 bin 类型中的每个 bin,bin 中的块数及其大小。对于快速、小型和 tcache 容器,我知道我只需要索引来计算大小。我已经研究了这些结构是如何实现的以及如何遍历它们。所以我所需要的只是获得对这些内部结构的访问权限。

我查看了 malloc_info,这是我的后备方案,但我还想获得有关 tcache 的信息,但我认为 malloc_info 中没有。

我考虑过的一个选项是构建自定义版本的 glibc,其中包含非静态声明的 malloc_struct 变量。但据我所知,构建您自己的自定义 glibc 并不是很简单,因为您必须构建整个工具链。我正在使用 clang,所以我必须根据我的自定义 glibc 从源代码构建 LLVM(至少这是我从研究这种方法中了解到的)。

I am writing some code to help me do heap analysis.

什么样的堆分析?

I want want to basically get information about all the different freelists. So I want, for every bin in each bin type, the number of chunks in the bin and their sizes. For fast, small, and tcache bins I know that I just need the index to figure out the size.

如果您计划更改 malloc 实施,此信息 有意义。如果您的目标是分析或改进 应用程序 的堆使用情况,那么尝试收集它 没有 是有意义的,所以听起来您有一个 XY problem.

此外,像 bin 和 tcache 这样的东西只有在特定 malloc 实现的上下文中才有意义(TCMalloc 和 jemalloc 不会有任何 bin)。

要分析应用程序堆使用情况,您可能需要使用 TCmalloc,因为它提供了 很多 工具用于 heap profiling 和自省。

我最近有类似的要求,所以我确实认为能够为给定进程达到 main_arena 确实有其价值,一个例子是 post-mortem 内存使用分析。

使用dl_iterate_phdrelf.h,根据本地符号解析main_arena比较简单:

#define _GNU_SOURCE
#include <fcntl.h>
#include <link.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>

// Ignored:
// - Non-x86_64 architectures
// - Resource and error handling
// - Style
static int cb(struct dl_phdr_info *info, size_t size, void *data)
{
  if (strcmp(info->dlpi_name, "/lib64/libc.so.6") == 0) {
    int fd = open(info->dlpi_name, O_RDONLY);
    struct stat stat;
    fstat(fd, &stat);
    char *base = mmap(NULL, stat.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
    Elf64_Ehdr *header = (Elf64_Ehdr *)base;
    Elf64_Shdr *secs = (Elf64_Shdr*)(base+header->e_shoff);
    for (unsigned secinx = 0; secinx < header->e_shnum; secinx++) {
      if (secs[secinx].sh_type == SHT_SYMTAB) {
        Elf64_Sym *symtab = (Elf64_Sym *)(base+secs[secinx].sh_offset);
        char *symnames = (char *)(base + secs[secs[secinx].sh_link].sh_offset);
        unsigned symcount = secs[secinx].sh_size/secs[secinx].sh_entsize;
        for (unsigned syminx = 0; syminx < symcount; syminx++) {
          if (strcmp(symnames+symtab[syminx].st_name, "main_arena") == 0) {
            void *mainarena = ((char *)info->dlpi_addr)+symtab[syminx].st_value;
            printf("main_arena found: %p\n", mainarena);
            raise(SIGTRAP);
            return 0;
          }
        }
      }
    }
  }
  return 0;
}

int main()
{
  dl_iterate_phdr(cb, NULL);
  return 0;
}

dl_iterate_phdr用于获取映射的glibc的基地址。映射不包含需要的符号table(.symtab),所以必须重新映射库。最终地址由基地址加上符号值决定。

(gdb) run
Starting program: a.out 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
[New Thread 0x7ffff77f0700 (LWP 24834)]
main_arena found: 0x7ffff7baec60

Thread 1 "a.out" received signal SIGTRAP, Trace/breakpoint trap.
raise (sig=5) at ../sysdeps/unix/sysv/linux/raise.c:50
50    return ret;
(gdb) select 1
(gdb) print mainarena
 = (void *) 0x7ffff7baec60 <main_arena>
(gdb) print &main_arena
 = (struct malloc_state *) 0x7ffff7baec60 <main_arena>

该值与 main_arena 的值匹配,因此找到了正确的地址。

还有其他 ways 可以在不依赖库本身的情况下到达 main_arena。例如,遍历现有堆允许发现 main_arena,但该策略远不那么简单。

当然,一旦您拥有 main_arena,您需要所有内部类型定义才能检查数据。