菜鸟笔记
提升您的技术认知

如何高效解决 c 内存问题-ag真人游戏

导读:apache doris 使用 c 语言实现了执行引擎,c 开发过程中,影响开发效率的一个重要因素是指针的使用,包括非法访问、泄露、强制类型转换等。本文将会通过对 sanitizer 和 core dump 分析工具的介绍来为大家分享:如何快速定位 apache doris 中的 c 问题,帮助开发者提升开发效率并掌握更高效的开发技巧。

​作者|apache doris committer杨勇强

apache doris 是一款高性能 mpp 分析型数据库,出于性能的考虑,apache doris 使用了 c 语言实现了执行引擎。在 c 开发过程中,影响开发效率的一个重要因素是指针的使用,包括非法访问、泄露、强制类型转换等。google sanitizer 是由 google 设计的用于动态代码分析的工具,在 apache doris 开发过程中遭遇指针使用引起的内存问题时,正是因为有了 sanitizer,使得问题解决效率可以得到数量级的提升。除此以外,当出现一些内存越界或非法访问的情况导致 be 进程 crash 时,core dump 文件是非常有效的定位和复现问题的途径,因此一款高效分析 coredump 的工具也会进一步帮助更加快捷定位问题。

本文将会通过对 sanitizer 和 core dump 分析工具的介绍来为大家分享:如何快速定位 apache doris 中的 c 问题,帮助开发者提升开发效率并掌握更高效的开发技巧。

sanitizer 介绍

定位 c 程序内存问题常用的工具有两个,valgrind 和 sanitizer。

二者的对比可以参考:https://developers.redhat.com/blog/2021/05/05/memory-error-checking-in-c-and-c-comparing-sanitizers-and-valgrind

其中 valgrind 通过运行时软件翻译二进制指令的执行获取相关的信息,所以 valgrind 会非常大幅度的降低程序性能,这就导致在一些大型项目比如 apache doris 使用 valgrind 定位内存问题效率会很低。

而 sanitizer 则是通过编译时插入代码来捕获相关的信息,性能下降幅度比 valgrind 小很多,使得能够在单测以及其它测试环境默认使用 saintizer。

sanitizer 的算法可以参考:https://github.com/google/sanitizers/wiki/addresssanitizeralgorithm

在 apache doris 中,我们通常使用 sanirizer 来定位内存问题。llvm 以及 gnu c 有多个 sanitizer:

  • addresssanitizer(asan)可以发现内存错误问题,比如 use after free,heap buffer overflow,stack buffer overflow,global buffer overflow,use after return,use after scope,memory leak,super large memory allocation;
  • addresssanitizerleaksanitizer (lsan)可以发现内存泄露;
  • memorysanitizer(msan)可以发现未初始化的内存使用;
  • undefinedbehaviorsanitizer (ubsan)可以发现未定义的行为,比如越界数组访问、数值溢出等;
  • threadsanitizer (tsan)可以发现线程的竞争行为;

其中 addresssanitizer, addresssanitizerleaksanitizer 以及 undefinedbehaviorsanitizer 对于解决指针相关的问题最为有效。

sanitizer 不但能够发现错误,而且能够给出错误源头以及代码位置,这就使得问题的解决效率很高,通过一些例子来说明 sanitizer 的易用程度。

可以参考此处使用 sanitizer:https://github.com/apache/doris/blob/master/be/cmakelists.txt

sanitizer 和 core dump 配合定位问题非常高效,默认 sanitizer 不生成 core dump 文件,可以使用如下环境变量生成 core dump文件,建议默认打开。

可以参考:https://github.com/apache/doris/blob/master/bin/start_be.sh

export asan_options=symbolize=1:abort_on_error=1:disable_coredump=0:unmap_shadow_on_exit=1

使用如下环境变量让 ubsan 生成代码栈,默认不生成。

export ubsan_options=print_stacktrace=1

有时候需要显示指定 symbolizer 二进制的位置,这样 sanitizer 就能够直接生成可读的代码栈。

export asan_symbolizer_path=your path of llvm-symbolizer

sanitizer 使用举例

use after free

user after free 是指访问释放的内存,针对 use after free 错误,addresssanitizer 能够报出使用释放地址的代码栈,地址分配的代码栈,地址释放的代码栈。比如:https://github.com/apache/doris/issues/9525中,使用释放地址的代码栈如下:

82849==error: addresssanitizer: heap-use-after-free on address 0x60300074c420 at pc 0x56510f61a4f0 bp 0x7f48079d89a0 sp 0x7f48079d8990
read of size 1 at 0x60300074c420 thread t94 (memtableflushth)
    #0 0x56510f61a4ef in doris::faststring::append(void const*, unsigned long) /mnt/ssd01/tjp/incubator-doris/be/src/util/faststring.h:120
