eBPF also has an eBPF VM like JAVA’s jvm.

Claude drew a diagram of the approximate overall architecture.

Overall architecture diagram drawn by Claude

Overall architecture diagram drawn by Claude

eBPF VM is also a sandbox execution environment similar to JVM, but the difference is that eBPF VM runs inside the kernel.

What is eBPF VM?

It is a RISC style virtual machine built into the kernel. Just as the JVM safely executes bytecode on the OS, eBPF VM allows user programs to safely execute on the kernel.

Core Components of eBPF

  • eBPF program execution structure: How code written in user space is loaded into the kernel and executed
  • eBPF programs written by users in high-level languages (C, Rust, etc.) are compiled into eBPF bytecode by the Clang/LLVM compiler. The eBPF VM within the kernel then executes this bytecode.
    • To protect system stability, the program must first pass the Verifier’s safety checks. The JIT (Just-In-Time) compiler can then convert it to native machine code for faster execution.

Key Components of the eBPF VM

eBPF VM internal architecture
	- eBPF Registers
	- eBPF Instruction Set
	- BPF Map (data storage)
	- Helper Functions

eBPF Registers

Classic BPF (cBPF) used only two registers, A and X. After the extension to eBPF, programs primarily use eleven 64-bit registers that map almost one-to-one to modern 64-bit CPU architectures such as x86-64 and ARM64.

Because the register structure is similar to actual hardware, JIT is easily converted to native instructions without overhead when compiling.

Register Roles (R0–R10)

The most important concept when understanding eBPF VM is the defined role of each register.

  • R0: Register that stores the return value of the function call and the final exit code of the eBPF program.
  • R1 ~ R5: Registers used to pass arguments when calling a function
  • R6 ~ R9: Callee-saved registers (registers whose values must be preserved even after function calls)
  • R10: Read-only Stack Frame Pointer

For example, if you call the BPF Helper function,

bpf_trace_printk("Hello eBPF\\n", 11);

To call the bpf_trace_printk() function, the address of the string to be output is entered in R1, and the length of the string (11) is entered in R2.

When a function call occurs, the following flows occur inside the eBPF VM.

  • The eBPF program contains the arguments required by the kernel helper function in R1 to R5.
  • The eBPF program calls the kernel’s helper function.
  • When the helper function completes execution, it transfers the return value to R0.
  • The eBPF program continues the existing execution flow after checking the R0 value.

In this process, data transfer occurs at very high speeds thanks to 1:1 hardware mapping.

eBPF Stack Space (Stack Frame)

Stack Frame is a limited-sized stack area used when an eBPF program is executed.

eBPF VM strictly limits the size of the stack that each program can use to 512 bytes for security and performance.

The following information is generally stored in the stack area.

  • Local variables in functions
  • BPF Temporary data structure to be passed to Map, etc.
  • Small string buffer

The eBPF stack is mainly accessed based on the r10 register.

  • r10: Read-only register pointing to the stack frame base point of the current eBPF program.

Users cannot directly modify the r10 register value and must access the stack through relative address operations.

mov r1, r10
add r1, -8
stx [r1], r2

The code above subtracts 8 bytes from r10, the frame pointer, and stores the value of r2 at that address. This reserves space for a local variable or pointer.

BPF Maps (Data Sharing)

BPF Map is a storage for exchanging data with the outside world after or during the execution of an eBPF program.

Because eBPF programs run inside the kernel, they cannot exchange data with user-space programs through ordinary application mechanisms. BPF maps provide this exchange mechanism.

For example, if you have the following code:

bpf_map_update_elem(&my_map, &key, &value, BPF_ANY);

When the bpf_map_update_elem helper function is executed, the key and value are stored in the BPF Map space located in the kernel.

Afterwards, the user space program can retrieve the data stored in the Map and read it through a system call.

