Understanding the Trampoline Mechanism in Linux eBPF

Trampolines

In the field of computing, the term “trampoline” has multiple meanings. In the context of Linux systems, it specifically refers to the location in memory where the target logical address is stored, from which the program execution jumps to the actual processing logic. Such structures are also commonly referred to as “indirect jump vectors.” The trampoline mechanism has a wide range of applications, often seen in interrupt service routines (ISRs), I/O handling routines, and other scenarios. In these classic use cases, hardware hard-codes the entry point for handling certain events (such as an interrupt occurring) to a fixed memory address; when the event is triggered, the CPU automatically jumps to that address. The trampoline itself typically performs only a single jump, immediately transferring control to the function that implements the actual processing logic—hence the name “trampoline” due to this “jump and leave” behavior.

Function Tracing (ftrace)

Kernel version 2.6.27

Function tracing (Ftrace) is a mechanism in the kernel used to observe the execution of functions, traditionally for debugging purposes. Ftrace also utilizes the trampoline mechanism, but in a slightly different manner. When compiled with the <span>CONFIG_FUNCTION_TRACER</span> kernel configuration option, this mechanism is enabled. Once enabled, most files in the kernel are compiled with the <span>-pg</span> and <span>-mnop-mcount=...</span> flags. The <span>-pg</span> flag enables profiling functionality, adding some extra CPU instructions at the beginning of most global functions. The <span>-mnop-mcount=...</span> flag turns these CPU instructions into <span>NOP</span> (no operation) instructions instead of the normal profiling logic.

Therefore, by default, when a function is called, the CPU encounters several <span>NOP</span> instructions, which it ignores before continuing to execute the actual function. However, at runtime, the kernel can replace these <span>NOP</span> instructions with trampoline code, which in the case of ftrace means replacing them with some tracing logic.

If <span>CONFIG_FUNCTION_TRACER</span> is not enabled, the assembly code for a function might look like this:

some_kernel_func:
  PUSH RBP
  MOV RSP, RBP
  ...
  RET

If <span>CONFIG_FUNCTION_TRACER</span> is enabled but no tracing functionality is enabled, the assembly code for a function might look like this:

# The symbol to which instructions like CALL some_kernel_func will jump
some_kernel_func:
  NOP
  NOP
# The actual starting point of the function
  PUSH RBP
  MOV RSP, RBP
  ...
  RET

The CPU will ignore the <span>NOP</span> instructions as if they do not exist.

Then at runtime, the kernel can replace the <span>NOP</span> instructions with code that adds tracing functionality. For example, after enabling tracing functionality, the assembly code for a function might look like this:

# The symbol to which instructions like CALL some_kernel_func will jump
some_kernel_func:
  CALL trace_call
  NOP
# The actual starting point of the function
  PUSH RBP
  MOV RSP, RBP
  [...] 
  RET

In this case, the <span>trace_call</span> function will implement the actual tracing logic. The second <span>NOP</span> instruction here is to illustrate that different sizes of code can be inserted into the space left by the <span>NOP</span> instructions.

It is important to note that certain directories, files, or functions in the kernel are excluded. For example, the tracing subsystem (kernel/trace) and any functions with the <span>notrace</span> attribute will be excluded. This is done to prevent users from falling into infinite recursion or to avoid breaking assumptions in critical sections of the kernel code.

BPF Trampoline Mechanism

Kernel version 5.5

BPF programs can be attached to any function in the kernel that has sufficient <span>NOP</span> instructions. When this is done, a “BPF trampoline” is created. This can be somewhat confusing because the segment of <span>NOP</span> instructions is typically referred to as a trampoline, but in this case, when we mention “BPF trampoline,” we are referring to the code that jumps from the trampoline.

When a BPF program is attached, the kernel modifies the <span>NOP</span> instructions in the original function as follows:

# The symbol to which instructions like CALL some_kernel_func will jump
some_kernel_func:
  CALL generated_bpf_trampoline
  RET
# The actual starting point of the function
  PUSH RBP
  MOV RSP, RBP
  [...] 
  RET

This is different from simply adding some extra logic during tracing; it actually redirects the execution of the original function to the generated BPF trampoline. Now, this trampoline can choose to either not call the original function (<span>freplace</span> use case), call a BPF program before calling the original function (<span>fentry</span> use case), call a BPF program after calling the original function (<span>fexit</span> use case), or modify the return value of the original function (<span>fmodify_return</span> use case). It is also this generated trampoline that allows multiple programs to coexist on the same function.

The generated BPF trampoline is architecture-specific dynamically generated machine code, and its main steps are as follows:

  • Allocate space on the stack for all function parameters and return values

  • Copy all parameters from registers to the stack according to the calling convention

  • For each attached fentry program

    • Disable CPU migration before calling the program (in older kernels, disable preemption), re-enable it afterward
    • If statistical tracing is enabled, start a timer before execution and add the runtime to the statistics afterward
    • Call the fentry program (if attached) with the stack pointer as context
  • For each attached <span>fmodify_return</span> program

    • Disable CPU migration before calling the program (in older kernels, disable preemption), re-enable it afterward
    • If statistical tracing is enabled, start a timer before execution and add the runtime to the statistics afterward
    • If the return value is non-zero, return that value without continuing execution
    • Call the <span>fmodify_return</span> program (if attached) with the stack pointer as context
  • Call the original function (unless a <span>fmodify_return</span> program returned a non-zero value)

  • Copy the return value from registers to the stack

  • For each attached fexit program

    • Disable CPU migration before calling the program (in older kernels, disable preemption), re-enable it afterward
    • If statistical tracing is enabled, start a timer before execution and add the runtime to the statistics afterward
    • Call the fexit program (if attached) with the stack pointer as context
  • Return the return value from the stack

Architecture Support

Since BPF trampolines are architecture-specific, support for a given architecture may come later than the initial support for BPF trampolines. The following table outlines the timeline for adding support for various architectures:

Architecture Version Added
X86 – 64 v5.5
ARM64 v6.0
RISC – V v6.3
S390 v6.3

Other architectures currently do not support BPF trampolines.

fentry/fexit/fmodify_return

v5.5

<span>BPF_PROG_TYPE_TRACING</span> type fentry/fexit/fmodify_return programs utilize these trampolines to mount and execute immediately before entering a function or after exiting a function. Since trampolines are essentially just function calls, the overhead is very low. This is a much faster alternative than using interrupts with <span>kprobe</span>, as <span>kprobe</span> uses interrupts that cause context switches, which are much more costly.

Not only native kernel functions will have blank areas for trampolines. When BPF programs are just-in-time compiled (JIT), the kernel also provides these areas that can be instrumented in the same way. This allows fentry/fexit/fmodify_return programs to mount onto other programs for observability purposes.

Program Replacement

v5.6

Trampolines are also used to implement freplace programs, which replace one program with another. When a freplace program is mounted, it installs a trampoline before the original program, jumps to the extended program, executes it, and then returns directly without calling the original program.

Linux Security Modules (LSM)

v5.7

LSM programs are also mounted via trampolines that are very similar to fexit/fmodify_return programs. The kernel defines placeholder functions for each hook, which always start with the prefix <span>bpf_lsm_</span>. These placeholder functions simply return the default return value. After mounting, for BPF trampolines on these placeholder hooks, LSM programs act as fexit or fmodify_return probes.

Src

https://docs.ebpf.io/linux/concepts/trampolines/

Last updated: March 2, 2025

Leave a Comment