在 Gnu 调试器下运行 GHC 程序
GHC 编译器为debugging programs under GDB 提供了一些支持。如果你有一个程序:
-- Quot.hs
{-# NOINLINE quot2 #-}
quot2 :: Int -> Int
quot2 n = n `quot` 2
main = print $ quot2 10
您可以使用-g 标志编译它,并使用上面链接中的说明在 GDB 下运行它:
$ ghc -O2 -g -rtsopts Quot.hs # Note: I'm using GHC 8.6.5
$ gdb --args ./Quot +RTS -V0
可以在quot2 n = n `quot` 2这一行设置断点,反汇编,单步执行相关代码:
(gdb) break Quot.hs:5
Breakpoint 1 at 0x4062c0: file Quot.hs, line 5.
(gdb) run
Starting program: /u/buhr/src/haskell/Quot +RTS -V0
...
Breakpoint 1, Main_zdwquot2_info () at Quot.hs:5
(gdb) display/10i $rip
1: x/10i $rip
=> 0x4062c0 <Main_zdwquot2_info>: mov %r14,%rax
0x4062c3 <Main_zdwquot2_info+3>: shr $0x3f,%rax
0x4062c7 <Main_zdwquot2_info+7>: mov %r14,%rbx
0x4062ca <Main_zdwquot2_info+10>: add %rax,%rbx
0x4062cd <Main_zdwquot2_info+13>: sar %rbx
0x4062d0 <Main_zdwquot2_info+16>: jmpq *0x0(%rbp)
...
(gdb) p $r14
$1 = 10
(gdb)
(这里的Main_zdwquot2_info 符号是一个错位的名称。有关这方面的一些信息,请参见下文。)无论如何,从这个反汇编中您可以看到优化的quot2 代码使用几个右移来执行等效的:
((n < 0 : 1 ? 0) + n) >> 1
将负数截断到零。
如果改用div,代码将如下所示:
1: x/10i $rip
=> 0x4062c0 <Main_zdwdiv2_info>: mov %r14,%rbx
0x4062c3 <Main_zdwdiv2_info+3>: sar %rbx
0x4062c6 <Main_zdwdiv2_info+6>: jmpq *0x0(%rbp)
...
所以,即使我会说你无法从反汇编 GHC 代码中学到任何东西,我想我错了。我们已经了解到:
转储 GHC 中间表格
然而,使用 GDB 跟踪 Haskell 代码执行仍然真的困难。如果您在主入口点(通常称为Main_main_info)设置了断点:
(gdb) break Main_main_info
Breakpoint 2 at 0x4063c8: file Div.hs, line 7.
(gdb) run
...
Breakpoint 2, Main_main_info () at Div.hs:7
1: x/10i $rip
=> 0x4063c8 <Main_main_info>: mov $0x4ac622,%edi
0x4063cd <Main_main_info+5>: mov $0x4a4348,%esi
0x4063d2 <Main_main_info+10>: mov $0x4a73b8,%r14d
0x4063d8 <Main_main_info+16>: jmpq 0x433de8 <base_GHCziIOziHandleziText_hPutStrzq_info>
...
很容易迷路。这里,$0x4ac622 是 Haskell 常量 True 的闭包,$0x4a4348 是 quot2 10 结果的可打印字符串表示形式的闭包,$0x4a73b8 是 stdout 的闭包,所有这些都设置为GHC.IO.Handle.Text.hPutStr' 来电。
在 GHC 代码中,函数是用jmpq 调用的,而不是callq,所以你不能真正通过(gdb) next 调用你对跟踪不感兴趣的调用。此外,由于惰性评估模型,在调用函数之前不会评估参数,因此要从头开始跟踪 quot2 10 调用的执行,您需要跟踪 hPutStr' 的执行以找到它强制对结果的可打印表示的闭包进行评估,然后跟踪对该闭包的评估以查看它在何处强制对 quot2 10 本身进行评估。
另一个问题是与原始 Haskell 代码相比,该程序集确实低级。尽管您可能能够学到一些关于“微优化”的知识,例如 `div` 2 是如何实现的,但大多数 Haskell 性能问题是由更高级别的低效率引起的(例如,不必要的值装箱、列表融合失败等) .从 GDB 会话中诊断这些问题非常困难。
因此,大多数寻求理解 GHC 编译代码的人都会查看各种形式的中间编译器输出。正如您可能从研究中发现的那样,GHC 编译器通过中间形式执行许多转换。这些可以使用一系列-ddump-xxx 标志转储。通常,有很多不必要的细节,因此添加标志 -dsuppress-all 有时还添加标志 -dsuppress-uniques 有助于降低噪音。此外,-fforce-recomp 在多次运行ghc 以查看不同的形式时很有帮助,因为即使源没有更改,它也会强制重新编译(并转储所需的输出)。因此,您可以使用以下方式转储脱糖过程的输出:
ghc -O2 -fforce-recomp -ddump-ds -dsuppress-all -dsuppress-uniques Quot.hs
无论如何,这里是最感兴趣的中间形式:
- 原始源代码已“脱糖”到 Core (
-ddump-ds),它是 Haskell 语言的简化版本,仍然可以识别为 Haskell。此时尚未执行任何优化,因此此转储对于了解 Core 语法以及它与“真正的”Haskell 有何不同非常有用。 Core 的一些最佳文档位于 GHC 源代码树中 - 请参阅 ghc/compiler/coreSyn/CoreSyn.hs 和 Expr 类型的定义。
- 大多数高级优化都是通过应用核心到核心的转换函数在核心上执行的。通过研究简化器 (
-ddump-simpl) 产生的核心输出,您可以了解很多关于程序可能性能的信息。
- 简化后的 Core 转换为 STG (
-ddump-stg)。与 Core 不同,STG 显然是“不是 Haskell”。它是一种无类型的函数式语言,最能代表 GHC 程序“实际运行”的方式。将您面前的 STG 作为路线图/参考,您可能可以处理较低级别的表示,包括使用调试器逐步执行反汇编代码,并了解它实际在做什么。我发现了解 STG 的最佳参考是论文 How to make a fast curry。在论文中,STG 基本上是图 1 中描述的语言,生成的汇编代码对应于图 2 中的“评估规则”。您可能还会发现I know kung fu: learning STG by example 有帮助,尽管我认为那里的示例已经过时了。李>
- 在 STG 之下,较低级别的形式是 CMM (
-ddump-cmm) 和程序集 (-ddump-asm)。 CMM 是一种类似 C 的汇编语言。我没有找到任何特别好的/准确的文档,尽管它通常比汇编更容易理解,而汇编通常比 GDB 下的反汇编更容易理解,因为它包含调试信息中缺少的其他符号.通过将 GHC 转储的程序集与我在 GDB 下看到的程序集进行比较,我能够识别上述示例中 True 等的闭包。
我发现我通常必须并排查看 STG、CMM 和程序集,才能弄清楚某些 GHC 代码是如何工作的。
所以,也许这会让你开始。
名称修改
如上所述,您将在 GDB 中看到的可执行文件中的名称已从转储编译器输出(包括 -ddump-asm)中的表单中删除。我不知道这是否记录在任何地方,但这是我从 GHC 源代码 (ghc/compiler/utils/Encoding.hs) 获得的表格:
'(' ="ZL" 'Z' ="ZZ" '$' ="zd" '<' ="zl" '\\'="zr"
')' ="ZR" 'z' ="zz" '=' ="ze" '-' ="zm" '/' ="zs"
'[' ="ZM" '&' ="za" '>' ="zg" '!' ="zn" '*' ="zt"
']' ="ZN" '|' ="zb" '#' ="zh" '+' ="zp" '_' ="zu"
':' ="ZC" '^' ="zc" '.' ="zi" '\''="zq" '%' ="zv"
所以 GDB 中的名称 Main_zdwquot2_info 对应于转储 GHC 输出中的 Main_$wquot2_info。那看起来仍然很混乱,但那是因为它是一个“系统名称”。正如 ghc/compiler/basicTypes/OccName 中的 cmets 中所述:
Making System Names
-------------------
Here's our convention for splitting up the interface file name space:
d... dictionary identifiers
(local variables, so no name-clash worries)
All of these other OccNames contain a mixture of alphabetic
and symbolic characters, and hence cannot possibly clash with
a user-written type or function name
$f... Dict-fun identifiers (from inst decls)
$dmop Default method for 'op'
$pnC n'th superclass selector for class C
$wf Worker for function 'f'
$sf.. Specialised version of f
D:C Data constructor for dictionary for class C
NTCo:T Coercion connecting newtype T with its representation type
TFCo:R Coercion connecting a data family to its representation type R
所以上面的函数是quot2 函数的“工人”。通常,对装箱整数进行操作的函数会有一个对未装箱整数进行操作的“worker”对应项。在我们的 Quot.hs 示例中,只需要这个未装箱的工作函数,因此在最终的编译器输出中删除了原始的 quot2 函数。