From 3ce455b071508c27b3e11d1f6d6d76dc244eaa59 Mon Sep 17 00:00:00 2001 From: HSP18SCM23P <38229980+HSP18SCM23P@users.noreply.github.com> Date: Thu, 24 Sep 2026 21:15:59 -0700 Subject: [PATCH 1/4] Add mmap-based DWARF section loading for elfutils --- pkg/symbol/elfutils/dwarfdata.go | 177 +++++++++++++++++++++++++ pkg/symbol/elfutils/dwarfdata_other.go | 24 ++++ pkg/symbol/elfutils/dwarfdata_unix.go | 51 +++++++ 3 files changed, 252 insertions(+) create mode 100644 pkg/symbol/elfutils/dwarfdata.go create mode 100644 pkg/symbol/elfutils/dwarfdata_other.go create mode 100644 pkg/symbol/elfutils/dwarfdata_unix.go diff --git a/pkg/symbol/elfutils/dwarfdata.go b/pkg/symbol/elfutils/dwarfdata.go new file mode 100644 index 00000000000..70ef841d9fc --- /dev/null +++ b/pkg/symbol/elfutils/dwarfdata.go @@ -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 +} diff --git a/pkg/symbol/elfutils/dwarfdata_other.go b/pkg/symbol/elfutils/dwarfdata_other.go new file mode 100644 index 00000000000..082ad54f4ba --- /dev/null +++ b/pkg/symbol/elfutils/dwarfdata_other.go @@ -0,0 +1,24 @@ +// 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. + +//go:build !unix + +package elfutils + +import "errors" + +// mmapReadOnly is unsupported on this platform; callers fall back to the +// copying DWARF loader. +func mmapReadOnly(path string) ([]byte, func() error, error) { + return nil, nil, errors.New("mmap-backed DWARF loading is not supported on this platform") +} diff --git a/pkg/symbol/elfutils/dwarfdata_unix.go b/pkg/symbol/elfutils/dwarfdata_unix.go new file mode 100644 index 00000000000..f5548b0037d --- /dev/null +++ b/pkg/symbol/elfutils/dwarfdata_unix.go @@ -0,0 +1,51 @@ +// 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. + +//go:build unix + +package elfutils + +import ( + "fmt" + "os" + "syscall" +) + +// mmapReadOnly memory-maps the whole file at path read-only and returns its +// contents along with a cleanup function releasing the mapping. The file can +// be closed right after the mapping is established. +func mmapReadOnly(path string) ([]byte, func() error, error) { + f, err := os.Open(path) + if err != nil { + return nil, nil, err + } + defer f.Close() + + fi, err := f.Stat() + if err != nil { + return nil, nil, err + } + size := fi.Size() + if size <= 0 { + return nil, nil, fmt.Errorf("cannot mmap empty file %q", path) + } + if int64(int(size)) != size { + return nil, nil, fmt.Errorf("file %q too large to mmap", path) + } + + b, err := syscall.Mmap(int(f.Fd()), 0, int(size), syscall.PROT_READ, syscall.MAP_SHARED) + if err != nil { + return nil, nil, fmt.Errorf("mmap %q: %w", path, err) + } + return b, func() error { return syscall.Munmap(b) }, nil +} From 9cd01c849ae281eb641e60ea3e0cb26060424a7a Mon Sep 17 00:00:00 2001 From: HSP18SCM23P <38229980+HSP18SCM23P@users.noreply.github.com> Date: Thu, 24 Sep 2026 21:17:11 -0700 Subject: [PATCH 2/4] Create dwarfdata_test.go --- pkg/symbol/addr2line/dwarfdata_test.go | 152 +++++++++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 pkg/symbol/addr2line/dwarfdata_test.go diff --git a/pkg/symbol/addr2line/dwarfdata_test.go b/pkg/symbol/addr2line/dwarfdata_test.go new file mode 100644 index 00000000000..5e75c0e199a --- /dev/null +++ b/pkg/symbol/addr2line/dwarfdata_test.go @@ -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) + } + } + } +} From 24b973a3ac045c1dc5bb1cd114a87a39eaf47d3f Mon Sep 17 00:00:00 2001 From: HSP18SCM23P <38229980+HSP18SCM23P@users.noreply.github.com> Date: Thu, 24 Sep 2026 21:18:37 -0700 Subject: [PATCH 3/4] Use mmap-based DWARF loading in DwarfLiner --- pkg/symbol/addr2line/dwarf.go | 92 ++++++++++++----------------------- 1 file changed, 32 insertions(+), 60 deletions(-) diff --git a/pkg/symbol/addr2line/dwarf.go b/pkg/symbol/addr2line/dwarf.go index d9e655a26c8..5ce2bd16779 100644 --- a/pkg/symbol/addr2line/dwarf.go +++ b/pkg/symbol/addr2line/dwarf.go @@ -11,8 +11,10 @@ // See the License for the specific language governing permissions and // limitations under the License. + package addr2line + import ( "context" "debug/dwarf" @@ -20,100 +22,70 @@ import ( "fmt" "runtime/debug" + "github.com/go-kit/log" "github.com/go-kit/log/level" + "github.com/parca-dev/parca/pkg/profile" "github.com/parca-dev/parca/pkg/symbol/demangle" "github.com/parca-dev/parca/pkg/symbol/elfutils" ) + // DwarfLiner is a symbolizer that uses DWARF debug info to symbolize addresses. type DwarfLiner struct { logger log.Logger + debugData *dwarf.Data 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() -} - -func (dl *DwarfLiner) File() string { - return dl.filename -} -func (dl *DwarfLiner) PCRange() ([2]uint64, error) { - r := dl.debugData.Reader() - - minSet := false - var min, max uint64 - for { - e, err := r.Next() - if err != nil { - return [2]uint64{}, fmt.Errorf("read DWARF entry: %w", err) - } - if e == nil { - break - } - - ranges, err := dl.debugData.Ranges(e) - if err != nil { - return [2]uint64{}, err - } - for _, pcs := range ranges { - if !minSet { - min = pcs[0] - minSet = true - } - if pcs[1] > max { - max = pcs[1] - } - if pcs[0] < min { - min = pcs[0] - } - } +func (dl *DwarfLiner) Close() error { + // Release the mmap-backed DWARF sections, if any, before closing the file. + var unmapErr error + if dl.unmapDWARF != nil { + unmapErr = dl.unmapDWARF() } - - return [2]uint64{min, max}, nil -} - -// PCToLines returns the resolved source lines for a program counter (memory address). -func (dl *DwarfLiner) PCToLines(ctx context.Context, addr uint64) (lines []profile.LocationLine, err error) { - defer func() { - if r := recover(); r != nil { - level.Debug(dl.logger).Log("msg", "recovered stack trace", "trace", string(debug.Stack())) - err = fmt.Errorf("recovering from panic in DWARF add2line: %v", r) - } - }() - - lines, err = dl.dbgFile.SourceLines(addr) - if err != nil { - return nil, err + if err := dl.f.Close(); err != nil { + return err } - return lines, nil + return unmapErr } From 51307d8f156d9cf9bcedeb2fc0cb7db0498f99be Mon Sep 17 00:00:00 2001 From: HSP18SCM23P <38229980+HSP18SCM23P@users.noreply.github.com> Date: Thu, 24 Sep 2026 21:19:34 -0700 Subject: [PATCH 4/4] Use mmap-based DWARF loading in DwarfLiner --- pkg/symbol/addr2line/dwarf.go | 65 +++++++++++++++++++++++++++++------ 1 file changed, 55 insertions(+), 10 deletions(-) diff --git a/pkg/symbol/addr2line/dwarf.go b/pkg/symbol/addr2line/dwarf.go index 5ce2bd16779..d97740a3c5c 100644 --- a/pkg/symbol/addr2line/dwarf.go +++ b/pkg/symbol/addr2line/dwarf.go @@ -11,10 +11,8 @@ // See the License for the specific language governing permissions and // limitations under the License. - package addr2line - import ( "context" "debug/dwarf" @@ -22,22 +20,18 @@ import ( "fmt" "runtime/debug" - "github.com/go-kit/log" "github.com/go-kit/log/level" - "github.com/parca-dev/parca/pkg/profile" "github.com/parca-dev/parca/pkg/symbol/demangle" "github.com/parca-dev/parca/pkg/symbol/elfutils" ) - // DwarfLiner is a symbolizer that uses DWARF debug info to symbolize addresses. type DwarfLiner struct { logger log.Logger - debugData *dwarf.Data dbgFile elfutils.DebugInfoFile f *elf.File @@ -48,7 +42,6 @@ type DwarfLiner struct { unmapDWARF func() error } - // DWARF creates a new DwarfLiner. func DWARF(logger log.Logger, filename string, f *elf.File, demangler *demangle.Demangler) (*DwarfLiner, error) { debugData, unmapDWARF, err := elfutils.LoadDWARFData(f, filename) @@ -56,7 +49,6 @@ func DWARF(logger log.Logger, filename string, f *elf.File, demangler *demangle. 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. @@ -66,7 +58,6 @@ func DWARF(logger log.Logger, filename string, f *elf.File, demangler *demangle. return nil, err } - return &DwarfLiner{ logger: log.With(logger, "liner", "dwarf"), dbgFile: dbgFile, @@ -77,7 +68,6 @@ func DWARF(logger log.Logger, filename string, f *elf.File, demangler *demangle. }, nil } - func (dl *DwarfLiner) Close() error { // Release the mmap-backed DWARF sections, if any, before closing the file. var unmapErr error @@ -89,3 +79,58 @@ func (dl *DwarfLiner) Close() error { } return unmapErr } + +func (dl *DwarfLiner) File() string { + return dl.filename +} + +func (dl *DwarfLiner) PCRange() ([2]uint64, error) { + r := dl.debugData.Reader() + + minSet := false + var min, max uint64 + for { + e, err := r.Next() + if err != nil { + return [2]uint64{}, fmt.Errorf("read DWARF entry: %w", err) + } + if e == nil { + break + } + + ranges, err := dl.debugData.Ranges(e) + if err != nil { + return [2]uint64{}, err + } + for _, pcs := range ranges { + if !minSet { + min = pcs[0] + minSet = true + } + if pcs[1] > max { + max = pcs[1] + } + if pcs[0] < min { + min = pcs[0] + } + } + } + + return [2]uint64{min, max}, nil +} + +// PCToLines returns the resolved source lines for a program counter (memory address). +func (dl *DwarfLiner) PCToLines(ctx context.Context, addr uint64) (lines []profile.LocationLine, err error) { + defer func() { + if r := recover(); r != nil { + level.Debug(dl.logger).Log("msg", "recovered stack trace", "trace", string(debug.Stack())) + err = fmt.Errorf("recovering from panic in DWARF add2line: %v", r) + } + }() + + lines, err = dl.dbgFile.SourceLines(addr) + if err != nil { + return nil, err + } + return lines, nil +}