// 更详细的代码栈请前往https://github.com/apache/doris/issues/9525查看

此地址初次分配的代码栈如下:

previously allocated by thread t94 (memtableflushth) here:
    #0 0x56510e9b74b7 in __interceptor_malloc (/mnt/ssd01/tjp/regression_test/be/lib/palo_be 0x536a4b7)
    #1 0x56510ee77745 in allocator::alloc_no_track(unsigned long, unsigned long) /mnt/ssd01/tjp/incubator-doris/be/src/vec/common/allocator.h:223
    #2 0x56510ee68520 in allocator::alloc(unsigned long, unsigned long) /mnt/ssd01/tjp/incubator-doris/be/src/vec/common/allocator.h:104

地址释放的代码栈如下:

0x60300074c420 is located 16 bytes inside of 32-byte region [0x60300074c410,0x60300074c430)
freed by thread t94 (memtableflushth) here:
    #0 0x56510e9b7868 in realloc (/mnt/ssd01/tjp/regression_test/be/lib/palo_be 0x536a868)
    #1 0x56510ee8b913 in allocator::realloc(void*, unsigned long, unsigned long, unsigned long) /mnt/ssd01/tjp/incubator-doris/be/src/vec/common/allocator.h:125
    #2 0x56510ee814bb in void doris::vectorized::podarraybase<1ul, 4096ul, allocator, 15ul, 16ul>::realloc<>(unsigned long) /mnt/ssd01/tjp/incubator-doris/be/src/vec/common/pod_array.h:147

有了详细的非法访问地址代码栈、分配代码栈、释放代码栈,问题定位就会非常容易。

说明:限于文章篇幅,示例中的栈展示不全,完整代码栈可以前往对应 issue 中进行查看。

heap buffer overflow

addresssanitizer 能够报出 heap buffer overflow 的代码栈。

比如https://github.com/apache/doris/issues/5951 里的,结合运行时生成的 core dump 文件就可以快速定位问题。

==3930==error: addresssanitizer: heap-buffer-overflow on address 0x60c000000878 at pc 0x000000ae00ce bp 0x7ffeb16aa660 sp 0x7ffeb16aa658
read of size 8 at 0x60c000000878 thread t0
    #0 0xae00cd in doris::stringfunctions::substring(doris_udf::functioncontext*, doris_udf::stringval const&, doris_udf::intval const&, doris_udf::intval const&) ../src/exprs/string_functions.cpp:98

memory leak

addresssanitizer 能够报出哪里分配的内存没有被释放,就可以快速的分析出泄露原因。

==1504733==error: leaksanitizer: detected memory leaks
direct leak of 688128 byte(s) in 168 object(s) allocated from:
#0 0x560d5db51aac in __interceptor_posix_memalign (/mnt/ssd01/doris-master/vec_asan/be/lib/doris_be 0x9227aac)
#1 0x560d5fbb3813 in doris::coredatablock::operator new(unsigned long) /home/zcp/repo_center/doris_master/be/src/util/core_local.cpp:35
#2 0x560d5fbb65ed in doris::coredataallocatorimpl<8ul>::get_or_create(unsigned long) /home/zcp/repo_center/doris_master/be/src/util/core_local.cpp:58
#3 0x560d5e71a28d in doris::corelocalvalue::corelocalvalue(long)

https://github.com/apache/doris/issues/10926

https://github.com/apache/doris/pull/3326

异常分配

分配过大的内存 addresssanitizer 会报出 oom 错误,根据栈以及 core dump 文件可以分析出何处分配了过大内存。栈举例如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n90fgxa8-1662366773751)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/06861813865e4e8d805b02a208e1ada8~tplv-k3u1fbpfcp-zoom-1.image)]

fix pr 见:https://github.com/apache/doris/pull/10289

ubsan 能够高效发现强制类型转换的错误,如下方 issue 链接中描述,它能够精确的描述出强制类型转换带来错误的代码,如果不能在第一现场发现这种错误,后续因为指针错误使用,会比较难定位。

issue:https://github.com/apache/doris/issues/9105

undefinedbehaviorsanitizer 也比 addresssanitizer 及其它的更容易发现死锁

比如:https://github.com/apache/doris/issues/10309

程序维护内存 pool 时 addresssanitizer 的使用

