Skip to content

Global Variables

5.2

Like typical C programs, BPF programs allow the use of global variables. These variables can be initialized from the BPF C code itself, or they can be modified by the loading user space application before handing it off to the kernel.

The abstraction ebpf-go provides to interact with global variables is the VariableSpec, found in the CollectionSpec.Variables field. This page describes how to declare variables in BPF C and how to interact with them in Go.

Runtime Constants

5.2

Global runtime constants are typically used for configuration values that influence the functionality of a BPF program. Think all sorts of network or hardware addresses for network filtering, or timeouts for rate limiting. The C compiler will reject any runtime modifications to these variables in the BPF program, like a typical const.

Crucially, the BPF verifier will also perform dead code analysis if constants are used in if statements. If a condition is always true or false, it will remove unused code paths from the BPF program, reducing verification time and increasing runtime performance.

This enables many features like portable kfuncs, allowing C code to refer to kfuncs that may not exist in some kernels, as long as those code paths are guaranteed not to execute at runtime. Similarly, this can be used to your advantage to disable code paths that are not needed in certain configurations, or would result in a verifier error on some kernels or in some contexts.

Consider the following C BPF program that reads a global constant and returns it:

BPF C program declaring global constant const_u32
1
2
3
4
5
volatile const __u32 const_u32;

SEC("socket") int const_example() {
    return const_u32;
}
Why is const_u32 declared volatile?

In short: without the volatile qualifier, the variable would be optimized away and not appear in the BPF object file, leaving us unable to modify it from our user space application.

In this program, the compiler (in)correctly deduces two things about const_u32: it is never assigned a value, and it doesn't change over the course of the program. Implementation details aside, it will now assume that the return value of const_example() is always 0 and omit the variable from the ELF altogether.

For BPF programs, it's common practice to declare all global variables that need to be accessed from user space as volatile, especially non-const globals. Doing so ensures the compiler reliably allocates them in a data section in the ELF.

First, let's take a look at a full Go example that will comprise the majority of interactions with constants. In the example below, we'll load a BPF object from disk, pull out a variable, set its value and call the BPF program once with an empty context. Variations on this pattern will follow later.

Go program modifying a const, loading and running the BPF program
// Load the object file from disk using a bpf2go-generated scaffolding.
spec, err := loadVariables()
if err != nil {
    panicf("loading CollectionSpec: %s", err)
}

// Set the 'const_u32' variable to 42 in the CollectionSpec.
want := uint32(42) // (1)!
if err := spec.Variables["const_u32"].Set(want); err != nil {
    panicf("setting variable: %s", err)
}

// Load the CollectionSpec.
//
// Note: modifying spec.Variables after this point is ineffectual!
// Modifying *Spec resources does not affect loaded/running BPF programs.
var obj variablesPrograms
if err := spec.LoadAndAssign(&obj, nil); err != nil {
    panicf("loading BPF program: %s", err)
}

fmt.Println("Running program with const_u32 set to", want)

// Dry-run the BPF program with an empty context.
ret, _, err := obj.ConstExample.Test(make([]byte, 15)) // (2)!
if err != nil {
    panicf("running BPF program: %s", err)
}

if ret != want {
    panicf("unexpected return value %d", ret)
}

fmt.Println("BPF program returned", ret)

// Output:
// Running program with const_u32 set to 42
// BPF program returned 42
  1. Any values passed into VariableSpec.Set must marshal to a fixed width. This behaviour is identical to Map.Put and friends. Using untyped integers is not supported since their size is platform dependent. We recommend the same approach in BPF C to keep data size predictable.
  2. A 15-byte context is the minimum the kernel will accept for dry-running a BPF program. If your BPF program reads from its context, populating this slice is a great way of doing unit testing without setting up a live testing environment.

Global Variables

Non-const global variables are mutable and can be modified by both the BPF program and the user space application. They are typically used for keeping state like metrics, counters, rate limiting, etc.

These variables can also be initialized from user space, much like their const counterparts, and can be both read and written to from the BPF program as well as the user space application. More on that in a future section.

The following C BPF program reads a global variable and returns it:

BPF C program declaring global variable global_u16
1
2
3
4
5
6
volatile __u16 global_u16;

SEC("socket") int global_example() {
    global_u16++;
    return global_u16;
}
Why is global_u16 declared volatile?

Similar to volatile const in a prior example, volatile is used here to make compiler output more deterministic. Without it, the compiler may choose to optimize away a variable if it's never assigned to, not knowing its value is actually provided by user space. The volatile qualifier doesn't change the variable's semantics.

Next, in user space, initialize global_u16 to 9000:

1
2
3
4
set := uint16(9000)
if err := spec.Variables["global_u16"].Set(set); err != nil {
    panicf("setting variable: %s", err)
}

Dry-running global_example() a few times results in our value increasing on every invocation:

for range 3 {
    ret, _, err := obj.GlobalExample.Test(make([]byte, 15))
    if err != nil {
        panicf("running BPF program: %s", err)
    }
    fmt.Println("BPF program returned", ret)
}

// Output:
// Running program with global_u16 set to 9000
// BPF program returned 9000
// BPF program returned 9001
// BPF program returned 9002

Internal/Hidden Global Variables

By default, all global variables described in an ELF's data sections are exposed through CollectionSpec.Variables. However, there may be cases where you don't want user space to interfere with a variable (either on purpose or by accident) and you want to keep the variable internal to the BPF program.

BPF C program declaring internal global variable internal_var
1
2
3
4
5
6
__hidden __u64 hidden_var;

SEC("socket") int hidden_example() {
    hidden_var++;
    return hidden_var;
}

The __hidden macro is found in Linux' <bpf/bpf_helpers.h> as of version 5.13 and is defined as follows:

#define __hidden __attribute__((visibility("hidden")))

This will cause the VariableSpec for hidden_var to not be included in the CollectionSpec.

Static Global Variables

With the introduction of bpftool gen object. BPF received a full-blown static linker, giving the static keyword for declaring objects local to a single .c file an actual semantic meaning.

ebpf-go follows the convention set by libbpf to not expose static variables to user space. In our case, this means that static variables are not included in the CollectionSpec.Variables field or emitted in bpf2go-generated code.

The ELF loader has no way to differentiate function-scoped local variables (also not exposed) and static variables, since they're both marked with LOCAL linkage in the ELF. If you need to expose a variable to user space, drop the static keyword and declare it in the global scope of your BPF C program.


Last updated 2024-09-12
Authored by Timo Beckers