使用 XRay 进行调试

本文档展示了一个分析使用 XRay 检测构建的应用程序的示例。在这里,我们将尝试调试 llc 编译 Clang 生成的某些示例 LLVM IR。

使用 XRay 构建

要使用 XRay 检测调试应用程序,我们需要使用支持 -fxray-instrument 选项的 Clang 构建它。有关 XRay 工作原理的更多技术细节,请参阅 XRay 以获取背景信息。

在我们的示例中,我们需要将 -fxray-instrument 添加到构建二进制文件时传递给 Clang 的标志列表中。请注意,我们还需要链接 Clang 以使 XRay 运行时正确链接。对于使用 XRay 构建 llc,我们对 LLVM 构建执行类似的操作,如下所示

$ mkdir -p llvm-build && cd llvm-build
# Assume that the LLVM sources are at ../llvm
$ cmake -GNinja ../llvm -DCMAKE_BUILD_TYPE=Release \
    -DCMAKE_C_FLAGS_RELEASE="-fxray-instrument" -DCMAKE_CXX_FLAGS="-fxray-instrument" \
# Once this finishes, we should build llc
$ ninja llc

要验证我们是否拥有 XRay 检测的二进制文件,我们可以使用 objdump 来查找 xray_instr_map 部分。

$ objdump -h -j xray_instr_map ./bin/llc
./bin/llc:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
 14 xray_instr_map 00002fc0  00000000041516c6  00000000041516c6  03d516c6  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA

获取跟踪信息

默认情况下,XRay 不会在主函数开始之前写入跟踪文件或修补应用程序。如果我们运行 llc,它应该像正常构建的二进制文件一样工作。如果我们想获取应用程序操作的完整跟踪信息(我们最终使用 XRay 检测的函数),则需要在应用程序启动时启用 XRay。为此,XRay 检查 XRAY_OPTIONS 环境变量。

# The following doesn't create an XRay trace by default.
$ ./bin/llc input.ll

# We need to set the XRAY_OPTIONS to enable some features.
$ XRAY_OPTIONS="patch_premain=true xray_mode=xray-basic verbosity=1" ./bin/llc input.ll
==69819==XRay: Log file in 'xray-log.llc.m35qPB'

此时,我们拥有了一个可以开始分析的 XRay 跟踪信息。

llvm-xray 工具

有了跟踪信息,我们就可以对已检测的函数进行基本统计,以及了解我们在代码部分花费了多少时间。为了理解这些数据,我们使用 llvm-xray 工具,该工具有一些子命令可以帮助我们理解跟踪信息。

我们可以做的一件事是获取已检测函数的统计信息。我们可以使用 llvm-xray account 查看统计示例。

$ llvm-xray account xray-log.llc.m35qPB --top=10 --sort=sum --sortorder=dsc --instr_map=./bin/llc
Functions with latencies: 29
   funcid      count [      min,       med,       90p,       99p,       max]       sum  function
      187        360 [ 0.000000,  0.000001,  0.000014,  0.000032,  0.000075]  0.001596  LLLexer.cpp:446:0: llvm::LLLexer::LexIdentifier()
       85        130 [ 0.000000,  0.000000,  0.000018,  0.000023,  0.000156]  0.000799  X86ISelDAGToDAG.cpp:1984:0: (anonymous namespace)::X86DAGToDAGISel::Select(llvm::SDNode*)
      138        130 [ 0.000000,  0.000000,  0.000017,  0.000155,  0.000155]  0.000774  SelectionDAGISel.cpp:2963:0: llvm::SelectionDAGISel::SelectCodeCommon(llvm::SDNode*, unsigned char const*, unsigned int)
      188        103 [ 0.000000,  0.000000,  0.000003,  0.000123,  0.000214]  0.000737  LLParser.cpp:2692:0: llvm::LLParser::ParseValID(llvm::ValID&, llvm::LLParser::PerFunctionState*)
       88          1 [ 0.000562,  0.000562,  0.000562,  0.000562,  0.000562]  0.000562  X86ISelLowering.cpp:83:0: llvm::X86TargetLowering::X86TargetLowering(llvm::X86TargetMachine const&, llvm::X86Subtarget const&)
      125        102 [ 0.000001,  0.000003,  0.000010,  0.000017,  0.000049]  0.000471  Verifier.cpp:3714:0: (anonymous namespace)::Verifier::visitInstruction(llvm::Instruction&)
       90          8 [ 0.000023,  0.000035,  0.000106,  0.000106,  0.000106]  0.000342  X86ISelLowering.cpp:3363:0: llvm::X86TargetLowering::LowerCall(llvm::TargetLowering::CallLoweringInfo&, llvm::SmallVectorImpl<llvm::SDValue>&) const
      124         32 [ 0.000003,  0.000007,  0.000016,  0.000041,  0.000041]  0.000310  Verifier.cpp:1967:0: (anonymous namespace)::Verifier::visitFunction(llvm::Function const&)
      123          1 [ 0.000302,  0.000302,  0.000302,  0.000302,  0.000302]  0.000302  LLVMContextImpl.cpp:54:0: llvm::LLVMContextImpl::~LLVMContextImpl()
      139         46 [ 0.000000,  0.000002,  0.000006,  0.000008,  0.000019]  0.000138  TargetLowering.cpp:506:0: llvm::TargetLowering::SimplifyDemandedBits(llvm::SDValue, llvm::APInt const&, llvm::APInt&, llvm::APInt&, llvm::TargetLowering::TargetLoweringOpt&, unsigned int, bool) const

