如何调试我们 LLVM 的代码 - 入门

Posted by 叉叉敌 on March 18, 2023

开篇

LLVM 是一个独立的大项目,可能需要有自己的调试文档,我似乎没有找到这样的文档,估计做 LLVM 都是的大佬或者是骨灰级的人物,不需要文档吧,注释就在代码中。

比如在开发一个 llvm 的 pass,或者一个 feature,在编译的过程中 crash 了,怎么去定位和修复的喃?

注意官方编译器禁用了 LLVM 断言,这意味着 LLVM 断言失败可能会显示为编译器崩溃(不是 ICE-内部编译错误,而是 “真正的”崩溃)和其他各种奇怪的行为。编译 llvm 的时候最好使能 assertion,在 cmake 的时候记得带上参数即可:assertions=true

尽量最小化例子

一般来说,编译器从分析代码中产生大量的信息。因此,一个有用的第一步通常是找到一个最小的例子。做到这一点的一个方法是

  1. 创建一个重现该问题的新 case(例如,将有问题的地方添加为依赖关系,并从这里开始)
  2. 通过移除外部依赖关系来最小化这个板块;也就是说,将所有相关的东西移到新文件中去
  3. 通过缩短代码来进一步减少问题(有一些工具可以帮助实现这一点,比如 creduce)。

在实际的工程中,是通过如下步骤,不过工程有一点差异,但是可以参考这个过程:

  1. 用编译器(gcc/clang,或者其他)编译 cpp 文件为一个 ll 文件
  2. 通过上面生成的 ll 文件,从中提取报错函数的最小 ll 文件
  3. 然后带上 mtriple,map,mcpu 等参数用 llc 来编译这个 ll 文件为汇编,一般在这个时候就会出问题了。
# 编译cpp为一个ll文件
clang -S -emit-llvm test.cpp

# 提取文件里面的该funtion有关的ir
llvm-ir-extractor test.ll --save-mode -name="functioName" &> new.ll

# 用llc来编译
llc -mtriple=amdgpucn -verify=machineinstrs new.ll

获得一个原始的输入文件

大致情况是把一个 crash 文件,一个 bc 文件转化为一个 ll 文件。

  • 如果在调用 LLVM 时遇到了来自 LLVM 后台的断言失败或分段故障,不妨尝试将这些 .bc 文件分别传递给 llc 命令,看看是否得到了同样的失败。(LLVM 的开发者通常更喜欢被简化为.bc 文件的 bug,而不是使用工具最小化再来复现)。
  • 为了得到人类可读的 LLVM bc 文件,人们只需要使用 llvm-dis 将位码(.bc)文件转换为.ll 文件。

如果只想在 LLVM pipeline 中获得 LLVM 的 IR,例如查看哪个 IR 导致优化时间断言失败,或者查看 LLVM 何时执行特定的优化,可以通过 llvm 标志位-print-after-all,这样就可以获得更多的报错信息,从而精准找到问题所在。

这将产生大量的输出到标准错误中,所以要把这些输出管到某个文件中。另外,如果既没有使用-filter-print-funcs,也没有使用-C codegen-units=1,那么,由于多个 codegen 单元并行运行,打印结果会混在一起,会有很多的无效信息,将无法阅读任何东西,或者很难找到问题所在。

如果是单个函数在一个 ll 文件或者是 bc 文件里面,这样用这个方法是好处理,但是有多个函数,估计就不好处理了,还可以继续精简么?

精简到只有这个对应函数和其依赖,这样就好定位处理了,同时也能把这个 case 加入到日常的看护中。

可以用到 2 个工具,一个是 llvm-extract/llvm-ir-extractor。

先说说第一个的用法,还是举个例子吧。

准备一个 ir 文件,在 x86 的机器上运行,内容如下:

declare {i32, i1} @llvm.sadd.with.overflow.i32(i32, i32)
declare {i32, i1} @llvm.uadd.with.overflow.i32(i32, i32)

; The immediate can be encoded in a smaller way if the
; instruction is a sub instead of an add.
define i32 @test1(i32 inreg %a) nounwind {
entry:
  %b = add i32 %a, 128
  ret i32 %b
}

define i32 @test1b(i32* %p) nounwind {
entry:
  %a = load i32, i32* %p
  %b = add i32 %a, 128
  ret i32 %b
}

这个 ir 文件里面,包含了 2 个函数,分别为 test1 和 test1b,现在需要把 test1 函数提出来,只需要这个函数。的 ll 文件。

llvm-extract \
    -func='test_add' \
    -S \
    < add.ll \
    > extracted.ll

执行上面命令后,查看 extracted.ll,这个文件里面的内容如下。

; ModuleID = '<stdin>'
source_filename = "<stdin>"

; Function Attrs: nounwind
define i32 @test1(i32 inreg %a) #0 {
entry:
  %b = add i32 %a, 128
  ret i32 %b
}

attributes #0 = { nounwind }

是不是简单了。今天就先来一个简单的,后面继续学习下一个关于优化 pass 的调试和定位。

更多阅读

github 博客

微信公众号:cdtfug,欢迎关注一起吹牛逼,也可以加微信号「xiaorik」朋友圈围观。