... | ... | @@ -2,25 +2,25 @@ |
|
|
As of 7.10, GHC has basic functionality to generate DWARF-compliant
|
|
|
debugging information with its binaries. This opens up a completely
|
|
|
new way of debugging or profiling Haskell programs: Instead of
|
|
|
instrumenting the program or the runtime system, we "simply" teach
|
|
|
standard debugging tools to make sense of a running Haskell program.
|
|
|
instrumenting the program or the runtime system, we teach
|
|
|
external debugging tools to make sense of a running Haskell program.
|
|
|
This means that we gain debugging and profiling capabilities in
|
|
|
situations where existing profiling approaches could not help us, such
|
|
|
as for crashes or code that we cannot meaningfully instrument.
|
|
|
|
|
|
|
|
|
There are a few caveats to this method. Firstly, GHC optimisations can
|
|
|
There are a few caveats to this method. GHC optimisations can
|
|
|
be very aggressive in reorganizing the code and avoiding redundancies
|
|
|
at runtime. This means that even with good heuristics, there will be
|
|
|
situations where information is lost. On the other hand, DWARF-based
|
|
|
situations where information is lost. DWARF-based
|
|
|
debugging tools also make assumptions about the code that are more
|
|
|
geared towards C-like languages, and get confused for Haskell programs.
|
|
|
|
|
|
|
|
|
Bottom line: While the infrastructure should be perfectly stable and
|
|
|
While the infrastructure should be perfectly stable and
|
|
|
safe to use, inspecting Haskell programs like this is still very much
|
|
|
"wild west". Having a good working knowledge of low-level Haskell
|
|
|
execution is definetely a good idea. We hope that some experience will
|
|
|
execution is definitely a good idea. We hope that some experience will
|
|
|
help us improve the situation.
|
|
|
|
|
|
## Basic Set-Up
|
... | ... | @@ -35,27 +35,26 @@ fib 0 = 0 |
|
|
fib 1 = 1
|
|
|
fib n = fib (n-1) + fib (n-2)
|
|
|
main :: IO ()
|
|
|
main = putStrln $ fib 20
|
|
|
main = print $ fib 20
|
|
|
```
|
|
|
|
|
|
|
|
|
Just like with GCC, the "magic" command line flag here is `-g`. This
|
|
|
causes GHC to generate DWARF sections into produced binaries:
|
|
|
Just like with GCC, the command line flag to generate DWARF sections is `-g`:
|
|
|
|
|
|
```wiki
|
|
|
ghc -O -g -rtsopts fib.hs
|
|
|
```
|
|
|
|
|
|
|
|
|
For Mac Os, debug information is actually kept separate from the
|
|
|
binary, so we first have to package it using `dsymutil` at this point:
|
|
|
For Mac Os, debug information is kept separate from the
|
|
|
binary, so we first have to package it using `dsymutil`:
|
|
|
|
|
|
```wiki
|
|
|
dsymutil fib
|
|
|
```
|
|
|
|
|
|
|
|
|
If we want, we can now check that DWARF information was generated
|
|
|
We can now check that DWARF information was generated
|
|
|
correctly using `objdump` or `dwarfdump` respectively:
|
|
|
|
|
|
```wiki
|
... | ... | @@ -74,14 +73,14 @@ TAG_compile_unit [1] * |
|
|
```
|
|
|
|
|
|
|
|
|
Which tells us that we have DWARF information about the compilation
|
|
|
This tells us that we have DWARF information about the compilation
|
|
|
unit `fib.hs`. Note that the Haskell language ID `0x18` is not
|
|
|
recognized by all tools yet, so you might see an "Unknown" there
|
|
|
instead.
|
|
|
|
|
|
|
|
|
It is important to realize that - in contrast to instrumentation -
|
|
|
adding DWARF debug information does not actually change the executable
|
|
|
adding DWARF debug information does not change the code
|
|
|
sections of the executable. In fact, we can strip them without
|
|
|
changing the performance characteristics of the program at all:
|
|
|
|
... | ... | @@ -113,7 +112,7 @@ real-life performance. |
|
|
|
|
|
|
|
|
At this point, we can simply invoke `gdb` and set breakpoints (TODO -
|
|
|
doesn't work on Mac yet):
|
|
|
doesn't quite work on Mac yet):
|
|
|
|
|
|
```wiki
|
|
|
gdb -q fib
|
... | ... | @@ -129,7 +128,7 @@ Breakpoint 1, Main_zdwfib_info () at fib.hs:3 |
|
|
```
|
|
|
|
|
|
|
|
|
At this point we have actually stopped the Haskell program at the
|
|
|
At this point we have stopped the Haskell program at the
|
|
|
given line. We can now step a bit further to see the program working
|
|
|
its magic:
|
|
|
|
... | ... | @@ -157,8 +156,9 @@ c3Vy_info () at fib.hs:4 |
|
|
```
|
|
|
|
|
|
|
|
|
Note that passing `+RTS -V0` as we did above is necessary for this to
|
|
|
work, as otherwise we would end up stepping into the RTS timer.
|
|
|
Note that passing `+RTS -V0` as program command line parameter is
|
|
|
necessary for this to work, as otherwise we would end up stepping into
|
|
|
the RTS timer.
|
|
|
|
|
|
|
|
|
Furthermore, we can back-trace to get an idea of where we were coming
|
... | ... | @@ -192,22 +192,21 @@ Let us have a closer look at the stack: |
|
|
|
|
|
- First we have a number of return closures to our `fib`
|
|
|
function. That the stack is so "clean" means that we have no lazy
|
|
|
evaluation going on, which in this example is a result of compiling
|
|
|
evaluation going on, which is a result of compiling
|
|
|
with optimisations above (`-O`). Perhaps unintuitively, simple
|
|
|
optimisations often have a beneficial effect on stack trace clarity.
|
|
|
optimisations often improve stack trace clarity.
|
|
|
|
|
|
- On the other hand, we see that we have no reference to the `main`
|
|
|
function any more. In fact, the stack only references `show` and
|
|
|
`putStrLn` respectively, which is what `print` unfolds to. Here GHC
|
|
|
figured out that it can tail-call, causing a "hole" in our stack
|
|
|
trace.
|
|
|
- On the other hand, `main` does not appear in the stack any more. In
|
|
|
fact, where it should be we only see `show` and `putStrLn`
|
|
|
respectively, which is what `print` unfolds to. Here GHC figured out
|
|
|
that it can tail-call, causing a "hole" in our stack trace.
|
|
|
|
|
|
- Finally, the two last frames are RTS frames, corresponding to
|
|
|
`stg_catch_frame` (the top-level exception handler) and
|
|
|
`stg_stop_thread`, which is the stack frame handling returning the
|
|
|
thread to the RTS. This is where the debugger runs out of guidance
|
|
|
from the DWARF information and complains with "corrupt stack?"
|
|
|
error.
|
|
|
from the DWARF information and complains with the "corrupt stack?"
|
|
|
error message.
|
|
|
|
|
|
## Profiling
|
|
|
|
... | ... | @@ -244,8 +243,10 @@ This yields us a profiling view that looks as follows: |
|
|
```
|
|
|
|
|
|
|
|
|
Note that due to DWARF information, `perf` has managed to locate the
|
|
|
source code that belongs to the "hot" assembler code.
|
|
|
DWARF information allows `perf` to locate the source code that belongs
|
|
|
to the "hot" assembler code. Note that the fairly nonsensical \`add
|
|
|
%al,(%r8)` instructions is info table data, which `perf\` is
|
|
|
interpreting as code.
|
|
|
|
|
|
## Open Issues
|
|
|
|
... | ... | @@ -262,14 +263,14 @@ debugging tools or replacing them with our own: |
|
|
- Note that both `perf` as well as `gdb` refer to blocks using their
|
|
|
backend names (e.g. `c3Vl_info`). This is not GHC's fault - we
|
|
|
correctly set the `DW_AT_name` for `DW_TAG_subprogram` records. The
|
|
|
current state of investigation is that debugging tools seemingly
|
|
|
current state of investigation is that debugging tools
|
|
|
prefer to use language-dependent "unmangling" of symbol names. We
|
|
|
probably will have to patch these programs to do something sensible
|
|
|
for Haskell code.
|
|
|
|
|
|
- Furthermore, the symbol names in `gbd`'s backtrace are actually
|
|
|
- Furthermore, the symbol names in `gbd`'s backtrace are
|
|
|
wrong -- notice the `??` entries. On the other hand, note that \`info
|
|
|
symbol\` still gives us the right answer. What is actually happening
|
|
|
symbol\` still gives us the right answer. What is happening
|
|
|
here is that `gdb` looks up the symbol with an offset of 1, as that
|
|
|
makes sense for C-like programs (see `[Note: Info Offset]` in
|
|
|
`compiler/nativeGen/Dwarf/Types.hs` for details). This is another
|
... | ... | @@ -291,7 +292,7 @@ Runtime problems - can probably be fixed by using Haskell-specific |
|
|
tools and/or improving infrastructure further:
|
|
|
|
|
|
- When trying to look at stack traces, there are two characteristics
|
|
|
of Haskell code that give us a hard time. The first one is that
|
|
|
of Haskell code leading to problems. The first one is that
|
|
|
tail-calls are very common, and not entirely straight-forward to
|
|
|
prevent. This is significant, because sometimes we positively want
|
|
|
to know whether a certain function is on the stack frame or not.
|
... | ... | @@ -311,7 +312,7 @@ tools and/or improving infrastructure further: |
|
|
almost entirely useless - we would essentially be looking at
|
|
|
variants of `$`, `.` and `>>=` all the time. Instead, we look for
|
|
|
the most specific source note that belongs to the currently compiled
|
|
|
compilation unit. This heuristic works quite well, but it is not
|
|
|
compilation unit. This heuristic works well, but it is not
|
|
|
hard to demonstrate situations where it goes wrong.
|
|
|
|
|
|
|
... | ... | @@ -330,7 +331,8 @@ a bit: |
|
|
> (Also note that tools with more knowledge about the Haskell stack
|
|
|
> can work around this issue somewhat by using info tables to traverse
|
|
|
> the Haskell stack. Explaining this strategy to DWARF readers might
|
|
|
> also be possible, Nathan Howell had done some experiments on this.)
|
|
|
> also be possible, Nathan Howell has done some experiments on this if
|
|
|
> I remember correctly.)
|
|
|
|
|
|
- Implementing our own tools depends on our ability to read binaries
|
|
|
and the contained DWARF information. So far we have used `libdwarf`,
|
... | ... | |