<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Mahesh Rayas</title>
    <description>The latest articles on Forem by Mahesh Rayas (@maheshrayas).</description>
    <link>https://forem.com/maheshrayas</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3324179%2F5559c37b-fa57-4aa0-9e57-c6501cdc2030.jpg</url>
      <title>Forem: Mahesh Rayas</title>
      <link>https://forem.com/maheshrayas</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/maheshrayas"/>
    <language>en</language>
    <item>
      <title>04 – eBPF Uprobes: Tracing gRPC Headers by Unpacking Go Function Internals</title>
      <dc:creator>Mahesh Rayas</dc:creator>
      <pubDate>Sun, 03 Aug 2025 08:43:21 +0000</pubDate>
      <link>https://forem.com/maheshrayas/04-ebpf-uprobes-decoding-go-function-arguments-registers-memory-layout-to-parse-grpc-headers-6n8</link>
      <guid>https://forem.com/maheshrayas/04-ebpf-uprobes-decoding-go-function-arguments-registers-memory-layout-to-parse-grpc-headers-6n8</guid>
      <description>&lt;h2&gt;
  
  
  Introduction to Uprobes
&lt;/h2&gt;

&lt;p&gt;In the &lt;a href="https://dev.to/maheshrayas/-03-lets-understand-kprobes-kretprobes-4ke"&gt;previous post&lt;/a&gt;, we explored kprobes and demonstrated a simple eBPF example that instruments kernel-space HTTP servers to extract HTTP headers—without modifying the source code. In today's blog, we shift our focus to uprobes, which enable similar instrumentation for user-space applications, addressing use cases that kprobes cannot cover.&lt;/p&gt;

&lt;p&gt;Much like how kprobes allow us to attach eBPF programs to kernel functions, uprobes let us hook into functions in user-space binaries. This provides powerful observability capabilities for modern applications written in languages like Go. In particular, we'll explore how to instrument Go binaries by attaching uprobes to standard library functions (e.g., &lt;code&gt;net/http&lt;/code&gt;, &lt;code&gt;google.golang.org/grpc&lt;/code&gt;) and user-defined logic.&lt;/p&gt;

&lt;p&gt;To hook into a Go function using uprobes, we must understand how Go lays out its function arguments in memory. This is especially important for Go 1.17+, which introduced a register-based calling convention that changed how arguments are passed between functions. We'll learn how to inspect process memory and extract function arguments by understanding Go's calling conventions, memory addressing, and pointer dereferencing techniques.&lt;br&gt;
Go binaries are increasingly common in cloud-native environments. Instrumenting them at runtime using eBPF uprobes enables deep observability without modifying or recompiling source code—making it ideal for production debugging and monitoring.&lt;/p&gt;

&lt;p&gt;In this post, we will:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Understand how Go passes function arguments (e.g., int, string, struct) in &lt;code&gt;Go 1.17&lt;/code&gt; and beyond&lt;/li&gt;
&lt;li&gt;Use tools like &lt;code&gt;nm&lt;/code&gt; and &lt;code&gt;objdump&lt;/code&gt; to locate function symbols in Go binaries.&lt;/li&gt;
&lt;li&gt;Attach uprobes to Go functions and extract runtime arguments.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;delve&lt;/code&gt; to debug Go programs and inspect memory layout for function hooking.&lt;/li&gt;
&lt;li&gt;Apply these techniques to parse gRPC headers from Go-based gRPC services using eBPF uprobes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By the end of this post, you'll have a solid understanding of how to leverage uprobes for deep introspection of Go applications, opening up new possibilities for observability and debugging in production environments.&lt;/p&gt;
&lt;h2&gt;
  
  
  Architecture Overview
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwzxl3zu1cj3s8sof9a9j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwzxl3zu1cj3s8sof9a9j.png" alt=" " width="661" height="334"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Prerequisites and Setup
&lt;/h2&gt;

&lt;p&gt;Before diving into the technical details, let's establish our testing environment and the tools we'll need throughout this tutorial.&lt;/p&gt;
&lt;h3&gt;
  
  
  System Requirements
&lt;/h3&gt;

&lt;p&gt;Linux x86_64 (kernel 4.1+ for uprobe support)&lt;br&gt;
Root privileges or appropriate capabilities for eBPF operations&lt;/p&gt;
&lt;h3&gt;
  
  
  Development Tools
&lt;/h3&gt;

&lt;p&gt;Rust: Our userspace framework for eBPF program development and attachment.&lt;/p&gt;

&lt;p&gt;Go 1.17+: Target language for our instrumentation examples&lt;br&gt;
delve: Go debugger for inspecting memory layouts and validating our approach&lt;/p&gt;

&lt;p&gt;binutils: For nm, objdump, and other binary analysis tools.&lt;/p&gt;
&lt;h3&gt;
  
  
  Installation and Setup
&lt;/h3&gt;

&lt;p&gt;Install delve if you haven't already:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/go-delve/delve/cmd/dlv@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enable ptrace for delve to attach to running processes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo &lt;/span&gt;0 | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /proc/sys/kernel/yama/ptrace_scope
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Sample Go Application
&lt;/h3&gt;

&lt;p&gt;We'll work with two Go applications throughout this tutorial:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/maheshrayas/blogs/tree/main/ebpf/04-tracing-grpc-headers/code/sample_go" rel="noopener noreferrer"&gt;sample_go&lt;/a&gt;: A simple Go program designed to help us analyze Go's memory layout and calling conventions for different argument types (int, string, struct).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/maheshrayas/blogs/tree/main/ebpf/04-tracing-grpc-headers/code/sample_grpc" rel="noopener noreferrer"&gt;sample_grpc&lt;/a&gt;: A Go gRPC server that we'll instrument to extract gRPC headers using our Rust+eBPF tracer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rust + eBPF
&lt;/h3&gt;

&lt;p&gt;trace-grpc-headers. Go through the &lt;a href="https://github.com/maheshrayas/blogs/tree/main/ebpf/04-tracing-grpc-headers/code/README.md" rel="noopener noreferrer"&gt;readme&lt;/a&gt; to get it up and running &lt;/p&gt;

&lt;h2&gt;
  
  
  Go Calling Convention: The Register-Based Revolution (Go 1.17+)
&lt;/h2&gt;

&lt;p&gt;Note: This blog focuses specifically on the amd64 architecture.&lt;/p&gt;

&lt;p&gt;Starting from Go 1.17, the Go compiler adopted a register-based calling convention, moving away from the traditional stack-only approach used in earlier versions. This change significantly impacts how we extract function arguments in eBPF uprobes.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Register Allocation Strategy
&lt;/h3&gt;

&lt;p&gt;Go 1.17+ uses a predefined set of 9 registers for passing function arguments on x86_64:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;RAX, RBX, RCX, RDI, RSI, R8, R9, R10, R11
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Our Instrumentation Strategy
&lt;/h3&gt;

&lt;p&gt;To successfully extract function arguments in our eBPF uprobe, we need to follow a systematic approach:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Locate the target function: Get the offset address of the Go function we want to instrument&lt;/li&gt;
&lt;li&gt;Analyze argument layout: Determine whether arguments are passed in registers, on the stack, or a combination of both&lt;/li&gt;
&lt;li&gt;Map arguments to locations: Identify which specific registers or stack offsets contain our target data&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once we have this information, we can accurately probe the respective Go function when it's invoked and extract the arguments we're interested in.&lt;/p&gt;

&lt;h3&gt;
  
  
  Essential Reading
&lt;/h3&gt;

&lt;p&gt;This approach is based on Go's official ABI specification. I highly recommend reading the Go &lt;a href="https://go.googlesource.com/go/+/refs/heads/master/src/cmd/compile/abi-internal.md#amd64-architecture" rel="noopener noreferrer"&gt;ABI Internal Documentation&lt;/a&gt; to understand the detailed rules governing how Go passes arguments to functions.&lt;br&gt;
With this foundation in place, let's move on to the practical steps of locating functions and determining memory offsets.&lt;/p&gt;
&lt;h2&gt;
  
  
  Locating Functions in Go Binaries
&lt;/h2&gt;