这向我们展示了对于我们的输入文件,llc 在词法分析器中花费了最多的累积时间(总共 1 毫秒)。例如,如果我们想在电子表格中使用这些数据,我们可以使用该命令的 -format=csv 选项将结果输出为 CSV,以进行进一步分析。

如果我们想获取原始跟踪信息的文本表示,可以使用 llvm-xray convert 工具获取 YAML 输出。对于示例跟踪信息,输出的前几行如下所示

$ llvm-xray convert -f yaml --symbolize --instr_map=./bin/llc xray-log.llc.m35qPB
---
header:
  version:         1
  type:            0
  constant-tsc:    true
  nonstop-tsc:     true
  cycle-frequency: 2601000000
records:
  - { type: 0, func-id: 110, function: __cxx_global_var_init.8, cpu: 37, thread: 69819, kind: function-enter, tsc: 5434426023268520 }
  - { type: 0, func-id: 110, function: __cxx_global_var_init.8, cpu: 37, thread: 69819, kind: function-exit, tsc: 5434426023523052 }
  - { type: 0, func-id: 164, function: __cxx_global_var_init, cpu: 37, thread: 69819, kind: function-enter, tsc: 5434426029925386 }
  - { type: 0, func-id: 164, function: __cxx_global_var_init, cpu: 37, thread: 69819, kind: function-exit, tsc: 5434426030031128 }
  - { type: 0, func-id: 142, function: '(anonymous namespace)::CommandLineParser::ParseCommandLineOptions(int, char const* const*, llvm::StringRef, llvm::raw_ostream*)', cpu: 37, thread: 69819, kind: function-enter, tsc: 5434426046951388 }
  - { type: 0, func-id: 142, function: '(anonymous namespace)::CommandLineParser::ParseCommandLineOptions(int, char const* const*, llvm::StringRef, llvm::raw_ostream*)', cpu: 37, thread: 69819, kind: function-exit, tsc: 5434426047282020 }
  - { type: 0, func-id: 187, function: 'llvm::LLLexer::LexIdentifier()', cpu: 37, thread: 69819, kind: function-enter, tsc: 5434426047857332 }
  - { type: 0, func-id: 187, function: 'llvm::LLLexer::LexIdentifier()', cpu: 37, thread: 69819, kind: function-exit, tsc: 5434426047984152 }
  - { type: 0, func-id: 187, function: 'llvm::LLLexer::LexIdentifier()', cpu: 37, thread: 69819, kind: function-enter, tsc: 5434426048036584 }
  - { type: 0, func-id: 187, function: 'llvm::LLLexer::LexIdentifier()', cpu: 37, thread: 69819, kind: function-exit, tsc: 5434426048042292 }
  - { type: 0, func-id: 187, function: 'llvm::LLLexer::LexIdentifier()', cpu: 37, thread: 69819, kind: function-enter, tsc: 5434426048055056 }
  - { type: 0, func-id: 187, function: 'llvm::LLLexer::LexIdentifier()', cpu: 37, thread: 69819, kind: function-exit, tsc: 5434426048067316 }

控制保真度

到目前为止,在我们的示例中,我们还没有获得二进制文件中所有函数的完整覆盖范围。要获得完整覆盖范围,我们需要修改编译器标志,以便我们可以检测更多(如果不是全部)二进制文件中的函数。我们有两种方法可以做到这一点,我们将在下面探讨这两种方法。

指令阈值

