在没有 ASLR 的情况下寻找(内存,GC 相关)heisenbug 消失

Posted

技术标签:

【中文标题】在没有 ASLR 的情况下寻找(内存,GC 相关)heisenbug 消失【英文标题】:hunting a (memory, GC related) heisenbug disappearing without ASLR 【发布时间】:2016-05-14 16:14:42 【问题描述】:

操作系统:Linux/Debian/Sid/x86_64(和 Linux/Debian/Testing/x86_64);我用于编译的系统 GCC 是 6.1.1(和 Debian/Testing 的 5.3)。 Gnu libc 是 2.22; Linux内核是4.5; GDB 是系统 7.10 或我自己的,从 FSF 源构建,7.11

我在 GCC 的 MELT 实验分支中寻找(近两周以来)记忆和 garbage collection 相关的 heisenbug(MELT 大体上讲是一种类似于 Lisp 的特定领域语言自定义 GCC 编译器;使用 MELT 本身将 MELT 方言转换为 C++),您可以使用

svn co -r236207 svn://gcc.gnu.org/svn/gcc/branches/melt-branch gcc-melt

然后(对于每个 GCC 变体或分支)在 outside 树中构建它,例如

mkdir _ObjMelt
cd _ObjMelt
../gcc-melt/configure  --disable-bootstrap --enable-checks=gc \
 --enable-plugins --disable-multilib --enable-languages=c,c++,lto

(如果您愿意,可以将其他选项传递给../gcc-melt/configure,例如CXXFLAGS='-g3 -O0 -DMELT_HAVE_RUNTIME_DEBUG=1';您可以删除--enable-checks=gc 选项)

当然还有make(或make -j4);该构建可能需要半个多小时(并且可能会因 ASLR 而失败,见下文)

MELT 有一个分代 copying 垃圾收集器(我怀疑这个 bug 是其中的一个角落)并使用了很多 metaprogramming(尤其是大多数扫描和转发代码复制 GC 是由 MELT 生成的)。

valgrind 在这里无济于事:我们正在实现复制 GC,而 GCC 本身 - 即使没有 MELT - 也会泄漏内存)

MELT 被引导。通常的构建过程是从 MELT 源代码重新生成两次发出的 C++ 代码。通常的方法是发出一些 C++ 代码,fork 一些 make 来获得一个共享对象,然后 dlopen 那个共享对象,如此反复。

没有ASLR,构建总是成功的(并且它正在运行一个重要的测试:MELT 的引导程序,以及通过 MELT 扩展的编译对 MELT 运行时的分析)。我什至可以使用make upgrade-warmelt 重新生成运行时代码。

但是启用 ASLR,构建失败,崩溃总是以相同的方式(注意 cc1plus 是 MELT 之一):

cc1plus: note: MELT got fatal failure from ../../gcc-melt/gcc/melt-runtime.h:900
cc1plus: fatal error: corrupted memory heap with null magic discriminant
                      in 0x2bab6a8; GC#11
compilation terminated.
MELT BUILD SCRIPT FAILURE: 
  melt-build-script.tpl:382/307-melt-build-script.tpl:459/382 failed 
  with arguments @meltbuild-stage2/warmelt-normatch.args

我正在禁用 ASLR,例如与exec setarch $(uname -m) -R /bin/bash;当然,在运行 uder gdb 时,默认情况下禁用 ASLR(除非我将 set disable-randomization 0 作为 GDB 命令执行)。

我的同事 Franck Védrine 建议我使用gdb 的reverse execution 设施;原则上,它应该像在我的 GC 中设置断点一样简单(以及在 melt_fatal_error 宏调用的 fatal_errormelt_fatal_info 中...),达到 GC#11 状态,执行 record for在向后执行之后,运行故障案例(使用set disable-randomization 0 禁用ASLR)直到“崩溃”,然后reverse-cont 直到GC 中的断点,并明智地使用watch。可悲的是,这触发了一个广为人知的GDB 错误(Sourceware#19365、Ubuntu#1573786、Redhat#1136403、...)——最近的 GDB 快照(如 gdb-7.11.50.20160514)没有正确——

(我现在很想避免这个 GDB 错误,也许是通过在我自己的 memsetmemcpy 例程之前加上 #pragma GCC optimize ("-Og");但这看起来太过分了)

不管怎样,崩溃消息由以下代码给出(在我的melt-runtime.h 的第 900 行附近):

static inline int
melt_magic_discr (melt_ptr_t p)

  if (!p)
    return 0;
#if MELT_HAVE_DEBUG > 0 || MELT_HAVE_RUNTIME_DEBUG > 0
  if (MELT_UNLIKELY(!p->u_discr))
    
      /* This should never happen, we are asking the discriminant of a
      not yet filled, since cleared, memory zone. */
      melt_fatal_error
      ("corrupted memory heap with null discriminant in %p; GC#%ld",
       (void*) p, melt_nb_garbcoll);
    
#endif /*MELT_HAVE_DEBUG or MELT_HAVE_RUNTIME_DEBUG */
  gcc_assert (p->u_discr != NULL);
  return p->u_discr->meltobj_magic;
    

我的猜测是,该错误可能是围绕“判别式”(每个 MELT 值中的一种“类型”或“类”或“元数据”字段的转发的一个困难的 GC 错误) 在该判别式仍在年轻一代中的极少数情况下...添加一些代码以避免确实使该错误稍后发生,但我完全不确定。

欢迎提供任何调试与实际虚拟地址相关的 heisenbug 的线索或建议(因此对 ASLR 来说是明智的!)。

我什至添加了一些初始化代码,以便能够可选地mmapsbrk几个无用的兆字节,希望“重现”由mmap给出的随机地址(由@987654364调用@ 由 MELT 及其 GC 使用)。那还没有帮助!

【问题讨论】:

【参考方案1】:

我在我的 Smalltalk 垃圾收集器中一直使用的方法是在每次 GC 之前复制堆并在副本中执行 GC,然后在副本崩溃时重复调试。如果像我这样的系统是用高级 oo 语言开发的,那么这相对简单;复制堆只是复制构成 VM 模拟的对象图(在模拟中,堆位于单个大字节数组中)。

在您的上下文中应用此技术可能会更具挑战性,但并非不可能。让我在这里画一下...

我将调用您尝试调试“主”的进程以及那些被克隆以尝试 GC 的子进程。

在 master GC 之前,做一个 fork 并让 child 执行 GC,在 child 中运行泄漏检查器并退出,退出状态反映 GC 是否成功。如果孩子成功,则主服务器会继续进行自己的 GC。否则它会循环,产生重复失败 GC 的子代。然后你调试孩子。

孩子需要在两种状态下启动。每个 GC 中的初始启动只是运行 GC 并以成功状态退出。我们现在知道将失败的后续分叉可以进入等待状态,以便您可以将 gdb 附加到子节点。

我称之为“旅鼠调试”,因为在调试崩溃之前,可以让尽可能多的克隆跳过悬崖。让我知道你是否能正常工作。

【讨论】:

我不能说 Basile 是否能够很好地利用这项技术,但我可以说这是一个聪明的技术。

以上是关于在没有 ASLR 的情况下寻找(内存,GC 相关)heisenbug 消失的主要内容,如果未能解决你的问题,请参考以下文章

iOS app 的 ASLR

我可以使用如此大的Eden空间启动JVM,它可以在没有任何GC的情况下运行完成。假设我有一堆免费的mem

内存泄露,GC相关

二.GC相关之Java内存模型

关于Linux下ASLR与PIE的一些理解

JAVA的GC(GarbageCollection)机制