在没有 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_error
和 melt_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 错误,也许是通过在我自己的 memset
和 memcpy
例程之前加上 #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 来说是明智的!)。
我什至添加了一些初始化代码,以便能够可选地将mmap
或sbrk
几个无用的兆字节,希望“重现”由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 消失的主要内容,如果未能解决你的问题,请参考以下文章