&lt;p&gt;Before we can attach uprobes, we need to identify the specific functions we want to instrument within the Go binary. Go provides several approaches for function discovery.&lt;/p&gt;
&lt;h3&gt;
  
  
  Symbol Tables and Debugging Information
&lt;/h3&gt;

&lt;p&gt;Go binaries contain symbol tables that map function names to their memory addresses. However, the availability and format of these symbols depend on how the binary was compiled.&lt;/p&gt;

&lt;p&gt;In our case,lets compile it with&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Preserves all symbols and disables optimizations&lt;/span&gt;
go build &lt;span class="nt"&gt;-gcflags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"all=-N -l"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; sample
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Finding Target Functions
&lt;/h3&gt;

&lt;p&gt;Use these tools to explore available functions in your Go binary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# List all functions with addresses:&lt;/span&gt;
nm &lt;span class="nt"&gt;-n&lt;/span&gt; myapp | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;" T "&lt;/span&gt;

&lt;span class="c"&gt;# Search for specific function:&lt;/span&gt;
nm &lt;span class="nt"&gt;-n&lt;/span&gt; myapp | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"main.hello"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;you can also use &lt;code&gt;objdump&lt;/code&gt; or &lt;code&gt;go tool&lt;/code&gt; to extract the info&lt;/p&gt;

&lt;h2&gt;
  
  
  Determining Memory Offsets
&lt;/h2&gt;

&lt;p&gt;Once we've located our target function in the symbol table, we need to calculate the correct offset address for uprobe attachment. This offset represents the function's location within the process's virtual address space.&lt;/p&gt;

&lt;h3&gt;
  
  
  Calculating Function Offsets
&lt;/h3&gt;

&lt;p&gt;The offset address is part of the virtual address space memory layout. We attach our uprobe to this calculated offset address.&lt;/p&gt;

&lt;p&gt;Step 1: Get the function's virtual address&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nm &lt;span class="nt"&gt;-n&lt;/span&gt; server | &lt;span class="nb"&gt;grep &lt;/span&gt;MyHandler
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;0000000000498f60 T main.MyHandler
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step 2: Find the base load address&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;readelf &lt;span class="nt"&gt;-l&lt;/span&gt; server | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"LOAD"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step 3: Calculate the offset&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;offset &lt;span class="o"&gt;=&lt;/span&gt; function_address - base_load_address
offset &lt;span class="o"&gt;=&lt;/span&gt; 0x498f60 - 0x400000 &lt;span class="o"&gt;=&lt;/span&gt; 0x98f60
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Programmatic Offset Calculation
&lt;/h3&gt;

&lt;p&gt;In our Rust userspace program, this offset calculation is handled by &lt;a href="https://github.com/maheshrayas/blogs/blob/main/ebpf/04-tracing-grpc-headers/code/trace-grpc-headers/src/common.rs" rel="noopener noreferrer"&gt;helper functions&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Calculate offset for the target function&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;calculate_function_offset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;binary_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"main.MyHandler"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Attach uprobe to the calculated offset&lt;/span&gt;
&lt;span class="n"&gt;go_sk&lt;/span&gt;
    &lt;span class="py"&gt;.progs&lt;/span&gt;
    &lt;span class="py"&gt;.handle_hello_int&lt;/span&gt;
    &lt;span class="nf"&gt;.attach_uprobe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;binary_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Deep Dive: Go Argument Passing Patterns
&lt;/h2&gt;

&lt;p&gt;Let's examine how Go passes different data types as function arguments, following the official &lt;a href="https://go.googlesource.com/go/+/refs/heads/master/src/cmd/compile/abi-internal.md#function-call-argument-and-result-passing" rel="noopener noreferrer"&gt;ABI specification&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Integers
&lt;/h3&gt;

&lt;p&gt;Simple types occupy one register each:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;hello_int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"hello_int %d&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;eBPF extraction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;ax&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Important: If the function has a receiver, the receiver takes RAX and arguments shift to subsequent registers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Strings: Pointer + Length Structure
&lt;/h3&gt;

&lt;p&gt;Go strings are internally represented as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ptr&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt; &lt;span class="c"&gt;//pointer to data&lt;/span&gt;
    &lt;span class="nb"&gt;len&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="c"&gt;// Length&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a function with multiple arguments:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;hello_int_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"hello_int_string %d %s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;eBPF extraction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;ax&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;            &lt;span class="c1"&gt;// First argument (int) -&amp;gt; RAX&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;str_ptr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;bx&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// String data pointer -&amp;gt; RBX&lt;/span&gt;
    &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;str_len_raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;cx&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// String length (as long) -&amp;gt; RCX&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Structs: Value vs Pointer Passing
&lt;/h3&gt;

&lt;p&gt;Struct by value: Individual fields are assigned to registers sequentially&lt;/p&gt;

&lt;p&gt;"If T is a struct type, recursively register-assign each field of V."&lt;/p&gt;

&lt;p&gt;Struct by pointer: Only the pointer occupies one register&lt;/p&gt;

&lt;p&gt;"If T is a pointer type, assign V to register I and increment I."&lt;/p&gt;

&lt;p&gt;The choice dramatically affects extraction strategy. See our &lt;a href="https://github.com/maheshrayas/blogs/blob/49b6a65b84b264f42c1270ecee904340d73d23ca/ebpf/04-tracing-grpc-headers/code/trace-grpc-headers/src/bpf/go.bpf.c#L87-L215" rel="noopener noreferrer"&gt;sample code&lt;/a&gt; for both scenarios.&lt;/p&gt;

&lt;h3&gt;
  
  
  Slices: Three-Field Structure
&lt;/h3&gt;

&lt;p&gt;Go slices contain three components:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;slice&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ptr&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;elementType&lt;/span&gt;  &lt;span class="c"&gt;// Data pointer&lt;/span&gt;
    &lt;span class="nb"&gt;len&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;          &lt;span class="c"&gt;// Element count  &lt;/span&gt;
    &lt;span class="nb"&gt;cap&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;          &lt;span class="c"&gt;// Capacity&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each field occupies consecutive registers when passed by value.&lt;/p&gt;

&lt;h3&gt;
  
  
  Floating Point Numbers: The XMM Challenge
&lt;/h3&gt;

&lt;p&gt;Floats use separate XMM registers (X0-X14), not general-purpose registers. This creates limitations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Extractable: Floats in structs passed by pointer (dereference from memory)&lt;/li&gt;
&lt;li&gt;Not directly extractable: Individual float arguments or floats in value-passed structs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;eBPF currently cannot access XMM registers directly. See this &lt;a href="https://stackoverflow.com/questions/54217836/how-do-i-access-xmm-registers-in-an-ebpf-program" rel="noopener noreferrer"&gt;StackOverflow discussion&lt;/a&gt;for details.&lt;/p&gt;

&lt;p&gt;Workaround: Use pointer-based struct passing for float extraction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Application: gRPC Header Extraction
&lt;/h2&gt;

&lt;p&gt;Now comes the exciting part—applying everything we've learned to solve a real-world problem: extracting gRPC headers from a running Go service without touching the source code.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Challenge: gRPC vs HTTP/1.1
&lt;/h3&gt;

&lt;p&gt;Unlike our previous blog where we parsed HTTP/1.1 headers (which are plain text), gRPC presents a tougher challenge. gRPC headers are compressed using &lt;a href="https://pkg.go.dev/golang.org/x/net/http2/hpack" rel="noopener noreferrer"&gt;HPACK&lt;/a&gt;, making direct parsing from network packets nearly impossible.&lt;br&gt;
But here's where our uprobe knowledge becomes powerful—we can intercept the headers after they've been decompressed by the Go runtime.&lt;/p&gt;
&lt;h3&gt;
  
  
  Finding the Right Function to Hook
&lt;/h3&gt;