第一种“粗暴”的方法是将函数体的最小阈值设置为 1。我们可以在构建二进制文件时使用 -fxray-instruction-threshold=N 标志来实现。我们使用此选项重新构建 llc 并观察结果

$ rm CMakeCache.txt
$ cmake -GNinja ../llvm -DCMAKE_BUILD_TYPE=Release \
    -DCMAKE_C_FLAGS_RELEASE="-fxray-instrument -fxray-instruction-threshold=1" \
    -DCMAKE_CXX_FLAGS="-fxray-instrument -fxray-instruction-threshold=1"
$ ninja llc
$ XRAY_OPTIONS="patch_premain=true" ./bin/llc input.ll
==69819==XRay: Log file in 'xray-log.llc.5rqxkU'

$ llvm-xray account xray-log.llc.5rqxkU --top=10 --sort=sum --sortorder=dsc --instr_map=./bin/llc
Functions with latencies: 36652
 funcid      count [      min,       med,       90p,       99p,       max]       sum  function
     75          1 [ 0.672368,  0.672368,  0.672368,  0.672368,  0.672368]  0.672368  llc.cpp:271:0: main
     78          1 [ 0.626455,  0.626455,  0.626455,  0.626455,  0.626455]  0.626455  llc.cpp:381:0: compileModule(char**, llvm::LLVMContext&)
 139617          1 [ 0.472618,  0.472618,  0.472618,  0.472618,  0.472618]  0.472618  LegacyPassManager.cpp:1723:0: llvm::legacy::PassManager::run(llvm::Module&)
 139610          1 [ 0.472618,  0.472618,  0.472618,  0.472618,  0.472618]  0.472618  LegacyPassManager.cpp:1681:0: llvm::legacy::PassManagerImpl::run(llvm::Module&)
 139612          1 [ 0.470948,  0.470948,  0.470948,  0.470948,  0.470948]  0.470948  LegacyPassManager.cpp:1564:0: (anonymous namespace)::MPPassManager::runOnModule(llvm::Module&)
 139607          2 [ 0.147345,  0.315994,  0.315994,  0.315994,  0.315994]  0.463340  LegacyPassManager.cpp:1530:0: llvm::FPPassManager::runOnModule(llvm::Module&)
 139605         21 [ 0.000002,  0.000002,  0.102593,  0.213336,  0.213336]  0.463331  LegacyPassManager.cpp:1491:0: llvm::FPPassManager::runOnFunction(llvm::Function&)
 139563      26096 [ 0.000002,  0.000002,  0.000037,  0.000063,  0.000215]  0.225708  LegacyPassManager.cpp:1083:0: llvm::PMDataManager::findAnalysisPass(void const*, bool)
 108055        188 [ 0.000002,  0.000120,  0.001375,  0.004523,  0.062624]  0.159279  MachineFunctionPass.cpp:38:0: llvm::MachineFunctionPass::runOnFunction(llvm::Function&)
  62635         22 [ 0.000041,  0.000046,  0.000050,  0.126744,  0.126744]  0.127715  X86TargetMachine.cpp:242:0: llvm::X86TargetMachine::getSubtargetImpl(llvm::Function const&) const

检测属性

另一种方法是使用配置文件来选择编译器应始终检测哪些函数。这使我们能够确保某些函数始终或从不检测,而不必向源代码添加属性。

要使用此功能,您可以为始终检测的函数定义一个文件,为从不检测的函数定义另一个文件。这些文件的格式与控制清理程序实现的类似内容的 SanitizerLists 文件完全相同。例如

# xray-attr-list.txt
# always instrument functions that match the following filters:
[always]
fun:main

# never instrument functions that match the following filters:
[never]
fun:__cxx_*

给定以上文件,我们可以通过将其提供给 Clang 的 -fxray-attr-list= 标志来重新构建。您可以拥有多个文件,每个文件定义不同的属性集,这些属性集可以由 Clang 合并到一个列表中。

XRay 堆栈工具

给定一个跟踪信息和一个检测映射(可选),可以使用 llvm-xray stack 命令分析从函数调用时间线构建的调用堆栈图。

使用该命令的方法是按调用次数和花费的时间输出前几个堆栈。

$ llvm-xray stack xray-log.llc.5rqxkU --instr_map=./bin/llc

Unique Stacks: 3069
Top 10 Stacks by leaf sum:

