别人的精品

debug Swift compiler

2018-10-11  本文已影响103人  sea_biscute

简介

作为工程师,我们几乎花费了70%时间来调试,剩下的20%来思考架构和团队协作,只有10%的时间来用写码.

Debugging is like being the detective in a crime movie where you are also the murderer.
Filipe Fortes via Twitter

下面我们会对如何调试表一起和编译器的输出指令进行说明。

在遇到一些方法分派的问题时,因为编译器实现方式不同,导致调用结果不符合预期的"bug",都可以通过对编译中间结果的查看来追踪问题.下面我们除了讲解几种调试方式,还会针对Swift常见的几个"bug",从编译器的角度进行说明.

基础工具

通常调试一个crash追踪或者构建log的编译器问题的第一步是通过命令行重新运行编译器。
utils/dev-scripts内的split-cmdline脚本将命令行拆分成了多个部分。这样用于理解和编辑比较长的指令。

打印中间语言

调试编译器最重要的环节就是检查IR。 以下是如何在编译器的主流程中输出IR的指令:

  1. 语法分析:打印语法分析后的AST:lisp
swiftc -dump-ast -O file.swift
  1. SILGen:在SILGen之后立刻打印SIL
swiftc -emit-silgen -O file.swift

what is dmangle??

  1. 强制SIL检测:打印强制检测后的SIL
swiftc -emit-sil -Onone file.swift

其他指令

  1. SIL性能检查:打印SIL通道优化完成之后的SIL
swiftc -emit-sil -O file.swift
  1. IRGen:在IR生成之后打印LLVM IR
swiftc -emit-ir -Xfrontend -disable-llvm-optzns -O file.swift
  1. LLVM检测:打印LLVM检测之后的LLVM IR
swiftc -emit-ir -O file.swift
  1. 生成代码:打印最终生成的代码
swiftc -S -O file.swift

编译器会在打印完对应阶段后终止。所以如果想打印SIL和LLVM IR,需要运行编译器两次。

调试类型检查

允许打印

可以通过以下参数来允许类型检查的打印-Xfrontend -debug-constraints。使用该指令会打印类型检查器内部的状态,打印已经解决的约束,展示最终的检查结果:

---Constraint solving for the expression at [test.swift:3:10 - line:3:10]---
---Initial constraints for the given expression---
(integer_literal_expr type='$T0' location=test.swift:3:10 range=[test.swift:3:10 - line:3:10] value=0)
Score: 0 0 0 0 0 0 0 0 0 0 0 0 0
Contextual Type: Int
Type Variables:
  #0 = $T0 [inout allowed]

Active Constraints:

Inactive Constraints:
  $T0 literal conforms to ExpressibleByIntegerLiteral [[locator@0x7ffa3a865a00 [IntegerLiteral@test.swift:3:10]]];
  $T0 conv Int [[locator@0x7ffa3a865a00 [IntegerLiteral@test.swift:3:10]]];
($T0 literal=3 bindings=(subtypes of) (default from ExpressibleByIntegerLiteral) Int)
Active bindings: $T0 := Int
(trying $T0 := Int
  (found solution 0 0 0 0 0 0 0 0 0 0 0 0 0)
)
---Solution---
Fixed score: 0 0 0 0 0 0 0 0 0 0 0 0 0
Type variables:
  $T0 as Int

Overload choices:

Constraint restrictions:

Disjunction choices:

Conformances:
  At locator@0x7ffa3a865a00 [IntegerLiteral@test.swift:3:10]
(normal_conformance type=Int protocol=ExpressibleByIntegerLiteral lazy
  (normal_conformance type=Int protocol=_ExpressibleByBuiltinIntegerLiteral lazy))
(found solution 0 0 0 0 0 0 0 0 0 0 0 0 0)
---Type-checked expression---
(call_expr implicit type='Int' location=test.swift:3:10 range=[test.swift:3:10 - line:3:10] arg_labels=_builtinIntegerLiteral:
  (constructor_ref_call_expr implicit type='(_MaxBuiltinIntegerType) -> Int' location=test.swift:3:10 range=[test.swift:3:10 - line:3:10]
    (declref_expr implicit type='(Int.Type) -> (_MaxBuiltinIntegerType) -> Int' location=test.swift:3:10 range=[test.swift:3:10 - line:3:10] decl=Swift.(file).Int.init(_builtinIntegerLiteral:) function_ref=single)
    (type_expr implicit type='Int.Type' location=test.swift:3:10 range=[test.swift:3:10 - line:3:10] typerepr='Int'))
  (tuple_expr implicit type='(_builtinIntegerLiteral: Int2048)' location=test.swift:3:10 range=[test.swift:3:10 - line:3:10] names=_builtinIntegerLiteral
    (integer_literal_expr type='Int2048' location=test.swift:3:10 range=[test.swift:3:10 - line:3:10] value=0)))