addresssanitizer 是编译器针对内存分配、释放、访问 生成额外代码来实现内存问题分析的,如果程序维护了自己的内存 pool,addresssanitizer 就不能发现 pool 中内存非法访问的问题。这种情况下需要做一些额外的工作来使得 addresssanitizer 尽可能工作,主要是使用 asan_poison_memory_region 和 asan_unpoison_memory_region 管理内存是否可以访问,这种方法使用比较难,因为 addresssanitizer 内部有地址对齐等的处理。出于性能以及内存释放等原因,apache doris 也维护了内存分配 pool ,这种方法不能确保 addresssanitizer 能够发现所有问题。

可以参考:https://github.com/apache/doris/pull/8148

当程序维护自己的内存池时,按照 https://github.com/apache/dorisw/pull/8148 中方法,use after free 错误会变成 use after poison。但是 use after poison 不能够给出地址失效的栈(https://github.com/google/sanitizers/issues/191),从而导致问题的定位分析仍然很困难。

因此建议程序维护的内存 pool 可以通过选项关闭,这样在测试环境就可以使用 addresssanitizer 高效地定位内存问题。

core dump 分析工具

分析 c 程序生成的 core dump 文件经常遇到的问题就是怎么打印出 stl 容器中的值以及 boost 中容器的值,有如下三个工具可以高效的查看 stl 和 boost 中容器的值。

stl-view

可以将此文件 https://github.com/dataroaring/tools/blob/main/gdb/dbinit_stl_views-1.03.txt 放置到~/.gdbinit中使用 stl-view。stl-view 输出非常友好,支持 pvector,plist,plist_member,pmap,pmap_member,pset,pdequeue,pstack,pqueue,ppqueue,pbitset,pstring,pwstring。以 apache doris 中使用 pvector 为例,它能够输出 vector 中的所有元素。

(gdb) pvector block.data
elem[0]: $5 = {
  column = {
    ::intrusive_ptr> = {
      t = 0x606000fdc820
    }, },
  type = {
    > = {
      > = {},
      members of std::__shared_ptr:
      _m_ptr = 0x6030069e9780,
      _m_refcount = {
        _m_pi = 0x6030069e9770
      }
    }, },
  name = {
    static npos = 18446744073709551615,
    _m_dataplus = {
      > = {
        <__gnu_cxx::new_allocator> = {}, },
      members of std::__cxx11::basic_string, std::allocator >::_alloc_hider:
      _m_p = 0x61400006e068 "n_nationkey"
    },
    _m_string_length = 11,
    {
      _m_local_buf = "n_nationkey\000\276\276\276\276",
      _m_allocated_capacity = 7957695015158701934
    }
  }
}
elem[1]: $6 = {
  column = {
    ::intrusive_ptr> = {
      t = 0x6080001ec220
    }, },
  type = {
  ...

pretty-printer

gcc 7.0 开始支持了 pretty-printer 打印 stl 容器,可以将以下代码放置到~/.gdbinit中使 pretty-printer 生效。

注意:/usr/share/gcc/python需要更换为本机对应的地址。

python
import sys
sys.path.insert(0, '/usr/share/gcc/python')
from libstdcxx.v6.printers import register_libstdcxx_printers
register_libstdcxx_printers (none)
end

以 vector 为例, pretty-printer 能够打印出详细内容。

(gdb) p block.data
$1 = std::vector of length 7, capacity 8 = {
  {
    column = {
      ::intrusive_ptr> = {
        t = 0x606000fdc820
      }, },
    type = std::shared_ptr (use count 1, weak count 0) = {
      get() = 0x6030069e9780
    },
    name = "n_nationkey"
  }, {
    column = {
      ::intrusive_ptr> = {
        t = 0x6080001ec220
      }, },
    type = std::shared_ptr (use count 1, weak count 0) = {
      get() = 0x6030069e9750
    },
    name = "n_name"
  }, {
    column = {
      ::intrusive_ptr> = {
        t = 0x606000fd52c0
      }, },
    type = std::shared_ptr (use count 1, weak count 0) = {
      get() = 0x6030069e9720
    },
    name = "n_regionkey"
  }, {
    column = {
      ::intrusive_ptr> = {
        t = 0x6030069e96b0
      }, },
    type = std::shared_ptr (use count 1, weak count 0) = {
      get() = 0x604000a66160
    },
    name = "n_comment"

boost pretty printer

因为 apache doris 使用 boost 不多,因此不再举例。

可以参考:https://github.com/ruediger/boost-pretty-printer

总结

有了 sanitizer 能够在单测、功能、集成、压力测试环境及时发现问题,最重要的是大多数时候都可以给出程序出问题的关联现场,比如内存分配的调用栈,释放内存的调用栈,非法访问内存的调用栈,配合 core dump 可以查看现场状态,解决 c 内存问题从猜测变成了有证据的现场分析。

网站地图