Sum: 9633790
lvl   function                                                            count              sum
#0    main                                                                    1         58421550
#1    compileModule(char**, llvm::LLVMContext&)                               1         51440360
#2    llvm::legacy::PassManagerImpl::run(llvm::Module&)                       1         40535375
#3    llvm::FPPassManager::runOnModule(llvm::Module&)                         2         39337525
#4    llvm::FPPassManager::runOnFunction(llvm::Function&)                     6         39331465
#5    llvm::PMDataManager::verifyPreservedAnalysis(llvm::Pass*)             399         16628590
#6    llvm::PMTopLevelManager::findAnalysisPass(void const*)               4584         15155600
#7    llvm::PMDataManager::findAnalysisPass(void const*, bool)            32088          9633790

..etc..

在默认模式下,不同线程上的相同堆栈会独立聚合。在多线程程序中,您最终可能会发现相同的调用堆栈填充了您的前几个调用列表。

为了解决这个问题,您可以指定 --aggregate-threads--per-thread-stacks 标志。--per-thread-stacks 将线程 ID 视为每个调用堆栈树中的隐式根,而 --aggregate-threads 将来自所有线程的相同堆栈合并。

火焰图生成

llvm-xray stack 工具也可用于生成火焰图以可视化您检测的调用。该工具本身不会生成图形,而是生成一种可以使用 Brendan Gregg 的 FlameGraph 工具(目前可在 github 上获得)的格式。

要为火焰图生成输出,需要一些其他选项。

  • --all-stacks - 发出所有堆栈。

  • --stack-format - 选择火焰图输出格式“flame”。

  • --aggregation-type - 选择要绘制的指标。

您可以将命令输出直接管道到火焰图工具以获得 svg 文件。

$ llvm-xray stack xray-log.llc.5rqxkU --instr_map=./bin/llc --stack-format=flame --aggregation-type=time --all-stacks | \
/path/to/FlameGraph/flamegraph.pl > flamegraph.svg

如果在浏览器中打开 svg,鼠标事件允许探索调用堆栈。

Chrome 跟踪查看器可视化

我们还可以生成一个可以由 Chrome 跟踪查看器从相同生成的跟踪信息中加载的跟踪信息。

$ llvm-xray convert --symbolize --instr_map=./bin/llc \
  --output-format=trace_event xray-log.llc.5rqxkU \
    | gzip > llc-trace.txt.gz

在 Chrome 浏览器中,导航到 chrome:///tracing 允许我们加载 sample-trace.txt.gz 文件以可视化执行跟踪信息。

进一步探索

llvm-xray 工具有一些其他子命令处于不同的开发阶段。一个有趣的子命令可以突出一些有趣的事物是 graph 子命令。例如,给定以下我们使用 XRay 检测构建的玩具程序,我们可以看到生成的图形可能是应用程序时间消耗的指示器。

// sample.cc
#include <iostream>
#include <thread>

[[clang::xray_always_instrument]] void f() {
  std::cerr << '.';
}

[[clang::xray_always_instrument]] void g() {
  for (int i = 0; i < 1 << 10; ++i) {
    std::cerr << '-';
  }
}

int main(int argc, char* argv[]) {
  std::thread t1([] {
    for (int i = 0; i < 1 << 10; ++i)
      f();
  });
  std::thread t2([] {
    g();
  });
  t1.join();
  t2.join();
  std::cerr << '\n';
}

然后我们使用 XRay 检测构建上述程序。

$ clang++ -o sample -O3 sample.cc -std=c++11 -fxray-instrument -fxray-instruction-threshold=1
$ XRAY_OPTIONS="patch_premain=true xray_mode=xray-basic" ./sample

然后,我们可以探索此示例应用程序生成的跟踪信息的图形渲染。我们假设您的系统中有 graphviz 工具可用,包括 unflattendot。如果您更喜欢使用其他工具渲染或探索图形,那么也应该可行。llvm-xray graph 将创建 DOT 格式的图形,这些图形应该可以在大多数图形渲染应用程序中使用。llvm-xray graph 命令的一个示例调用应该会对 C++ 应用程序的工作原理提供一些有趣的见解

$ llvm-xray graph xray-log.sample.* -m sample --color-edges=sum --edge-label=sum \
    | unflatten -f -l10 | dot -Tsvg -o sample.svg

后续步骤

如果您想将一些有趣的分析作为 llvm-xray 工具的一部分实现,请随时在 llvm-dev@ 邮件列表中提出建议。以下是一些想法,可以激发您参与并潜在地改进。

  • 实现一个查询/过滤库,允许查找 XRay 跟踪信息中的模式。

  • 收集函数调用堆栈以及在 XRay 跟踪信息中遇到的频率。