在使用整体的Swift的REPL(read-eval-print loop: 读取-求值-输出 循环),可以输出每个表达式的结果,这个结果和通过:constraints debug on指令允许约束打印的结果相同:

$ swift -frontend -repl -enable-objc-interop -module-name REPL
***  You are running Swift's integrated REPL,  ***
***  intended for compiler and stdlib          ***
***  development and testing purposes only.    ***
***  The full REPL is built as part of LLDB.   ***
***  Type ':help' for assistance.              ***
(swift) :constraints debug on

捕获第一个错误的

在修改类型检查器时,会引发一系列的连锁错误。因为Swift不会抛出错误,所以开发者需要对类型检查器足够理解,通过判断觉得如何停止调试器。比起这样,开发者还可以使用条件-Xllvm -swift-diagnostics-assert-on-error=1来唤起DiagnosticsEngine诊断引擎抛出第一个错误,用于向开发者提供捕获的信息。

调试SIL的级别

SIL输出指令选项

SILPassManager提供了有效的选项用于输出各阶段的SIL
-Xllvm -sil-print-all选项用于输出全部检测之后的整个SIL组件。尽管打印结果只是检测后被修改的函数,但是输出非常巨大.
可以通过函数名过滤输出信息:-Xllvm -sil-print-only-function/s或指定区段-Xllvm -sil-print-before/after/around.
更多信息可以参考PassManager.cpp.

在LLDB中输出SIL和其他数据

使用LLDB调试Swift编译器,可以发挥非常强大的检测编译数据(比如SIL)能力.遵循LLVM dump()协议,很多SIL类(包括AST类)提供了dump()方法.可以通过LLDB的expression --,print或者p指令来调用dump()方法.
例如,检测SIL结构体

(lldb) p Inst->dump()
%12 = struct_extract %10 : $UnsafeMutablePointer<X>, #UnsafeMutablePointer._rawValue // user: %13

输出检测后的整个方法

(lldb) p getFunction()->dump()

SIL组件和方法都可能非常大,所以将结果输出到一个文件内更为方便

(lldb) p getFunction()->dump("myfunction.sil")

也可以输出方法的CFG(control flow graph: 控制流程图)

(lldb) p Func->viewCFG()

这个指令会打开一个包含函数CFG的预览窗口.

在SIL层调试和推断

如果想要SIL调试需要同时增加 front-end option -gsil 和 -g :

swiftc -g -Xfrontend -gsil -O test.swift -o a.out

以上指令会在优化之后将SIL写入一个文件,并且生成相关的调试信息.在调试器内看到的是SIL代码而非Swift源码.详情可以查看SILDebugInfoGenerator.

如果想调试Swift标准库,可以使用 build-script-impl的选项--build-sil-debugging-stdlib.

ViewCFG: 基于CFG打印的正则表达式

ViewCFG(./utils/viewcfg)是一个脚本,作用是解析文本的CFG然后通过.dot格式展示.解析是通过正则表达式完成的. ViewCFG具有以下能力

  1. 解析SIL和LLVM IR
  2. 解析block和函数,不需要知晓上下文信息.(类型或者声明等信息)

使用断点

LLDB具有强大的断点能力.下面通过LLDB在命令行的使用示例来说明.
有时我们在查看SIL输出的函数时,想要知道函数是在编译器哪里创造的.这时就可以SILFunction结构体设置一个断点.

(lldb) br set -c 'hasName("_TFC3nix1Xd")' -f SILFunction.cpp -l 91

如果想知道插入了哪些优化,移除或者移动了哪些说明,可以使用在SILInstruction.cpp内的ilist_traits<SILInstruction>::addNodeToListilist_traits<SILInstruction>::removeNodeFromList来设置断点.以下示例是在strong_retain说明被移除时设置断点的指令

(lldb) br set -c 'I->getKind() == ValueKind::StrongRetainInst' -f SILInstruction.cpp -l 63

还可以测试是在哪个方法内发生的

(lldb) br set -c 'I->getKind() == ValueKind::StrongRetainInst &&
           I->getFunction()->hasName("_TFC3nix1Xd")'
           -f SILInstruction.cpp -l 63
introduction to sIL
default auguments sil summary

参考资料

上一篇下一篇

猜你喜欢

热点阅读