# go-ftrace: BPF-based function call tracing for Go binaries

> go-ftrace attaches uprobes to Go ELF executables and reconstructs nested call graphs with arguments and return values from DWARF. It works on x86-64 Linux with specific build constraints, and the trade-offs are worth knowing before you adopt it.

**hitzhangjie/go-ftrace** — bpf(2)-based ftrace(1) like function call graph for Go

- Repository: https://github.com/hitzhangjie/go-ftrace
- Stars: 25 · Forks: 3
- Language: C
- License: not declared
- Published: 2026-08-22 · Updated: 2026-08-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/hitzhangjie-go-ftrace

## What go-ftrace actually does

go-ftrace is a command-line tool that attaches BPF uprobes to functions in a running Go process and prints a nested call graph, similar to ftrace(1) but for Go. The README lists two use cases: wall time profiling and execution flow observing. It is aimed at developers who need to see which Go functions call which, with arguments and return values, without modifying the source or recompiling with instrumentation. The tool reads DWARF debug info from the binary, compiles fetch rules, and copies values when the uprobe fires. The output shows a tree with indentation, per-frame latency, source file and line, and Go-like structured values for arguments and returns.

## The mechanism: uprobes, DWARF, and BPF maps

The core mechanism is a BPF program attached to user-space probes (uprobes). When a traced function is entered, the uprobe fires and the BPF program captures the current register state. The tool then reads DWARF from the ELF binary to understand the function signature and layout of arguments and return values. It compiles that into fetch rules that copy values from registers and memory when the probe fires. The README mentions BPF_MAP_TYPE_QUEUE, which is likely used to pass events from kernel to user space. The design document, linked as a blog post, covers the internals in Chinese, but the README gives enough to see the data flow: binary ELF sections provide symbol addresses and type info; uprobes trigger on function entry and exit; the BPF program records timestamps and register contents; user-space code formats the output. The tool supports tracing specific functions via a pattern like 'main.add*', and it can include runtime functions like 'runtime.chan*' if you ask for them.

## Getting it running: commands and constraints

Installation is straightforward: `go install github.com/hitzhangjie/go-ftrace/cmd/ftrace@latest` or `make install` from a source checkout. Running as root is simplest; for non-root users, `make install` performs privilege setup (symlink, ownership, setuid). The README gives concrete examples: `sudo ftrace -u 'main.add*' ./main` traces all functions matching that pattern. You can specify multiple `-u` flags to trace multiple patterns, and you can trace methods like `main.(*Student).String`. The tool requires the binary to be a Go ELF executable, non-stripped, built with non-PIE mode, and with ELF sections .symtab and .(z)debug_info present. The examples directory has a demo you build with `make -C examples`, and testdata fixtures with `make -C testdata`.

## Auto-fetching arguments and return values

A standout feature is automatic fetching of arguments and return values, on by default. The README says it reads DWARF, compiles fetch rules, and prints Go-like structured values. For common types (integers, bools, strings, slices, pointers to structs, interfaces), you need no extra flags. The example output shows `main.add(a=1, b=2)` and `ret0=3`. You can turn off auto-fetch with `--fargs-auto` and `--frets-auto` independently. A nil error prints as `nil`. There is a known limitation: the first hit of a new interface concrete type may show `<unavailable>` for nested bytes (like the string inside an `error`); later hits capture the full type. `--hide-unexported` (off by default) omits unexported struct fields, useful for generated types like `proto.Message`. This auto-fetch is a real time-saver compared to manual register-level tracing.

## Manual fetch rules for precise control

When auto-fetch is too noisy, you can write hand-crafted `--fargs` and `--frets` rules. The README gives an example: `--fargs 'main.add(a=(%ax):s64, b=(%bx):s64)'`. Any `--fargs` on the command line turns off entry auto for the whole run; any `--frets` turns off return auto. This is useful when you only want one field, or when you need a layout auto does not cover. The rule syntax is documented in `docs/FetchArgRule.md`, with examples in `docs/FetchArgExamples.md`. The trade-off is that you must know register names and types, which is more work but gives you finer control over histogram aggregation. The README suggests manual rules for cleaner aggregate histograms, implying the auto-fetch may produce too many distinct values for aggregation.

## Limitations and failure modes