&lt;p&gt;The key insight came from studying how observability tools like &lt;a href="https://blog.px.dev/ebpf-http2-tracing/#uprobe-based-http2-tracing" rel="noopener noreferrer"&gt;Pixie&lt;/a&gt; solve this problem. However, their &lt;a href="https://github.com/pixie-io/pixie-demos/blob/6806d843e10a01134db273fa7e713124128c5e89/http2-tracing/uprobe_trace/bpf_program.go#L20" rel="noopener noreferrer"&gt;examples&lt;/a&gt; target older Go versions and don't work with Go 1.17+'s register-based calling convention.&lt;br&gt;
After digging through the gRPC-Go codebase, I identified the perfect target:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;func &lt;span class="o"&gt;(&lt;/span&gt;t &lt;span class="k"&gt;*&lt;/span&gt;http2Server&lt;span class="o"&gt;)&lt;/span&gt; operateHeaders&lt;span class="o"&gt;(&lt;/span&gt;
    ctx context.Context, 
    frame &lt;span class="k"&gt;*&lt;/span&gt;http2.MetaHeadersFrame, 
    handle func&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;ServerStream&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;)&lt;/span&gt; error
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This function in the &lt;a href="https://github.com/grpc/grpc-go/blob/9186ebd774370e3b3232d1b202914ff8fc2c56d6/internal/transport/http2_server.go#L368" rel="noopener noreferrer"&gt;grpc-go&lt;/a&gt; package processes decompressed header fields just before they're sent to the client—exactly what we need!&lt;/p&gt;

&lt;h3&gt;
  
  
  Detective Work with Delve
&lt;/h3&gt;

