Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 24 additions & 7 deletions pkg/symbol/addr2line/dwarf.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,31 +36,48 @@ type DwarfLiner struct {
dbgFile elfutils.DebugInfoFile
f *elf.File
filename string

// unmapDWARF releases the memory mapping backing debugData. It is nil
// when the DWARF sections were loaded into heap buffers instead.
unmapDWARF func() error
}

// DWARF creates a new DwarfLiner.
func DWARF(logger log.Logger, filename string, f *elf.File, demangler *demangle.Demangler) (*DwarfLiner, error) {
debugData, err := f.DWARF()
debugData, unmapDWARF, err := elfutils.LoadDWARFData(f, filename)
if err != nil {
return nil, fmt.Errorf("failed to read DWARF data: %w", err)
}

dbgFile, err := elfutils.NewDebugInfoFile(debugData, demangler)
if err != nil {
// Release the mapping if initialization fails half-way.
if unmapDWARF != nil {
_ = unmapDWARF()
}
return nil, err
}

return &DwarfLiner{
logger: log.With(logger, "liner", "dwarf"),
dbgFile: dbgFile,
debugData: debugData,
f: f,
filename: filename,
logger: log.With(logger, "liner", "dwarf"),
dbgFile: dbgFile,
debugData: debugData,
f: f,
filename: filename,
unmapDWARF: unmapDWARF,
}, nil
}

func (dl *DwarfLiner) Close() error {
return dl.f.Close()
// Release the mmap-backed DWARF sections, if any, before closing the file.
var unmapErr error
if dl.unmapDWARF != nil {
unmapErr = dl.unmapDWARF()
}
if err := dl.f.Close(); err != nil {
return err
}
return unmapErr
}

func (dl *DwarfLiner) File() string {
Expand Down
152 changes: 152 additions & 0 deletions pkg/symbol/addr2line/dwarfdata_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
// Copyright 2022-2026 The Parca Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

package addr2line

import (
"context"
"debug/dwarf"
"debug/elf"
"runtime"
"testing"

"github.com/go-kit/log"
"github.com/stretchr/testify/require"

"github.com/parca-dev/parca/pkg/symbol/demangle"
"github.com/parca-dev/parca/pkg/symbol/elfutils"
)

func openTestELFFixture(t testing.TB) (*elf.File, string) {
t.Helper()
filename := "testdata/basic-cpp-no-fp-with-debuginfo"
f, err := elf.Open(filename)
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, f.Close()) })
return f, filename
}

// countEntries counts every entry a fresh DWARF reader yields.
func countEntries(t *testing.T, d *dwarf.Data) int {
t.Helper()
r := d.Reader()
n := 0
for {
e, err := r.Next()
require.NoError(t, err)
if e == nil {
break
}
n++
}
return n
}

func unixOnly(t testing.TB) {
t.Helper()
switch runtime.GOOS {
case "windows", "plan9", "js":
t.Skipf("mmap-backed DWARF loading is not supported on %s", runtime.GOOS)
}
}

func TestLoadDWARFDataMmap(t *testing.T) {
unixOnly(t)

f, filename := openTestELFFixture(t)

d, unmap, err := elfutils.LoadDWARFData(f, filename)
require.NoError(t, err)
require.NotNil(t, d)
// The fixture is an uncompressed ET_EXEC binary: the mmap path must be taken.
require.NotNil(t, unmap, "expected mmap-backed DWARF loading for uncompressed sections")

// The mmap-backed data must describe the same debug info as the copying loader.
want, err := f.DWARF()
require.NoError(t, err)
require.Equal(t, countEntries(t, want), countEntries(t, d))
require.Greater(t, countEntries(t, d), 0)

require.NoError(t, unmap())
}

func TestLoadDWARFDataFallback(t *testing.T) {
f, _ := openTestELFFixture(t)

// Without a filename there is nothing to mmap: fall back to the copying loader.
d, unmap, err := elfutils.LoadDWARFData(f, "")
require.NoError(t, err)
require.NotNil(t, d)
require.Nil(t, unmap, "expected no mmap cleanup when falling back")

want, err := f.DWARF()
require.NoError(t, err)
require.Equal(t, countEntries(t, want), countEntries(t, d))
}

func TestDwarfLinerMmapClose(t *testing.T) {
unixOnly(t)

logger := log.NewNopLogger()
demangler := demangle.NewDemangler("simple", true)
f, filename := openTestELFFixture(t)

liner, err := DWARF(logger, filename, f, demangler)
require.NoError(t, err)
require.NotNil(t, liner.unmapDWARF, "expected mmap-backed DWARF loading")

// Symbolization must work off the mapped sections.
lines, err := liner.PCToLines(context.Background(), 0x401125)
require.NoError(t, err)
require.NotEmpty(t, lines)

// Close must release the mapping without error.
require.NoError(t, liner.Close())
}

func BenchmarkDWARFLoadStdlib(b *testing.B) {
f, _ := openTestELFFixture(b)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
d, err := f.DWARF()
if err != nil {
b.Fatal(err)
}
if d == nil {
b.Fatal("nil dwarf data")
}
}
}

func BenchmarkDWARFLoadMmap(b *testing.B) {
unixOnly(b)
f, filename := openTestELFFixture(b)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
d, unmap, err := elfutils.LoadDWARFData(f, filename)
if err != nil {
b.Fatal(err)
}
if d == nil {
b.Fatal("nil dwarf data")
}
if unmap != nil {
if err := unmap(); err != nil {
b.Fatal(err)
}
}
}
}
177 changes: 177 additions & 0 deletions pkg/symbol/elfutils/dwarfdata.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
// Copyright 2022-2026 The Parca Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package elfutils

