使用 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 工具可用,包括 unflatten
和 dot
。如果您更喜欢使用其他工具渲染或探索图形,那么也应该可行。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 跟踪信息中遇到的频率。