# Introduction

> Source: https://www.ymotongpoo.com/books/go-ebpf-primer/00-introduction/


This book accompanies my Go Conference 2026 talk "[Behind the Scenes of OpenTelemetry eBPF Instrumentation](https://gocon.jp/2026/timetable/1263399/)" (in Japanese). The 40-minute talk assumes an advanced audience. The book starts with the computer fundamentals needed to follow the same material.

**eBPF** loads small monitoring programs into the Linux kernel without any change to the application. Later chapters cover how it does that. With eBPF you can capture distributed traces of HTTP and gRPC without changing the application source code, which the industry calls **zero-code instrumentation**. Product descriptions promise that it needs neither a rebuild nor a redeployment, and that it works regardless of language.

Apply a standard return hook to a Go binary, and the observed program can crash with this error.

```
fatal error: unknown caller pc
```

The observer has crashed the program it was only supposed to watch. A technique that works on other languages fails on Go before it collects a single trace.

This book groups these failures into four **hurdles**. Each hurdle comes from the design that makes Go fast and easy to write. Standard eBPF instrumentation assumes that the OS manages a stack that does not move, and that functions follow the platform's calling convention. Threads in C, and platform threads in Java, meet both assumptions. Go moves a goroutine's stack while the program runs, uses its own calling convention, and keeps scheduling and memory management inside the runtime. All of the assumptions fail at once.

This book uses **OpenTelemetry eBPF Instrumentation** (OBI) as its case study. OBI grew out of Grafana Beyla after Grafana donated Beyla to the OpenTelemetry project. Go is the only language for which OBI instruments **library** functions. Other languages do get dedicated hooks inside their runtimes, but only Go gets a path that targets the functions of a library such as `net/http` or gRPC. Chapter 8 explains the difference. Each hurdle includes code you can run and the corresponding OBI implementation.

{{< message >}}
I verified the program output and the quotations in this book against these versions.

- Go 1.26.5 (linux/amd64). The build in Chapter 9 alone uses `cgr.dev/chainguard/go:latest` (Go 1.27.1)
- OBI v0.13.0, released September 4, 2026
- Linux kernel 7.0.0-31-generic (x86_64)
- Docker 29.0.0, Docker Compose v2.40.3, and grafana/otel-lgtm 0.32.1 for the hands-on chapter, Chapter 9

Register assignments, field offsets, and the limit on how far the parent walk goes are all version-dependent. They change with the version of Go and with the OBI release. When you cite a detail of the implementation, check it against the source for the version you target.
{{< /message >}}

## Distributed traces and traceparent

**Instrumentation** means adding observation points to a program to record processing times and call relationships. In this book, **binary** means the executable file produced by compilation, not an arbitrary sequence of 0s and 1s.

A **distributed trace** is the record that lets you read one operation across several services as a single flow. When service A calls service B, both services tag their work with the same **trace ID**. The **backend**, which is the server that collects and displays the records, receives the two records separately and joins them on that ID. Each segment of the trace is a **span**, and B's span carries A's span ID as its parent.

The HTTP header that carries this identification is `traceparent`. W3C Trace Context defines its format, which looks like `00-<trace ID>-<span ID>-01`. Under SDK-based instrumentation, the application attaches this header itself. Zero-code instrumentation attaches it from the outside, with no change to the application's code.

![The basics of distributed tracing](20260911-distributed-trace-basics.png)
*Figure 1: Solid arrows show the flow of requests; dashed arrows show each service sending its record to the backend. The trace IDs match, so the backend can display the two spans as one trace, and B's span holds A's span ID as its parent.*

eBPF reaches this result along two paths. Instrumentation that watches communication sees byte sequences such as `GET /items` cross a socket and recognizes them as HTTP. It then measures elapsed time by matching each request to its response. Instrumentation that watches functions instead reads the arguments of specific `net/http` functions at the moment the program calls them. The first path works for any language. The second path needs an implementation for each one, and all four hurdles in this book live there.

## How to read this book

Chapters 2 through 6 explain how a computer and an operating system run a program, and what the Go runtime manages on top of that. They settle where the stack lives, where a call puts its arguments, and how a tool finds the address of a function. With that in hand, you can see why each of the four hurdles is hard.

The background chapters connect to the hurdles as follows.

| Background chapter | Mainly leads to |
|---|---|
| Chapter 2: How Computers Run Programs | Background for everything. Especially the registers in Hurdle 2 and the offsets in Hurdle 3 |
| Chapter 3: The OS and the Kernel | eBPF in Chapter 7, and the difference between goroutines and threads in Hurdle 4 |
| Chapter 4: From Source Code to Executable | Disassembly in Hurdle 1, and symbols and DWARF in Hurdle 3 |
| Chapter 5: How Function Calls Work | Return addresses in Hurdle 1 and the calling convention in Hurdle 2 |
| Chapter 6: What the Go Runtime Manages | Movable stacks in Hurdle 1 and goroutines in Hurdle 4 |

Chapters 10 through 13 cover the four hurdles themselves. Before them, Chapter 7 introduces eBPF and Chapter 8 introduces OBI. Chapter 9 then runs OBI for real and views the resulting traces and metrics in Grafana. Chapter 14 examines the consequences for Go developers, Chapter 15 covers developments in upstream Go, and the appendix contains a glossary.

The order of the background chapters does not match the timeline along which your source code becomes a running program. Use the next figure whenever you want to check where in that flow the current discussion sits.

![The flow from source to execution](20260911-source-to-execution.png)
*Figure 2: Arrows show the flow of time and transformation. Below each stage sits the chapter that covers it.*

The one prerequisite is that you can read Go code. You need no knowledge of operating systems, assembly, eBPF, or distributed tracing. I explain each concept as it comes up.

The sample code links to the Go Playground. The isolated environment there changes some results: process IDs stay constant, and a repeated run can return a cached result. To watch the addresses change, edit at least one character between runs, or run the code on your own machine. The disassembly and the build-setting sections need a local build.

If you already know Go's internals and Linux's execution model, skip Chapters 2 through 6 and start from Chapter 7, "What Is eBPF?" The glossary points back to the chapter that introduces each term.

The hurdle chapters quote OBI's implementation code, but the surrounding explanation stands on its own. Long excerpts are collapsed, and each hurdle ends with a summary.

