实时安全分析器

简介

实时安全分析器 (也称为 RTSan) 是一款用于 C 和 C++ 项目的实时安全测试工具。RTSan 可用于检测实时违规,即调用在具有确定性运行时间要求的函数中不安全的那些方法。RTSan 将使用 [[clang::nonblocking]] 属性标记的任何函数视为实时函数。在运行时,如果 RTSan 检测到对 mallocfreepthread_mutex_lock 或任何其他已知在标记为 [[clang::nonblocking]] 的函数中具有非确定性执行时间的函数的调用,它将引发错误。

RTSan 在运行时执行其分析,但与 函数效果分析 系统共享 [[clang::nonblocking]] 属性,该系统在编译时运行以检测潜在的实时安全违规。为了全面检测实时安全问题,建议将这两个系统结合使用。

实时安全分析器引入的运行时减速可以忽略不计。

构建方法

使用 CMake 构建 LLVM/Clang,并启用 compiler-rt 运行时。一个允许使用/测试实时安全分析器的 CMake 配置示例

$ cmake -DCMAKE_BUILD_TYPE=Release -DLLVM_ENABLE_PROJECTS="clang" -DLLVM_ENABLE_RUNTIMES="compiler-rt" <path to source>/llvm

用法

有两个要求

  1. 代码必须使用 -fsanitize=realtime 标志进行编译。

  2. 受实时约束的函数必须使用 [[clang::nonblocking]] 属性标记。

通常,应将这些属性添加到充当具有实时优先级的线程的入口点的函数上。这些线程受固定的回调时间约束,例如音频回调线程或视频游戏代码中的渲染循环。

% cat example_realtime_violation.cpp
#include <vector>

void violation() [[clang::nonblocking]]{
  std::vector<float> v;
  v.resize(100);
}

int main() {
  violation();
  return 0;
}
# Compile and link
% clang++ -fsanitize=realtime example_realtime_violation.cpp

如果在 [[clang::nonblocking]] 上下文中或该函数调用的任何函数中检测到实时安全违规,程序将以非零退出代码退出。

% clang++ -fsanitize=realtime example_realtime_violation.cpp
% ./a.out
==76290==ERROR: RealtimeSanitizer: unsafe-library-call
Intercepted call to real-time unsafe function `malloc` in real-time context!
    #0 0x000102a7b884 in malloc rtsan_interceptors.cpp:426
    #1 0x00019c326bd0 in operator new(unsigned long)+0x1c (libc++abi.dylib:arm64+0x16bd0)
    #2 0xa30d0001024f79a8  (<unknown module>)
    #3 0x0001024f794c in std::__1::__libcpp_allocate[abi:ne200000](unsigned long, unsigned long)+0x44
    #4 0x0001024f78c4 in std::__1::allocator<float>::allocate[abi:ne200000](unsigned long)+0x44
    ... snip ...
    #9 0x0001024f6868 in std::__1::vector<float, std::__1::allocator<float>>::resize(unsigned long)+0x48
    #10 0x0001024f67b4 in violation()+0x24
    #11 0x0001024f68f0 in main+0x18 (a.out:arm64+0x1000028f0)
    #12 0x00019bfe3150  (<unknown module>)
    #13 0xed5efffffffffffc  (<unknown module>)

阻塞函数

对系统库函数(如 malloc)的调用会自动被实时安全分析器捕获。实时程序员还可以编写他们希望实时安全分析器了解的自己的阻塞(实时不安全)函数。如果在 [[clang::nonblocking]] 上下文中调用了使用 [[clang::blocking]] 属性标记的任何函数,实时安全分析器将在运行时引发错误。

$ cat example_blocking_violation.cpp
#include <atomic>
#include <thread>

std::atomic<bool> has_permission{false};

int wait_for_permission() [[clang::blocking]] {
  while (has_permission.load() == false)
    std::this_thread::yield();
  return 0;
}

int real_time_function() [[clang::nonblocking]] {
  return wait_for_permission();
}

int main() {
  return real_time_function();
}

$ clang++ -fsanitize=realtime example_blocking_violation.cpp && ./a.out
==76131==ERROR: RealtimeSanitizer: blocking-call
Call to blocking function `wait_for_permission()` in real-time context!
    #0 0x0001000c3db0 in wait_for_permission()+0x10 (a.out:arm64+0x100003db0)
    #1 0x0001000c3e3c in real_time_function()+0x10 (a.out:arm64+0x100003e3c)
    #2 0x0001000c3e68 in main+0x10 (a.out:arm64+0x100003e68)
    #3 0x00019bfe3150  (<unknown module>)
    #4 0x5a27fffffffffffc  (<unknown module>)

运行时标志

实时安全分析器支持多个运行时标志,这些标志可以在 RTSAN_OPTIONS 环境变量中指定

% RTSAN_OPTIONS=option_1=true:path_option_2="/some/file.txt" ./a.out
...

或者在编译时通过提供符号 __rtsan_default_options