import (
"debug/dwarf"
"debug/elf"
"fmt"
"strings"
)

// LoadDWARFData loads the DWARF debug info of f.
//
// Where possible, uncompressed DWARF sections are served as read-only
// memory-mapped views of the underlying file instead of being copied into Go
// heap buffers (see https://github.com/parca-dev/parca/issues/6404). The
// returned cleanup function releases the mapping and must be called once the
// dwarf.Data is no longer needed; it is nil when the mmap path was not taken.
//
// The previous loading behavior (debug/elf's File.DWARF, which copies section
// contents into heap buffers) is retained for compressed sections, for inputs
// whose DWARF sections require relocations to be applied, and on platforms
// without mmap support.
func LoadDWARFData(f *elf.File, filename string) (*dwarf.Data, func() error, error) {
if d, unmap, ok, err := mmapDWARFData(f, filename); err != nil || ok {
return d, unmap, err
}

d, err := f.DWARF()
if err != nil {
return nil, nil, err
}
return d, nil, nil
}

// dwarfSection is a DWARF debug section along with its index in the ELF
// section table and its suffix (e.g. "info" for ".debug_info").
type dwarfSection struct {
index int
suffix string
sec *elf.Section
}

// dwarfSections returns all sections of f that carry DWARF debug info.
func dwarfSections(f *elf.File) []dwarfSection {
var out []dwarfSection
for i, s := range f.Sections {
suffix := dwarfSuffix(s)
if suffix == "" {
continue
}
out = append(out, dwarfSection{index: i, suffix: suffix, sec: s})
}
return out
}

// mmapEligible reports whether every DWARF section of f can be served from a
// read-only memory mapping of the file.
func mmapEligible(f *elf.File, secs []dwarfSection) bool {
// debug/elf applies relocations to DWARF section bytes for non-ET_EXEC
// files. A read-only mapping cannot be modified in place, so retain the
// copying loader for inputs with relocations against DWARF sections.
relocated := map[int]bool{}
if f.Type != elf.ET_EXEC {
for _, r := range f.Sections {
if r.Type != elf.SHT_REL && r.Type != elf.SHT_RELA {
continue
}
relocated[int(r.Info)] = true
}
}

for _, ds := range secs {
s := ds.sec
if s.Type != elf.SHT_PROGBITS {
return false
}
if s.Flags&elf.SHF_COMPRESSED != 0 || strings.HasPrefix(s.Name, ".zdebug_") {
// Section.Data transparently decompresses such sections; a
// mapping would expose the raw compressed bytes.
return false
}
if relocated[ds.index] {
return false
}
}
return true
}

// mmapDWARFData builds dwarf.Data from read-only memory-mapped views of the
// file's DWARF sections. It reports ok=false when the mmap path is not
// applicable so the caller can fall back to the copying loader; a non-nil err
// means the input is corrupt and the error is propagated.
func mmapDWARFData(f *elf.File, filename string) (d *dwarf.Data, unmap func() error, ok bool, err error) {
secs := dwarfSections(f)
if len(secs) == 0 || filename == "" || !mmapEligible(f, secs) {
return nil, nil, false, nil
}

m, unmap, err := mmapReadOnly(filename)
if err != nil {
return nil, nil, false, nil
}
// Release the mapping on any failure below (cleanup on initialization
// failure); the caller owns it once we report success.
success := false
defer func() {
if !success {
_ = unmap()
}
}()

// Mirror debug/elf.File.DWARF: first the sections debug/dwarf started
// with, then DWARF4 .debug_types and DWARF5 sections.
dat := map[string][]byte{"abbrev": nil, "info": nil, "str": nil, "line": nil, "ranges": nil}
for _, ds := range secs {
if _, ok := dat[ds.suffix]; !ok {
continue
}
b, ok := sectionBytes(m, ds.sec)
if !ok {
// Section extends beyond the mapped file (e.g. truncated after
// open); let the copying loader surface the proper error.
return nil, nil, false, nil
}
dat[ds.suffix] = b
}

d, err = dwarf.New(dat["abbrev"], nil, nil, dat["info"], dat["line"], nil, dat["ranges"], dat["str"])
if err != nil {
return nil, nil, false, err
}

for _, ds := range secs {
if _, ok := dat[ds.suffix]; ok {
// Already handled.
continue
}
b, ok := sectionBytes(m, ds.sec)
if !ok {
return nil, nil, false, nil
}
if ds.suffix == "types" {
if err := d.AddTypes(fmt.Sprintf("types-%d", ds.index), b); err != nil {
return nil, nil, false, err
}
} else if err := d.AddSection(".debug_"+ds.suffix, b); err != nil {
return nil, nil, false, err
}
}

success = true
return d, unmap, true, nil
}

// sectionBytes returns the contents of s as a view into the memory mapping m.
// It reports ok=false when the section lies outside the mapped file.
func sectionBytes(m []byte, s *elf.Section) (b []byte, ok bool) {
// Guard against overflow and out-of-bounds sections (e.g. a file that was
// truncated after it was opened).
if s.Size > uint64(len(m)) || s.Offset > uint64(len(m))-s.Size {
return nil, false
}
off := int(s.Offset)
return m[off : off+int(s.Size)], true
}
Loading