diff --git a/nativeunwind/elfunwindinfo/elfgopclntab.go b/nativeunwind/elfunwindinfo/elfgopclntab.go index 9cfab82d6..8fec6ad75 100644 --- a/nativeunwind/elfunwindinfo/elfgopclntab.go +++ b/nativeunwind/elfunwindinfo/elfgopclntab.go @@ -10,6 +10,8 @@ package elfunwindinfo // import "go.opentelemetry.io/ebpf-profiler/nativeunwind/ import ( "bytes" "debug/elf" + "encoding/binary" + "errors" "fmt" "go/version" "io" @@ -50,6 +52,12 @@ const ( go1_16 = 16 go1_18 = 18 go1_20 = 20 + + // Offset of the text field in moduledata struct for Go 1.16+ + // https://github.com/golang/go/blob/release-branch.go1.16/src/runtime/symtab.go#L370 + textOffset = 22 * 8 + // section name for the module data for Go 1.26+ + moduleDataSectionName = ".go.module" ) func goMagicToVersion(magic uint32) uint8 { @@ -469,16 +477,13 @@ func NewGopclntab(ef *pfelf.File) (*Gopclntab, error) { g.funcdata = g.functab g.textStart = hdr118.textStart if g.textStart == 0 { - // Starting with Go 1.26 the field textStart is set to 0 but moduledata.text - // which contains the same value is unaffected. - // The following logic assumes that .text matches moduledata.text - // which might not always be true (in which case we need to switch to moduledata.text - // which can be found through runtime.firstmoduledata). - // - //nolint:lll - // See https://github.com/golang/go/commit/0e1bd8b5f17e337df0ffb57af03419b96c695fe4 - if sec := ef.Section(".text"); sec != nil { - g.textStart = uintptr(sec.Addr) + // Starting from Go 1.26, textStart address in pclntab is always set to 0. + // Therefore we need to get it from either `runtime.text` symbol or moduledata. + // Note that it does not always match the address of `.text` section + // (for example with cgo binaries or when built with -linkmode=external). + g.textStart, err = findTextStart(ef) + if err != nil { + return nil, fmt.Errorf("failed to find text start: %w", err) } } // With the change of the type of the first field of _func in Go 1.18, this @@ -590,6 +595,25 @@ func (g *Gopclntab) Symbolize(pc uintptr) (sourceFile string, line uint, funcNam return sourceFile, line, funcName } +func findTextStart(ef *pfelf.File) (uintptr, error) { + // Get textstart from moduledata + // Starting from Go 1.26, moduledata has its own `.go.module` section. + // Since this function is expected to be called only for Go 1.26+ binaries, + // we can expect that the section exists and error out if it does not. + moduleDataSection := ef.Section(moduleDataSectionName) + if moduleDataSection == nil || moduleDataSection.Type == elf.SHT_NOBITS { + return 0, errors.New("could not find .go.module section or it is empty") + } + + var textBytes [8]byte + _, err := moduleDataSection.ReadAt(textBytes[:], textOffset) + if err != nil { + return 0, fmt.Errorf("could not read .go.module section at offset %v: %w", textOffset, err) + } + + return uintptr(binary.LittleEndian.Uint64(textBytes[:])), nil +} + type strategy int const ( diff --git a/nativeunwind/elfunwindinfo/elfgopclntab_test.go b/nativeunwind/elfunwindinfo/elfgopclntab_test.go index 679fc1186..b49520f52 100644 --- a/nativeunwind/elfunwindinfo/elfgopclntab_test.go +++ b/nativeunwind/elfunwindinfo/elfgopclntab_test.go @@ -7,6 +7,7 @@ import ( "debug/elf" "testing" + "go.opentelemetry.io/ebpf-profiler/libpf" "go.opentelemetry.io/ebpf-profiler/libpf/pfelf" sdtypes "go.opentelemetry.io/ebpf-profiler/nativeunwind/stackdeltatypes" @@ -96,3 +97,37 @@ func TestParseGoPclntab(t *testing.T) { }) } } + +func TestTextStart(t *testing.T) { + ef, err := pfelf.Open("testdata/helloworld.linkexternal") + require.NoError(t, err) + defer ef.Close() + + var runtimeTextAddr uintptr + ef.VisitSymbols(func(sym libpf.Symbol) bool { + if sym.Name == "runtime.text" { + runtimeTextAddr = uintptr(sym.Address) + return false + } + return true + }) + require.NotZero(t, runtimeTextAddr) + + g, err := NewGopclntab(ef) + require.NoError(t, err) + require.NotNil(t, g) + defer g.Close() + + require.Equal(t, runtimeTextAddr, g.textStart) + + // stripped binary should have the same text start + efStripped, err := pfelf.Open("testdata/helloworld.linkexternal.stripped") + require.NoError(t, err) + defer efStripped.Close() + gStripped, err := NewGopclntab(efStripped) + require.NoError(t, err) + require.NotNil(t, gStripped) + defer gStripped.Close() + + require.Equal(t, runtimeTextAddr, gStripped.textStart) +} diff --git a/nativeunwind/elfunwindinfo/testdata/Makefile b/nativeunwind/elfunwindinfo/testdata/Makefile index 6fabd59f8..748fec7d9 100644 --- a/nativeunwind/elfunwindinfo/testdata/Makefile +++ b/nativeunwind/elfunwindinfo/testdata/Makefile @@ -3,7 +3,9 @@ BINARIES=helloworld \ helloworld.pie \ helloworld.stripped.pie \ - helloworld.arm64 + helloworld.arm64 \ + helloworld.linkexternal \ + helloworld.linkexternal.stripped # Use the default go executable if it is not specified otherwise. GO_BINARY ?= go @@ -24,3 +26,9 @@ helloworld.stripped.pie: helloworld.arm64: GOARCH=arm64 $(GO_BINARY) build -o $@ helloworld.go + +helloworld.linkexternal: + CGO_ENABLED=1 GOTOOLCHAIN=go1.26rc2 $(GO_BINARY) build -ldflags="-linkmode=external" -o $@ helloworld.go + +helloworld.linkexternal.stripped: helloworld.linkexternal + objcopy -S $< $@