__attribute__((__visibility__("default")))
extern "C" const char *__rtsan_default_options() {
  return "symbolize=false:abort_on_error=0:log_to_syslog=0";
}

可以使用 help 标志查看所有安全分析器选项(其中一些不受支持)

% RTSAN_OPTIONS=help=true ./a.out

实时安全分析器尊重的部分标志列表

运行时标志

标志名称

默认值

类型

简短描述

halt_on_error

true

布尔值

在第一个报告的错误后退出。如果为 false(在检测到错误后继续),则对错误堆栈进行重复数据删除,因此错误仅显示一次。

print_stats_on_exit

false

布尔值

在退出时打印统计信息。包括总错误和唯一错误。

color

"auto"

字符串

为报告着色:(always|never|auto)。

fast_unwind_on_fatal

false

布尔值

如果可用,则在检测到错误时使用基于快速帧指针的展开器。如果为 true,请确保被测代码已使用帧指针编译,使用 -fno-omit-frame-pointers 或类似方法。

abort_on_error

操作系统相关

布尔值

如果为 true,则该工具在打印错误报告后调用 abort() 而不是 _exit()。在某些操作系统(例如 OSX)上,这非常有用,因为在崩溃时会发出更好的堆栈跟踪。

symbolize

true

布尔值

如果设置,则使用符号化器将虚拟地址转换为文件/行位置。如果为 false,可以极大地加快错误报告速度。

suppressions

“”

路径

如果设置为有效的抑制文件,则将抑制问题报告。有关详细信息,请参阅下面的“禁用”。

可以使用 verbosity=$NUM 标志调试与标志相关的某些问题

% RTSAN_OPTIONS=verbosity=1:misspelled_flag=true ./a.out
WARNING: found 1 unrecognized flag(s):
misspelled_flag
...

禁用和抑制

使用实时安全分析器时,有多种方法可以禁用错误报告。

一般来说,ScopedDisabler 应该是首选,因为它性能最高。

抑制方法

方法

在何处指定?

范围

运行时成本

描述

作用域禁用器

编译时

堆栈

非常低

ScopedDisabler 对象的生存期内,将忽略违规。

function-name-matches 抑制

运行时

单个函数

中等

通过名称抑制拦截和 [[clang::blocking]] 函数调用。

call-stack-contains 抑制

运行时

堆栈

抑制任何包含指定模式的堆栈跟踪。

ScopedDisabler

在编译时,可以使用 __rtsan::ScopedDisabler 禁用实时安全分析器。RTSan 会忽略源于 ScopedDisabler 实例变量作用域内的任何错误。

#include <sanitizer/rtsan_interface.h>

void process(const std::vector<float>& buffer) [[clang::nonblocking]] {
    {
        __rtsan::ScopedDisabler d;
        ...
    }
}

如果在编译时未启用实时安全分析器(即代码未使用 -fsanitize=realtime 标志编译),则 ScopedDisabler 将被编译为无操作。

在 C 中,可以使用 __rtsan_disable()rtsan_enable() 函数手动禁用和重新启用实时安全分析器检查。

#include <sanitizer/rtsan_interface.h>

int process(const float* buffer) [[clang::nonblocking]]
{
    {
        __rtsan_disable();

        ...

        __rtsan_enable();
    }
}

__rtsan_disable() 的每次调用都必须与随后的对 __rtsan_enable() 的调用配对,以恢复正常的安全分析器功能。如果未进行相应的 rtsan_enable() 调用,则行为未定义。

抑制文件

在运行时,可以使用 RTSAN_OPTIONS 中传递的抑制文件指定抑制。如果无法更改源代码,则运行时抑制可能很有用。

> cat suppressions.supp
call-stack-contains:MallocViolation
call-stack-contains:std::*vector
function-name-matches:free
function-name-matches:CustomMarkedBlocking*
> RTSAN_OPTIONS="suppressions=suppressions.supp" ./a.out
...

在此文件中指定的抑制有两种形式。

function-name-matches 会抑制对任何拦截的库调用或通过名称标记为 [[clang::blocking]] 的函数的报告。例如,如果您知道 malloc 在您的系统上是实时安全的,则可以通过 function-name-matches:malloc 禁用对它的检查。

call-stack-contains 会抑制对任何包含与指定模式匹配的字符串的堆栈中的错误的报告。例如,抑制对 std::vector 中任何非实时安全行为的错误报告可以通过 call-stack-contains:std::*vector 指定。您必须在构建中包含符号才能使此方法有效,因为未符号化的堆栈跟踪无法匹配。call-stack-contains 具有所有抑制方法中最高的运行时成本。

模式可以是完全匹配,也可以是“简易正则表达式”模式,其中包含特殊字符,例如 ^$*

可以使用 print_stats_on_exit 标志在退出时查看通过此方法抑制的潜在错误数量。

编译时安全分析器检测

Clang 提供了预处理器宏 __has_feature,它可用于检测在编译时是否启用了实时安全分析器。

#if defined(__has_feature) && __has_feature(realtime_sanitizer)
...
#endif