&lt;p&gt;But where does the frame argument live—register or stack? Time for some debugging:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dlv attach 366200
&lt;span class="o"&gt;(&lt;/span&gt;dlv&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;break &lt;/span&gt;google.golang.org/grpc/internal/transport.&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;http2Server&lt;span class="o"&gt;)&lt;/span&gt;.operateHeaders
&lt;span class="o"&gt;(&lt;/span&gt;dlv&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;

&lt;span class="c"&gt;# When breakpoint hits...&lt;/span&gt;
&lt;span class="o"&gt;(&lt;/span&gt;dlv&lt;span class="o"&gt;)&lt;/span&gt; args
frame &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"*golang.org/x/net/http2.MetaHeadersFrame"&lt;/span&gt;&lt;span class="o"&gt;)(&lt;/span&gt;0xc00020e0c0&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="o"&gt;(&lt;/span&gt;dlv&lt;span class="o"&gt;)&lt;/span&gt; regs
Rdi &lt;span class="o"&gt;=&lt;/span&gt; 0x000000c00020e0c0  &lt;span class="c"&gt;# Bingo! Frame pointer is in RDI&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Perfect match! The frame pointer lives in the RDI register, exactly as our calling convention knowledge predicted.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Structs We Need to Parse
&lt;/h3&gt;

&lt;p&gt;Armed with the register location, we now need to parse two key structures:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://github.com/grpc/grpc-go/blob/ac13172781fae5593fd97ce07c3019c4044a71cd/internal/transport/grpchttp2/framer.go#L301" rel="noopener noreferrer"&gt;MetaHeadersFrame&lt;/a&gt; : Contains the frame metadata&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://pkg.go.dev/golang.org/x/net/http2/hpack#HeaderField" rel="noopener noreferrer"&gt;Fields&lt;/a&gt; : Individual header name-value pairs&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The eBPF Implementation
&lt;/h3&gt;

&lt;p&gt;Using our memory layout knowledge from the previous sections, we can now traverse these structures in eBPF:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;void &lt;span class="k"&gt;*&lt;/span&gt;frame_ptr &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;void &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;ctx-&amp;gt;di&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;p&gt;Full Working code: &lt;a href="https://github.com/maheshrayas/blogs/tree/main/ebpf/04-tracing-grpc-headers/code" rel="noopener noreferrer"&gt;https://github.com/maheshrayas/blogs/tree/main/ebpf/04-tracing-grpc-headers/code&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The repo includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sample &lt;a href="https://github.com/maheshrayas/blogs/tree/49b6a65b84b264f42c1270ecee904340d73d23ca/ebpf/04-tracing-grpc-headers/code/sample_grpc" rel="noopener noreferrer"&gt;gRPC server&lt;/a&gt; with various header types &lt;/li&gt;
&lt;li&gt;Complete &lt;a href="https://github.com/maheshrayas/blogs/blob/49b6a65b84b264f42c1270ecee904340d73d23ca/ebpf/04-tracing-grpc-headers/code/trace-grpc-headers/src/bpf/grpc.bpf.c#L31" rel="noopener noreferrer"&gt;eBPF program&lt;/a&gt; with detailed comments&lt;/li&gt;
&lt;li&gt;Rust userspace code for uprobe attachment&lt;/li&gt;
&lt;li&gt;Step-by-step instructions to reproduce the results : &lt;a href="https://github.com/maheshrayas/blogs/tree/49b6a65b84b264f42c1270ecee904340d73d23ca/ebpf/04-tracing-grpc-headers/code/trace-grpc-headers#trace-grpc-headers" rel="noopener noreferrer"&gt;readme.md&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What You'll See&lt;br&gt;
Currently, the basic implementation outputs header data to the kernel trace pipe:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7ieikgog3086rudrgx7n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7ieikgog3086rudrgx7n.png" alt=" " width="800" height="434"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Currently it prints all the headers key, value in &lt;code&gt;/sys/kernel/tracing/trace_pipe&lt;/code&gt;, we can also pass these headers to userspace using Maps(RingBuffer or PerfEventArray) just like we did in our pervious example.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging, Troubleshooting and Learnings
&lt;/h2&gt;

&lt;p&gt;Working with Go uprobes can be challenging, especially when dealing with memory layout parsing and argument location detection. Here are the key issues you'll likely encounter and approaches to solve them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Memory Layout Parsing: The Silent Killer
&lt;/h3&gt;

&lt;p&gt;The most frustrating bugs come from incorrect memory address parsing. A single byte offset error can lead to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Garbage data extraction&lt;/li&gt;
&lt;li&gt;Kernel panics in extreme cases&lt;/li&gt;
&lt;li&gt;Silent failures with no obvious symptoms&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Best practices:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Always validate extracted pointers before dereferencing&lt;/li&gt;
&lt;li&gt;Use boundary checks in your eBPF programs&lt;/li&gt;
&lt;li&gt;Test with simple data types first, then move to complex structs. Thats how I got started doing a simple types and I have attached the sample for you to familar.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Register vs Stack: The Ongoing Mystery
&lt;/h3&gt;

&lt;p&gt;Determining whether arguments are passed in registers or on the stack remains tricky. While I used delve for this analysis and my understanding on ABI specs, I'm not entirely confident this is the most reliable approach.&lt;/p&gt;

&lt;h4&gt;
  
  
  Potential alternative approaches (still researching):
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;DWARF debug information: Parse function signatures and calling conventions from debug symbols&lt;/li&gt;
&lt;li&gt;Static analysis using go tool compile -S&lt;/li&gt;
&lt;li&gt;Assembly inspection with objdump -d&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Call for input: If you have experience with static analysis techniques for determining Go argument passing, I'd love to hear your insights! Feel free to reach out or contribute to the repository.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Throughout this post, we've journeyed from understanding Go's register-based calling convention to successfully extracting gRPC headers from running applications using eBPF uprobes. We've learned how to locate functions in Go binaries, calculate memory offsets, parse different argument types, and apply these techniques to solve real observability challenges.&lt;br&gt;
The techniques we've covered form the foundation for powerful runtime introspection capabilities. By combining uprobe instrumentation with efficient userspace communication through ring buffers, we can build sophisticated observability tools that operate without any application code changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's Coming Next
&lt;/h3&gt;

&lt;p&gt;I'm currently building a comprehensive observability tool that combines both gRPC and HTTP/1.1 header parsing capabilities, designed specifically for Kubernetes environments. This tool will provide complete HTTP path visibility and distributed tracing across your entire service mesh—all through eBPF instrumentation.&lt;br&gt;
But that's just the beginning. My next blog post will be even more exciting, where I'll share a complete open-source project ready for production use in Kubernetes environments. We'll explore how to deploy eBPF-based observability at scale, handle multi-node clusters, and use the tool to increase the kubernetes security.&lt;/p&gt;

&lt;p&gt;Stay tuned for the next installment where theory meets production reality. The best is yet to come!&lt;/p&gt;

</description>
      <category>ebpf</category>
      <category>go</category>
      <category>grpc</category>
      <category>rust</category>
    </item>
    <item>
      <title># 03- Lets understand Kprobes &amp; Kretprobes</title>
      <dc:creator>Mahesh Rayas</dc:creator>
      <pubDate>Sun, 20 Jul 2025 09:16:07 +0000</pubDate>
      <link>https://forem.com/maheshrayas/-03-lets-understand-kprobes-kretprobes-4ke</link>
      <guid>https://forem.com/maheshrayas/-03-lets-understand-kprobes-kretprobes-4ke</guid>
      <description>&lt;p&gt;When it comes to tracing the Linux kernel, kprobes and kretprobes are some of the oldest and most flexible tools in the eBPF ecosystem. In fact, &lt;a href="https://lwn.net/Articles/132196/" rel="noopener noreferrer"&gt;kprobes&lt;/a&gt; were introduced long before eBPF, first appearing in Linux kernel 2.6.9 (2004), enabling dynamic tracing of almost any kernel function without requiring kernel recompilation.&lt;/p&gt;

&lt;p&gt;eBPF came later (around kernel 3.15+) and builds upon kprobes by allowing users to attach safe, sandboxed, and highly programmable code to kernel functions, including kprobes and kretprobes. This combination provides a powerful foundation for modern observability and performance tooling.&lt;/p&gt;

&lt;p&gt;While kprobes and kretprobes typically have higher overhead compared to other eBPF program types like XDP or tracepoints, their versatility makes them invaluable. They can attach to virtually any kernel function, enabling deep visibility into system behavior — which is why they are widely adopted in many popular eBPF-based observability tools.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Note: Kprobe cannot probe the &lt;a href="https://www.kernel.org/doc/html/latest/trace/kprobes.html#blacklist" rel="noopener noreferrer"&gt;blacklist function&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Inner workings
&lt;/h2&gt;

&lt;p&gt;With kprobes, you can attach custom eBPF programs to nearly any kernel function. When that function is called, the attached eBPF program is triggered at its &lt;strong&gt;entry point&lt;/strong&gt;, then the original function continues as usual.&lt;br&gt;
If you want to know the details on how kprobe works, I would recommend reading &lt;a href="https://www.kernel.org/doc/html/latest/trace/kprobes.html#how-does-a-kprobe-work" rel="noopener noreferrer"&gt;https://www.kernel.org/doc/html/latest/trace/kprobes.html#how-does-a-kprobe-work&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In contrast, &lt;strong&gt;kretprobes&lt;/strong&gt; are triggered when the function &lt;strong&gt;returns&lt;/strong&gt;. This allows you to capture return values or output arguments — making them ideal for debugging, performance monitoring, or return-value-based logic.&lt;/p&gt;

&lt;p&gt;In this post, we’ll demonstrate how to trace HTTP/1.1 headers using kprobes and extract them for observability. Since SSL/TLS encrypts the payload, this technique only applies to unencrypted HTTP traffic.&lt;/p&gt;

&lt;p&gt;In a future post, we'll explore how to trace HTTP/2 (gRPC) headers using uprobes, which allow visibility into user-space libraries like golang.org/x/net/http2.&lt;/p&gt;

&lt;p&gt;More details on Probing eBPF: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.ebpf.io/linux/program-type/BPF_PROG_TYPE_KPROBE" rel="noopener noreferrer"&gt;https://docs.ebpf.io/linux/program-type/BPF_PROG_TYPE_KPROBE&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.kernel.org/doc/html/latest/trace/kprobes.html" rel="noopener noreferrer"&gt;https://www.kernel.org/doc/html/latest/trace/kprobes.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Before we dive into examples
&lt;/h2&gt;

&lt;p&gt;Kprobes provide unmatched flexibility to dynamically instrument almost any kernel function. However, attaching probes indiscriminately can introduce performance overhead or stability risks. It’s best practice to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Target well-known, stable kernel functions related to syscalls, networking, or drivers.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Confirm symbol presence and type in &lt;code&gt;/proc/kallsyms&lt;/code&gt; (look for uppercase T symbols).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Test probes carefully on a non-production system.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keep in mind kernel version differences and module load/unload events may affect probe reliability.&lt;/p&gt;

&lt;p&gt;Typical kprobe targets include kernel functions related to syscalls, networking stacks, drivers, and filesystem operations, enabling tracing of specific kernel events.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Performance impact&lt;/strong&gt;: Kprobes add instrumentation overhead because they inject code on function entry or return. While usually small, probing high-frequency functions can cause noticeable CPU overhead. Hence needs to be performance tested before its production ready.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/proc/kallsyms&lt;/code&gt; is a special file in Linux that lists all currently loaded kernel symbols, including function names, memory addresses, and variable symbols. This file is particularly useful when working with kprobes, as it helps identify kernel functions that can be dynamically instrumented.&lt;/p&gt;

&lt;p&gt;You can find all the symbols using&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo cat&lt;/span&gt; /proc/kallsyms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, it’s important to note:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Function availability can differ based on kernel version, architecture (e.g., x86 vs ARM), and build configurations.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Some symbols may be inlined, optimized away, or renamed in newer kernels.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, when writing portable eBPF programs using kprobes, always validate your target symbol on the specific kernel using /proc/kallsyms.&lt;/p&gt;

&lt;p&gt;for example&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;sudo cat&lt;/span&gt; /proc/kallsyms | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"sys_read"&lt;/span&gt;


ffffffffb52edf30 T __pfx___x64_sys_read
ffffffffb52edf40 T __x64_sys_read
ffffffffb52edf70 T __pfx___ia32_sys_read
ffffffffb52edf80 T __ia32_sys_read
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Symbols marked with &lt;code&gt;T&lt;/code&gt; indicate global (exported) kernel text symbols, i.e., functions you can reliably attach kprobes to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's Dive into the Example
&lt;/h2&gt;

&lt;p&gt;Working example: &lt;a href="https://github.com/maheshrayas/blogs/tree/main/ebpf/03-tracing-http-headers/code/trace-http1-headers" rel="noopener noreferrer"&gt;https://github.com/maheshrayas/blogs/tree/main/ebpf/03-tracing-http-headers/code/trace-http1-headers&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This example traces HTTP/1.1 headers of a server using eBPF. Here's the setup:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;The HTTP server exposes a basic REST API and is not SSL encrypted. This is important because we cannot trace encrypted data in the kernel—encryption and decryption happen in user-space libraries like OpenSSL.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A Rust user-space application is run, and the PID of the HTTP server is supplied as input.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The user-space program loads and attaches kprobes to relevant kernel functions (explained in detail below).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;From user-space, we update an eBPF map to track the PID of http server.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;⚠️ Note: In containerized environments, the PID inside a container differs from what the host kernel sees. So, we must trace using the host PID and map it the container/pod process id. We shall see in future post how exactly we can map the PID on host to container/pod process in kubernetes echo system.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="nf"&gt;.to_ne_bytes&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1u8&lt;/span&gt;&lt;span class="nf"&gt;.to_ne_bytes&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;http_sk&lt;/span&gt;&lt;span class="py"&gt;.maps.tracked_pids&lt;/span&gt;&lt;span class="nf"&gt;.update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;MapFlags&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ANY&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt;
    &lt;span class="nd"&gt;warn!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"failed to update maps for pid {}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  eBPF Program Logic to extract http headers
&lt;/h3&gt;

&lt;p&gt;We attach three probes to two different kernel functions.&lt;/p&gt;

&lt;p&gt;1.&lt;code&gt;kretprobe/__sys_accept4&lt;/code&gt; -&amp;gt; handle_accept4_ret&lt;/p&gt;

&lt;p&gt;The first kretprobe is attached to the kernel function __sys_accept4, which is invoked during the accept4() syscall.&lt;br&gt;
This is where a server accepts a new incoming connection.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;From this kretprobe, we extract the file descriptor (fd) returned by accept4.&lt;/li&gt;
&lt;li&gt;We check if the calling process's PID exists in the &lt;code&gt;tracked_pids&lt;/code&gt; eBPF map.&lt;/li&gt;
&lt;li&gt;If it matches, we store the new fd in another eBPF map called &lt;code&gt;active_app_sockets&lt;/code&gt;.
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;SEC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"kretprobe/__sys_accept4"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;handle_accept4_ret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;pt_regs&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;new_fd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PT_REGS_RC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// Logic to filter and store fd&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;2.&lt;code&gt;kprobe/ksys_read&lt;/code&gt; -&amp;gt; kprobe_ksys_read_entry&lt;/p&gt;

&lt;p&gt;This kprobe is attached to the &lt;code&gt;ksys_read()&lt;/code&gt; kernel function, which is called when a process reads from an fd.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We check if the &lt;code&gt;fd&lt;/code&gt; is already in the &lt;code&gt;active_app_sockets&lt;/code&gt; map.&lt;/li&gt;
&lt;li&gt;If yes, we store the &lt;code&gt;buf&lt;/code&gt; pointer in the &lt;code&gt;read_args_map&lt;/code&gt;.
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;SEC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"kprobe/ksys_read"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;kprobe_ksys_read_entry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;pt_regs&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Check if fd is tracked&lt;/span&gt;
    &lt;span class="c1"&gt;// Save the 'buf' argument into a map for use in the return probe&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;What is &lt;a href="https://github.com/maheshrayas/blogs/blob/af668405f746e2f2744e7c80f89c14737df8210d/ebpf/03-tracing-http-headers/code/trace-http1-headers/src/bpf/http.bpf.c#L125" rel="noopener noreferrer"&gt;buf&lt;/a&gt;?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The buf argument is a pointer to a user-space memory buffer.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The kernel will copy data into this buffer as a result of the read syscall.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Since eBPF programs run in kernel space, accessing this pointer directly is not safe or valid unless we carefully copy from user memory using helpers like &lt;code&gt;bpf_probe_read_user&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;3.&lt;code&gt;kretprobe/ksys_read&lt;/code&gt; -&amp;gt; kretprobe_ksys_read_exit &lt;/p&gt;

&lt;p&gt;Once the syscall ksys_read() is invoked, the actual data is not yet read into user space — it happens later, by the time the function returns. Therefore, to access the actual read data, we attach a &lt;code&gt;kretprobe&lt;/code&gt; to &lt;code&gt;ksys_read&lt;/code&gt;. This allows us to retrieve the number of bytes that were read and use that length to copy the data from user space buffer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;SEC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"kretprobe/ksys_read"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;kretprobe_ksys_read_exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;pt_regs&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Get the Pid &lt;/span&gt;
    &lt;span class="c1"&gt;// Read the actual data using bpf_probe_read_user&lt;/span&gt;
    &lt;span class="c1"&gt;// Check if its HTTP data&lt;/span&gt;
    &lt;span class="c1"&gt;// and send it the data to user space if the pid is that of http server.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;4.Parsing Perf Event Data in Userspace&lt;/p&gt;

&lt;p&gt;The user-space process continuously polls the perf event array for incoming data. Once data is received, it parses the raw binary payload emitted from the eBPF program.&lt;/p&gt;

&lt;p&gt;One critical aspect to consider during this parsing step is struct alignment and memory layout compatibility between kernel space (C) and user space (Rust). When decoding the raw binary data from the perf buffer into a Rust struct, the layout of the Rust struct must exactly match the layout of the C struct used in the eBPF program.&lt;/p&gt;

&lt;p&gt;To ensure compatibility:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use the same field order and types in your Rust struct.&lt;/li&gt;
&lt;li&gt;Add the #[repr(C)] attribute to enforce C-like memory layout in Rust.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This ensures safe and correct parsing of the data, avoiding undefined behavior due to padding or misalignment between the two languages.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[repr(C)]&lt;/span&gt;
&lt;span class="nd"&gt;#[derive(Clone,&lt;/span&gt; &lt;span class="nd"&gt;Copy)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;HttpEventData&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;i32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;data_len&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;handle_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_cpu&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;i32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;unsafe&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;ptr&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;read_unaligned&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="nf"&gt;.as_ptr&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;HttpEventData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="c1"&gt;// Convert the raw data to a string&lt;/span&gt;
   &lt;span class="c1"&gt;//...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;In this post, we explored:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How to use kprobes and kretprobes to trace syscalls like &lt;code&gt;ksys_read&lt;/code&gt; &amp;amp; &lt;code&gt;accept&lt;/code&gt;, paving the way to trace HTTP headers.&lt;/li&gt;
&lt;li&gt;How to capture user-space data (like HTTP headers) inside the kernel using eBPF.&lt;/li&gt;
&lt;li&gt;Safe parsing of raw perf event data in Rust, using #[repr(C)] and field alignment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This lays the groundwork for powerful observability tools that require no code changes in the application layer — everything is captured transparently from the kernel.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;In the next part of this series, we'll explore how to parse gRPC headers from a Go HTTP/2 server, diving deeper into protocol parsing. &lt;br&gt;
Later, we’ll bring everything together to build a Kubernetes-native observability tool that can trace HTTP headers — all without any modification or instrumentation to the applications themselves.&lt;/p&gt;

&lt;p&gt;Stay tuned — it gets even more exciting from here!&lt;/p&gt;

</description>
      <category>ebpf</category>
      <category>rust</category>
      <category>linux</category>
      <category>tracing</category>
    </item>
    <item>
      <title># 02 - Understanding eBPF Core Building Blocks</title>
      <dc:creator>Mahesh Rayas</dc:creator>
      <pubDate>Sun, 13 Jul 2025 08:32:19 +0000</pubDate>
      <link>https://forem.com/maheshrayas/-02-understanding-ebpf-core-building-blocks-1221</link>
      <guid>https://forem.com/maheshrayas/-02-understanding-ebpf-core-building-blocks-1221</guid>
      <description>&lt;p&gt;In our &lt;a href="https://dev.to/maheshrayas/-intro-into-ebpf-and-rust-l2m"&gt;previous post&lt;/a&gt;, we built a syscall tracer with Rust + eBPF. Today, we’ll unpack the core components that made it work — and the deeper mechanics powering real-world eBPF tools. Understanding these components will help you build more sophisticated observability, security and networking tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. BPF Program Types &amp;amp; Hook Points
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Program types&lt;/strong&gt; define &lt;em&gt;what&lt;/em&gt; the eBPF program can do, while &lt;strong&gt;hook points&lt;/strong&gt; define &lt;em&gt;where&lt;/em&gt; it runs in the kernel.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Program Types:
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;XDP (eXpress Data Path)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Runs at the &lt;strong&gt;earliest&lt;/strong&gt; point in network packet processing&lt;/li&gt;
&lt;li&gt;Perfect for DDoS protection, load balancing&lt;/li&gt;
&lt;li&gt;Can DROP, PASS, REDIRECT, or TX packets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;TC (Traffic Control)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Attaches to network &lt;strong&gt;ingress/egress&lt;/strong&gt; points&lt;/li&gt;
&lt;li&gt;More context than XDP, can modify packets&lt;/li&gt;
&lt;li&gt;Used for advanced packet filtering and shaping&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Kprobes/Kretprobes&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dynamic tracing of &lt;strong&gt;any kernel function&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Kprobe = function entry, Kretprobe = function return&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Uprobes/Uretprobes&lt;/strong&gt; &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Trace &lt;strong&gt;userspace applications&lt;/strong&gt; (like Go, Python apps)&lt;/li&gt;
&lt;li&gt;Can inspect function arguments, return values&lt;/li&gt;
&lt;li&gt;Great for application performance monitoring&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Tracepoints&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Static&lt;/strong&gt; kernel instrumentation points&lt;/li&gt;
&lt;li&gt;More stable than kprobes across kernel versions&lt;/li&gt;
&lt;li&gt;Predefined events like &lt;code&gt;sys_enter_openat&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;You can view all available tracepoints in: &lt;code&gt;/sys/kernel/tracing/events&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;our &lt;a href="https://github.com/maheshrayas/blogs/blob/96176c3d0b2d1367daa4b56c1d8e5b63c6166d1c/ebpf/01-intro-ebpf-rust/code/syscall-tracer/src/bpf/syscall.bpf.c#L18" rel="noopener noreferrer"&gt;syscall tracer&lt;/a&gt; used &lt;code&gt;sys_enter&lt;/code&gt; !&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;CGROUP&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Control resource access per container/process group&lt;/li&gt;
&lt;li&gt;Implement custom policies (network, file access)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Socket Programs&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Filter/redirect packets at socket level&lt;/li&gt;
&lt;li&gt;Implement custom load balancers, firewalls&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note: While I’ve covered the most commonly used program types here—those I’m most familiar with—there are several more (and evolving) types supported by the Linux kernel. For a complete and up-to-date list, refer to the official documentation: &lt;a href="https://docs.ebpf.io/linux/program-type/" rel="noopener noreferrer"&gt;Supported eBPF Program Types&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. BPF Verifier: The Safety Guardian
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;BPF verifier&lt;/strong&gt; is what makes eBPF safe. It performs &lt;strong&gt;static analysis&lt;/strong&gt; on the program before loading it into the kernel.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the Verifier Checks:
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Memory Safety&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No out-of-bounds array access&lt;/li&gt;
&lt;li&gt;No null pointer dereferences&lt;/li&gt;
&lt;li&gt;Proper initialization of variables&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Control Flow&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No infinite loops (bounded loops only since Linux 5.3)&lt;/li&gt;
&lt;li&gt;All code paths must exit&lt;/li&gt;
&lt;li&gt;No unreachable code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Helper Function Usage&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Only approved helper functions can be called&lt;/li&gt;
&lt;li&gt;Correct argument types and counts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Stack Usage&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Limited to 512 bytes of stack space&lt;/li&gt;
&lt;li&gt;No stack overflow protection needed
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ This will be REJECTED by verifier&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  &lt;span class="c1"&gt;// Infinite loop&lt;/span&gt;
    &lt;span class="c1"&gt;// code&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ This will be ACCEPTED&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  &lt;span class="c1"&gt;// Bounded loop&lt;/span&gt;
    &lt;span class="c1"&gt;// code  &lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why This Matters:&lt;/strong&gt; The verifier ensures the eBPF program cannot crash the kernel, making it safe to run in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. BPF Maps: Data Storage &amp;amp; Communication
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;BPF Maps&lt;/strong&gt; are the primary way to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Store state between eBPF program invocations&lt;/li&gt;
&lt;li&gt;Communicate between kernel and userspace&lt;/li&gt;
&lt;li&gt;Share data between different eBPF programs&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Essential Map Types:
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Hash Maps&lt;/strong&gt; - Key-value storage&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;__uint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BPF_MAP_TYPE_HASH&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;__uint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_entries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;__type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;           &lt;span class="c1"&gt;// PID&lt;/span&gt;
    &lt;span class="n"&gt;__type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;         &lt;span class="c1"&gt;// Syscall count&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;syscall_counts&lt;/span&gt; &lt;span class="nf"&gt;SEC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;".maps"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Arrays&lt;/strong&gt; - Index-based access&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;__uint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BPF_MAP_TYPE_ARRAY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;__uint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_entries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// One per syscall number&lt;/span&gt;
    &lt;span class="n"&gt;__type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;__type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;syscall_stats&lt;/span&gt; &lt;span class="nf"&gt;SEC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;".maps"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Perf Event Arrays&lt;/strong&gt; - High-speed data streaming&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;__uint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BPF_MAP_TYPE_PERF_EVENT_ARRAY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;__uint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key_size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="n"&gt;__uint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value_size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;syscall_events&lt;/span&gt; &lt;span class="nf"&gt;SEC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;".maps"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can find the usage of this in the pervious blog post &lt;a href="https://github.com/maheshrayas/blogs/blob/eb7c98e2c088e70bf69c1e31abc1f643f9cd7dfb/ebpf/01-intro-ebpf-rust/code/syscall-tracer/src/bpf/syscall.bpf.c#L7" rel="noopener noreferrer"&gt;code&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Send data to userspace&lt;/span&gt;
&lt;span class="n"&gt;bpf_perf_event_output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BPF_F_CURRENT_CPU&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Ring Buffers&lt;/strong&gt; - Modern alternative to perf events (Linux 5.8+)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lower overhead, better memory efficiency&lt;/li&gt;
&lt;li&gt;Built-in backpressure handling&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Map Usage Patterns:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Counters&lt;/strong&gt;: Track metrics (packets, syscalls, errors)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caching&lt;/strong&gt;: Store lookup results to avoid repeated work
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State machines&lt;/strong&gt;: Track connection states, user sessions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configuration&lt;/strong&gt;: Runtime configuration from userspace&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. Helper Functions: Kernel API
&lt;/h2&gt;

&lt;p&gt;eBPF programs can't directly call kernel functions. Instead, they use &lt;strong&gt;helper functions&lt;/strong&gt; - a curated set of safe kernel APIs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Common Helper Categories:
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Debugging &amp;amp; Logging
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;
&lt;span class="n"&gt;bpf_printk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"PID %d called syscall %d&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;syscall_nr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Map Operations
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// reading from the map&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpf_map_lookup_elem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;my_map&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// updating/inserting into map&lt;/span&gt;
&lt;span class="n"&gt;bpf_map_update_elem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;my_map&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BPF_ANY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// delete the Key/Value from map&lt;/span&gt;
&lt;span class="n"&gt;bpf_map_delete_elem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;my_map&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Context Information
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// This is to get the Pid and Tid of a process&lt;/span&gt;
&lt;span class="c1"&gt;// Usage: We did use this helper function to read the pid from kernel in the previous blog&lt;/span&gt;
&lt;span class="n"&gt;u64&lt;/span&gt; &lt;span class="n"&gt;pid_tgid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpf_get_current_pid_tgid&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;u32&lt;/span&gt; &lt;span class="n"&gt;pid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pid_tgid&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Network Operations
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Redirect packet to another interface&lt;/span&gt;
&lt;span class="n"&gt;bpf_redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ifindex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Modify packet data&lt;/span&gt;
&lt;span class="n"&gt;bpf_skb_store_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;skb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Time &amp;amp; Random
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;u64&lt;/span&gt; &lt;span class="n"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpf_ktime_get_ns&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;u32&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpf_get_prandom_u32&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Usage of helper function depends on the eBPF program types.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  5. BTF &amp;amp; CO-RE: Write Once, Run Everywhere
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;BTF (BPF Type Format)&lt;/strong&gt; and &lt;strong&gt;CO-RE (Compile Once, Run Everywhere)&lt;/strong&gt; solve the kernel compatibility problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Problem:
&lt;/h3&gt;

&lt;p&gt;Kernel structures change between versions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Kernel 5.4&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;task_struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;comm&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="c1"&gt;// ... other fields&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// Kernel 5.10 - field moved!&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;task_struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;comm&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;  
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;        &lt;span class="c1"&gt;// Different offset!&lt;/span&gt;
    &lt;span class="c1"&gt;// ... other fields  &lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Solution - CO-RE:
&lt;/h3&gt;

&lt;p&gt;We briefly introduced this in the &lt;a href="https://dev.to/maheshrayas/-intro-into-ebpf-and-rust-l2m"&gt;previous post&lt;/a&gt;. Now, let’s explore how CO-RE works and why it's crucial for building portable eBPF programs.&lt;/p&gt;

&lt;p&gt;CO-RE leverages BTF (BPF Type Format) to make eBPF programs kernel-version aware at runtime, without recompiling for each kernel.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;"vmlinux.h"&lt;/span&gt;&lt;span class="c1"&gt;  // Generated BTF types&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;task_struct&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;task_struct&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;bpf_get_current_task&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// CO-RE automatically adjusts field offsets at load time!&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;pid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BPF_CORE_READ&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here’s what’s happening:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;vmlinux.h contains type definitions extracted from the kernel using BTF.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;BPF_CORE_READ() safely reads the pid field from task_struct, regardless of its offset.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;When you load the eBPF program, libbpf compares compiled offsets with the actual kernel’s layout and relocates field access as needed.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Benefits:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Compile once, run on any kernel version&lt;/li&gt;
&lt;li&gt;Automatic field offset relocation&lt;/li&gt;
&lt;li&gt;Runtime kernel adaptation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is why &lt;a href="https://github.com/maheshrayas/blogs/blob/96176c3d0b2d1367daa4b56c1d8e5b63c6166d1c/ebpf/01-intro-ebpf-rust/code/syscall-tracer/build.rs#L6" rel="noopener noreferrer"&gt;build.rs&lt;/a&gt;  includes &lt;code&gt;vmlinux&lt;/code&gt; - it provides BTF types for the target kernel!&lt;/p&gt;

&lt;h2&gt;
  
  
  6. BPF Tail Calls: Program Chaining
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Tail calls&lt;/strong&gt; allow one eBPF program to call another, enabling:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Complex logic across multiple programs&lt;/li&gt;
&lt;li&gt;Runtime program updates&lt;/li&gt;
&lt;li&gt;Modular architectures
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Program array map&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;__uint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BPF_MAP_TYPE_PROG_ARRAY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;__uint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_entries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;__type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;__type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;programs&lt;/span&gt; &lt;span class="nf"&gt;SEC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;".maps"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Tail call to another program&lt;/span&gt;
&lt;span class="n"&gt;bpf_tail_call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;programs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;program_index&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// This program ends here - control transfers completely&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Use Cases:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Packet processing pipelines&lt;/li&gt;
&lt;li&gt;Feature toggles (enable/disable functionality)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  7. BPF Token: Delegated Privileges
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;BPF Token&lt;/strong&gt; enables &lt;strong&gt;secure eBPF in containers&lt;/strong&gt; by allowing privilege delegation.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Problem:
&lt;/h3&gt;

&lt;p&gt;eBPF traditionally requires &lt;code&gt;CAP_BPF&lt;/code&gt; or &lt;code&gt;CAP_SYS_ADMIN&lt;/code&gt;, which gives too many privileges for containers.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Solution:
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Privileged process (container runtime) creates token&lt;/span&gt;
bpftool token create /sys/fs/bpf/token delegate_cmds prog_load,map_create

&lt;span class="c"&gt;# Unprivileged container uses token&lt;/span&gt;
bpf_prog_load_token&lt;span class="o"&gt;(&lt;/span&gt;prog_fd, token_fd, ...&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Benefits:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Containers can run eBPF without root&lt;/li&gt;
&lt;li&gt;Fine-grained permission control
&lt;/li&gt;
&lt;li&gt;Better security isolation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Note: BPF Token is available starting Linux 6.7. See kernel docs for full usage.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  8. eBPF Tools Ecosystem
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;bpftool&lt;/strong&gt; - The Swiss Army Knife
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/libbpf/bpftool" rel="noopener noreferrer"&gt;bpftool&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# List all programs&lt;/span&gt;
bpftool prog list

&lt;span class="c"&gt;# Show program details&lt;/span&gt;
bpftool prog show &lt;span class="nb"&gt;id &lt;/span&gt;123

&lt;span class="c"&gt;# Dump program bytecode&lt;/span&gt;
bpftool prog dump xlated &lt;span class="nb"&gt;id &lt;/span&gt;123

&lt;span class="c"&gt;# Pin programs to filesystem&lt;/span&gt;
bpftool prog pin &lt;span class="nb"&gt;id &lt;/span&gt;123 /sys/fs/bpf/my_prog
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;bpftrace&lt;/strong&gt; - Dynamic Tracing Scripts
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/bpftrace/bpftrace" rel="noopener noreferrer"&gt;bpftrace&lt;/a&gt; is a quick way to debug your eBPF program.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Trace file opens&lt;/span&gt;
bpftrace &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'tracepoint:syscalls:sys_enter_openat { printf("%s opened %s\n", comm, str(args-&amp;gt;filename)); }'&lt;/span&gt;

&lt;span class="c"&gt;# Network packet counting&lt;/span&gt;
bpftrace &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'kprobe:dev_queue_xmit { @packets[comm] = count(); }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;BCC&lt;/strong&gt; - Python eBPF Framework
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/iovisor/bcc" rel="noopener noreferrer"&gt;bcc&lt;/a&gt; is a powerful toolkit for writing and running eBPF programs.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Higher-level Python interface&lt;/li&gt;
&lt;li&gt;Great for prototyping and learning&lt;/li&gt;
&lt;li&gt;Compiles C code at runtime&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Rust/libbpf-rs approach which we discussed in the previous blog, is more modern and performant than BCC!&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Putting It All Together
&lt;/h2&gt;

&lt;p&gt;Here's how these components work in syscall tracer which is demostrated in the previous blog:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Program Type&lt;/strong&gt;: Tracepoint (&lt;code&gt;sys_enter_*&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hook Point&lt;/strong&gt;: Syscall entry points&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verifier&lt;/strong&gt;: Validated the program safety&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maps&lt;/strong&gt;: Perf event array for data streaming&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Helper Functions&lt;/strong&gt;: &lt;code&gt;bpf_get_current_pid_tgid()&lt;/code&gt;, &lt;code&gt;bpf_perf_event_output()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BTF/CO-RE&lt;/strong&gt;: Automatic kernel compatibility&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tools&lt;/strong&gt;: &lt;code&gt;bpftool&lt;/code&gt; for debugging, Rust for userspace&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;p&gt;With a solid understanding of eBPF’s foundational building blocks, you’re now ready to go beyond theory and start building powerful, production-grade tools.&lt;/p&gt;

&lt;p&gt;In upcoming posts, we’ll use these primitives to implement practical, real-world applications—tapping into different eBPF program types and kernel hook points. While we won’t spoil what’s next, expect deep dives into applied observability, security, and networking powered by eBPF and Rust.&lt;/p&gt;

&lt;p&gt;To explore more in the meantime:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://ebpf.io/what-is-ebpf/#introduction-to-ebpf" rel="noopener noreferrer"&gt;Introduction to eBPF&lt;/a&gt; – Learn the concepts behind the technology&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.ebpf.io/" rel="noopener noreferrer"&gt;eBPF Documentation Hub&lt;/a&gt; – Official docs, tutorials, and developer guides&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ebpf</category>
      <category>programming</category>
      <category>monitoring</category>
    </item>
    <item>
      <title># Intro into eBPF and Rust</title>
      <dc:creator>Mahesh Rayas</dc:creator>
      <pubDate>Mon, 07 Jul 2025 08:20:02 +0000</pubDate>
      <link>https://forem.com/maheshrayas/-intro-into-ebpf-and-rust-l2m</link>
      <guid>https://forem.com/maheshrayas/-intro-into-ebpf-and-rust-l2m</guid>
      <description>&lt;p&gt;In this post, we’ll explore how to build high-performance Linux tracing tools using eBPF and Rust. Whether you're just getting started with eBPF or curious about integrating it with modern Rust tooling, this guide will walk you through the fundamentals, tooling choices, and a working syscall tracing example.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is eBPF
&lt;/h2&gt;

&lt;p&gt;eBPF is a technology that allows developers to run safe, sandboxed programs in the Linux kernel. These programs can be used for networking, observability, and security, without modifying kernel source code or loading kernel modules.&lt;/p&gt;

&lt;p&gt;eBPF runs inside a restricted virtual machine in the kernel — somewhat like how WebAssembly (WASM) or the JVM operates in userspace — ensuring safety and control over what the program can do.&lt;br&gt;
One of the biggest advantages of eBPF is that it allows extending kernel behavior dynamically, without waiting for upstream patches or rebooting systems. Since the programs run in kernel context, they can observe and act on events with minimal overhead, making them ideal for high-performance tracing, filtering, and enforcement tasks.&lt;/p&gt;

&lt;p&gt;You can find a more in-depth explanation at &lt;a href="https://ebpf.io/what-is-ebpf/" rel="noopener noreferrer"&gt;ebpf.io&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why Rust
&lt;/h2&gt;

&lt;p&gt;Rust is a powerful, modern systems programming language focused on performance, memory safety, and concurrency — all without a garbage collector.&lt;/p&gt;

&lt;p&gt;It is an excellent fit for working with eBPF for several reasons:&lt;/p&gt;

&lt;p&gt;Memory Safety: Unlike C, Rust prevents common bugs like null pointer dereferences, buffer overflows, and use-after-free — which are critical when interacting with low-level kernel interfaces.&lt;/p&gt;

&lt;p&gt;Strong Tooling: Tools like Cargo, rust-analyzer, clippy, and libbpf-cargo make building and testing eBPF applications in Rust a seamless experience.&lt;/p&gt;
&lt;h2&gt;
  
  
  What are the ready-to-use rust libraries
&lt;/h2&gt;
&lt;h3&gt;
  
  
  libbpf-rs
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A safe and idiomatic Rust wrapper around the widely-used C libbpf library.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Designed and maintained by some of the same people behind libbpf.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Integrates well with tools like bpftool and libbpf-cargo.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You still write your eBPF programs in C, but use Rust for the userspace loader.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Best for: Production-grade systems, high compatibility, and CO-RE (Compile Once Run Everywhere).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Link: &lt;a href="https://github.com/libbpf/libbpf-rs" rel="noopener noreferrer"&gt;https://github.com/libbpf/libbpf-rs&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Aya
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A pure Rust eBPF toolchain — you write both the userspace and eBPF program in Rust.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No need for C or clang toolchain.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Includes support for XDP, tracepoints, uprobes, perf buffers, maps, etc.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Still maturing, but advancing fast with great community support.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Best for: All-Rust toolchains, embedded use cases, and easier portability.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Link: &lt;a href="https://aya-rs.dev/" rel="noopener noreferrer"&gt;https://aya-rs.dev/&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Why libbpf-rs
&lt;/h2&gt;

&lt;p&gt;Before diving deeper into libbpf-rs, let’s understand the role and significance of libbpf itself.&lt;/p&gt;
&lt;h3&gt;
  
  
  What is libbpf?
&lt;/h3&gt;

&lt;p&gt;When you write and compile an eBPF program, it often relies on the internal structure of kernel data types. However, these kernel structures can change between Linux versions — for example, a struct in Linux 5.6 may have an additional field or a different layout in Linux 6.1. This mismatch can cause your eBPF program to behave incorrectly or even fail verification when loaded into a newer kernel.&lt;/p&gt;

&lt;p&gt;To address this versioning problem, the libbpf library — often referred to as the eBPF loader — introduced a feature called CO-RE (Compile Once, Run Everywhere). This powerful capability allows you to compile your eBPF program once and run it across different kernel versions without recompilation.&lt;/p&gt;
&lt;h3&gt;
  
  
  How CO-RE Works
&lt;/h3&gt;

&lt;p&gt;Under the hood, CO-RE leverages vmlinux.h, a header file that contains all the type definitions used by your running kernel.&lt;/p&gt;

&lt;p&gt;To generate it, you can run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bpftool btf dump file /sys/kernel/btf/vmlinux format c &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; vmlinux.h
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This &lt;code&gt;vmlinux.h&lt;/code&gt; file is created from the kernel’s BTF (BPF Type Format) data and can be used in your &lt;code&gt;.bpf.c&lt;/code&gt; programs to access kernel structures safely and portably.&lt;/p&gt;

&lt;p&gt;When the eBPF program is loaded, libbpf inspects the fields your program accesses and ensures they align correctly with the actual in-kernel data layout — even if the structure has shifted. CO-RE handles this through relocation logic at load time, making your program kernel-version aware without being hardcoded to one layout.&lt;/p&gt;

&lt;p&gt;If you're interested in a deep technical dive, I highly recommend Andrii Nakryiko’s blog post on &lt;a href="https://nakryiko.com/posts/bpf-portability-and-co-re/" rel="noopener noreferrer"&gt;CO-RE&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why I Chose libbpf-rs
&lt;/h3&gt;

&lt;p&gt;I initially explored aya-rs, a pure-Rust eBPF toolchain that allows writing both userspace and kernel-space components in Rust. While Aya is ergonomic and modern, I encountered issues when using certain eBPF helper functions — the verifier would reject the program.&lt;/p&gt;

&lt;p&gt;After some investigation and community input (&lt;a href="https://github.com/aya-rs/aya/issues/349" rel="noopener noreferrer"&gt;gh issue&lt;/a&gt;) I learned that these problems were caused by CO-RE incompatibilities with the Rust compiler (rustc). Since CO-RE support in aya is still maturing and relies on experimental LLVM features for Rust, I decided to switch.&lt;/p&gt;

&lt;p&gt;That’s when I discovered libbpf-rs, a safe and idiomatic Rust wrapper around the stable and battle-tested libbpf C library.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why libbpf-rs Works Well for My Use Case
&lt;/h3&gt;

&lt;p&gt;It provides first-class CO-RE support, meaning portability is built-in.&lt;/p&gt;

&lt;p&gt;It allows me to write the eBPF program in C, which aligns with most of the production-grade open source projects like Cilium, Tracee, and others.&lt;/p&gt;

&lt;p&gt;Because much of the ecosystem and documentation around eBPF is written in or assumes C, using C for the eBPF part makes it easier to understand, reuse, and extend existing examples.&lt;/p&gt;

&lt;p&gt;The userspace logic remains in Rust, giving me the safety, ergonomics, and modern tooling of the Rust ecosystem while relying on a stable foundation for kernel interaction.&lt;/p&gt;

&lt;h2&gt;
  
  
  eBPF + Rust: Architecture Diagram
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fefwtpot3eo3kby2syohh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fefwtpot3eo3kby2syohh.png" alt=" " width="800" height="248"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here’s a high-level look at the lifecycle of an eBPF program integrated with a Rust userspace application:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Write an eBPF program alongside the userspace Rust code.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Compile the source to generate eBPF bytecode and the Rust binary.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Run the userspace binary.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The userspace binary attempts to load the eBPF bytecode into the kernel.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The eBPF verifier (more on this in future posts) checks that the eBPF program is safe to run in the kernel.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If verification fails, the program is rejected. Otherwise, the eBPF bytecode is JIT-compiled into machine instructions and successfully loaded into the kernel.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Once loaded, the userspace binary attaches the eBPF program to relevant kernel hook points.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Communication between the Rust userspace and kernel space is handled using eBPF Maps.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We'll explore these primitives in depth with hands-on examples in upcoming posts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example: Tracing Syscalls with eBPF and Rust
&lt;/h2&gt;

&lt;p&gt;Let’s walk through a simple example that traces all syscalls invoked on a Linux system.&lt;/p&gt;

&lt;p&gt;Below is a snapshot of the project structure. You can also explore and run the full code from the &lt;a href="https://github.com/maheshrayas/blogs/tree/main/ebpf/01-intro-ebpf-rust/code/syscall-tracer" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt;.&lt;br&gt;
Make sure your system meets the &lt;a href="https://docs.kernel.org/bpf/bpf_devel_QA.html#q-what-s-the-minimal-kernel-version-that-supports-bpf" rel="noopener noreferrer"&gt;eBPF prerequisites&lt;/a&gt; before running.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;└── syscall-tracer
    ├── build.rs   &lt;span class="c"&gt;# Builds the C code and generates eBPF bytecode &amp;amp; Rust bindings&lt;/span&gt;
    ├── Cargo.lock
    ├── Cargo.toml
    ├── Cross.toml &lt;span class="c"&gt;# Cross-compilation configuration&lt;/span&gt;
    ├── README.md
    └── src
        ├── bpf
        │   ├── syscall.bpf.c  &lt;span class="c"&gt;# eBPF program&lt;/span&gt;
        │   ├── syscall.skel.rs &lt;span class="c"&gt;# Auto-generated bindings by libbpf-cargo&lt;/span&gt;
        │   └── vmlinux.h &lt;span class="c"&gt;# CO-RE header for kernel type definitions&lt;/span&gt;
        ├── lib.rs
        ├── log.rs
        └── main.rs   &lt;span class="c"&gt;# Rust userspace: loads, attaches eBPF and handles kernel events&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is how the output might look when tracing syscalls:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3il91jyqrdrz0two5r32.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3il91jyqrdrz0two5r32.png" alt=" " width="800" height="285"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What’s Next?
&lt;/h2&gt;

&lt;p&gt;This post is just the beginning. There’s a lot more to explore with eBPF and Rust — from powerful capabilities to real-world applications.&lt;/p&gt;

&lt;p&gt;I’ll be sharing more advanced and interesting examples in future posts. Stay tuned — you won’t want to miss what’s coming next!&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Whether you're building observability tools or experimenting with kernel internals, eBPF + Rust offers a powerful, safe foundation. The possibilities ahead are exciting — and we're just getting started.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>ebpf</category>
    </item>
  </channel>
</rss>