The README is explicit about limits: Linux with bpf(2), uprobe, and BPF_MAP_TYPE_QUEUE; x86-64 little endian; Go ELF executable, non-stripped, non-PIE, with .symtab and .(z)debug_info. That excludes many production binaries, which are often PIE and stripped. The tool handles namespaced PIDs using `bpf_get_ns_current_pid_tgid` when the kernel provides it (upstream 5.8; some distros backport), otherwise `bpf_get_current_pid_tgid`. Tested kernels are Linux 5.4 and 6.6 (WSL2). Earlier kernels are untested, not a hard cutoff, but load success depends on actual BPF helpers and verifier, which distros backport unpredictably. If your binary lacks debug info, the tool cannot auto-fetch arguments; you would need manual rules based on registers, but even that requires symbols. A stripped binary means no function names, so tracing patterns like 'main.add' would not match. This is a tool for development and debugging, not for tracing optimized release binaries.

## Alternatives and how they differ

The closest alternative is the standard Go toolchain's `go tool pprof` with CPU profiling, which samples the program counter and produces a call graph without requiring non-PIE or non-stripped binaries. It works on any Go binary that has pprof enabled, but it samples, not traces every call, and it does not show arguments or return values. Another alternative is `bpftrace`, a general-purpose BPF tracing tool that can attach uprobes to any function, but it requires manual scripting to extract Go arguments from registers and DWARF, and it does not automatically understand Go types. go-ftrace's advantage is that it reads DWARF and reconstructs Go values automatically, saving you from writing complex BPF scripts. The trade-off is the build constraints: pprof works on stripped PIE binaries, go-ftrace does not.

## Maintenance and licensing

The repository shows no recent releases and the license is unknown, which is a risk for enterprise adoption. The README links to a design blog post from 2023, suggesting the project has been around but may not be actively maintained. The last push date is unknown, and there are no releases retrieved. This means you should treat it as a source-level tool: clone, build, and fix issues yourself if they arise. The code is in C and Go, and the build uses `make`, so you need a Go toolchain and a C compiler. The lack of a license is a significant concern: without a license, you have no legal right to use, modify, or redistribute the code beyond what copyright law allows. Before adopting it, contact the author or check the repository for a license file. The privilege setup for non-root users (setuid) also has security implications; you should understand what `make install` does before running it.

## Conclusion

Adopt go-ftrace if you need to observe wall-time execution flow of a Go binary on x86-64 Linux, and you can control the build (non-PIE, non-stripped, with .symtab and .(z)debug_info). Skip it for stripped or PIE binaries, for other architectures, or if you need kernel-level tracing beyond user-space functions. Before relying on it, verify that your kernel supports the required BPF helpers (bpf_get_ns_current_pid_tgid or fallback) and that your Go binary meets the ELF section requirements. The project is a focused tool, not a general-purpose profiler; expect to rebuild your binary for tracing.

## FAQ

### What are the differences between ftrace and strace?

go-ftrace positions itself as an ftrace(1)-like tool built on bpf(2): it attaches uprobes to functions inside a Go ELF binary and prints a nested call graph with arguments and return values read from DWARF. The README does not compare it against strace or describe syscall-level behavior.

### What is DTrace and what is it used for in Linux?

The README does not mention DTrace at all. What it describes is a bpf(2)-based ftrace(1)-like function call graph tracer for Golang processes, limited to Linux with bpf(2), uprobe and BPF_MAP_TYPE_QUEUE support.

### What is kernel tracing?

In this project kernel tracing means loading an eBPF program into the kernel, which the Makefile calls out as requiring special privileges, and then attaching uprobes to Go functions. The binary is installed setuid into /usr/sbin for that reason, and missing helpers and stricter verifiers are handled at load time.

### What is a stack trace in Linux?

The tracer prints one: the output shows the full call tree of who called whom and when, with per-frame latency in seconds accumulating as the stack unwinds, and calls from other goroutines of the same binary included.

## Sources

- [Official README](https://github.com/hitzhangjie/go-ftrace#readme)
- [Project repository](https://github.com/hitzhangjie/go-ftrace)
- [README](https://github.com/hitzhangjie/go-ftrace/blob/master/README.md)
- [Releases](https://github.com/hitzhangjie/go-ftrace/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/hitzhangjie-go-ftrace
