以下示例应用程序 outofbounds.c 分配了 12 个字节的内存,但初始化了 14 个字节:
#include <stdlib.h>#include <string.h>int main(int argc, char argv[]) { char p = malloc(12); memset(p, 0, 14); return (int)p;}
你可以编译并运行这个应用程序而不会注意到问题,因为这种行为对于这种类型的错误是典型的即使在更大的应用程序中,这类错误也可能长时间不被注意到然而,如果你使用 -fsanitize=address 选项启用了地址 sanitizer,那么应用程序在检测到错误后会停止同时,启用调试符号与 –g 选项也很有用,因为它有助于识别源代码中错误的位置以下代码是如何使用地址 sanitizer 和启用调试符号编译源文件的示例:$ clang -fsanitize=address -g outofbounds.c -o outofbounds
现在,当你运行应用程序时,会得到一个冗长的错误报告:$ ./outofbounds================================================================1067==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60200000001c at pc 0x00000023a6ef bp 0x7fffffffeb10 sp 0x7fffffffe2d8WRITE of size 14 at 0x60200000001c thread T0 #0 0x23a6ee in __asan_memset /usr/src/contrib/llvm-project/compiler-rt/lib/asan/asan_interceptors_memintrinsics.cpp:26:3 #1 0x2b2a03 in main /home/kai/sanitizers/outofbounds.c:6:3 #2 0x23331f in _start /usr/src/lib/csu/amd64/crt1.c:76:7
报告还包含有关内存内容的详细信息重要信息是错误类型——在这种情况下是堆缓冲区溢出——以及违规的源代码行要找到源代码行,你必须查看位置 #1 的堆栈跟踪,这是地址 sanitizer 拦截应用程序执行的最后一个位置它显示了 outofbounds.c 文件的第 6 行,即包含对 memset() 的调用的行这是发生缓冲区溢出的确切位置如果你将 outofbounds.c 文件中包含 memset(p, 0, 14); 的行替换为以下代码,那么你可以引入一旦你释放了内存就访问内存的情况你需要将源代码保存在 useafterfree.c 文件中:memset(p, 0, 12);free(p);
再次编译并运行它,sanitizer 会检测到释放内存后使用的指针:$ clang -fsanitize=address -g useafterfree.c -o useafterfree$ ./useafterfree================================================================1118==ERROR: AddressSanitizer: heap-use-after-free on address 0x602000000010 at pc 0x0000002b2a5c bp 0x7fffffffeb00 sp 0x7fffffffeaf8READ of size 1 at 0x602000000010 thread T0 #0 0x2b2a5b in main /home/kai/sanitizers/useafterfree.c:8:15 #1 0x23331f in _start /usr/src/lib/csu/amd64/crt1.c:76:7
这次,报告指向了包含 p 指针解引用的第 8 行在 x86_64 Linux 和 macOS 上,你还可以启用泄漏检测器如果你在运行应用程序之前将 ASAN_OPTIONS 环境变量设置为 detect_leaks=1,那么你还可以得到有关内存泄漏的报告在命令行上,你可以这样做:$ ASAN_OPTIONS=detect_leaks=1 ./useafterfree
地址 sanitizer 非常有用,因为它捕获了一类别通常难以检测到的错误内存 sanitizer 执行类似的任务在下一节中,我们将检查它的用例使用内存 sanitizer 查找未初始化的内存访问 使用未初始化的内存是另一类难以发现的错误在 C 和 C++ 中,常规的内存分配例程不使用默认值初始化内存缓冲区自动变量在栈上也是如此这里有很多出错的机会,内存 sanitizer 可以帮助发现这些错误如果你对实现细节感兴趣,你可以在 llvm/lib/Transforms/Instrumentation/MemorySanitizer.cpp 文件中找到内存 sanitizer pass 的源代码文件顶部的注释解释了实现背后的想法让我们运行一个小型示例,并将以下源代码保存为 memory.c 文件请注意,变量 x 未初始化并用作返回值:int main(int argc, char argv[]) { int x; return x;}
如果没有 sanitizer,应用程序将正常运行然而,如果你使用 -fsanitize=memory 选项,你将得到一个错误报告:$ clang -fsanitize=memory -g memory.c -o memory$ ./memory==1206==WARNING: MemorySanitizer: use-of-uninitialized-value #0 0x10a8f49 in main /home/kai/sanitizers/memory.c:3:3 #1 0x1053481 in _start /usr/src/lib/csu/amd64/crt1.c:76:7SUMMARY: MemorySanitizer: use-of-uninitialized-value /home/kai/sanitizers/memory.c:3:3 in mainExiting
像地址 sanitizer 一样,内存 sanitizer 在发现第一个错误时停止应用程序如上所示,内存 sanitizer 提供了一个使用初始化值的警告最后,在下一节中,我们将看到如何使用线程 sanitizer 来检测多线程应用程序中的竞态条件使用线程 sanitizer 指出数据竞争 为了利用现代 CPU 的能力,应用程序现在使用多个线程这是一种强大的技术,但它也引入了新的错误来源多线程应用程序中一个非常常见的问题是对全局数据的访问未受保护,例如,使用互斥锁或信号量这被称为数据竞争线程 sanitizer 可以检测基于 Pthreads 的应用程s/Instrumentation/ThreadSanitizer.cpp 文件中找到实现为了演示线程 sanitizer 的功能,我们将创建一个非常简单的生产者-消费者风格的应用程序生产者线程增加一个全局变量,而消费者线程减少同一个变量对全局变量的访问没有受到保护,因此这是一次数据竞争你需要将以下源代码保存在 thread.c 文件中:#include <pthread.h>int data = 0;void producer(void x) { for (int i = 0; i < 10000; ++i) ++data; return x;}void consumer(void x) { for (int i = 0; i < 10000; ++i) --data; return x;}int main() { pthread_t t1, t2; pthread_create(&t1, NULL, producer, NULL); pthread_create(&t2, NULL, consumer, NULL); pthread_join(t1, NULL); pthread_join(t2, NULL); return data;}
在前面的代码中,data 变量在两个线程之间共享这里,它被声明为 int 类型,以简化示例,因为在实际情况下,通常会使用类似 std::vector 类或类似的数据结构此外,这两个线程运行 producer() 和 consumer() 函数producer() 函数只增加 data 变量,而 consumer() 函数减少它没有实现访问保护,所以这是一次数据竞争main() 函数使用 pthread_create() 函数启动两个线程,使用 pthread_join() 函数等待线程结束,并返回 data 变量的当前值如果你编译并运行这个应用程序,你将不会注意到错误——也就是说,返回值总是零如果将执行的循环次数增加 100 倍,那么就会出现错误——在这种情况下,将出现其他值我们可以使用线程 sanitizer 来识别程序中的数据竞争要启用线程 sanitizer 编译,你需要传递 -fsanitize=thread 选项给 clang加上生成调试符号的 -g 选项可以在报告中给出行号,这也很有帮助注意,你还需要链接 pthread 库:$ clang -fsanitize=thread -g thread.c -o thread -lpthread$ ./thread==================..........ThreadSanitizer: reported 1 warnings
报告指向源文件的 6 行和 11 行,全局变量被访问的地方它还显示了两个名为 T1 和 T2 的线程访问了变量,以及 pthread_create() 函数调用的文件和行号通过这些,我们学会了如何使用三种不同类型的 sanitizers 来识别应用程序中的常见问题地址 sanitizer 帮助我们识别常见的内存访问错误,例如越界访问或在释放内存后使用内存使用内存 sanitizer,我们可以找到未初始化内存的访问,线程 sanitizer 帮助我们识别数据竞争在接下来的部分中,我们将尝试通过在随机数据上运行应用程序来触发 sanitizers这个过程被称为模糊测试使用 libFuzzer 查找错误 要测试你的应用程序,你需要编写单元测试这是一个很好的方式来确保你的软件按照预期正确运行然而,由于可能的输入数量呈指数级增长,你可能会错过某些奇怪的输入,以及一些错误模糊测试可以在此处提供帮助其思想是向应用程序呈现随机生成的数据,或基于有效输入但带有随机变化的数据这是重复进行的,以便你的应用程序被大量输入测试,这就是为什么模糊测试可以成为一种强大的测试方法已有记录显示,模糊测试帮助在网络浏览器和其他软件中发现了数百个错误有趣的是,LLVM 附带了自己的模糊测试库最初是 LLVM 核心库的一部分,libFuzzer 实现最终被移到了 compiler-rt 中该库旨在测试小型和快速的函数让我们通过一个小型示例来看看 libFuzzer 的工作原理首先,你需要提供 LLVMFuzzerTestOneInput() 函数这个函数由模糊测试驱动调用,并为你提供一些输入下面的函数计算输入中的连续 ASCII 数字完成这个之后,我们将向它提供随机输入你需要将示例保存在 fuzzer.c 文件中:#include <stdint.h>#include <stdlib.h>int count(const uint8_t Data, size_t Size) { int cnt = 0; if (Size) while (Data[cnt] >= '0' && Data[cnt] <= '9') ++cnt; return cnt;}int LLVMFuzzerTestOneInput(const uint8_t Data, size_t Size) { count(Data, Size); return 0;}
在前面的代码中,count() 函数计算 Data 变量指向的内存中的数字数量数据的大小只检查是否有可用的字节在 while 循环中,不检查大小使用常规的 C 字符串时,不会有错误,因为 C 字符串总是以 0 字节结尾LLVMFuzzerTestOneInput() 函数是所谓的模糊测试目标,它是由 libFuzzer 调用的函数它调用我们想要测试的函数,并返回 0,目前是唯一允许的值要使用 libFuzzer 编译文件,你必须添加 -fsanitize=fuzzer 选项建议同时启用地址 sanitizer 和生成调试符号我们可以使用以下命令来编译 fuzzer.c 文件:$ clang -fsanitize=fuzzer,address -g fuzzer.c -o fuzzer
当你运行测试时,它会发出一个详细的报告报告包含比堆栈跟踪更多的信息,让我们仔细看看它:第一行告诉你初始化随机数生成器所使用的种子你可以使用 –seed= 选项来重复此执行:INFO: Seed: 1297394926
默认情况下,libFuzzer 将输入限制在最多 4096 字节你可以通过使用 –max_len= 选项来更改默认值:INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes
现在,我们可以在不提供示例输入的情况下运行测试所有示例输入的集合称为语料库,对于这次运行它是空的:INFO: A corpus is not provided, starting from an empty corpus
接下来是一些关于生成的测试数据的信息它显示你尝试了 28 个输入,并找到了 6 个输入,这些输入的总长度为 19 字节,它们共同涵盖了 6 个覆盖点或基本块:#28 NEW cov: 6 ft: 9 corp: 6/19b lim: 4 exec/s: 0 rss: 29Mb L: 4/4 MS: 4 CopyPart-PersAutoDict-CopyPart-ChangeByte- DE: "1\x00"-
在此之后,检测到缓冲区溢出,并跟随来自地址 sanitizer 的信息最后,报告告诉你导致缓冲区溢出的输入保存在哪里:artifact_prefix='./'; Test unit written to ./crash-17ba0791499db908433b80f37c5fbc89b870084b
使用保存的输入,可以使用相同的崩溃输入再次执行测试用例:$ ./fuzzer crash-17ba0791499db908433b80f37c5fbc89b870084b
这有助于识别问题,因为我们可以利用保存的输入作为直接的复制品来修复可能出现的任何问题然而,仅使用随机数据通常在每种情况下并不是很有帮助如果你尝试模糊测试 tinylang 词法分析器或解析器,那么纯随机数据会导致输入立即被拒绝,因为找不到有效的标记在这种情况下,提供一组有效的输入更有用,称为语料库在这种情况下,语料库中的文件被随机变异并用作输入你可以认为输入大部分是有效的,只是翻转了一些位这对于必须具有特定格式的其他输入也有效例如,对于处理 JPEG 和 PNG 文件的库,你将提供一些小的 JPEG 和 PNG 文件作为语料库提供语料库的示例如下你可以在一个或多个目录中保存语料库文件,你可以使用 printf 命令的帮助为我们的模糊测试创建一个简单的语料库:$ mkdir corpus$ printf "012345\0" >corpus/12345.txt$ printf "987\0" >corpus/987.txt
运行测试时,你必须在命令行上提供目录:$ ./fuzzer corpus/
然后,语料库被用作生成随机输入的基础,报告告诉你:INFO: seed corpus: files: 2 min: 4b max: 7b total: 11b rss: 29Mb
此外,如果你正在测试一个在标记或其他魔术值上工作的函数,例如一个编程语言,那么通过提供一个包含标记的字典,可以加快进程对于编程语言,字典将包含语言中使用的所有关键字和特殊符号此外,字典定义遵循一个简单的键值风格例如,要在字典中定义 if 关键字,你可以添加以下内容:kw1="if"
然而,键是可选的,你可以省略它现在,你可以在命令行上使用 –dict= 选项指定字典文件现在我们已经介绍了如何使用 libFuzzer 查找错误,让我们看看 libFuzzer 实现的限制和替代方案限制和替代方案 libFuzzer 实现很快,但对测试目标提出了几个限制它们如下:被测试的函数必须接受内存中的数组作为输入一些库函数需要数据的文件路径,它们不能使用 libFuzzer 进行测试 不应调用 exit() 函数 不应改变全局状态 不应使用硬件随机数生成器 前两个限制是 libFuzzer 作为库实现的含义后两个限制是为了避免评估算法中的混淆如果这些限制中的任何一个没有得到满足,那么两个相同的对模糊测试目标的调用可能会产生不同的结果最著名的替代模糊测试工具是 AFL,可以在 https://github.com/google/AFL 上找到AFL 需要一个有工具的二进制文件(提供 LLVM 插件进行工具化),并要求应用程序在命令行上将输入作为文件路径AFL 和 libFuzzer 可以共享相同的语料库和相同的字典文件因此,可以使用这两种工具测试应用程序此外,在 libFuzzer 不适用的情况下,AFL 可能是一个很好的替代品还有很多方法可以影响 libFuzzer 的工作方式你可以在 https://llvm.org/docs/LibFuzzer.html 上阅读参考页面以获取更多详细信息在接下来的部分中,我们将查看应用程序可能存在的不同问题 - 我们将尝试使用 XRay 工具识别性能瓶颈使用 XRay 进行性能分析 如果你的应用程序运行缓慢,那么你可能想要知道代码中的时间花费在哪里在这里,使用 XRay 对代码进行检测可以帮助完成这项任务基本上,在每个函数的入口和出口处,都会插入一个特殊的调用到运行时库这允许你计算函数被调用了多少次,以及在函数中花费了多少时间你可以在 llvm/lib/XRay/ 目录中找到工具传递的实现运行时部分是 compiler-rt 的一部分在以下示例源代码中,通过调用 usleep() 函数来模拟实际工作func1() 函数睡眠 10 µsfunc2() 函数根据 n 参数是奇数还是偶数,调用 func1() 或睡眠 100 µs在 main() 函数中,两个函数都在循环中被调用这已经足够获得有趣的信息了你需要将以下源代码保存在 xraydemo.c 文件中:#include <unistd.h>void func1() { usleep(10); }void func2(int n) { if (n % 2) func1(); else usleep(100);}int main(int argc, char argv[]) { for (int i = 0; i < 100; i++) { func1(); func2(i); } return 0;}
要在编译期间启用 XRay 检测,你需要指定 -fxray-instrument 选项值得注意的是,少于 200 条指令的函数不会被检测这是因为这是开发者定义的任意阈值,在我们的案例中,函数将不会被检测可以使用 -fxray-instruction-threshold= 选项来指定阈值或者,我们可以添加一个函数属性来控制是否应该检测一个函数例如,添加以下原型将始终导致我们检测该函数:void func1() __attribute__((xray_always_instrument));
同样,通过使用 xray_never_instrument 属性,你可以关闭一个函数的检测现在,我们将使用命令行选项并编译 xraydemo.c 文件,如下所示:$ clang -fxray-instrument -fxray-instruction-threshold=1 -g\ xraydemo.c -o xraydemo
在生成的二进制文件中,默认情况下检测是关闭的如果你运行二进制文件,你会注意到与非检测二进制文件相比没有区别XRAY_OPTIONS 环境变量用于控制运行时数据的记录要启用数据收集,你可以按如下方式运行应用程序:$ XRAY_OPTIONS="patch_premain=true xray_mode=xray-basic"\ ./xraydemo
xray_mode=xray-basic 选项告诉运行时我们想要使用基本模式在这种模式下,收集了所有的运行时数据,这可能会导致大型日志文件当给出 patch_premain=true 选项时,也会检测在 main() 函数之前运行的函数运行此命令后,将在目录中创建一个新文件,存储收集的数据你需要使用 llvm-xray 工具从该文件中提取任何可读信息llvm-xray 工具支持各种子命令首先,你可以使用 account 子命令提取一些基本统计信息例如,要获取最常调用的前 10 个函数,你可以添加 -top=10 选项来限制输出,并使用 -sort=count 选项指定函数调用计数作为排序标准你还可以使用 -sortorder= 选项影响排序顺序以下命令可以运行以获取我们程序的统计信息:$ llvm-xray account xray-log.xraydemo.xVsWiE --sort=count\ --sortorder=dsc --instr_map ./xraydemo
具有延迟的函数:3 函数id 计数 总和 函数 1 150 0.166002 demo.c:4:0: func1 2 100 0.543103 demo.c:9:0: func2 3 1 0.655643 demo.c:17:0: main如你所见,func1() 函数被调用得最频繁;你还可以看到在这个函数中花费的累积时间这个例子只有三个函数,所以 –top= 选项在这里没有可见效果,但对于真实应用程序,它非常有用从收集的数据中,可以重建运行时发生的所有堆栈帧你使用 stack 子命令来查看前 10 个堆栈这里显示的输出已经简化,以便于理解:$ llvm-xray stack xray-log.xraydemo.xVsWiE –instr_map\ ./xraydemo
Unique Stacks: 3 Top 10 Stacks by leaf sum: Sum: 1325516912 lvl function 计数 总和 #0 main 1 1777862705 #1 func2 50 1325516912 Top 10 Stacks by leaf count: Count: 100 lvl function 计数 总和 #0 main 1 1777862705 #1 func1 100 303596276堆栈帧是函数调用的序列func2() 函数由 main() 函数调用,这是累积时间最长的堆栈帧深度取决于调用了多少个函数,堆栈帧通常很大这个子命令也可以用来从堆栈帧创建火焰图使用火焰图,你可以很容易地识别哪些函数有大量的累积运行时间输出是带有计数和运行时间信息的堆栈帧使用 flamegraph.pl 脚本,你可以将数据转换为可缩放矢量图形(SVG)文件,你可以在浏览器中查看使用以下命令,你指示 llvm-xray 输出所有堆栈帧与 –all-stacks 选项使用 –stack-format=flame 选项,输出格式符合 flamegraph.pl 脚本的预期此外,使用 –aggregation-type 选项,你可以选择按总时间或按调用次数聚合堆栈帧llvm-xray 的输出被管道传输到 flamegraph.pl 脚本,并将结果输出保存在 flame.svg 文件中:$ llvm-xray stack xray-log.xraydemo.xVsWiE --all-stacks\ --stack-format=flame --aggregation-type=time\ --instr_map ./xraydemo | flamegraph.pl >flame.svg
运行命令并生成新的火焰图后,你可以在浏览器中打开生成的 flame.svg 文件图形如下所示:图 10.1 - llvm-xray 生成的火焰图火焰图乍一看可能会让人感到困惑,因为 X 轴没有经过时间的通常意义相反,函数按名称字母顺序简单排序此外,火焰图的 Y 轴显示堆栈深度,从零开始计数颜色被选择为具有良好的对比度,没有其他含义从前面的图中,你可以轻松地确定函数的调用层次结构和在函数中花费的时间将鼠标光标移动到表示框架的矩形上,只会显示有关堆栈框架的信息通过单击框架,你可以放大此堆栈框架火焰图对于帮助你识别值得优化的函数非常有用要了解有关火焰图的更多信息,请访问火焰图的发明者 Brendan Gregg 的网站:http://www.brendangregg.com/flamegraphs.html此外,你可以使用 convert 子命令将数据转换为 .yaml 格式或 Chrome Trace Viewer Visualization 使用的格式后者是另一种很好的方式,可以从数据中创建图形要将数据保存在 xray.evt 文件中,你可以运行以下命令:$ llvm-xray convert --output-format=trace_event\ --output=xray.evt --symbolize --sort\ --instr_map=./xraydemo xray-log.xraydemo.xVsWiE
如果你没有指定 –symbolize 选项,那么结果图形中将不显示函数名称一旦你这样做了,在 Chrome 中打开并输入 chrome:///tracing接下来,点击加载按钮加载 xray.evt 文件你将看到以下数据的可视化:图 10.2 - llvm-xray 生成的 Chrome Trace Viewer 可视化在这个视图中,堆栈框架按函数调用发生的时间排序有关可视化的进一步解释,请阅读 https://www.chromium.org/developers/how-tos/trace-event-profiling-tool 上的教程提示llvm-xray 工具具有更多适用于性能分析的功能你可以在 LLVM 网站上的 https://llvm.org/docs/XRay.html 和 https://llvm.org/docs/XRayExample.html 上阅读有关它的更多信息在本节中,我们学习了如何使用 XRay 对应用程序进行检测,如何收集运行时信息,以及如何可视化这些数据我们可以使用这些知识来识别应用程序中的性能瓶颈识别应用程序错误的另一种方法是分析源代码,这是使用 Clang 静态分析器完成的使用 Clang 静态分析器检查源代码 Clang 静态分析器是一个工具,它对 C、C++ 和 Objective-C 源代码执行额外的检查静态分析器执行的检查比编译器执行的检查更彻底它们在时间和所需资源方面也更昂贵静态分析器有一组检查器,用于检查某些错误该工具执行源代码的符号解释,它查看应用程序的所有代码路径,并从中推导出应用程序中使用的值的约束符号解释是编译器中常用的一种技术,例如,用于识别常量值在静态分析器的上下文中,检查器应用于推导出的值例如,如果除法的除数为零,静态分析器会警告我们我们可以通过存储在 div.c 文件中的以下示例来检查这一点:int divbyzero(int a, int b) { return a / b; }int bug() { return divbyzero(5, 0); }
静态分析器将警告此示例中的除以零错误然而,当使用命令 clang -Wall -c div.c 编译文件时,将不显示任何警告有两种方式可以从命令行调用静态分析器较旧的工具是 scan-build,它包含在 LLVM 中,并且可以用于简单场景较新的工具是 CodeChecker,可在 https://github.com/Ericsson/codechecker/ 上获得要检查单个文件,scan-build 工具是最简单的解决方案你只需将编译命令传递给工具;其他所有事情都会自动完成:$ scan-build clang -c div.c
scan-build: 使用 '/usr/home/kai/LLVM/llvm-17/bin/clang-17' 进行静态分析 div.c:2:12: 警告:零除错误 [core.DivideZero] return a / b; ~~^~~~ 1 个警告已生成 scan-build: 分析运行完成 scan-build: 发现 1 个错误 scan-build: 运行 'scan-view /tmp/scan-build-2021-03-01-023401-8721-1' 以检查错误报告 屏幕上的输出已经告诉你发现了一个错误 - 也就是说,触发了 core.DivideZero 检查器然而,这还不是全部在 /tmp 目录的指定子目录中,你将找到一个完整的 HTML 报告然后,你可以使用 scan-view 命令查看报告,或者在子目录中找到的 index.html 文件在你的浏览器中打开报告的第一页向你展示了发现的错误的摘要:图 10.3 - 摘要页面对于发现的每个错误,摘要页面显示错误类型、源代码中的位置以及分析器发现错误后的路径长度还提供了指向错误详细报告的链接以下截图显示了错误的详细报告:图 10.4 - 详细报告有了这个详细报告,你可以通过跟随编号的气泡来验证错误我们的简单示例展示了如何将 0 作为参数值传递导致除以零错误因此,需要人工验证如果某个检查器的派生约束不够精确,则可能会出现误报 - 也就是说,对于完全正常的代码报告了一个错误根据报告,你可以使用它们来识别误报你不限于工具提供的检查器 - 你还可以添加新的检查器下一节展示了如何做到这一点向 Clang 静态分析器添加新检查器 许多 C 库提供必须成对使用的函数例如,C 标准库提供了 malloc() 和 free() 函数由 malloc() 函数分配的内存必须由 free() 函数精确地释放一次不调用 free() 函数或调用多次是编程错误还有更多这种编码模式的实例,静态分析器为其中一些提供了检查器iconv 库提供将文本从一种编码转换为另一种编码的函数 - 例如,从 Latin-1 编码转换为 UTF-16 编码执行转换时,实现需要分配内存为了透明地管理内部资源,iconv 库提供了 iconv_open() 和 iconv_close() 函数,它们必须成对使用,类似于内存管理函数这些函数没有实现检查器,所以让我们实现一个要向 Clang 静态分析器添加新检查器,你必须创建一个 Checker 类的新子类静态分析器尝试所有可能的代码路径分析器引擎在某些点生成事件 - 例如,在函数调用之前或之后此外,你的类必须为这些事件提供回调,如果你需要处理它们Checker 类和事件的注册在 clang/include/clang/StaticAnalyzer/Core/Checker.h 头文件中提供通常,检查器需要跟踪一些符号然而,检查器不能管理状态,因为它不知道分析器引擎当前正在尝试哪个代码路径因此,跟踪的状态必须使用 ProgramStateRef 实例向引擎注册为了检测错误,检查器需要跟踪从 iconv_open() 函数返回的描述符分析器引擎为 iconv_open() 函数的返回值返回一个 SymbolRef 实例我们将此符号与状态关联,以反映 iconv_close() 是否被调用对于状态,我们创建了 IconvState 类,它封装了一个布尔值新的 IconvChecker 类需要处理四种类型的事件:PostCall,在函数调用后发生在调用 iconv_open() 函数后,我们检索了返回值的符号,并将其记住为处于“打开”状态 PreCall,在函数调用前发生在 iconv_close() 函数被调用之前,我们检查描述符的符号是否处于“打开”状态如果没有,那么 iconv_close() 函数已经对该描述符调用过了,我们已经检测到了对该函数的重复调用 DeadSymbols,当未使用的符号被清理时发生我们检查一个未使用的描述符符号是否仍然处于“打开”状态如果是,那么我们就检测到了对 iconv_close() 的缺失调用,这是一个资源泄漏 PointerEscape,当符号不再能被分析器跟踪时调用在这种情况下,我们从状态中删除符号,因为我们不能再推断描述符是否已关闭 我们可以创建一个新目录来实现新检查器作为一个 Clang 插件,并在 IconvChecker.cpp 文件中添加实现:为了实现,我们需要包括几个头文件BugType.h 头文件需要用于发出报告Checker.h 头文件提供了 Checker 类的声明和事件的回调,这些回调在 CallEvent 文件中声明此外,CallDescription.h 文件有助于匹配函数和方法最后,CheckerContext.h 文件需要用于声明 CheckerContext 类,这是提供对分析器状态访问的中心类:#include "clang/StaticAnalyzer/Core/BugReporter/BugType.h"#include "clang/StaticAnalyzer/Core/Checker.h"#include "clang/StaticAnalyzer/Core/PathSensitive/CallDescription.h"#include "clang/StaticAnalyzer/Core/PathSensitive/CallEvent.h"#include "clang/StaticAnalyzer/Core/PathSensitive/CheckerContext.h"#include "clang/StaticAnalyzer/Frontend/CheckerRegistry.h"#include <optional>
为了避免输入命名空间名称,我们可以使用 clang 和 ento 命名空间:using namespace clang;using namespace ento;
我们为每个表示 iconv 描述符的符号关联一个状态状态可以是打开或关闭的,我们使用一个布尔类型变量,对于打开状态,其值为 true状态值被封装在 IconvState 结构体中这个结构体使用一个 FoldingSet 数据结构,这是一个哈希集合,可以过滤重复条目为了能够使用这个数据结构的实现,这里添加了 Profile() 方法,它设置了这个结构体的唯一位我们把结构体放在一个匿名命名空间中,以避免污染全局命名空间类不直接暴露布尔值,而是提供了 getOpened() 和 getClosed() 工厂方法以及 isOpen() 查询方法:namespace {class IconvState { const bool IsOpen; IconvState(bool IsOpen) : IsOpen(IsOpen) {}public: bool isOpen() const { return IsOpen; } static IconvState getOpened() { return IconvState(true); } static IconvState getClosed() { return IconvState(false); } bool operator==(const IconvState &O) const { return IsOpen == O.IsOpen; } void Profile(llvm::FoldingSetNodeID &ID) const { ID.AddInteger(IsOpen); }};} // namespace
IconvState 结构体表示一个 iconv 描述符的状态,该状态由 SymbolRef 类的符号表示这最好通过一个映射来完成,映射的键是符号,值是状态正如前面解释的,检查器不能持有状态相反,状态必须使用 REGISTER_MAP_WITH_PROGRAMSTATE 宏与全局程序状态注册这个宏引入了 IconvStateMap 名称,我们将在后面用来访问映射:REGISTER_MAP_WITH_PROGRAMSTATE(IconvStateMap, SymbolRef, IconvState)
我们还在一个匿名命名空间中实现了 IconvChecker 类请求的 PostCall、PreCall、DeadSymbols 和 PointerEscape 事件是 Checker 基类的模板参数:namespace {class IconvChecker : public Checker<check::PostCall, check::PreCall, check::DeadSymbols, check::PointerEscape> { // ...};} // namespace
IconvChecker 类有 CallDescription 类型的字段,用于识别程序中对 iconv_open()、iconv() 和 iconv_close() 的函数调用:CallDescription IconvOpenFn, IconvFn, IconvCloseFn;
类还持有检测到的错误类型的引用:std::unique_ptr<BugType> DoubleCloseBugType;std::unique_ptr<BugType> LeakBugType;
最后,类有几个方法除了构造函数和用于调用事件的方法,我们还需要一个方法来发出错误报告:void report(ArrayRef<SymbolRef> Syms, const BugType &Bug, StringRef Desc, CheckerContext &C, ExplodedNode ErrNode, std::optional<SourceRange> Range = std::nullopt) const;
public:IconvChecker();void checkPostCall(const CallEvent &Call, CheckerContext &C) const;void checkPreCall(const CallEvent &Call, CheckerContext &C) const;void checkDeadSymbols(SymbolReaper &SymReaper, CheckerContext &C) const;ProgramStateRefcheckPointerEscape(ProgramStateRef State, const InvalidatedSymbols &Escaped, const CallEvent Call, PointerEscapeKind Kind) const;
我们可以开始实现 IconvChecker 类的构造函数,使用函数名称初始化 CallDescription 字段,并创建表示错误的 BugType 对象:IconvChecker::IconvChecker() : IconvOpenFn({"iconv_open"}), IconvFn({"iconv"}), IconvCloseFn({"iconv_close"}, 1) { DoubleCloseBugType.reset(new BugType( this, "Double iconv_close", "Iconv API Error")); LeakBugType.reset(new BugType( this, "Resource Leak", "Iconv API Error", /SuppressOnSink=/true));}
现在,我们可以实施第一个调用事件方法 checkPostCall()此方法在分析器执行函数调用后被调用如果执行的函数不是全局 C 函数且名称不是 iconv_open,则无需进行任何操作:void IconvChecker::checkPostCall( const CallEvent &Call, CheckerContext &C) const { if (!Call.isGlobalCFunction()) return; if (!IconvOpenFn.matches(Call)) return;
否则,我们可以尝试将函数的返回值获取为一个符号为了将符号与打开状态存储在全局程序状态中,我们需要从 CheckerContext 实例中获取一个 ProgramStateRef 实例状态是不可变的,所以向状态中添加符号会得到一个新的状态最后,通过调用 addTransition() 方法通知分析器引擎新的状态:if (SymbolRef Handle = Call.getReturnValue().getAsSymbol()) { ProgramStateRef State = C.getState(); State = State->set<IconvStateMap>( Handle, IconvState::getOpened()); C.addTransition(State);}
同样,checkPreCall() 方法在分析器执行函数之前被调用我们只对名为 iconv_close 的全局 C 函数感兴趣:void IconvChecker::checkPreCall( const CallEvent &Call, CheckerContext &C) const { if (!Call.isGlobalCFunction()) { return; } if (!IconvCloseFn.matches(Call)) { return; }
如果已知函数的第一个参数的符号(即 iconv 描述符),那么我们可以程序状态中检索符号的状态:if (SymbolRef Handle = Call.getArgSVal(0).getAsSymbol()) { ProgramStateRef State = C.getState(); if (const IconvState St = State->get<IconvStateMap>(Handle)) {
如果状态表示已关闭状态,那么我们已经检测到一个双重关闭错误,可以为其生成一个错误报告调用 generateErrorNode() 可能会返回一个空值,如果已经为这个路径生成了错误报告,所以我们需要检查这种情况:if (!St->isOpen()) { if (ExplodedNode N = C.generateErrorNode()) { report(Handle, DoubleCloseBugType, "Closing a previous closed iconv " "descriptor", C, N, Call.getSourceRange()); } return;}
否则,我们必须为符号设置状态为“已关闭”状态:State = State->set<IconvStateMap>( Handle, IconvState::getClosed());C.addTransition(State);
checkDeadSymbols() 方法被调用以清理未使用的符号我们循环遍历我们跟踪的所有符号,并询问 SymbolReaper 实例当前符号是否已死亡:void IconvChecker::checkDeadSymbols( SymbolReaper &SymReaper, CheckerContext &C) const { ProgramStateRef State = C.getState(); SmallVector<SymbolRef, 8> LeakedSyms; for (auto [Sym, St] : State->get<IconvStateMap>()) { if (SymReaper.isDead(Sym)) {
如果符号已死亡,那么我们需要检查状态如果状态仍然是打开的,那么这是一个潜在的资源泄漏有一个例外:iconv_open() 在出错时返回 -1如果分析器处于处理此错误的代码路径中,那么假设资源泄漏是错误的,因为函数调用失败了我们尝试从 ConstraintManager 实例获取符号的值,如果这个值是 -1,我们不考虑符号作为资源泄漏我们向 SmallVector 实例添加一个泄漏符号,稍后生成错误报告最后,我们从程序状态中删除死亡符号:if (St.isOpen()) { bool IsLeaked = true; if (const llvm::APSInt Val = State->getConstraintManager().getSymVal( State, Sym)) IsLeaked = Val->getExtValue() != -1; if (IsLeaked) LeakedSyms.push_back(Sym);}State = State->remove<IconvStateMap>(Sym);
}}
循环结束后,我们调用 generateNonFatalErrorNode() 方法这个方法转换到新的程序状态,并在此路径上还没有错误节点时返回错误节点LeakedSyms 容器保存了(可能是空的)泄漏符号列表,我们调用 report() 方法来生成错误报告:if (ExplodedNode N = C.generateNonFatalErrorNode(State)) { report(LeakedSyms, LeakBugType, "Opened iconv descriptor not closed", C, N);}
checkPointerEscape() 函数在分析器检测到一个函数调用,其参数不能被跟踪时被调用在这种情况下,我们必须假设我们不知道 iconv 描述符是否会在函数内部关闭例外情况是对 iconv() 的调用,它执行转换,已知不会调用 iconv_close() 函数,以及我们在 checkPreCall() 方法中处理的 iconv_close() 函数本身如果调用在系统头文件中,并且我们知道调用的函数中参数不会逃逸,我们也不会改变状态在所有其他情况下,我们从状态中删除符号:ProgramStateRef IconvChecker::checkPointerEscape( ProgramStateRef State, const InvalidatedSymbols &Escaped, const CallEvent Call, PointerEscapeKind Kind) const { if (Kind == PSK_DirectEscapeOnCall) { if (IconvFn.matches(Call) || IconvCloseFn.matches(Call)) return State; if (Call->isInSystemHeader() || !Call->argumentsMayEscape()) return State; } for (SymbolRef Sym : Escaped) State = State->remove<IconvStateMap>(Sym); return State;}
report() 方法生成一个错误报告该方法的重要参数是符号数组、错误类型和错误描述在方法内部,为每个符号创建一个错误报告,并将符号标记为该错误的相关项如果提供了源范围作为参数,也会将其添加到报告中最后,发出报告:void IconvChecker::report( ArrayRef<SymbolRef> Syms, const BugType &Bug, StringRef Desc, CheckerContext &C, ExplodedNode ErrNode, std::optional<SourceRange> Range) const { for (SymbolRef Sym : Syms) { auto R = std::make_unique<PathSensitiveBugReport>( Bug, Desc, ErrNode); R->markInteresting(Sym); if (Range) R->addRange(Range); C.emitReport(std::move(R)); }}
现在,需要在 CheckerRegistry 实例中注册新检查器当插件加载时,使用 clang_registerCheckers() 函数执行注册每个检查器都有一个名称并属于一个包我们称 IconvChecker 检查器,将其放入 unix 包中,因为 iconv 库是一个标准的 POSIX 接口这是 addChecker() 方法的第一个参数第二个参数是功能的简要文档,第三个参数可以是提供有关检查器的更多信息的文档的 URI:extern "C" voidclang_registerCheckers(CheckerRegistry ®istry) { registry.addChecker<IconvChecker>( "unix.IconvChecker", "Check handling of iconv functions", "");}
最后,需要声明我们使用的静态分析器 API 的版本,这允许系统确定插件是否兼容:extern "C" const char clang_analyzerAPIVersionString[] = CLANG_ANALYZER_API_VERSION_STRING;
这完成了新检查器的实现要构建插件,我们还需要在与 IconvChecker.cpp 相同的目录中创建 CMakeLists.txt 文件的构建描述:首先定义所需的 CMake 版本和项目名称:cmake_minimum_required(VERSION 3.20.0)project(iconvchecker)
接下来,包含 LLVM 文件如果 CMake 无法自动找到文件,则需要设置 LLVM_DIR 变量,使其指向包含 CMake 文件的 LLVM 目录:find_package(LLVM REQUIRED CONFIG)
将包含 CMake 文件的 LLVM 目录添加到搜索路径,并包含 LLVM 的所需模块:list(APPEND CMAKE_MODULE_PATH ${LLVM_DIR})include(AddLLVM)include(HandleLLVMOptions)
然后,加载 Clang 的 CMake 定义如果 CMake 无法自动找到文件,则需要设置 Clang_DIR 变量,使其指向包含 CMake 文件的 Clang 目录:find_package(Clang REQUIRED)
接下来,定义头文件和库文件的位置,并确定要使用的定义:include_directories("${LLVM_INCLUDE_DIR}" "${CLANG_INCLUDE_DIRS}")add_definitions("${LLVM_DEFINITIONS}")link_directories("${LLVM_LIBRARY_DIR}")
前面的定义设置了构建环境插入以下命令,它定义了你的插件的名称、插件的源文件,并指出它是一个 Clang 插件:add_llvm_library(IconvChecker MODULE IconvChecker.cpp PLUGIN_TOOL clang)
在 Windows 上,插件支持与 Unix 不同,必须链接所需的 LLVM 和 Clang 库以下代码确保了这一点:if(WIN32 OR CYGWIN) set(LLVM_LINK_COMPONENTS Support) clang_target_link_libraries(IconvChecker PRIVATE clangAnalysis clangAST clangStaticAnalyzerCore clangStaticAnalyzerFrontend)endif()
现在,我们可以配置并构建插件,假设 CMAKE_GENERATOR 和 CMAKE_BUILD_TYPE 环境变量已设置:$ cmake -DLLVM_DIR=~/LLVM/llvm-17/lib/cmake/llvm \ -DClang_DIR=~/LLVM/llvm-17/lib/cmake/clang \ -B build$ cmake --build build
这些步骤在构建目录中创建了 IconvChecker.so 共享库要测试新检查器,请将以下源代码保存在 conv.c 文件中,该文件对 iconv_close() 函数进行了两次调用:#include <iconv.h>void doconv() { iconv_t id = iconv_open("Latin1", "UTF-16"); iconv_close(id); iconv_close(id);}
要使用插件与 scan-build 脚本,你需要通过 -load-plugin 选项指定插件的路径使用 conv.c 文件运行看起来像这样:$ scan-build -load-plugin build/IconvChecker.so clang-17 \ -c conv.c
scan-build: 使用 '/home/kai/LLVM/llvm-17/bin/clang-17' 进行静态分析 conv.c:6:3: 警告: 关闭之前已关闭的 iconv 描述符 [unix.IconvChecker] 6 | iconv_close(id); | ^~~~~~~~~~~~~~~~ 1 个警告已生成 scan-build: 分析运行完成 scan-build: 发现 1 个错误 scan-build: 运行 'scan-view /tmp/scan-build-2023-08-08-114154-12451-1' 以检查错误报告有了这个,你已经学会了如何使用自己的检查器扩展 Clang 静态分析器你可以利用这些知识创建新的通用检查器并为社区做出贡献,或者专门为你的需求创建检查器,以提高你的产品质量静态分析器是利用 Clang 基础设施构建的下一节将向你介绍如何构建你自己的 Clang 插件创建你自己的基于 Clang 的工具 静态分析器是一个令人印象深刻的例子,展示了你可以用 Clang 基础设施做什么你也可以通过插件扩展 Clang,以便你可以向 Clang 添加你自己的功能这种技术与向 LLVM 添加传递插件非常相似让我们通过一个简单的插件探索这个功能LLVM 编码标准要求函数名称以小写字母开始然而,编码标准已经发展,有许多函数名称以大写字母开始的情况一个警告命名规则违规的插件可以帮助解决这个问题,让我们试试因为你想在 AST 上运行用户定义的操作,你需要定义一个 PluginASTAction 类的子类如果你使用 Clang 库编写自己的工具,那么你可以为操作定义 ASTFrontendAction 类的子类PluginASTAction 类是 ASTFrontendAction 类的子类,具有解析命令行选项的额外能力你还需要一个 ASTConsumer 类的子类AST 消费者是一个类,通过它你可以在 AST 上运行一个操作,无论 AST 的来源如何对于我们的第一个插件,不需要更多的东西你可以在 NamingPlugin.cpp 文件中按如下方式创建实现:首先包括所需的头文件除了提到的 ASTConsumer 类,你还需要一个编译器实例和插件注册表:#include "clang/AST/ASTConsumer.h"#include "clang/Frontend/CompilerInstance.h"#include "clang/Frontend/FrontendPluginRegistry.h"
使用 clang 命名空间,并将你的实现放入匿名命名空间以避免名称冲突:using namespace clang;namespace {
接下来,定义你的 ASTConsumer 类的子类稍后,你将希望在检测到命名规则违规时发出警告为此,你需要一个 DiagnosticsEngine 实例的引用你需要在类中存储一个 CompilerInstance 实例,之后你可以请求一个 DiagnosticsEngine 实例:class NamingASTConsumer : public ASTConsumer { CompilerInstance &CI;public: NamingASTConsumer(CompilerInstance &CI) : CI(CI) {}
ASTConsumer 实例有几个入口方法HandleTopLevelDecl() 方法适合我们的目的这个方法对于每个顶层声明都会被调用这不仅仅是函数 - 例如,变量所以,你必须使用 LLVM RTTI dyn_cast<>() 函数来确定声明是否是函数声明HandleTopLevelDecl() 方法有一个声明组作为参数,其中可以包含多个声明这需要一个循环来遍历声明以下代码显示了 HandleTopLevelDecl() 方法:bool HandleTopLevelDecl(DeclGroupRef DG) override { for (DeclGroupRef::iterator I = DG.begin(), E = DG.end(); I != E; ++I) { const Decl D = I; if (const FunctionDecl FD = dyn_cast<FunctionDecl(D)) {
在找到函数声明后,你需要检索函数的名称你还需要确保名称不为空: std::string Name = FD->getNameInfo().getName().getAsString(); assert(Name.length() > 0 && "Unexpected empty identifier");
如果函数名称不以小写字母开头,那么你发现了命名规则的违规: char &First = Name.at(0); if (!(First >= 'a' && First <= 'z')) {
要发出警告,你需要一个 DiagnosticsEngine 实例此外,你需要一个消息 ID在 clang 内部,消息 ID 被定义为一个枚举因为你的插件不是 clang 的一部分,你需要创建一个自定义 ID,然后使用它来发出警告: DiagnosticsEngine &Diag = CI.getDiagnostics(); unsigned ID = Diag.getCustomDiagID( DiagnosticsEngine::Warning, "Function name should start with " "lowercase letter"); Diag.Report(FD->getLocation(), ID);
除了关闭所有打开的大括号,你需要从这个函数返回 true 以表示可以继续处理: } } } return true; }};
接下来,你需要创建 PluginASTAction 子类,它实现了 clang 调用的接口:class PluginNamingAction : public PluginASTAction {public:
你必须实现的第一个方法是 CreateASTConsumer(),它返回你的 NamingASTConsumer 类的实例这个方法由 clang 调用,传递的 CompilerInstance 实例让你可以访问编译器的所有重要类以下代码演示了这一点: std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef file) override { return std::make_unique<NamingASTConsumer>(CI); }
插件还可以访问命令行选项你的插件没有命令行参数,你只会返回 true 以表示成功: bool ParseArgs(const CompilerInstance &CI, const std::vector<std::string> &args) override { return true; }
插件的操作类型描述了何时调用操作默认值是 Cmdline,这意味着必须在命令行上命名插件才能调用它你需要重写方法并更改值为 AddAfterMainAction,这将自动运行操作: PluginASTAction::ActionType getActionType() override { return AddAfterMainAction; }
你的 PluginNamingAction 类的实现完成了;缺少的只是类和匿名命名空间的关闭大括号按如下方式将它们添加到代码中:};}
最后,需要注册插件第一个参数是插件的名称,而第二个参数是帮助文本:static FrontendPluginRegistry::Add<PluginNamingAction> X("naming-plugin", "naming plugin");
这完成了插件的实现要编译插件,请在与 IconvChecker.cpp 相同的目录中创建 CMakeLists.txt 文件的构建描述插件位于 Clang 源代码树之外,因此你需要设置一个完整的项目你可以通过按照以下步骤来完成:首先定义所需的 CMake 版本和项目名称:cmake_minimum_required(VERSION 3.20.0)project(naminglugin)
接下来,包含 LLVM 文件如果 CMake 无法自动找到文件,则需要设置 LLVM_DIR 变量,使其指向包含 CMake 文件的 LLVM 目录:find_package(LLVM REQUIRED CONFIG)
将包含 CMake 文件的 LLVM 目录添加到搜索路径,并包含一些所需的模块:list(APPEND CMAKE_MODULE_PATH ${LLVM_DIR})include(AddLLVM)include(HandleLLVMOptions)
然后,加载 Clang 的 CMake 定义如果 CMake 无法自动找到文件,则需要设置 Clang_DIR 变量,使其指向包含 CMake 文件的 Clang 目录:find_package(Clang REQUIRED)
接下来,定义头文件和库文件的位置,并确定要使用的定义:include_directories("${LLVM_INCLUDE_DIR}" "${CLANG_INCLUDE_DIRS}")add_definitions("${LLVM_DEFINITIONS}")link_directories("${LLVM_LIBRARY_DIR}")
前面的定义设置了构建环境插入以下命令,它定义了你的插件的名称、插件的源文件,并指出它是一个 Clang 插件:add_llvm_library(NamingPlugin MODULE NamingPlugin.cpp PLUGIN_TOOL clang)
在 Windows 上,插件支持与 Unix 不同,必须链接所需的 LLVM 和 Clang 库以下代码确保了这一点:if(WIN32 OR CYGWIN) set(LLVM_LINK_COMPONENTS Support) clang_target_link_libraries(NamingPlugin PRIVATE clangAST clangBasic clangFrontend clangLex)endif()
现在,我们可以配置并构建插件,假设 CMAKE_GENERATOR 和 CMAKE_BUILD_TYPE 环境变量已设置:$ cmake -DLLVM_DIR=~/LLVM/llvm-17/lib/cmake/llvm \ -DClang_DIR=~/LLVM/llvm-17/lib/cmake/clang \ -B build$ cmake --build build
这些步骤在构建目录中创建了 NamingPlugin.so 共享库要测试插件,请将以下源代码保存为 naming.c 文件函数名称 Func1 违反了命名规则,但 main 名称没有:int Func1() { return 0; }int main() { return Func1(); }
要调用插件,你需要指定 –fplugin= 选项:$ clang -fplugin=build/NamingPlugin.so naming.cnaming.c:1:5: warning: Function name should start with lowercase letterint Func1() { return 0; } ^1 warning generated.
这种调用方式需要你重写 PluginASTAction 类的 getActionType() 方法,并返回一个不同于默认 Cmdline 值的值如果你没有这样做 - 例如,因为你想要对插件操作的调用有更多的控制 - 那么你可以按如下方式从编译器命令行运行插件:$ clang -cc1 -load ./NamingPlugin.so -plugin naming-plugin\ naming.c
恭喜 - 你已经构建了你的第一个 Clang 插件这种方法的缺点是它有一些限制ASTConsumer 类有不同的入口方法,但它们都是粗粒度的这可以通过使用 RecursiveASTVisitor 类来解决这个类遍历所有 AST 节点,你可以覆盖你感兴趣的 VisitXXX() 方法你可以通过按照以下步骤将插件重写为使用访问者:你需要一个额外的 include,用于 RecursiveASTVisitor 类的定义按如下方式插入:
#include "clang/AST/RecursiveASTVisitor.h"
然后,在匿名命名空间中定义访问者作为第一个类你将只存储对 AST 上下文的引用,这将让你访问所有重要的 AST 操作方法,包括 DiagnosticsEngine 实例,这是发出警告所需的:class NamingVisitor : public RecursiveASTVisitor<NamingVisitor> {private: ASTContext &ASTCtx;public: explicit NamingVisitor(CompilerInstance &CI) : ASTCtx(CI.getASTContext()) {}
在遍历过程中,每当发现函数声明时,都会调用 VisitFunctionDecl() 方法将 HandleTopLevelDecl() 函数内部的内层循环的主体复制到这里: virtual bool VisitFunctionDecl(FunctionDecl FD) { std::string Name = FD->getNameInfo().getName().getAsString(); assert(Name.length() > 0 && "Unexpected empty identifier"); char &First = Name.at(0); if (!(First >= 'a' && First <= 'z')) { DiagnosticsEngine &Diag = ASTCtx.getDiagnostics(); unsigned ID = Diag.getCustomDiagID( DiagnosticsEngine::Warning, "Function name should start with " "lowercase letter"); Diag.Report(FD->getLocation(), ID); } return true; }};
这完成了访问者的实现在你的 NamingASTConsumer 类中,你现在只需要存储一个访问者实例: std::unique_ptr<NamingVisitor> Visitor;public: NamingASTConsumer(CompilerInstance &CI) : Visitor(std::make_unique<NamingVisitor>(CI)) {}
移除 HandleTopLevelDecl() 方法 - 功能现在在访问者类中,所以你需要重写 HandleTranslationUnit() 方法这个类针对每个翻译单元调用一次你将在这里开始 AST 遍历: void HandleTranslationUnit(ASTContext &ASTCtx) override { Visitor->TraverseDecl( ASTCtx.getTranslationUnitDecl()); }
这个新实现具有相同的功能优点是它更容易扩展例如,如果你想检查变量声明,那么你必须实现 VisitVarDecl() 方法或者,如果你想处理语句,那么你必须实现 VisitStmt() 方法通过这种方法,你有了一个访问者方法,用于 C、C++ 和 Objective-C 语言的每个实体拥有对 AST 的访问权限,你可以构建执行复杂任务的插件正如本节所描述的,强制执行命名约定是 Clang 的一个有用补充作为插件,你也可以实现计算软件度量,例如圈复杂度你也可以添加或替换 AST 节点,允许你,例如,添加运行时插桩添加插件允许你以你需要的方式扩展 Clang总结 在本章中,你学习了如何应用各种 sanitizers你使用地址 sanitizer 检测指针错误,使用内存 sanitizer 检测未初始化的内存访问,并使用线程 sanitizer 执行数据竞争应用程序错误通常由格式错误的输入触发,你实现了模糊测试,以随机数据测试你的应用程序你还使用 XRay 对应用程序进行了检测,以识别性能瓶颈,并学习了如何以各种方式可视化数据本章还教你如何利用 Clang 静态分析器通过解释源代码来识别潜在错误,以及如何创建你自己的 Clang 插件这些技能将帮助你提高所构建应用程序的质量,因为在应用程序用户抱怨之前发现运行时错误肯定是好的应用你在本章中学到的知识,你不仅可以找到广泛的常见错误,还可以通过新功能扩展 Clang在下一章中,你将学习如何向 LLVM 添加新的后端(图片来源网络,侵删)
0 评论