-
Notifications
You must be signed in to change notification settings - Fork 398
fix: go unwinding stops at systemstacks #1313
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
77a7c18
b2e943d
2f2182b
dbf35f9
c1f140a
56cbf94
0650fdb
f248918
82dee94
24fb9e4
d8ebcaf
eac0e41
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -126,9 +126,11 @@ func NewExecutableInfoManager( | |||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| interpreterLoaders = append(interpreterLoaders, apmint.Loader) | ||||||||||||||
| if includeTracers.Has(types.Labels) { | ||||||||||||||
| interpreterLoaders = append(interpreterLoaders, golabels.Loader) | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| // Register golabels loader. The native unwinder needs Go runtime | ||||||||||||||
| // offsets (m, curg, g.sched) to cross the systemstack boundary, | ||||||||||||||
| // to perform Go stack unwinding. | ||||||||||||||
| interpreterLoaders = append(interpreterLoaders, golabels.Loader) | ||||||||||||||
|
Comment on lines
+132
to
+133
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Load this interpreter only if it is configured.
Suggested change
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we want to tie loading Go labels (and thus unwinding) to symbolication (
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. While I don't have a strong preference, there is a clear use case for users who need native unwinding and want to disable all other interpreters, including labels. This is often done to minimize memory usage. For approaches that are specifically focused (e.g., only on Go), it is reasonable to expect users will enable
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yes. When Go symbolization is done on the backend, we want to disable local symbolization (noticeable memory overhead on Go-heavy nodes) while still unwinding past That said, What do you think about splitting the offsets out into a dedicated interpreter that's always loaded for Go binaries (e.g. a new
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we need to follow up here. There's now three separate Go support things: unwinding, labels and symbolization. Depending on configuration all three might want to be managed separately. It would also reduce memory overhead if all three shared the same ebpf per-pid structures. It might make even sense to preload the data to But agreed, worth a separate follow up PR / ticket.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sounds good to me! |
||||||||||||||
|
|
||||||||||||||
| deferredFileIDs, err := lru.NewSynced[host.FileID, libpf.Void](deferredFileIDSize, | ||||||||||||||
| func(id host.FileID) uint32 { return uint32(id) }) | ||||||||||||||
|
|
||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| // This file contains helpers for reading Go runtime structures from eBPF programs. | ||
|
|
||
| #ifndef OPTI_GO_RUNTIME_H | ||
| #define OPTI_GO_RUNTIME_H | ||
|
|
||
| #include "bpfdefs.h" | ||
| #include "tsd.h" | ||
| #include "types.h" | ||
|
|
||
| // go_get_g_ptr reads the current G (goroutine) pointer from thread-local storage. | ||
| // TLS always contains the G that is currently executing on the thread. During | ||
| // systemstack/mcall, this is g0 (the system goroutine) since we are on the | ||
| // system stack. | ||
| // | ||
| // On aarch64, the resolution path depends on whether the binary actually uses | ||
| // cgo at runtime, which is not the same as buildinfo CGO_ENABLED: | ||
| // | ||
| // - Pure-Go binaries (no `import "C"`): runtime.iscgo is false. The Go runtime | ||
| // never initialises TPIDR_EL0 for its threads ([1]), and load_g keeps g in R28. | ||
| // - Cgo binaries: libc initialises TPIDR_EL0 via pthread_create; load_g reads | ||
| // g from *(TPIDR_EL0 + tls_g_offset). | ||
| // | ||
| // The userspace TLS-offset extractor gates on buildinfo CGO_ENABLED only [2], | ||
| // so it returns a non-zero offset for any binary built with CGO_ENABLED=1, | ||
| // including pure-Go binaries where runtime.iscgo is false. | ||
| // To handle this safely we try TLS first and fall back to r28 if the read fails | ||
| // or returns 0. R28 is the ABI-reserved register for the current goroutine | ||
| // on aarch64 [3] and is always populated while executing pure Go code. | ||
| // | ||
| // [1] https://github.com/golang/go/blame/0259df17feb288f1e24517516939b67876c2627b/src/runtime/sys_linux_arm64.s#L705 | ||
| // [2] https://github.com/open-telemetry/opentelemetry-ebpf-profiler/blob/abd95fe39bdfcd00c8079d152123d38f459a6ff0/libpf/pfelf/file.go#L615 | ||
| // [3] https://github.com/golang/go/blob/0259df17feb288f1e24517516939b67876c2627b/src/cmd/compile/abi-internal.md?plain=1#L549 | ||
| static inline EBPF_INLINE u64 go_get_g_ptr(struct GoLabelsOffsets *offs, UnwindState *state) | ||
| { | ||
| #if defined(__x86_64__) | ||
| (void)state; | ||
| #endif | ||
| u64 g_addr = 0; | ||
| void *tls_base = NULL; | ||
| if (tsd_get_base(&tls_base) < 0) { | ||
| DEBUG_PRINT("cl: failed to get tsd base; can't read g_addr"); | ||
| return 0; | ||
| } | ||
| DEBUG_PRINT( | ||
| "cl: read tsd_base at 0x%lx, g offset: %d", (unsigned long)tls_base, offs->tls_offset); | ||
|
|
||
| if (offs->tls_offset != 0 && tls_base != NULL) { | ||
| if (bpf_probe_read_user(&g_addr, sizeof(void *), (void *)((s64)tls_base + offs->tls_offset))) { | ||
| DEBUG_PRINT("cl: failed to read g_addr via TLS, tls_base(%lx)", (unsigned long)tls_base); | ||
| g_addr = 0; | ||
| } | ||
| } | ||
|
|
||
| #if defined(__aarch64__) | ||
| // Fallback to r28 when TLS is either not configured (pure-Go binary or | ||
| // mis-detected as cgo at build time) or when the TLS read failed. On | ||
| // aarch64 r28 holds the current g while executing Go runtime code. | ||
| if (g_addr == 0) { | ||
| g_addr = state->r28; | ||
| DEBUG_PRINT("cl: g_addr fallback via r28 = 0x%lx", (unsigned long)g_addr); | ||
| } | ||
| #elif defined(__x86_64__) | ||
| if (g_addr == 0) { | ||
| DEBUG_PRINT("cl: TLS offset for g pointer missing for amd64"); | ||
| return 0; | ||
| } | ||
| #endif | ||
|
|
||
| DEBUG_PRINT("cl: g_addr 0x%lx", (unsigned long)g_addr); | ||
| return g_addr; | ||
| } | ||
|
|
||
| // go_get_m_ptr reads the M (machine/OS thread) pointer for the current goroutine. | ||
| // It does so by reading the G (goroutine) pointer from thread-local storage, | ||
| // then following the g.m pointer. | ||
| static inline EBPF_INLINE void *go_get_m_ptr(struct GoLabelsOffsets *offs, UnwindState *state) | ||
| { | ||
| u64 g_addr = go_get_g_ptr(offs, state); | ||
| if (!g_addr) { | ||
| return NULL; | ||
| } | ||
|
|
||
| DEBUG_PRINT("cl: reading m_ptr_addr at 0x%lx + 0x%x", (unsigned long)g_addr, offs->m_offset); | ||
| void *m_ptr_addr; | ||
| if (bpf_probe_read_user(&m_ptr_addr, sizeof(void *), (void *)(g_addr + offs->m_offset))) { | ||
| DEBUG_PRINT("cl: failed m_ptr_addr"); | ||
| return NULL; | ||
| } | ||
| DEBUG_PRINT("cl: m_ptr_addr 0x%lx", (unsigned long)m_ptr_addr); | ||
| return m_ptr_addr; | ||
| } | ||
|
|
||
| #endif // OPTI_GO_RUNTIME_H |
Uh oh!
There was an error while loading. Please reload this page.