使用 C++ 动态追踪 C++ 应用
动态追踪是深入研究复杂在线软件系统细节的一个重要工具。它使我们能够在不需要对软件本身进行任何修改的情况下,准确定位这些系统中各种问题的根本原因。在 OpenResty XRay 中,我们的高级工具包涵盖了与 C 兼容的 Y 语言(Ylang),它可以有效地使用标准 C 代码来追踪目标 C 程序。一旦通过我们的 ylang
优化编译器编译了 Ylang 工具套件,就可以生成与 eBPF+1、Stap+ 2 或 GDB 兼容的 动态追踪 分析器。然而,在处理现实世界中非常普遍的 C++ 应用时,包括广泛使用的开源软件如 HotSpot JVM 和 NodeJS,却存在一定的空白。
为了填补这一空白,我们开发了一个自定义的 C++ 编译器,名为 Y++
,目的是生成 Ylang 代码或 GNU C 的超集。值得注意的是,Y++
已经支持了几乎整个 C++23 语言标准。本文将使用很简单的示例演示 Y++
的一些功能。
设置目标 C++ 程序
我们将使用一个简单但完整的 C++ 程序作为我们的追踪目标。该程序在其全局变量 int_vec
中利用了 STL 容器 std::vector
。
/* target.cxx:用于追踪的示例 C++ 程序 */
#include <vector>
#include <cstdio>
std::vector<int> int_vec;
int main(void) {
int sum = 0;
std::vector<int>::iterator it;
for (int i = 1; i <= 3; i++) {
int_vec.push_back(i * 2);
}
for (it = int_vec.begin(); it != int_vec.end(); ++it) {
sum += *it;
}
return sum - 12;
}
该程序可以使用标准的 C++ 编译器命令进行编译:
g++ -g -O2 target.cxx -o target
如果需要的话,也可以包含 -fPIC -pie
这样的选项。
执行目标程序确认其预期功能:
$ ./target; echo $?
0
接下来,我们将继续编写一个外部的 C++ 代码片段来动态追踪这个进程。
编写 C++(或 Y++)分析器
我们将利用 eBPF+ 或 Stap+ 工具链开发一个 动态追踪 分析器。该分析器将监视全局 C++ 变量 int_vec
并实时输出它的所有元素。下面是用于达成这一目的的 analyzer.yxx
工具。(.yxx
文件扩展名表示 Y++
源文件。)
/* analyzer.yxx:用于 C++ 的动态追踪工具 */
#include <vector>
#include <cstdio>
_target std::vector<int> int_vec;
static void probe_handler(void) {
for (auto it = int_vec.begin(); it != int_vec.end(); ++it) {
printf("%d\n", *it);
}
}
_probe main() -> int {
probe_handler();
}
此代码片段利用了一些标准 C++ 语言的扩展功能。 _target
关键字指定了目标程序中的符号,在这里将 int_vec
变量声明为在目标应用中找到的变量。 _probe
关键字声明了一个动态探针位置,类似于我们的 Y 语言 中的语法结构,其定义了在目标程序的 main
函数返回时的一个探针位置。请注意,在我们的追踪器代码中包含了像 vector
和 cstdio
这样的标准头文件,使追踪器能够输出目标程序中 int_vec
的值。
使用我们的 y++
编译器将 analyzer.yxx
文件编译成 Ylang 代码:
y++ -o analyzer.y analyzer.yxx
随后,我们可以使用 Ylang 工具链将此分析器编译成能在各种平台上运行的可执行工具,如 eBPF+、Stap+ 或 GDB。这是一个使用 eBPF+ 的示例:
ylang --gen-bpf --symtab debuginfo.jl analyzer.y
debuginfo.jl
文件是自动从 target
二进制文件中的 DWARF 调试符号生成的,这个过程需要使用 -g
编译选项。我们将在本文后面讨论如何放宽这一要求。
ylang
命令为 eBPF+ 工具链生成两个文件:analyzer.bpf.c
用于内核空间,analyzer.ubpf.c
用于用户态。
这些生成的 C 语言源文件可以使用 clang
和 gcc
工具链分别编译成 eBPF 字节码和可执行文件 analyzer
,用于加载 eBPF 字节码(我们还增强了 clang
编译器的 eBPF 后端)。
将目标和分析器投入运行
按照上述描述生成的 analyzer
可执行文件,我们能够启动目标程序并加载 eBPF 程序进行分析,示例如下:
$ ./analyzer ~/work/target
Start tracing...
2
4
6
此输出验证了分析器动态追踪并输出目标程序中全局变量 int_vec
的值的能力。对于正在进行的目标进程,分析器支持使用 PID 以进行有针对性的追踪:
./analyzer -p 12345 -t 3
这个命令会追踪指定的 PID 的进程,最多分析 3 秒。
虽然描述的过程可能看起来复杂,但我们的 OpenResty XRay 产品可以自动化整个工作流程。
提升对复杂 C++ 应用的支持
我们目前已经支持许多 C++ 功能,比如类继承、虚函数表、使用 new
和 delete
运算符进行动态内存分配。
我们也正在努力扩展对更复杂的追踪器 C++ 程序的支持范围,比如 HotSpot JVM、NodeJS 或 MySQL 中的 C++ 代码片段。这将有助于我们能够创建更多非侵入式的 动态追踪 分析器,以应用于 JVM、Java 程序以及更广泛的 C++ 应用。我们在这方面正在迅速取得进展,敬请期待我们的后续更新。
关于调试符号
调试符号对于追踪(或调试)二进制可执行程序至关重要,这需要在使用 g++
命令编译时使用 -g
选项。这一要求与调试构建不同,调试构建引入的调试代码可能会减慢应用的运行速度。重要的是,调试符号不会产生运行时开销,并且在进程执行期间不会加载到内存中。
在现实世界的场景中,调试符号可能会丢失,或者目标程序可能没有使用 -g
选项进行编译。幸运的是,OpenResty XRay 私有的 AI 和机器学习算法可以从被 strip 过的 ELF 二进制文件中重新生成调试符号(通过我们内部的 symgen
工具),这适用于像 Nginx、LuaJIT 和 Python 等常见的开源软件和许多其他软件。我们还为客户特定的程序提供定制模型训练,有效地将 动态追踪 的能力扩展到几乎所有的进程。
欢迎观看 我们的 bilibili 视频 来亲眼见证这里的魔法。
结论
我们自主研发的 C++ 编译器 Y++,标志着在 动态追踪 方面的重大进展。通过将 动态追踪 的能力扩展到 C++ 应用,我们不仅填补了一个重要的空白,还为更复杂的分析和可观测技术铺平了道路。本文提供的示例仅仅是展示了使用 Y++ 和我们的 动态追踪(包括 eBPF+ 和 Stap+)所有可能性的冰山一角。
随着我们不断完善工具并扩展其功能,我们期待着一个美好未来:开发人员和系统管理员将能够更加深入了解其应用,从而创造出更可靠和性能更优的软件。无论您是在大规模分布式系统中解决复杂问题,还是试图优化软件性能,本文讨论的工具和方法都为实现您的目标提供了一个坚实的框架,而无需进行侵入式的代码修改。
此外,我们正不断努力支持更复杂的 C++ 应用,包括 HotSpot JVM 中的应用,这也彰显了我们对创新和更广泛的软件开发社区的承诺。通过充分利用动态追踪的力量,结合我们的私有的技术,如 OpenResty XRay,我们正处于制定解决当今开发人员面临的真实挑战的解决方案的前沿。
关于作者
章亦春(Github ID: agentzh)是开源 OpenResty® 项目创始人兼 OpenResty Inc. 公司 CEO 和创始人。
章亦春是中国早期开源技术和文化的倡导者和领军人物,曾供职于多家国际知名的高科技企业,如 Cloudflare、雅虎、阿里巴巴,是 “边缘计算”、“动态追踪” 和 “机器编程” 的先驱,拥有超过 22 年的编程及 16 年的开源经验。在开源领域中以 OpenResty® 项目负责人的身份而知名,该项目已被全球超过 4000 万个网站领域采用。
章亦春于 2017 年创立的企业软件初创公司 OpenResty Inc.,拥有来自全球一些最大公司的客户。其主打产品,OpenResty XRay,是一款非侵入式的性能分析和故障排除工具,极大地增强并利用了 动态追踪 技术。并且其 OpenResty Edge 产品是一款强大的分布式流量管理和私有 CDN 软件产品。
作为一名热衷于开源贡献的人士,章亦春为多个开源项目贡献了累计超过百万行代码,其中包括,Linux 内核、Nginx、LuaJIT、GDB、SystemTap、LLVM、Perl 等,并编写过 60 多个开源软件库。
关注我们
如果您喜欢本文,欢迎关注我们 OpenResty Inc. 公司的 博客网站。也欢迎扫码关注我们的微信公众号:
翻译
我们提供了 英文版 原文和中译版(本文)。我们也欢迎读者提供其他语言的翻译版本,只要是全文翻译不带省略,我们都将会考虑采用,非常感谢!