In other words, data collection and sharing through eBPF proceeds roughly as follows.

  1. Create BPF Map in kernel or user space.
  2. As the eBPF program runs, data such as the number of packets is updated in the Map.
  3. User Space applications (e.g. Go, Python programs) query the Map.
  4. It is output to the user in the form of a monitoring dashboard or statistics.

Verifier

One of the most important security devices in eBPF VM is the Verifier, which checks the safety of code before it is executed.

Executing user-written code inside the kernel can cause a kernel panic or infinite loop if done incorrectly.

eBPF Verifier statically analyzes the following strict rules at the code level.

  • Infinite loop prohibited (DAG analysis, only the most recent Bounded Loop allowed)
  • Avoid using uninitialized registers
  • Prohibit unauthorized access to kernel memory

For example, if an out-of-bounds operation is attempted when accessing an array,

ldx r2, [r1+1000]

The verifier determines that this code is unsafe and refuses to load the kernel.

Because of this rigorous verification process, it is mathematically guaranteed that eBPF programs will not cause fatal errors in the system even when executed inside the kernel.

JIT (Just-In-Time) Compiler

When the eBPF bytecode passes the Verifier, it goes through the JIT compiler.

JIT Compiler is also a key element that determines the performance of eBPF. Because interpreting the virtual machine’s bytecode as is is slow, it is immediately translated into native machine language that the host’s CPU can directly understand.

In the x86-64 environment, eBPF instructions and native instructions are converted close to 1:1.

add r1, r2

The above code is immediately converted into actual hardware instructions when it goes through the JIT compiler.

add rdi, rsi

In other words, if it is translated only once when loading a program, when an event occurs in the kernel later, the code is executed at native speed without interpreter overhead.

Helper Functions

eBPF VM restricts arbitrarily calling arbitrary kernel functions.

Instead, a program may call only the safe APIs explicitly exposed by the kernel. These APIs are called Helper Functions.

Representative functions include the following:

  • bpf_map_lookup_elem(): Map data lookup
  • bpf_trace_printk(): Write message to kernel log buffer
  • bpf_get_current_pid_tgid(): Query PID of the currently running process

For example, if you need to record the time while parsing a packet,

u64 ts = bpf_ktime_get_ns();

In this way, the helper function helps the eBPF code safely use complex kernel subsystem functions (time, network, process information, etc.) without damaging arbitrary memory.

Tail Call

Tail Call is a special optimization technique used when calling another eBPF program from within an eBPF program.

The characteristics of Tail Call are as follows.

  • Terminates execution of the current eBPF program and immediately jumps to another eBPF program.
  • Even if the called program (Callee) is terminated, it does not return to the previous program (Caller).
  • Since the current stack frame is overwritten, there is no stack overhead due to the call.

For example, if the code branches in the following flow:

bpf_tail_call(ctx, &my_prog_array, index);

If this function is successfully executed, the current program terminates immediately and the eBPF program at the index specified in my_prog_array Map is executed subsequently.

Since eBPF is limited in the number of instructions or stack size (512 bytes) in a single program, it is suitable for implementing complex logic by connecting several small programs like a chain using tail calls rather than writing one huge program.

BPF to BPF Call

BPF to BPF Call is a general function call function introduced relatively recently.

Previously, when separate functions were defined within eBPF, the compiler expanded them all inline, causing the code size to grow exponentially.

The features of the BPF to BPF Call are as follows.

  • Like a general C language function call, it has a Caller-Callee relationship.
  • When the function execution ends, it returns to its original location.
  • 512 bytes of stack space are shared between the caller and callee.

For example, you can freely define and call functions inside an eBPF program as follows.

static int my_custom_add(int a, int b){
    return a + b;
}

On the caller side, the function is called like a normal C code.

int result = my_custom_add(10, 20);

In other words, the BPF to BPF Call function increases code reusability and effectively reduces the size of the eBPF binary.

eBPF Calling Convention

Even within the eBPF virtual machine, a calling convention for exchanging arguments and return values is defined when calling a function or helper function.

