Go DWARF Support

Why Discuss This Topic

When the Go compilation toolchain performs compilation and linking, it generates DWARF debugging information. What Go libraries can we use to read this data? When reading, for types, variables, constants, functions (including parameter lists and return values), etc., are there reference manuals that can tell us exactly how to read them (DWARF data varies between different languages and program constructs)? Now in 2025, are there more user-friendly open-source libraries, reference manuals, and documentation in this area?

Having thoroughly studied the DWARF specification, I can naturally understand how much work is involved in generating and parsing DWARF data. Some readers might wonder if there's a huge difference compared to reading and writing an ELF file? Yes, there is a very significant difference, they're completely incomparable. Just look at the amount of code in the pkg/dwarf directory of the delve debugger, and you'll understand why we need to discuss Go DWARF Support.

$ cloc path-to/delve/pkg/dwarf
      35 text files.
      34 unique files.                            
       3 files ignored.

github.com/AlDanial/cloc v 2.04  T=0.03 s (1200.7 files/s, 279205.9 lines/s)
-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
Go                              34            920            573           6413
-------------------------------------------------------------------------------
SUM:                            34            920            573           6413
-------------------------------------------------------------------------------

Developers focused on the debugging domain are quite niche, and Go is still a relatively young language. Therefore:

  • The Go team is unlikely to maintain a comprehensive DWARF implementation in the standard library for a small audience, as it would be very resource-intensive.
  • The Go compilation toolchain focuses on generating DWARF debugging information, which is relatively comprehensive - it generates everything that should be generated, but the documentation is sparse.
  • debug/dwarf focuses on reading DWARF data, but it doesn't support reading all the information generated by the Go compilation toolchain, such as .debug_frame.

This is why the unofficial Go debugger go-delve/delve implements its own DWARF generation and parsing logic internally. It even implements its own generation logic, on one hand to compare with the DWARF data generated by the Go compilation toolchain team, and on the other hand to provide feedback from a debugger developer's perspective when the Go compilation toolchain's DWARF data description is insufficient, thus forming collaboration and co-construction with the Go core team.

ps: A developer created a demo ggg while learning debugger development, which also customized debug/dwarf. See: ConradIrwin/go-dwarf. This is just an example of customizing debug/dwarf, not suggesting this library is usable, and it hasn't been updated in 11 years. Even if you want to use it, you should prioritize the implementation in go-delve/delve.

Implementation 1: Go Standard Library debug/dwarf

In the previous section, we introduced the Go standard library debug/dwarf, which provides parsing of DWARF debugging information. As the official implementation, it provides low-level APIs that allow you to traverse and inspect DWARF data structures.

Relatively speaking, the official library is the most basic and reliable choice. If more advanced analysis or integration is needed, you may need to build upon the official library, adding more features and providing higher-level abstractions. We've analyzed why the current debug/dwarf is unlikely to reach "perfection" and cited examples of go-delve/delve and ggg.

OK, let's look at the support level and limitations of debug/dwarf, using go1.24 as an example:

  • Supports reading .debug and .zdebug sections;
  • If debugging information is compressed with zlib or zstd, supports automatic decompression via debug/elf.(*Section).Data();
  • Some debugging information requires relocation operations, supports on-demand relocation via debug/elf.(*File).applyRelocations(a, b);
  • DWARFv4 multiple .debug_types, dwarf.Data adds additional numbering to section names for easier problem location;
  • All .debug and .zdebug sections are uniformly converted to .debug_ sections in dwarf.Data;
  • All DWARF sections are read normally!
