Engee 文档

用于c代码中正确垃圾回收的静态分析器注释

进行分析

分析器插件随Julia一起提供。 它的源代码可以在`src/clangsa’中找到。 要运行插件,您需要构建clang依赖项。 在Make中设置’BUILD_LLVM_CLANG’变量。户文件来构建相应的clang版本。 您可能还需要使用`USE_BINARYBUILDER_LLVM`参数使用预组装的二进制文件。

或者(或者如果这还不够)尝试使用

make -C src install-analysis-deps

来自Julia顶级目录。

之后,运行’make-C src analyzegc’命令来分析源代码树就足够了。

一般概览

由于Julia的垃圾收集是准确的,因此对于垃圾收集时可能引用的任何值都有正确的根信息是必要的。 这样的地方被称为’safepoints',在函数的本地上下文中,这个概念适用于任何可以递归终止于安全点的函数调用。

在生成的代码中,由于垃圾收集根的放置,这种情况会自动发生(请参阅面向开发人员的LLVM代码生成文档中关于垃圾收集根的章节)。 但是,在C代码中,运行时需要手动报告垃圾回收根。 这是使用以下宏完成的。

// The value assigned to any slot passed as an argument to these
// is rooted for the duration of this GC frame.
JL_GC_PUSH{1,...,6}(args...)
// The values assigned into the size `n` array `rts` are rooted
// for the duration of this GC frame.
JL_GC_PUSHARGS(rts, n)
// Pop a GC frame
JL_GC_POP

如果这些宏没有在它们应该在的地方使用,或者应用不正确,则会在没有通知的情况下发生内存损坏。 出于这个原因,在代码的所有相关位置正确添加它们是非常重要的。

为了确保正确使用这些宏,我们使用静态分析(特别是clang静态分析器)。 本文档的其余部分提供了静态分析的概述,并描述了Julia代码库工作所需的支持操作。

垃圾收集不变量

不变量的正确性由两个条件决定。

  • 所有GC_PUSH调用必须跟随相应的GC_POP调用(实际上,这是在函数级别实现的)。

  • 如果该值以前没有在任何安全点作为根,则将来不能引用它。

像往常一样,问题出在细节上。 特别是,为了满足上述条件中的第二个,有必要知道以下内容:

  • 哪些呼叫是安全点,哪些不是;

  • 哪些值是特定安全点的根值,哪些不是;

  • 值时被访问。

特别是关于第二点:有必要知道哪些内存区域在运行时将被视为根(即分配给这些区域的值将是根)。 这包括通过将它们传递给GC_PUSH宏之一而显式指定的区域、全局级别根的区域和值,以及从这些区域之一递归访问的区域。

静态分析算法

这个想法本身非常简单,尽管实现相当复杂(主要是由于C和C的许多特殊情况和微妙之处++实际上,我们跟踪所有根区域,所有可以是根的值以及所有表达式(分配,内存分配等)。)影响这些值是否为根。 然后,在任何安全点,我们执行符号垃圾收集并毒害所有未根植于该位置的值。 如果随后访问这些值,则返回错误。

Clang静态分析器的工作是构建一个状态图并检查它是否有错误源。 这个图的几个节点是由分析器自己创建的(例如,为了确保执行的顺序),但是上面的定义用我们自己的状态来补充这个图。

静态分析器是进程间的,可以分析函数边界交叉的执行顺序. 但是,静态分析器不是完全递归的,并且对哪些调用应该进行调查做出启发式决策(此外,一些调用需要在程序单元之间进行分析,并且对分析器不可见)。 在我们的情况下,确定正确性需要完整的信息。 在这方面,有必要使用分析所需的所有信息对任何函数调用的原型进行注释,即使这些信息可以作为过程间静态分析的结果获得。

幸运的是,我们可以使用这种过程间分析来确保添加到特定函数的注释是正确的,同时考虑到其实现。

分析器注释

这些注释位于src/support/analyzer_annotations中。h文件。 它们只有在使用分析器时才是活动的,并且它们扩展到无(对于原型注释)或空闲操作(对于像函数这样的注释)。

'JL_NOTSAFEPOINT`

这可能是最常见的注释。 它应该被添加到任何已知无法到达安全垃圾收集点的函数中。 通常,这些函数的安全操作只是算术计算、内存访问和具有注释`JL_NOTSAFEPOINT`或根据其他可用数据不是安全点的函数调用(例如,它可能是c标准库中的函数,在分析器中被硬编码)。

在对使用此属性注释的任何函数的调用中,值可能保持非根。

使用示例:

void jl_get_one() JL_NOTSAFEPOINT {
  return 1;
}

jl_value_t *example() {
  jl_value_t *val = jl_alloc_whatever();
  // This is valid, even though `val` is unrooted, because
  // jl_get_one is not a safepoint
  jl_get_one();
  return val;
}

JL_MAYBE_UNROOTED'/'JL_ROOTS_TEMPORARILY

当’JL_MAYBE_UNROOTED`被注释为函数参数时,这意味着即使它不是根,也可以传递此参数。 在正常的事件过程中,julia ABI确保调用者在将值传递给被调用的对象之前使值根。 但是,某些函数不遵循此ABI规则并允许传入值,即使它们不是根值。 请记住,它不会自动遵循这样的论点将持续存在。 'ROOTS_TEMPORARILY’注释提供了更可靠的保证,即该值不仅可以在传输过程中不再是根,而且还将由被调用方在内部安全点之间保留。

请注意``JL_NOTSAFEPOINT’实际上意味着’JL_MAYBE_UNROOTED`/`JL_ROOTS_TEMPORARILY',因为如果函数不包含安全点,参数是否为root并不重要。

同样重要的是要注意,这些注释在调用方和被调用方都应用。 在调用端,它们删除了对根字符的限制,这通常适用于julia ABI函数。 在被调用方,它们具有相反的效果:这样的参数不被隐含地视为根。

如果这些注释中的任何一个应用于一个函数作为一个整体,它将应用于它的所有参数。 一般来说,这只对参数数量可变的函数是必要的。

使用示例:

JL_DLLEXPORT void JL_NORETURN jl_throw(jl_value_t *e JL_MAYBE_UNROOTED);
jl_value_t *jl_alloc_error();

void example() {
  // The return value of the allocation is unrooted. This would normally
  // be an error, but is allowed because of the above annotation.
  jl_throw(jl_alloc_error());
}

`JL_PROPAGATES_ROOT'