This protocol is designed to be very similar to native architectures (x86-64 System V ABI, etc.).

Integer and pointer arguments are passed in the following order:

  • r1 (first argument)
  • r2 (second argument)
  • r3 (third argument)
  • r4 (fourth argument)
  • r5 (fifth argument)

The return value is always passed to the r0 register.

If the number of arguments exceeds 5, it is generally bypassed by putting multiple data in one structure and passing the pointer.

For example, if you have the following helper function call:

bpf_perf_event_output(ctx, &map, flags, &data, size);

At the eBPF assembly level, arguments are passed approximately as follows.

mov r1, r6  // ctx
mov r2, r7  // &map
mov r3, r8  // flags
mov r4, r9  // &data
mov r5, r10 // size
call bpf_perf_event_output

The first to fifth arguments are passed sequentially to registers. This is a protocol to ensure the most efficient mapping to hardware registers.

Caller-saved and Callee-saved Register

The eBPF protocol also defines register preservation rules.

Because register values may change when calling a function or helper function, it must be clear who is responsible for preserving each register.

  • Caller-saved Register: r0, r1 ~ r5 (registers whose values are not guaranteed when calling a function)
  • Callee-saved Register: r6 to r9 (registers whose values are preserved even after function calls)

All values in the caller-saved registers (r1 to r5) should be considered corrupted once the helper function is called.

Therefore, if previous data needs to be rewritten even after a function call, the value must be backed up in the stack or callee-saved register before the call.

On the other hand, in callee-saved registers (r6~r9), existing data is kept safe even if a helper function is called.

If there is a key pointer that needs to be maintained across multiple function call cycles, it is advantageous to store it in r6.

mov r6, r1
call bpf_map_lookup_elem
mov r1, r6

Because of these rules, the LLVM/Clang compiler can predictably optimize register allocation when compiling eBPF code.

Why the eBPF VM Matters

eBPF VM goes beyond simply the technology to run code within the kernel, and is completely changing the modern cloud native environment and Linux system ecosystem.

If you know the structure and constraints of eBPF VM, you can understand the following principles.

  • Kernel functionality can be extended without recompiling the kernel or loading modules.
  • System call hooking, network packet filtering, etc. can be performed safely without the risk of kernel crash.
  • By understanding memory usage limits and calling conventions, you can write efficient eBPF code with a high passing rate.

For example, in the past kernel module approach, a single pointer mistake could bring down the entire server.

int *ptr = NULL;
*ptr = 10;

On the other hand, in the eBPF VM environment, even if such code is written, it is completely blocked at the Verifier stage.

invalid mem access 'inv'

In addition, the traffic processing speed can be dramatically increased as the JIT compiled eBPF code immediately processes packets at the kernel level (XDP, etc.), which previously had to be uploaded to user space to process packets.

Packet reception -> Kernel (eBPF) -> Immediate packet drop/forwarding

In other words, knowing the architecture of eBPF VMs allows us to clearly analyze why this technology is dominating cloud-native networking and security infrastructure.

Summary

eBPF VM is a RISC style virtual machine for safely and quickly executing user code within the kernel.

During the execution process of the eBPF program, the following elements play designated roles.

  • Verifier that carefully checks bytecode to prevent system collapse
  • JIT Compiler converts to native code to maximize execution speed
  • BPF Map for exchanging data with external systems or other programs
  • Helper Functions to safely use kernel functions
  • eBPF’s own calling convention and storage register structure

In the past, it was just a simple packet filtering (cBPF), but by increasing the number of registers and introducing the JIT compiler and verifier, it has now evolved into a general-purpose technology that can safely extend the Linux kernel.

However, this virtual machine does not provide unlimited freedom, and there are strict rules such as a stack limit of 512 bytes and a loop limit.

Understanding the structure of the eBPF VM can help you understand how C and Rust code are converted to secure bytecode and efficiently control kernel resources, and further help you understand the fundamental operating principles of modern monitoring and network tools such as Cilium and BCC.