func (f *File) DWARF() (*dwarf.Data, error) {
    // Get the suffix after .[z]debug_ sections, return empty for other sections
    dwarfSuffix := func(s *Section) string { ... }
    // Get data from .[z]debug_ sections, decompress and relocate as needed
    sectionData := func(i int, s *Section) ([]byte, error) { ... }

    // DWARFv4 has many .[z]debug_ sections, initially debug/dwarf mainly handled these sections
    var dat = map[string][]byte{"abbrev": nil, "info": nil, "str": nil, "line": nil, "ranges": nil}
    for i, s := range f.Sections {
        suffix := dwarfSuffix(s)
        if suffix == "" {
            continue
        }
        if _, ok := dat[suffix]; !ok {
            continue
        }
        b, _ := sectionData(i, s)
        dat[suffix] = b
    }

    // Create dwarf.Data, only including processed .[z]debug_ sections
    d, _ := dwarf.New(dat["abbrev"], nil, nil, dat["info"], dat["line"], nil, dat["ranges"], dat["str"])

    // Continue processing multiple .debug_types sections and other DWARFv4/v5 sections
    for i, s := range f.Sections {
        suffix := dwarfSuffix(s)
        if suffix == "" {
            continue
        }
        if _, ok := dat[suffix]; ok {
            // Already handled
            continue
        }

        b, _ := sectionData(i, s)
        // If there are multiple .debug_types sections, add numbering to section names in dwarf.Data for easier problem location
        if suffix == "types" {
            _ = d.AddTypes(fmt.Sprintf("types-%d", i), b); err != nil {
        } else {
            // Other DWARF sections
            _ = d.AddSection(".debug_"+suffix, b); err != nil {
        }
    }

    return d, nil
}

debug/dwarf does read all DWARF data, but that's not enough! It's only truly useful to us after reading, parsing, and providing appropriate APIs. To implement conventional debugging capabilities, a debugger needs:

  • Support for viewing or modifying types, variables, and constants requires reading and parsing DIEs in .debug_info -- debug/dwarf supports this
  • Need to implement conversion between instruction addresses and source code locations, requires reading and parsing line number tables in .debug_line -- debug/dwarf supports this
  • Implementing call stack backtracing requires knowing pcsp relationships, requires reading and parsing call stack information tables in .debug_frame -- debug/dwarf doesn't support this!!!

    ps: go runtime uses .gopclntab combined with tls.g information to generate call stacks.

  • Other sections also don't provide corresponding APIs for operation.

In summary, debug/dwarf completes the reading, decompression, and relocation of DWARF data, but doesn't provide comprehensive and complete API coverage. It becomes tricky when we want to read different types of DWARF information. This also means that to implement various DWARF data query operations needed in a debugger, we need to implement them ourselves.

Implementation 2: Go Toolchain cmd/internal/dwarf

At the Go compilation toolchain level, DWARF debugging information generation is distributed across the compiler and linker, both of which are involved in DWARF debugging information generation work, with different responsibilities. The common library cmd/internal/dwarf is used by both the compiler and linker.

  • go tool compile records a series of link.LSym (link.LSym.Type=SDWARFXXX, link.LSym.P=DWARF encoded data);
  • go tool link integrates, converts, and processes the above information recorded by the compiler in the input object files, finally outputting debugging information to .debug_ sections;

In the next two sections, we'll detail the above work process of the compiler and linker, which is very valuable for our subsequent development and testing of our own debugger.

Now, let's look at what functionality the cmd/internal/dwarf package supports:

  • dwarf_defs.go defines some constants in DWARF, DW_TAG types, DW_CLS types, DW_AT attribute types, DW_FORM encoding forms, DW_OP operation instructions, DW_ATE attribute encoding types, DW_ACCESS access modifiers, DW_VIS visibility modifiers, DW_VIRTUALITY virtual function modifiers, DW_LANG language types (go is 22), DW_INL inline types, DW_ORD row (column) major order, DW_LNS line number table operations, DW_MACINFO macro definition operations, DW_CFA call stack information table operations, etc.;

    These definitions are categorized into different packages in go-delve/delve, making it clearer.

  • dwarf.go defines some common code for generating and encoding DWARF debugging information. DWARF debugging information generation is completed by the compiler and linker, and dwarf.go defines some exported functions for generating DWARF debugging information, which are used by both the compiler and linker.

ps: dwarf.go is very helpful to us and has great reference value, because the DWARF representation of various program constructs is implemented in this file. Reading this source file helps us understand the DIE TAG, Attr, and other DWARF description elements used to describe different program constructs, so when we implement our own debugger and need to extract necessary information, we know how to precisely reverse the operation.

This code is mainly for use by the Go compilation toolchain, and its design and implementation are tightly integrated with other parts of the compilation toolchain, making it difficult to extract for reuse. This package is also organized under the internal directory, unlike debug/dwarf which is exposed to ordinary Go developers. Even if this code is very useful, it would require copying, pasting, and making significant changes. go-delve/delve copied and pasted this code for generating DWARF data for comparison and testing, but except for the debugger project itself, it's hard to find other projects that would do this. If we really want to reuse this code, we can use the implementation in go-delve/delve.

Implementation 3: go-delve/delve/pkg/dwarf

How does dlv handle DWARF?

Taking the popular Go debugger go-delve/delve as an example, how does it handle DWARF debugging information? Does it use the standard library? To verify these points, you can execute git log -S "DWARF()" in the git repository to search for commit records, finding several key pieces of information:

  1. Delve initially also used the standard library debug/dwarf to implement debugging information parsing, at a time when both Go and delve were in relatively early stages, and not very mature in various aspects.

    commit f1e5a70a4b58e9caa4b40a0493bfb286e99789b9
    Author: Derek Parker <parkerderek86@gmail.com>
    Date:   Sat Sep 13 12:28:46 2014 -0500
    
    Update for Go 1.3.1
    
    I decided to vendor all debug/dwarf and debug/elf files so that the
    project can be go get-table. All changes that I am waiting to land in Go
    1.4 are now captured in /vendor/debug/*.
    
  2. Delve developers found issues with parsing certain type information using debug/dwarf, so they temporarily replaced it with package x/debug/dwarf to address this problem. Looking at the x/debug/dwarf package now, some source files are missing because they have been migrated to the Go source tree.

    commit 54f1c9b3d40f606f7574c971187e7331699f378e
    Author: aarzilli <alessandro.arzilli@gmail.com>
    Date:   Sun Jan 24 10:25:54 2016 +0100
    
        proc: replace debug/dwarf with golang.org/x/debug/dwarf
    
        Typedefs that resolve to slices are not recorded in DWARF as typedefs
        but instead as structs in a way that there is no way to know they
        are really slices using debug/dwarf.
        Using golang.org/x/debug/dwarf instead this problem is solved and
        as a bonus some types are printed with a nicer names: (struct string
        → string, struct []int → []int, etc)
    
         Fixes #356 and #293
    
  3. Later, debug/dwarf fixed the previous issues, and delve switched back from x/debug/dwarf to debug/dwarf.

    commit 1e3ff49610690e9890a669c95d903184baae1f4f
    Author: aarzilli <alessandro.arzilli@gmail.com>
    Date:   Mon May 29 15:20:01 2017 +0200
    
        pkg/dwarf/godwarf: split out type parsing from x/debug/dwarf
    
        Splits out type parsing and go-specific Type hierarchy from
        x/debug/dwarf, replace x/debug/dwarf with debug/dwarf everywhere,
        remove x/debug/dwarf from vendoring.
    
  4. Subsequently, delve implemented its own parsing of debug_line and compared the processing results with the standard library, finding that the functional correctness of processing was already consistent with the standard library.

    One might ask why implement it yourself? I understand that on one hand, both Go and delve are rapidly evolving, and the Go official team hasn't put as much effort into debugging aspects. On the other hand, delve inevitably needs to parse some debugging information itself. Eventually, delve developers rewrote the parsing of .debug_line along with other sections, giving delve better completeness in parsing debugging information.

    commit 3f9875e272cbaae7e507537346757ac4db6d25fa
    Author: aarzilli <alessandro.arzilli@gmail.com>
    Date:   Mon Jul 30 11:18:41 2018 +0200
    
        dwarf/line: fix some bugs with the state machine
    
        Adds a test that compares the output of our state machine with the
        output of the debug_line reader in the standard library and checks that
        they produce the same output for the debug_line section of grafana as
        compiled on macOS (which is the most interesting case since it uses cgo
        and therefore goes through dsymutil).
    
        ...
    

In summary, the standard library's support for reading and parsing debugging information is limited, and both Go and delve are rapidly evolving. Clearly, delve's need for DWARF is significantly stronger than Go itself. Delve initially used the standard library, later found limitations, and began rewriting the reading and parsing of DWARF debugging information. Of course, during this rewriting process, it also drew on implementations from the Go standard library, and the Go compilation toolchain has received optimization suggestions for DWARF debugging information generation from delve developers, forming a process of collaboration and co-construction.

We understand this level is sufficient. As long as the Go standard library supports it, delve's design and implementation will align with the Go standard library, which is definitely not a problem. But for what the Go standard library doesn't support yet, or doesn't plan to support, delve developers need to implement and verify it first, then provide feedback to Go compilation toolchain developers, improving it through co-construction. This part is also a continuous optimization process, for example, now or in the future, it will continue to align with excellent features in DWARF v5, and this processing logic will continue to be optimized.

We just need to understand this co-construction process. When we implement our own debugger, we can refer to the current best practices of the delve debugger.

Understanding delve pkg/dwarf

The DWARF operation-related parts in go-delve/delve are mainly in the package pkg/dwarf. Let's briefly list what it mainly implements.

  • pkg/dwarf/util: Some code in this package is copied and modified from the Go standard library, for example, pkg/dwarf/util/buf.go is mostly standard library code, with only minor adjustments, adding several utility functions to read variable-length encoded values, read strings, and read DWARF information at the beginning of compilation units.

  • pkg/dwarf/dwarfbuilder: This package provides utility classes and functions to quickly encode DWARF information, such as adding compilation units, functions, variables, and types to .debug_info. It also adds LocEntry information to .debug_loc. Why does go-delve/delve provide such a package implementation? I believe that on one hand, the Go standard library doesn't provide this information (the toolchain cmd/internal/dwarf has it, but as mentioned earlier, it's not included in the standard library and is difficult to copy & paste for reuse), and there hasn't been as much investment in how to use DWARF debugging information to comprehensively describe Go program constructs. go-delve/delve here is also doing some exploration in this area, then collaborating with the Go development team. So maintaining this DWARF data generation logic here is understandable.

  • pkg/dwarf/frame: This package provides parsing of .debug_frame and .zdebug_frame. Each compilation unit has its own .debug_frame, which the linker finally merges into one. For each compilation unit cu, it first encodes the corresponding CIE information, then follows with the FDE information contained in the compilation unit cu. Then comes the next compilation unit's CIE, FDEs... and so on. For this information, a state machine can be used for parsing.

  • pkg/dwarf/line: This package provides parsing of .debug_line. The reason for implementing it yourself instead of using debug/gosym in the Go standard library has been mentioned many times before - the standard library implementation only supports pure Go code, not cgo code, missing this part of line table data. The reason for not using the standard library debug/dwarf is also a implementation strategy of delve, relatively speaking, ensuring the completeness of delve's DWARF parsing and debugging functionality.

  • pkg/dwarf/godwarf: The code here, compared with the Go standard library debug/dwarf, has many similarities, likely modified on the basis of the standard library. It mainly implements reading of DWARF information and supports ZLIB decompression. It also adds code to support .debug_addr newly added in DWARF v5, which helps simplify existing relocation operations. It also provides reading of some type information specified in the DWARF standard. It also supports reading and parsing of DIEs in .debug_info, and for easier use, it organizes them into a DIE Tree form.

  • pkg/dwarf/loclist: The same object may change its location during its lifecycle, and location list information is used to describe this situation. DWARF v2~v4 all have descriptions in this area, and DWARF v5 also has improvements.

  • pkg/dwarf/op: DWARF defines many operation instructions, and this package mainly implements these instruction operations.

  • pkg/dwarf/reader: Further encapsulation on the standard library dwarf.Reader to achieve more convenient DWARF information access.

  • pkg/dwarf/util: Provides some buffer implementations needed for DWARF data parsing, as well as utility functions for reading LEB128 encoding/decoding and reading strings from string tables (null-terminated).

Summary

Later, we will refer to the implementation in go-delve/delve/pkg/dwarf for parsing DWARF data in our debugger. Before using it, I will lead everyone through the design and implementation of this part of the code, so we understand both how and why, making us confident in using it and truly "mastering" it. This part of the code is closely related to the DWARF debugging information standard. If readers can combine it with the DWARF standard content in the next chapter (or keep the DWARF v4/v5 debugging information standard handy) to read, and frequently write some test code to see what the generated DWARF debugging information looks like, understanding will be smoother and more thorough.

We only need to read for debugger development, but to help everyone better understand DWARF like derekparker and arzillia masters who can freely extend support for Go new features and collaborate with the Go toolchain core team, we also need to think about how to use DWARF to describe different program constructs. So we should also appropriately master DWARF data generation. Although it may seem "boring", what is boring? When I was organizing these seemingly boring words, I never found them boring.

In Chapter 8, we will introduce the DWARF debugging information standard, and in Chapter 9, we will implement symbolic debugger functionality. At that time, we "may" trim go-delve/delve and provide further detailed explanations and example code demonstrations. The code in go-delve/delve/pkg/dwarf alone exceeds 6500 lines. Since we are doing this for learning and exchange purposes, to save space and code volume in this book and get the first complete version to readers as soon as possible, I may consider trimming the delve code, such as keeping only the implementation for the linux+amd64 environment that everyone can easily obtain, but retaining necessary abstraction levels, so everyone can still understand more challenges faced by a truly usable debugger.

For example, deleting code unrelated to Linux ELF, such as some code related to Windows PE and Darwin macho, but retaining interface abstractions for different platforms and different executable file formats.

This can save the author's time, ensure the overall progress of the book, avoid getting stuck in too many details, and complete the book and start errata at a faster pace. Precipitating knowledge to enable each reader to develop symbolic debugger capabilities is my unchanging original intention in writing this book. We don't have such a necessity to write a DWARF read/write library from scratch, and I hope readers understand the reasons for this decision. Moreover, this e-book has already gone through too long a time, and it must release its first complete version as soon as possible. Perhaps after we have more contributors, we can consider providing a more streamlined, just-right implementation more suitable for our tutorial than go-delve/delve/pkg/dwarf.

results matching ""

    No results matching ""