From 28ceec52e664d8832545020d67c95ebb5672ef19 Mon Sep 17 00:00:00 2001 From: Thomas Legris Date: Fri, 2 Jan 2026 00:38:06 +0900 Subject: [PATCH] add amd64 support --- README.md | 5 +- gen.go | 2 +- xgotop.bpf.c | 82 ++++++++++++++--------------- xgotop.h | 143 +++++++++++++++++++++++++++------------------------ 4 files changed, 116 insertions(+), 116 deletions(-) diff --git a/README.md b/README.md index c4e9991..74d4559 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,6 @@ Whether you're debugging a production issue, optimizing performance, or just cur - **Session replay** to replay past observations - **Watch by binary or PID** of the target Go program -> [!NOTE] -> `xgotop` only supports `arm64` architecture for now, we will rollout support for `amd64` soon! - ## Usage Running `xgotop` is relatively straightforward. Get a Go binary ready, and run each of the commands below in a separate terminal: @@ -129,7 +126,7 @@ The sampling feature is one of the most powerful ways to reduce overhead when mo You can sample the following event types: - `newgoroutine`: Goroutine creation -- `goexit`: Goroutine exit +- `goexit`: Goroutine exit - `makemap`: Map allocation - `makeslice`: Slice allocation - `newobject`: Object allocation diff --git a/gen.go b/gen.go index 0a77b08..b0e73a4 100644 --- a/gen.go +++ b/gen.go @@ -1,3 +1,3 @@ package main -//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -type go_runtime_event_t -target arm64 -output-dir cmd/xgotop ebpf xgotop.bpf.c +//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -type go_runtime_event_t -target ${GOARCH} -output-dir cmd/xgotop ebpf xgotop.bpf.c diff --git a/xgotop.bpf.c b/xgotop.bpf.c index 1d8d41c..3ef78f9 100644 --- a/xgotop.bpf.c +++ b/xgotop.bpf.c @@ -2,10 +2,7 @@ // func casgstatus(gp *g, oldval, newval uint32) SEC("uprobe/runtime.casgstatus") -int BPF_KPROBE(uprobe_casgstatus, - const void *gp, - const u32 oldval, - const u32 newval) { +int BPF_KPROBE(uprobe_casgstatus, const void *gp, const u32 oldval, const u32 newval) { u64 probe_start_ns = bpf_ktime_get_ns(); u64 _ret, gp_id, g_id, g_parent_id; struct go_runtime_g g; @@ -22,7 +19,7 @@ int BPF_KPROBE(uprobe_casgstatus, bpf_printk("casgstatus: goid=%llu, oldval=%u, newval=%u", g.goid, oldval, newval); #endif - _ret = get_go_g_struct_arm(ctx, &g); + _ret = get_go_g_struct(ctx, &g); if (_ret < 0) { bpf_printk("Failed to read g, ret=%d", _ret); return 0; @@ -35,10 +32,12 @@ int BPF_KPROBE(uprobe_casgstatus, u64 *ts = bpf_map_lookup_elem(&goroutines_in_exit, &gp_id); if (ts != NULL) { // Still send the event for the CAS_G_STATUS - SEND_EVENT_WITH_SAMPLING(GO_RUNTIME_EVENT_TYPE_CAS_G_STATUS, g_id, g_parent_id, oldval, newval, gp_id, 0, 0, probe_start_ns); - + SEND_EVENT_WITH_SAMPLING(GO_RUNTIME_EVENT_TYPE_CAS_G_STATUS, g_id, g_parent_id, oldval, + newval, gp_id, 0, 0, probe_start_ns); + // Notify the userspace program that the goroutine has exited - SEND_EVENT_WITH_SAMPLING(GO_RUNTIME_EVENT_TYPE_GOEXIT, g_id, g_parent_id, gp_id, *ts, 0, 0, 0, probe_start_ns); + SEND_EVENT_WITH_SAMPLING(GO_RUNTIME_EVENT_TYPE_GOEXIT, g_id, g_parent_id, gp_id, *ts, 0, + 0, 0, probe_start_ns); _ret = bpf_map_delete_elem(&goroutines_in_exit, &g_id); if (_ret < 0) { bpf_printk("Failed to delete goroutines_in_exit, ret=%d", _ret); @@ -52,9 +51,10 @@ int BPF_KPROBE(uprobe_casgstatus, u64 *callerg_id = bpf_map_lookup_elem(&goroutines_in_creation, &g_id); if (callerg_id != NULL) { - // This function is called inside the newproc1 function, so we need to send an event for the caller. - // We cannot use a uretprobe on newproc1 so we're using this trick! - SEND_EVENT_WITH_SAMPLING(GO_RUNTIME_EVENT_TYPE_NEWGOROUTINE, g_id, g_parent_id, *callerg_id, gp_id, 0, 0, 0, probe_start_ns); + // This function is called inside the newproc1 function, so we need to send an event for the + // caller. We cannot use a uretprobe on newproc1 so we're using this trick! + SEND_EVENT_WITH_SAMPLING(GO_RUNTIME_EVENT_TYPE_NEWGOROUTINE, g_id, g_parent_id, *callerg_id, + gp_id, 0, 0, 0, probe_start_ns); _ret = bpf_map_delete_elem(&goroutines_in_creation, &g_id); if (_ret < 0) { bpf_printk("Failed to delete goroutines_in_creation, ret=%d", _ret); @@ -62,7 +62,8 @@ int BPF_KPROBE(uprobe_casgstatus, } } - SEND_EVENT_WITH_SAMPLING(GO_RUNTIME_EVENT_TYPE_CAS_G_STATUS, g_id, g_parent_id, oldval, newval, gp_id, 0, 0, probe_start_ns); + SEND_EVENT_WITH_SAMPLING(GO_RUNTIME_EVENT_TYPE_CAS_G_STATUS, g_id, g_parent_id, oldval, newval, + gp_id, 0, 0, probe_start_ns); return 0; } @@ -71,13 +72,12 @@ int BPF_KPROBE(uprobe_casgstatus, // } // _type is abi.Type SEC("uprobe/runtime.newobject") -int BPF_KPROBE(uprobe_newobject, - const void *typ) { +int BPF_KPROBE(uprobe_newobject, const void *typ) { u64 probe_start_ns = bpf_ktime_get_ns(); u64 _ret; struct go_runtime_g g; - _ret = get_go_g_struct_arm(ctx, &g); + _ret = get_go_g_struct(ctx, &g); if (_ret < 0) { bpf_printk("Failed to read g, ret=%d", _ret); return 0; @@ -97,21 +97,19 @@ int BPF_KPROBE(uprobe_newobject, bpf_printk("newobject: size=%llu, kind=%llu", go_type.size, go_type.kind); #endif - SEND_EVENT_WITH_SAMPLING(GO_RUNTIME_EVENT_TYPE_NEW_OBJECT, g.goid, g.parentGoid, go_type.size, go_type.kind, 0, 0, 0, probe_start_ns); + SEND_EVENT_WITH_SAMPLING(GO_RUNTIME_EVENT_TYPE_NEW_OBJECT, g.goid, g.parentGoid, go_type.size, + go_type.kind, 0, 0, 0, probe_start_ns); return 0; } -// func makeslice(et *_type, len, cap int) unsafe.Pointer +// func makeslice(et *_type, len, cap int) unsafe.Pointer SEC("uprobe/runtime.makeslice") -int BPF_KPROBE(uprobe_makeslice, - const void *typ, - const u64 len, - const u64 cap) { +int BPF_KPROBE(uprobe_makeslice, const void *typ, const u64 len, const u64 cap) { u64 probe_start_ns = bpf_ktime_get_ns(); u64 _ret; struct go_runtime_g g; - _ret = get_go_g_struct_arm(ctx, &g); + _ret = get_go_g_struct(ctx, &g); if (_ret < 0) { bpf_printk("Failed to read g, ret=%d", _ret); return 0; @@ -133,21 +131,19 @@ int BPF_KPROBE(uprobe_makeslice, bpf_printk("makeslice: len=%llu, cap=%llu", len, cap); #endif - SEND_EVENT_WITH_SAMPLING(GO_RUNTIME_EVENT_TYPE_MAKE_SLICE, g.goid, g.parentGoid, go_type.size, go_type.kind, len, cap, 0, probe_start_ns); + SEND_EVENT_WITH_SAMPLING(GO_RUNTIME_EVENT_TYPE_MAKE_SLICE, g.goid, g.parentGoid, go_type.size, + go_type.kind, len, cap, 0, probe_start_ns); return 0; } // func makemap(t *abi.MapType, hint int, m *maps.Map) *maps.Map SEC("uprobe/runtime.makemap") -int BPF_KPROBE(uprobe_makemap, - const void *typ, - const u64 hint, - const void *m) { +int BPF_KPROBE(uprobe_makemap, const void *typ, const u64 hint, const void *m) { u64 probe_start_ns = bpf_ktime_get_ns(); u64 _ret; struct go_runtime_g g; - _ret = get_go_g_struct_arm(ctx, &g); + _ret = get_go_g_struct(ctx, &g); if (_ret < 0) { bpf_printk("Failed to read g, ret=%d", _ret); return 0; @@ -162,34 +158,34 @@ int BPF_KPROBE(uprobe_makemap, bpf_printk("Failed to read go_map, ret=%d, go_map=%p", _ret, go_map); return 0; } - + struct go_abi_type key_type, elem_type; - _ret = bpf_probe_read(&key_type, sizeof(key_type), (void*)go_map.key_ptr); + _ret = bpf_probe_read(&key_type, sizeof(key_type), (void *)go_map.key_ptr); if (_ret < 0) { bpf_printk("Failed to read key_type, ret=%d, key_type=%p", _ret, key_type); return 0; } - - _ret = bpf_probe_read(&elem_type, sizeof(elem_type), (void*)go_map.elem_ptr); + + _ret = bpf_probe_read(&elem_type, sizeof(elem_type), (void *)go_map.elem_ptr); if (_ret < 0) { bpf_printk("Failed to read elem_type, ret=%d, elem_type=%p", _ret, elem_type); return 0; } - + #ifdef BPF_DEBUG bpf_printk("makemap: key size=%llu, key kind=%llu", key_type.size, key_type.kind); - bpf_printk("makemap: elem size=%llu, elem kind=%llu, hint=%llu", elem_type.size, elem_type.kind, hint); + bpf_printk("makemap: elem size=%llu, elem kind=%llu, hint=%llu", elem_type.size, elem_type.kind, + hint); #endif - - SEND_EVENT_WITH_SAMPLING(GO_RUNTIME_EVENT_TYPE_MAKE_MAP, g.goid, g.parentGoid, key_type.size, key_type.kind, elem_type.size, elem_type.kind, hint, probe_start_ns); + + SEND_EVENT_WITH_SAMPLING(GO_RUNTIME_EVENT_TYPE_MAKE_MAP, g.goid, g.parentGoid, key_type.size, + key_type.kind, elem_type.size, elem_type.kind, hint, probe_start_ns); return 0; } // func newproc1(fn *funcval, callergp *g, callerpc uintptr, parked bool, waitreason waitReason) *g SEC("uprobe/runtime.newproc1") -int BPF_KPROBE(uprobe_newproc1, - const void *__skip_fn, - const void *callergp) { +int BPF_KPROBE(uprobe_newproc1, const void *__skip_fn, const void *callergp) { u64 _ret, goid, callergoid; struct go_runtime_g g; @@ -200,8 +196,8 @@ int BPF_KPROBE(uprobe_newproc1, } callergoid = g.goid; - - _ret = get_go_g_struct_arm(ctx, &g); + + _ret = get_go_g_struct(ctx, &g); if (_ret < 0) { bpf_printk("newproc1: failed to read g, ret=%d", _ret); return 0; @@ -229,7 +225,7 @@ int BPF_KPROBE(uprobe_goexit1) { u64 _ret; struct go_runtime_g g; - _ret = get_go_g_struct_arm(ctx, &g); + _ret = get_go_g_struct(ctx, &g); if (_ret < 0) { bpf_printk("goexit1: failed to read g, ret=%d", _ret); return 0; @@ -247,4 +243,4 @@ int BPF_KPROBE(uprobe_goexit1) { } return 0; -} \ No newline at end of file +} diff --git a/xgotop.h b/xgotop.h index 77a634f..c844866 100644 --- a/xgotop.h +++ b/xgotop.h @@ -2,17 +2,22 @@ #define _XGOTOP_H #include "vmlinux.h" +// #include #include #include #include -#if !defined(bpf_target_arm64) - #error "This BPF program is only supported on arm64" +#if !defined(bpf_target_arm64) && !defined(bpf_target_x86) +#error "This BPF program is only supported on arm64 and x86_64" #endif -#define __ARM__GO_G_ADDR(x) (__PT_REGS_CAST(x)->regs[28]) +#if defined(bpf_target_arm64) +#define __GO_G_ADDR(ctx) (__PT_REGS_CAST(ctx)->regs[28]) +#elif defined(bpf_target_x86) +#define __GO_G_ADDR(ctx) (__PT_REGS_CAST(ctx)->r14) +#endif // #define BPF_DEBUG 1 @@ -28,48 +33,48 @@ char LICENSE[] SEC("license") = "GPL"; struct trace_event_raw_bpf_trace_printk___x {}; #undef bpf_printk -#define bpf_printk(fmt, ...) \ -({ \ - static char ____fmt[] = fmt "\0"; \ - if (bpf_core_type_exists(struct trace_event_raw_bpf_trace_printk___x)) { \ - bpf_trace_printk(____fmt, sizeof(____fmt) - 1, ##__VA_ARGS__); \ - } else { \ - ____fmt[sizeof(____fmt) - 2] = '\n'; \ - bpf_trace_printk(____fmt, sizeof(____fmt), ##__VA_ARGS__); \ - } \ -}) +#define bpf_printk(fmt, ...) \ + ({ \ + static char ____fmt[] = fmt "\0"; \ + if (bpf_core_type_exists(struct trace_event_raw_bpf_trace_printk___x)) { \ + bpf_trace_printk(____fmt, sizeof(____fmt) - 1, ##__VA_ARGS__); \ + } else { \ + ____fmt[sizeof(____fmt) - 2] = '\n'; \ + bpf_trace_printk(____fmt, sizeof(____fmt), ##__VA_ARGS__); \ + } \ + }) typedef struct go_runtime_g { uint8_t _pad1[G_GOID_OFFSET]; - uint64_t goid; // offset=152 size=8 + uint64_t goid; // offset=152 size=8 uint8_t _pad2[G_PARENT_GOID_OFFSET - G_GOID_OFFSET - sizeof(uint64_t)]; - uint64_t parentGoid; // offset=272 size=8 + uint64_t parentGoid; // offset=272 size=8 } __attribute__((packed)) go_runtime_g; typedef struct go_abi_type { - uint64_t size; // offset=0 size=8 + uint64_t size; // offset=0 size=8 uint8_t _pad1[15]; - uint8_t kind; // offset=23 size=1 + uint8_t kind; // offset=23 size=1 } __attribute__((packed)) go_abi_type; typedef struct go_abi_map_type { uint8_t _pad1[48]; - uint64_t key_ptr; // offset=48 size=8 - uint64_t elem_ptr; // offset=56 size=8 + uint64_t key_ptr; // offset=48 size=8 + uint64_t elem_ptr; // offset=56 size=8 } __attribute__((packed)) go_abi_map_type; typedef enum go_runtime_event_type { GO_RUNTIME_EVENT_TYPE_CAS_G_STATUS = 0, - GO_RUNTIME_EVENT_TYPE_MAKE_SLICE = 1, - GO_RUNTIME_EVENT_TYPE_MAKE_MAP = 2, - GO_RUNTIME_EVENT_TYPE_NEW_OBJECT = 3, + GO_RUNTIME_EVENT_TYPE_MAKE_SLICE = 1, + GO_RUNTIME_EVENT_TYPE_MAKE_MAP = 2, + GO_RUNTIME_EVENT_TYPE_NEW_OBJECT = 3, GO_RUNTIME_EVENT_TYPE_NEWGOROUTINE = 4, - GO_RUNTIME_EVENT_TYPE_GOEXIT = 5, + GO_RUNTIME_EVENT_TYPE_GOEXIT = 5, } __attribute__((packed)) go_runtime_event_type_t; typedef struct go_runtime_event { u64 timestamp; - + u32 event_type; u32 probe_duration_ns; @@ -91,7 +96,8 @@ const go_runtime_event_t *unused_go_runtime_event_t __attribute__((unused)); // TODO: This doesn't work and IDK why. // -// // Source: https://github.com/pixie-io/pixie/blob/a95d6617f17e374ff0a79b1a49fc1abc2ca0023a/src/stirling/source_connectors/socket_tracer/bcc_bpf/go_trace_common.h#L94 +// // Source: +// https://github.com/pixie-io/pixie/blob/a95d6617f17e374ff0a79b1a49fc1abc2ca0023a/src/stirling/source_connectors/socket_tracer/bcc_bpf/go_trace_common.h#L94 // // // // Gets the ID of the go routine currently scheduled on the current tgid and pid. // // We do that by accessing the thread local storage (fsbase) of the current pid from the @@ -102,7 +108,7 @@ const go_runtime_event_t *unused_go_runtime_event_t __attribute__((unused)); // if (!task_ptr) { // return 0; // } - + // #if defined(TARGET_ARCH_X86_64) // const void* fs_base = (void*)task_ptr->thread.fsbase; // #elif defined(TARGET_ARCH_AARCH64) @@ -110,21 +116,21 @@ const go_runtime_event_t *unused_go_runtime_event_t __attribute__((unused)); // #else // #error Target architecture not supported // #endif - + // // Get ptr to `struct g` from 8 bytes before fsbase and then access the goID. // u64 goid; // size_t g_addr; // bpf_probe_read_user(&g_addr, sizeof(void*), (void*)(fs_base + G_ADDR_OFFSET)); // bpf_probe_read_user(&goid, sizeof(void*), (void*)(g_addr + G_GOID_OFFSET)); // // bpf_probe_read_user(&parentGoid, sizeof(void*), (void*)(g_addr + G_PARENT_GOID_OFFSET)); - + // // return parentGoid << 32 | goid; // return goid; // } // Maps struct { - __uint(type, BPF_MAP_TYPE_RINGBUF); + __uint(type, BPF_MAP_TYPE_RINGBUF); // TODO: If this is too small and the read workers put more messages in the Go chan // than processors can handle, the ringbuffer is blocked and we start to get these // error messages in trace_pipe: @@ -132,7 +138,7 @@ struct { // We need to send another event to the Go prog with a new ringbuffer only for errors // (and stats in the future) and the new ACC metric (accuracy) should use that ringbuffer's // message count to calculate the accuracy as ACC = 100 * (total_events - errors) / total_events - __uint(max_entries, 1 << 24); // sizeof(go_runtime_event_t) = 64 = 2^6 => 2^(24 + 6) = 1 GB + __uint(max_entries, 1 << 24); // sizeof(go_runtime_event_t) = 64 = 2^6 => 2^(24 + 6) = 1 GB } events SEC(".maps"); struct { @@ -145,52 +151,53 @@ struct { struct { __uint(type, BPF_MAP_TYPE_LRU_HASH); __uint(max_entries, 1 << 16); - __type(key, u64); // Address of g.id - __type(value, u64); // Address of callerg.id + __type(key, u64); // Address of g.id + __type(value, u64); // Address of callerg.id } goroutines_in_creation SEC(".maps"); struct { __uint(type, BPF_MAP_TYPE_LRU_HASH); __uint(max_entries, 1 << 16); - __type(key, u64); // Address of g.id - __type(value, u64); // Timestamp of exit (unused for now) + __type(key, u64); // Address of g.id + __type(value, u64); // Timestamp of exit (unused for now) } goroutines_in_exit SEC(".maps"); -#define SEND_EVENT_WITH_SAMPLING(EVENT_TYPE, G_ID, G_PARENT_ID, ATTR0, ATTR1, ATTR2, ATTR3, ATTR4, START_NS_U64) \ - do { \ - u32 event_type = (EVENT_TYPE); \ - u32 *rate_ptr = bpf_map_lookup_elem(&sampling_rates, &event_type); \ - if (rate_ptr) { \ - u32 rate = *rate_ptr; \ - u32 rand = bpf_get_prandom_u32() % 100; \ - if (rand >= rate) { \ - break; \ - } \ - } \ - go_runtime_event_t *e = bpf_ringbuf_reserve(&events, sizeof(go_runtime_event_t), 0); \ - if (!e) { \ - bpf_printk("Failed to reserve ringbuf"); \ - break; \ - } \ - e->timestamp = bpf_ktime_get_ns(); \ - e->event_type = (EVENT_TYPE); \ - e->probe_duration_ns = (u32)(e->timestamp - (START_NS_U64)); \ - e->goroutine = (G_ID); \ - e->parent_goroutine = (G_PARENT_ID); \ - e->attributes[0] = (ATTR0); \ - e->attributes[1] = (ATTR1); \ - e->attributes[2] = (ATTR2); \ - e->attributes[3] = (ATTR3); \ - e->attributes[4] = (ATTR4); \ - bpf_ringbuf_submit(e, 0); \ - } while (0) - -__always_inline static int get_go_g_struct_arm(struct pt_regs *ctx, struct go_runtime_g *g) { - u64 x28 = __ARM__GO_G_ADDR(ctx); +#define SEND_EVENT_WITH_SAMPLING(EVENT_TYPE, G_ID, G_PARENT_ID, ATTR0, ATTR1, ATTR2, ATTR3, ATTR4, \ + START_NS_U64) \ + do { \ + u32 event_type = (EVENT_TYPE); \ + u32 *rate_ptr = bpf_map_lookup_elem(&sampling_rates, &event_type); \ + if (rate_ptr) { \ + u32 rate = *rate_ptr; \ + u32 rand = bpf_get_prandom_u32() % 100; \ + if (rand >= rate) { \ + break; \ + } \ + } \ + go_runtime_event_t *e = bpf_ringbuf_reserve(&events, sizeof(go_runtime_event_t), 0); \ + if (!e) { \ + bpf_printk("Failed to reserve ringbuf"); \ + break; \ + } \ + e->timestamp = bpf_ktime_get_ns(); \ + e->event_type = (EVENT_TYPE); \ + e->probe_duration_ns = (u32)(e->timestamp - (START_NS_U64)); \ + e->goroutine = (G_ID); \ + e->parent_goroutine = (G_PARENT_ID); \ + e->attributes[0] = (ATTR0); \ + e->attributes[1] = (ATTR1); \ + e->attributes[2] = (ATTR2); \ + e->attributes[3] = (ATTR3); \ + e->attributes[4] = (ATTR4); \ + bpf_ringbuf_submit(e, 0); \ + } while (0) + +__always_inline static int get_go_g_struct(struct pt_regs *ctx, struct go_runtime_g *g) { + u64 g_addr = __GO_G_ADDR(ctx); #ifdef BPF_DEBUG - bpf_printk("get_go_g_struct_arm: x28=%llu", x28); + bpf_printk("get_go_g_struct: g_addr=%llu", g_addr); #endif - return bpf_probe_read(g, sizeof(*g), (void*)x28); + return bpf_probe_read(g, sizeof(*g), (void *)g_addr); } -#endif \ No newline at end of file +#endif