此注释通常在返回一个对象的访问函数中找到,该对象可能是存储在另一个对象中的根。 当应用于函数参数时,此注释告诉分析器此参数的根也应用于函数返回的值。

使用示例:

jl_value_t *jl_svecref(jl_svec_t *t JL_PROPAGATES_ROOT, size_t i) JL_NOTSAFEPOINT;

size_t example(jl_svec_t *svec) {
  jl_value_t *val = jl_svecref(svec, 1)
  // This is valid, because, as annotated by the PROPAGATES_ROOT annotation,
  // jl_svecref propagates the rooted-ness from `svec` to `val`
  jl_gc_safepoint();
  return jl_unbox_long(val);
}

JL_ROOTING_ARGUMENT'/'JL_ROOTED_ARGUMENT

这本质上是’JL_PROPAGATES_ROOT’的模拟赋值。 将值分配给已为根的另一个值的字段时,分配的值将继承分配给它的值的根。

使用示例:

void jl_svecset(void *t JL_ROOTING_ARGUMENT, size_t i, void *x JL_ROOTED_ARGUMENT) JL_NOTSAFEPOINT


size_t example(jl_svec_t *svec) {
  jl_value_t *val = jl_box_long(10000);
  jl_svecset(svec, val);
  // This is valid, because the annotations imply that the
  // jl_svecset propagates the rooted-ness from `svec` to `val`
  jl_gc_safepoint();
  return jl_unbox_long(val);
}

JL_GC_DISABLED

此注释意味着只有在运行时禁用垃圾回收时才调用此函数。 此类函数最常用于启动期间和垃圾收集器代码本身。 请注意,检查此注释是否符合启用和禁用运行时的调用,因此clang将确定其正确性。 如果实际上没有禁用垃圾收集,这不是禁用某个函数处理的最佳方法(如有必要,使用`ifdef__clang_analyzer__`)。

使用示例:

void jl_do_magic() JL_GC_DISABLED {
  // Wildly allocate here with no regard for roots
}

void example() {
  int en = jl_gc_enable(0);
  jl_do_magic();
  jl_gc_enable(en);
}

JL_REQUIRE_ROOTED_SLOT

此注释要求调用方在作为根的槽中传递值(即,分配给此槽的值将是根)。

使用示例:

void jl_do_processing(jl_value_t **slot JL_REQUIRE_ROOTED_SLOT) {
  *slot = jl_box_long(1);
  // Ok, only, because the slot was annotated as rooting
  jl_gc_safepoint();
}

void example() {
  jl_value_t *slot = NULL;
  JL_GC_PUSH1(&slot;);
  jl_do_processing(&slot;);
  JL_GC_POP();
}

'JL_GLOBALLY_ROOTED`

此注释意味着此值始终是全局级别的根值。 它可以应用于全局变量的声明(在这种情况下,它适用于这些变量的值或数组的值,如果声明了数组)或函数(在这种情况下,它适用于它们的返回值;例如,这些函数可以是返回全局级别根的私有值的函数)。

使用示例:

extern JL_DLLEXPORT jl_datatype_t *jl_any_type JL_GLOBALLY_ROOTED;
jl_ast_context_t *jl_ast_ctx(fl_context_t *fl) JL_GLOBALLY_ROOTED;

JL_ALWAYS_LEAFTYPE

此注释本质上等同于`JL_GLOBALLY_ROOTED',除了只有当值在全局级别为根时才应使用它,因为它们属于最终类型。 将有限类型定义为根类型有其自身的特点。 它们通常通过相应的`TypeName`对象的`cache`字段被制成root,该对象本身通过包含模块被制成root(也就是说,只要对于包含模块来说这是真的,它们就会成为root)。 在一般情况下,我们可以假设有限类型是使用它们的根类型,但将来可能会澄清此属性,因此单独的注释有助于在全局级别证实根字符的原因。

分析器还会自动检测类型的最终性质的检查,并且不会警告沿着这些路径没有垃圾收集根。

JL_DLLEXPORT jl_value_t *jl_apply_array_type(jl_value_t *type, size_t dim) JL_ALWAYS_LEAFTYPE;

JL_GC_PROMISE_ROOTED

这是一个像函数一样的注释。 传递给此注释的任何值都将被视为当前函数范围内的根值。 它适用于分析仪操作不正确或困难的情况。 但是,它不应该被滥用-最好是优化分析器本身。

void example() {
  jl_value_t *val = jl_alloc_something();
  if (some_condition) {
    // We happen to know for complicated external reasons
    // that val is rooted under these conditions
    JL_GC_PROMISE_ROOTED(val);
  }
}

分析的完整性

分析器只搜索本地信息。 特别是,在上面的’PROPAGATES_ROOT`的情况下,它假设它看到所有相关的内存变化,即内存在被调用的函数(如果在分析过程中没有考虑到它们)和并行线程中没有变化。 出于这个原因,可能没有考虑到一些有问题的情况,尽管在实践中这种平行的变化是相当罕见的。 优化分析仪以复盖这种情况可能是未来一项有趣的工作。