报错排查
LLVM的报错排查,修复问题是一个很恐怖的事情,因为LLVM本身太庞大了,程序内输出+重新编译运行,每一次的调整需要等待重编译非常久,很浪费时间;搭建调试环境较麻烦,且调试本身吃电脑配置,如果是使用VS等IDE以工程模式+clang编译出现报错,连调试都无从下手。故本文介绍一些笔者所用的报错排查优化技巧
-v
如果clang出现未捕获的异常导致的崩溃,就会出现类似这样的报错,在Release版本中不会捕获内部异常输出堆栈,所以Release下的报错基本都是这个
clang-cl : error : clang frontend command failed due to signal (use -v to see invocation)
但是我实际测试,在只有这一句报错没有其他更多信息的情况下,传递verbose参数依旧没有更多的输出,应该是因为clang在一些异常崩溃的时候没有时机去走verbose的输出了或者说输出被截断了。
-###
这个是LLVM/Clang的特有调试选项,加上这个选项后,它会完整显示底层实际执行的命令,并且只打印命令,不执行编译
这个命令专门用于解决一些IDE+clang的场景下,在IDE里点击编译根本不知道底层实际实行了什么编译命令的问题。比如使用VisualStudio编译一个大工程,在VS里将编译工具链配置成clang,然后点击编译,VS就会自己根据工程的配置构造命令行传递到clang程序里,这个部分对于用户是透明的,即你不知道在用vs编译这个工程究竟会执行哪些编译命令。不知道编译命令就无法完全复现同样的环境去调试clang。
我们可以尝试在VisualStudio中配置这个选项,Project -> Properties -> C/C++ -> Command Line -> Additional Options 添加 -###
会输出类似如下的信息:
Build started...
1>------ Build started: Project: test, Configuration: Release x64 ------
1>clang version 19.0.0git QVMProtect Release
1>Target: x86_64-pc-windows-msvc
1>Thread model: posix
1>InstalledDir: D:\LLVM\WindowsRelWithDebInfo\bin
1> (in-process)
1> "D:\\LLVM\\WindowsRelWithDebInfo\\bin\\clang-cl.exe" "-cc1" "-triple" "x86_64-pc-windows-msvc19.29.30151" "-emit-obj" "-mrelax-all" "-mincremental-linker-compatible" "-disable-free" "-clear-ast-before-backend" "-disable-llvm-verifier" "-discard-value-names" "-main-file-name" "MyUtils.cpp" "-mrelocation-model" "pic" "-pic-level" "2" "-mframe-pointer=none" "-relaxed-aliasing" "-fmath-errno" "-ffp-contract=on" "-fno-rounding-math" "-mconstructor-aliases" "-fms-volatile" "-funwind-tables=2" "-target-cpu" "x86-64" "-target-feature" "+avx" "-target-feature" "+avx2" "-mllvm" "-x86-asm-syntax=intel" "-tune-cpu" "generic" "-D_MT" "-flto-visibility-public-std" "--dependent-lib=libcmt" "--dependent-lib=oldnames" "-stack-protector" "2" "-fcxx-exceptions" "-fexceptions" "-fexternc-nounwind" "-fdefault-calling-conv=cdecl" "-fdiagnostics-format" "msvc" "-gno-column-info" "-gcodeview" "-debug-info-kind=constructor" "-fdebug-compilation-dir=E:\\code\\test" "-object-file-name=E:\\code\\test\\x64\\Release\\MyUtils.obj" "-ffunction-sections" "-fcoverage-compilation-dir=E:\\code\\test" "-resource-dir" "D:\\LLVM\\WindowsRelWithDebInfo\\lib\\clang\\19" "-D" "NDEBUG" "-D" "_CONSOLE" "-D" "_UNICODE" "-D" "UNICODE" "-internal-isystem" "D:\\LLVM\\WindowsRelWithDebInfo\\lib\\clang\\19\\include" "-internal-isystem" "C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\Professional\\VC\\Tools\\MSVC\\14.29.30133\\include" "-internal-isystem" "C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\Professional\\VC\\Tools\\MSVC\\14.29.30133\\atlmfc\\include" "-internal-isystem" "C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\Professional\\VC\\Auxiliary\\VS\\include" "-internal-isystem" "C:\\Program Files (x86)\\Windows Kits\\10\\Include\\10.0.19041.0\\ucrt" "-internal-isystem" "C:\\Program Files (x86)\\Windows Kits\\10\\Include\\10.0.19041.0\\um" "-internal-isystem" "C:\\Program Files (x86)\\Windows Kits\\10\\Include\\10.0.19041.0\\shared" "-internal-isystem" "C:\\Program Files (x86)\\Windows Kits\\10\\Include\\10.0.19041.0\\winrt" "-internal-isystem" "C:\\Program Files (x86)\\Windows Kits\\10\\Include\\10.0.19041.0\\cppwinrt" "-internal-isystem" "C:\\Program Files (x86)\\Windows Kits\\NETFXSDK\\4.8\\Include\\um" "-internal-isystem" "C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\Professional\\VC\\Tools\\MSVC\\14.29.30133\\include" "-internal-isystem" "C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\Professional\\VC\\Tools\\MSVC\\14.29.30133\\atlmfc\\include" "-internal-isystem" "C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\Professional\\VC\\Auxiliary\\VS\\include" "-internal-isystem" "C:\\Program Files (x86)\\Windows Kits\\10\\Include\\10.0.19041.0\\ucrt" "-internal-isystem" "C:\\Program Files (x86)\\Windows Kits\\10\\Include\\10.0.19041.0\\um" "-internal-isystem" "C:\\Program Files (x86)\\Windows Kits\\10\\Include\\10.0.19041.0\\shared" "-internal-isystem" "C:\\Program Files (x86)\\Windows Kits\\10\\Include\\10.0.19041.0\\winrt" "-internal-isystem" "C:\\Program Files (x86)\\Windows Kits\\10\\Include\\10.0.19041.0\\cppwinrt" "-internal-isystem" "C:\\Program Files (x86)\\Windows Kits\\NETFXSDK\\4.8\\Include\\um" "-O0" "-Wall" "-Wno-error" "-Wsystem-headers" "-fdeprecated-macro" "-ferror-limit" "19" "-fno-use-cxa-atexit" "-fms-extensions" "-fms-compatibility" "-fms-compatibility-version=19.29.30151" "-std=c++20" "-fno-implicit-modules" "-fskip-odr-check-in-gmf" "-fno-caret-diagnostics" "-faddrsig" "-o" "x64\\Release\\MyUtils.obj" "-x" "c++" "MyUtils.cpp"
in-process后面的这一大串就是VisualStudio构造完的完整编译命令,我们可以直接手动调用这个命令去编译,这也意味着我们可以调试启动clang并完美复现在vs里编译工程时的环境。然后我们就可以用 LLVM调试环境配置 里的方法开始调试
print-after-all
print-after-all、print-before-all、print-after、print-before,是LLVM内置的一个调试工具,添加参数后,会在编译阶段的每个pass前后打印出IR的完整状态。用法:
clang: -mllvm -print-after-all
opt: --print-after-all
得到类似这样的效果
1>; *** IR Dump After AnnotationRemarksPass on scanf ***
1>; Function Attrs: mustprogress noinline nounwind optnone sspstrong uwtable
1>define linkonce_odr dso_local i32 @scanf(ptr noundef %0, ...) #2 comdat !dbg !494 {
1> %2 = alloca ptr, align 8
1> %3 = alloca i32, align 4
1> %4 = alloca ptr, align 8
1> store ptr %0, ptr %2, align 8
1> call void @llvm.dbg.declare(metadata ptr %2, metadata !1248, metadata !DIExpression()), !dbg !1249
1> call void @llvm.dbg.declare(metadata ptr %3, metadata !1250, metadata !DIExpression()), !dbg !1251
1> call void @llvm.dbg.declare(metadata ptr %4, metadata !1252, metadata !DIExpression()), !dbg !1253
1> call void @llvm.va_start(ptr %4), !dbg !1254
1> %5 = load ptr, ptr %4, align 8, !dbg !1255
1> %6 = load ptr, ptr %2, align 8, !dbg !1255
1> %7 = call ptr @__acrt_iob_func(i32 noundef 0) #5, !dbg !1255
1> %8 = call i32 @_vfscanf_l(ptr noundef %7, ptr noundef %6, ptr noundef null, ptr noundef %5) #5, !dbg !1255
1> store i32 %8, ptr %3, align 4, !dbg !1255
1> call void @llvm.va_end(ptr %4), !dbg !1256
1> %9 = load i32, ptr %3, align 4, !dbg !1257
1> ret i32 %9, !dbg !1257
1>}
1>; *** IR Dump After AnnotationRemarksPass on printf ***
1>; Function Attrs: mustprogress noinline nounwind optnone sspstrong uwtable
1>define linkonce_odr dso_local i32 @printf(ptr noundef %0, ...) #2 comdat !dbg !481 {
1> %2 = alloca ptr, align 8
1> %3 = alloca i32, align 4
1> %4 = alloca ptr, align 8
1> store ptr %0, ptr %2, align 8
1> call void @llvm.dbg.declare(metadata ptr %2, metadata !1258, metadata !DIExpression()), !dbg !1259
1> call void @llvm.dbg.declare(metadata ptr %3, metadata !1260, metadata !DIExpression()), !dbg !1261
1> call void @llvm.dbg.declare(metadata ptr %4, metadata !1262, metadata !DIExpression()), !dbg !1263
1> call void @llvm.va_start(ptr %4), !dbg !1264
1> %5 = load ptr, ptr %4, align 8, !dbg !1265
1> %6 = load ptr, ptr %2, align 8, !dbg !1265
1> %7 = call ptr @__acrt_iob_func(i32 noundef 1) #5, !dbg !1265
1> %8 = call i32 @_vfprintf_l(ptr noundef %7, ptr noundef %6, ptr noundef null, ptr noundef %5) #5, !dbg !1265
1> store i32 %8, ptr %3, align 4, !dbg !1265
1> call void @llvm.va_end(ptr %4), !dbg !1266
1> %9 = load i32, ptr %3, align 4, !dbg !1267
1> ret i32 %9, !dbg !1267
我们一般用的比较多的是print-after-all,它会在每个pass执行后对每个Function或Module做dump,我们可以依此结果分析IR的状态来判断我们的pass是否正确。但如果是在一个较大的工程里添加这个参数的话,会产生大量大量的输出,以致于找不到要分析的地方在哪里。LLVM添加了另一个参数帮助我们过滤需要分析的函数,比如添加以下参数后,将只会输出每个pass执行后main函数的IR状态
-mllvm -filter-print-funcs=main -mllvm -print-after-all
Pass内输出
返璞归真,笔者用的最多的其实还是直接输出log,只有极少数非常复杂的问题才会用其他技巧定位或者直接挂调试器单步分析。通过将pass编译成独立的模块,向其中插入修改输出的代码来定位问题,修改后只单独编译这个模块,感觉还是最快最方便的定位方式,具体方法参考这两篇文章
LLVM 编译与FirstPass
Windows下优雅使用LLVMPass
LLVM_DEBUGS dbgs()
这个配置我研究了很久没研究明白怎么用,怀疑是因为我的pass用的外部插件加载的形式所以用不了这个方法
首先这个宏只有在Debug模式编译的LLVM中才可以使用。使用这个方法需要首先在对应的.cpp上定义一个TAG,比如
|
|
然后在代码里需要输出的调试数据的内容用 LLVM_DEBUG 包装,比如
|
|
在编译的时候传参就可以获取输出
opt: -debug
clang: -mllvm -debug
但同样的,LLVM内部本身也有很多这样的封装输出,所以很难在大量的输出中找到我们需要的调试输出信息,为此才会需要用到前面定义的 DEBUG_TYPE
opt: --debug-only=IRPass
clang: -mllvm --debug-only=IRPass
用debug-only限制输出的TAG即可过滤其他我们不关心的输出
errs()
直接用 errs() 来输出,用法等同于dbgs(),但 errs() 会在任意的编译模式下均生效,约等于printf。返璞归真,我定位问题用的最多的还是这个,遇到问题的时候在可能的地方加输出定位