Skip to content

fix(ebpf): support Go 1.26 pclntab where pcHeader.textStart is always zero#1

Closed
harveywong98 wants to merge 1 commit intocoroot:mainfrom
harveywong98:pr/go126-coroot
Closed

fix(ebpf): support Go 1.26 pclntab where pcHeader.textStart is always zero#1
harveywong98 wants to merge 1 commit intocoroot:mainfrom
harveywong98:pr/go126-coroot

Conversation

@harveywong98
Copy link
Copy Markdown

Problem

In Go 1.26, the linker no longer writes pcHeader.textStart into the .gopclntab header (it is always zero). See the Go 1.26 linker release notes.

The profiler previously relied on this field to locate the base address for functab PC offset calculations:

abs_pc = functab_offset + textStart

With Go 1.26, textStart reads as 0, so the profiler falls back to .text.Addr (the section start address). For pure Go binaries this is often coincidentally correct, but for CGO + external link binaries it is wrong:

.text section layout (external link):
┌──────────────────────────────────┐  ← .text.Addr  (wrong fallback)
│  C runtime / jemalloc / re2 ...  │
├──────────────────────────────────┤
│  runtime.text                    │  ← actual base for functab offsets
│  Go runtime + user Go code       │
└──────────────────────────────────┘

Using .text.Addr instead of runtime.text shifts every resolved PC by the size of the C preamble, causing the entire flame graph to show wrong function names (consistently off by a fixed delta, not random noise).

Fix

When pcHeader.textStart == 0 and the binary is Go 1.26+, look up the runtime.text symbol from .symtab and use its value as textStart. Only fall back to .text.Addr if .symtab is missing (e.g. binary was stripped with -s).

Two helpers are added to MMapedElfFile:

  • findRuntimeTextSymbol() — walks .symtab / .strtab to find the runtime.text symbol value
  • isGo126OrLater() — detects Go 1.26+ binaries by checking pclntab magic 0xFFFFFFF0/F1 with textStart == 0

The modified NewGoTable() flow:

  1. Read textStart from pclntab header (as before)
  2. If textStart == 0 and isGo126OrLater: try findRuntimeTextSymbol()
  3. If still 0: fall back to .text.Addr (unchanged for Go ≤ 1.25 and stripped binaries)

Impact

Fixes flame graph corruption for Go 1.26 CGO binaries. The fix is a no-op for:

  • Go ≤ 1.25 (textStart comes from pclntab header, this code path is never reached)
  • Pure Go binaries where .text.Addr == runtime.text
  • Stripped binaries (-s): still falls back to .text.Addr, same as before

Test

Added a test ELF binary (testdata/elfs/go26) compiled with Go 1.26, verified that findRuntimeTextSymbol() returns the correct runtime.text value and that symbol resolution produces the expected function names.

… zero

Go 1.26 changed the linker so that the textStart field in pcHeader
(.gopclntab) is permanently set to zero on all platforms. Previously,
ParseRuntimeTextFromPclntab18 read this field to determine the base
address for functab PC offset calculations; it now always returns 0.

The existing fallback (use .text section address) is correct for
non-PIE Go binaries. This commit makes the fallback path explicit for
Go 1.26+ by:

1. Adding isGo126OrLater() to detect binaries affected by the change.
2. Adding findRuntimeTextSymbol() to look up "runtime.text" from
   the ELF .symtab section, which is more precise than .text.Addr for
   edge cases.
3. Falling back to .text.Addr if the symbol is not found (e.g. stripped
   binaries).
4. Adding a Go 1.26 test ELF and a corresponding test case.

Fixes inverted/corrupted flame graphs in coroot when profiling Go 1.26
applications.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant