AnyMath 文档

C代码中GC正确性的静态分析器注释

运行分析

驱动分析的分析器插件附带julia。 它的源代码可以在 src/clangsa. 运行它需要构建clang依赖项。 设置 BUILD_LLVM_CLANG 你的产品是可变的。用户为了建立一个适当的版本的clang. 您可能还想使用预构建的二进制文件,使用 USE_BINARYBUILDER_LLVM 选择。

或者(或者如果这些还不够),请尝试

make -C src install-analysis-deps

从朱莉娅的顶级目录。

然后,在源树上运行分析就像运行一样简单 make-C src分析仪.

一般概览

由于Julia的GC是精确的,因此它需要为任何可能在GC可能发生的任何时间引用的任何值维护正确的生根信息。 这些地方被称为 安全点 在函数本地上下文中,我们将此指定扩展到可能以递归方式结束在安全点的任何函数调用。

在生成的代码中,这由GC根放置传递自动处理(请参阅LLVM codegen devdocs中关于GC生根的章节)。 但是,在C代码中,我们需要手动通知运行时任何GC根。 这是使用以下宏完成的:

// 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不变量

有两个简单的不变量正确性:

*全部 GC_PUSH 电话后面需要一个适当的 GC_POP (实际上,我们在功能级别执行此操作) *如果一个值以前没有根植于任何安全点,那么以后可能不再引用它

当然,魔鬼在这里的细节。 特别是要满足上述条件中的第二个,我们需要知道:

*哪些电话是安全点,哪些不是 *哪些值根植于任何给定的安全点,哪些不是 *何时引用值

特别是对于第二点,我们需要知道哪些内存位置将在运行时被视为生根(即分配给这些位置的值是生根的)。 这包括通过将它们传递给 GC_PUSH 宏,全局根的位置和值,以及从这些位置之一递归可达的任何位置。

静态分析算法

这个想法本身非常简单,虽然实现相当多的复杂(主要是由于大量的特殊情况和复杂的C和C++). 本质上,我们跟踪所有生根的位置,所有可根的值以及任何表达式(赋值,分配等)都会影响任何可根值的根性。 然后,在任何安全点,我们执行"符号GC"并毒害任何未根植于所述位置的值。 如果这些值后来被引用,我们会发出错误。

Clang静态分析器的工作原理是构建一个状态图并探索这个图以寻找错误的来源。 这个图中的几个节点是由分析器本身生成的(例如用于控制流),但是上面的定义用我们自己的状态来增加这个图。

静态分析器是进程间的,可以跨函数边界分析控制流。 但是,静态分析器不是完全递归的,并且会对要探索的调用做出启发式决策(此外,一些调用是交叉转换单元,对分析器不可见)。 在我们的例子中,我们对正确性的定义需要全部信息。 因此,我们需要使用分析所需的任何信息来注释所有函数调用的原型,即使这些信息可以通过进程间静态分析获得。

然而,幸运的是,我们仍然可以使用这种过程间分析来确保我们在给定函数上放置的注释在给定函数的实现下确实是正确的。

分析器注释

这些注释在src/support/analyter_annotations中找到。h.只有当分析器被使用时,它们才是活动的,并且扩展到nothing(对于原型注释)或no-ops(对于像注释这样的函数)。

JL_NOTSAFEPOINT

这可能是最常见的注释,应该放在任何已知不会导致到达GC安全点的函数上。 一般来说,只有这样的函数执行算术,内存访问和对带注释的函数的调用才是安全的 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_无根/JL_ROOTS_TEMPORARILY

何时 JL_MAYBE_无根 被注释为函数上的参数,表示可以传递所述参数,即使它没有根。 在正常的事件过程中,julia ABI保证调用者在将其传递给callees之前根值。 但是,某些函数不遵循此ABI并允许将值传递给它们,即使它们没有根。 但是请注意,这并不自动意味着上述参数将被保留。 该 根/根 annotation提供了更强的保证,不仅可以在传递时取消引用该值,还可以在被调用方的任何内部安全点中保留该值。

请注意 JL_NOTSAFEPOINT 本质上暗示 JL_MAYBE_无根/JL_ROOTS_TEMPORARILY,因为如果函数不包含安全点,参数的根性是无关紧要的。

另外需要注意的一点是,这些注释适用于调用方和被调用方。 在调用方方面,它们解除了julia ABI函数通常需要的根性限制。 在被调用方方面,它们具有防止这些参数被视为隐式根的反向效果。

如果这些注释中的任何一个作为一个整体应用于函数,则它将应用于函数的所有参数。 这通常只应该是varargs函数所必需的。

使用示例:

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_目标

这基本上是与…​…​相对应的赋值。 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

此注释意味着此函数仅在gc运行时禁用的情况下调用。 这种函数在启动期间和GC代码本身中最常遇到。 请注意,此注释是针对运行时启用/禁用调用进行检查的,因此clang将知道您是否撒谎。 如果GC实际上没有被禁用,这不是禁用给定函数处理的好方法(使用 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,除了只有当这些值由于是leaftype而全局根植时,才应该使用is。 叶型的生根有点复杂。 它们通常是通过 缓存 对应的字段 类型名称,它本身是由包含模块根植的(所以只要包含模块是可以的,它们就根植),我们通常可以假设leaftypes是在使用它们的地方根植的,但是我们可能会在将来改进这个属性,所以单独的注释有助于区分全局根植的原因。

分析器还会自动检测leaftype-ness的检查,并且不会抱怨这些路径上缺少GC根。

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);
  }
}

分析的完整性

分析器只查看本地信息。 特别是,例如在 传播_根 在上面的情况下,它假设这样的内存只以它可以看到的方式被修改,而不是在任何被调用的函数中(除非它碰巧决定在其分析中考虑它们),而不是在任何并发运行的线程中。 因此,它可能会错过一些有问题的情况,尽管在实践中这种并发修改是相当罕见的。 改进分析器以处理更多此类情况可能是未来工作的有趣主题。