42 Commits

Author SHA1 Message Date
0fb1cafd20 Revise README for clarity and additional details
Updated README.md to enhance project description, installation instructions, and usage examples.
2025-09-30 03:05:27 +05:30
1adf7d7fcc Merge pull request #5 from pythonbpf/struct_refactor
Struct refactor
2025-09-30 02:01:58 +05:30
3ded17bf8b Fix size calc for ArrayType in structs 2025-09-30 01:59:18 +05:30
715442d7bf fix struct usage in expr_pass 2025-09-30 01:59:17 +05:30
e464a3fdd5 fix struct usage in handle_helper_functions 2025-09-30 01:59:16 +05:30
fed4c179e6 fix struct usage in functions_pass 2025-09-30 01:59:15 +05:30
32c22c3148 fix struct imports 2025-09-30 01:59:14 +05:30
4557b094e1 Use StructType in struct_pass, fix indexing 2025-09-30 01:59:13 +05:30
84500305db Move structs_pass under structs, create StructType 2025-09-30 01:59:12 +05:30
0d21f84529 Remove redundant functions from struct_pass 2025-09-30 01:59:11 +05:30
5bcc02a931 add parser_struct_fields 2025-09-30 01:59:10 +05:30
fe91a176e2 fix structs_proc 2025-09-30 01:59:09 +05:30
083ee21e38 structs_pass cleanup 2025-09-30 01:59:06 +05:30
ea5a1ab2de add jupyter notebook support 2025-09-27 12:24:49 +05:30
de5cc438ab Allow access from struct fields 2025-09-26 23:02:51 +05:30
8c2196c05c bump version
Signed-off-by: varun-r-mallya <varunrmallya@gmail.com>
2025-09-26 22:48:17 +05:30
a2f86d680d Merge pull request #4 from varun-r-mallya/type_system
Type system and strings
2025-09-26 18:27:10 +05:30
0f365be65e Add some support for strings in structs 2025-09-26 18:26:07 +05:30
4ebf0480dd tweak commit to add placeholder string 2025-09-26 04:54:01 +05:30
b9ddecd6b1 Add string as a primitve to struct defs 2025-09-26 04:44:38 +05:30
737c4d3039 Support storing and printing string type 2025-09-26 04:17:29 +05:30
da8a495da7 Fix handle_cond for new symtab convention 2025-09-26 04:05:37 +05:30
ee03ac04d0 Fix printk handler to comply with new symtab convention 2025-09-26 01:02:10 +05:30
51595f9ec2 Add types returns to bpf helpers 2025-09-26 00:28:10 +05:30
4cf284a81f provide type as weel in eval_expr 2025-09-26 00:24:10 +05:30
1517f6e052 Fix local_sym_tab accesses in expr_pass 2025-09-25 23:54:04 +05:30
95f360059b Fix local_sym_tab accesses in binary_ops 2025-09-25 23:53:04 +05:30
dad57bd340 Fix local_sym_tab accesses in bpf_helper_handler 2025-09-25 23:51:08 +05:30
529b0bde19 Fix local_sym_tab accesses in functions_pass 2025-09-25 23:49:28 +05:30
943697ac9f Pass down type info in local_sym_tab 2025-09-25 23:43:19 +05:30
ba90af9ff2 Allocate space for string consts 2025-09-25 22:24:55 +05:30
35969c4ff7 Add string example 2025-09-25 22:15:14 +05:30
9e87ee52f2 Move relevant vmlinux files to ex7.bpf.c 2025-09-25 00:10:39 +05:30
d0be8893eb Add setuid C example 2025-09-24 23:48:42 +05:30
dda05bd044 Add matplotlib example 2025-09-23 20:36:15 +05:30
28e6f97708 add support for compilation with pylibbpf
Signed-off-by: varun-r-mallya <varunrmallya@gmail.com>
2025-09-21 18:05:43 +05:30
a1bc813ec5 Small fix to enum va 2025-09-21 17:58:51 +05:30
fefd6840c8 finish perf_event_output helper integration 2025-09-21 17:50:58 +05:30
79f0949abc Fix calling conventions changed by structs 2025-09-21 16:19:12 +05:30
a1371697cc overhaul handle_helper_calls 2025-09-21 16:10:29 +05:30
3c976b88d3 pass down structs_sym_tab 2025-09-21 15:20:41 +05:30
69a86c2433 Add perf_event_output boilerplate 2025-09-21 15:14:55 +05:30
17 changed files with 1125 additions and 179 deletions

1
.gitignore vendored
View File

@ -6,3 +6,4 @@
__pycache__/ __pycache__/
*.ll *.ll
*.o *.o
.ipynb_checkpoints/

202
README.md
View File

@ -1,77 +1,183 @@
# Python-BPF # Python-BPF
<p align="center">
<a href="https://www.python.org/downloads/release/python-3080/"><img src="https://img.shields.io/badge/python-3.8-blue.svg"></a>
<a href="https://pypi.org/project/pythonbpf"><img src="https://badge.fury.io/py/pythonbpf.svg"></a>
</p>
This is an LLVM IR generator for eBPF programs in Python. We use llvmlite to generate LLVM IR from pure Python. This is then compiled to LLVM object files, which can be loaded into the kernel for execution. We do not rely on BCC to do our compilation. Python-BPF is an LLVM IR generator for eBPF programs written in Python. It uses [llvmlite](https://github.com/numba/llvmlite) to generate LLVM IR and then compiles to LLVM object files. These object files can be loaded into the kernel for execution. Unlike BCC, Python-BPF performs compilation without relying on its infrastructure.
# DO NOT USE IN PRODUCTION. IN DEVELOPMENT. > **Note**: This project is under active development and not ready for production use.
## Video Demo ---
[Video demo for code under demo/](https://youtu.be/eMyLW8iWbks)
## Slide Deck ## Overview
[Slide deck explaining the project](https://docs.google.com/presentation/d/1DsWDIVrpJhM4RgOETO9VWqUtEHo3-c7XIWmNpi6sTSo/edit?usp=sharing)
## Installation * Generate eBPF programs directly from Python.
- Have `clang` installed. * Compile to LLVM object files for kernel execution.
- `pip install pythonbpf` * Built with `llvmlite` for IR generation.
* Supports maps, helpers, and global definitions for BPF.
* Companion project: [pylibbpf](https://github.com/pythonbpf/pylibbpf), which provides the bindings required for object loading and execution.
---
## Installation
Dependencies:
* `clang`
* Python ≥ 3.8
Install via pip:
```bash
pip install pythonbpf pylibbpf
```
---
## Example Usage
## Usage
```python ```python
# pythonbpf_example.py import time
from pythonbpf import bpf, map, bpfglobal, section, compile from pythonbpf import bpf, map, section, bpfglobal, BPF
from pythonbpf.helpers import bpf_ktime_get_ns from pythonbpf.helpers import pid
from pythonbpf.maps import HashMap from pythonbpf.maps import HashMap
from pylibbpf import *
from ctypes import c_void_p, c_int64, c_uint64, c_int32
import matplotlib.pyplot as plt
from ctypes import c_void_p, c_int64, c_int32, c_uint64 # This program attaches an eBPF tracepoint to sys_enter_clone,
# counts per-PID clone syscalls, stores them in a hash map,
# and then plots the distribution as a histogram using matplotlib.
# It provides a quick view of process creation activity over 10 seconds.
@bpf @bpf
@map @map
def last() -> HashMap: def hist() -> HashMap:
return HashMap(key=c_uint64, value=c_uint64, max_entries=1) return HashMap(key=c_int32, value=c_uint64, max_entries=4096)
@bpf @bpf
@section("tracepoint/syscalls/sys_enter_execve") @section("tracepoint/syscalls/sys_enter_clone")
def hello(ctx: c_void_p) -> c_int32: def hello(ctx: c_void_p) -> c_int64:
print("entered") process_id = pid()
return c_int32(0) one = 1
prev = hist().lookup(process_id)
@bpf if prev:
@section("tracepoint/syscalls/sys_exit_execve") previous_value = prev + 1
def hello_again(ctx: c_void_p) -> c_int64: print(f"count: {previous_value} with {process_id}")
print("exited") hist().update(process_id, previous_value)
key = 0 return c_int64(0)
tsp = last().lookup(key) else:
print(tsp) hist().update(process_id, one)
ts = bpf_ktime_get_ns()
return c_int64(0) return c_int64(0)
@bpf @bpf
@bpfglobal @bpfglobal
def LICENSE() -> str: def LICENSE() -> str:
return "GPL" return "GPL"
def some_normal_function():
print("normal function")
# compiles and dumps object file in the same directory b = BPF()
compile() b.load_and_attach()
hist = BpfMap(b, hist)
print("Recording")
time.sleep(10)
counts = list(hist.values())
plt.hist(counts, bins=20)
plt.xlabel("Clone calls per PID")
plt.ylabel("Frequency")
plt.title("Syscall clone counts")
plt.show()
``` ```
- Run `python pythonbpf_example.py` to get the compiled object file that can be then loaded into the kernel. ---
## Architecture
Python-BPF provides a complete pipeline to write, compile, and load eBPF programs in Python:
1. **Python Source Code**
* Users write BPF programs in Python using decorators like `@bpf`, `@map`, `@section`, and `@bpfglobal`.
* Maps (hash maps), helpers (e.g., `ktime`, `deref`), and tracepoints are defined using Python constructs, preserving a syntax close to standard Python.
2. **AST Generation**
* The Python `ast` module parses the source code into an Abstract Syntax Tree (AST).
* Decorators and type annotations are captured to determine BPF maps, tracepoints, and global variables.
3. **LLVM IR Emission**
* The AST is transformed into LLVM Intermediate Representation (IR) using `llvmlite`.
* IR captures BPF maps, control flow, assignments, and calls to helper functions.
* Debug information is emitted for easier inspection.
4. **LLVM Object File Compilation**
* The LLVM IR (`.ll`) is compiled into a BPF target object file (`.o`) using `llc -march=bpf -O2`.
* This produces a kernel-loadable ELF object file containing the BPF bytecode.
5. **libbpf Integration (via pylibbpf)**
* The compiled object file can be loaded into the kernel using `pylibbpf`.
* Maps, tracepoints, and program sections are initialized, and helper functions are resolved.
* Programs are attached to kernel hooks (e.g., syscalls) for execution.
6. **Execution in Kernel**
* The kernel executes the loaded eBPF program.
* Hash maps, helpers, and global variables behave as defined in the Python source.
* Output can be read via BPF maps, helper functions, or trace printing.
This architecture eliminates the need for embedding C code in Python, allowing full Python tooling support while generating true BPF object files ready for kernel execution.
---
## Development ## Development
- Make a virtual environment and activate it using `python3 -m venv .venv && source .venv/bin/activate`.
- Run `make install` to install the required dependencies.
- Run `make` to see the compilation output of the example.
- Run `check.sh` to check if generated object file passes through the verifier inside the examples directory.
- Run `make` in the `examples/c-form` directory to modify the example C BPF program to check the actual LLVM IR generated by clang.
### Development Notes 1. Create a virtual environment and activate it:
- Run ` ./check.sh check execve2.o;` in examples folder to check if the object code passes the verifier.
- Run ` ./check.sh run execve2.o;` in examples folder to run the object code using `bpftool`. ```bash
python3 -m venv .venv
source .venv/bin/activate
```
2. Install dependencies:
```bash
make install
```
3. Build and test examples:
```bash
make
```
4. Verify an object file with the kernel verifier:
```bash
./check.sh check execve2.o
```
5. Run an object file using `bpftool`:
```bash
./check.sh run execve2.o
```
6. Explore LLVM IR output from clang in `examples/c-form` by running `make`.
---
## Resources
* [Video demonstration](https://youtu.be/eMyLW8iWbks)
* [Slide deck](https://docs.google.com/presentation/d/1DsWDIVrpJhM4RgOETO9VWqUtEHo3-c7XIWmNpi6sTSo/edit?usp=sharing)
---
## Authors ## Authors
- [@r41k0u](https://github.com/r41k0u)
- [@varun-r-mallya](https://github.com/varun-r-mallya) * [@r41k0u](https://github.com/r41k0u)
* [@varun-r-mallya](https://github.com/varun-r-mallya)
---

397
demo/clone-matplotlib.ipynb Normal file

File diff suppressed because one or more lines are too long

56
demo/pybpf4.py Normal file
View File

@ -0,0 +1,56 @@
import time
from pythonbpf import bpf, map, section, bpfglobal, BPF
from pythonbpf.helpers import pid
from pythonbpf.maps import HashMap
from pylibbpf import *
from ctypes import c_void_p, c_int64, c_uint64, c_int32
import matplotlib.pyplot as plt
# This program attaches an eBPF tracepoint to sys_enter_clone,
# counts per-PID clone syscalls, stores them in a hash map,
# and then plots the distribution as a histogram using matplotlib.
# It provides a quick view of process creation activity over 10 seconds.
# Everything is done with Python only code and with the new pylibbpf library.
# Run `sudo /path/to/python/binary/ pybpf4.py`
@bpf
@map
def hist() -> HashMap:
return HashMap(key=c_int32, value=c_uint64, max_entries=4096)
@bpf
@section("tracepoint/syscalls/sys_enter_clone")
def hello(ctx: c_void_p) -> c_int64:
process_id = pid()
one = 1
prev = hist().lookup(process_id)
if prev:
previous_value = prev + 1
print(f"count: {previous_value} with {process_id}")
hist().update(process_id, previous_value)
return c_int64(0)
else:
hist().update(process_id, one)
return c_int64(0)
@bpf
@bpfglobal
def LICENSE() -> str:
return "GPL"
b = BPF()
b.load_and_attach()
hist = BpfMap(b, hist)
print("Recording")
time.sleep(10)
counts = list(hist.values())
plt.hist(counts, bins=20)
plt.xlabel("Clone calls per PID")
plt.ylabel("Frequency")
plt.title("Syscall clone counts")
plt.show()

47
examples/c-form/ex7.bpf.c Normal file
View File

@ -0,0 +1,47 @@
// SPDX-License-Identifier: GPL-2.0
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
struct trace_entry {
short unsigned int type;
unsigned char flags;
unsigned char preempt_count;
int pid;
};
struct trace_event_raw_sys_enter {
struct trace_entry ent;
long int id;
long unsigned int args[6];
char __data[0];
};
struct event {
__u32 pid;
__u32 uid;
__u64 ts;
};
struct {
__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
__uint(key_size, sizeof(int));
__uint(value_size, sizeof(int));
} events SEC(".maps");
SEC("tp/syscalls/sys_enter_setuid")
int handle_setuid_entry(struct trace_event_raw_sys_enter *ctx) {
struct event data = {};
// Extract UID from the syscall arguments
data.uid = (unsigned int)ctx->args[0];
data.ts = bpf_ktime_get_ns();
data.pid = bpf_get_current_pid_tgid() >> 32;
bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &data, sizeof(data));
return 0;
}
char LICENSE[] SEC("license") = "GPL";

View File

@ -10,6 +10,7 @@ from ctypes import c_void_p, c_int64, c_int32, c_uint64
class data_t: class data_t:
pid: c_uint64 pid: c_uint64
ts: c_uint64 ts: c_uint64
comm: str(16)
@bpf @bpf
@ -24,9 +25,12 @@ def hello(ctx: c_void_p) -> c_int32:
dataobj = data_t() dataobj = data_t()
ts = ktime() ts = ktime()
process_id = pid() process_id = pid()
dataobj.pid = process_id strobj = "hellohellohello"
dataobj.ts = ts dataobj.pid = pid()
print(f"clone called at {ts} by pid {process_id}") dataobj.ts = ktime()
# dataobj.comm = strobj
print(f"clone called at {dataobj.ts} by pid {dataobj.pid}, comm {strobj}")
events.output(dataobj)
return c_int32(0) return c_int32(0)

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "pythonbpf" name = "pythonbpf"
version = "0.1.2" version = "0.1.3"
description = "Reduced Python frontend for eBPF" description = "Reduced Python frontend for eBPF"
authors = [ authors = [
{ name = "r41k0u", email="pragyanshchaturvedi18@gmail.com" }, { name = "r41k0u", email="pragyanshchaturvedi18@gmail.com" },
@ -16,7 +16,8 @@ requires-python = ">=3.8"
dependencies = [ dependencies = [
"llvmlite", "llvmlite",
"astpretty" "astpretty",
"pylibbpf"
] ]
[tool.setuptools.packages.find] [tool.setuptools.packages.find]

View File

@ -1,2 +1,2 @@
from .decorators import bpf, map, section, bpfglobal, struct from .decorators import bpf, map, section, bpfglobal, struct
from .codegen import compile_to_ir, compile from .codegen import compile_to_ir, compile, BPF

View File

@ -15,6 +15,7 @@ def recursive_dereferencer(var, builder):
else: else:
raise TypeError(f"Unsupported type for dereferencing: {var.type}") raise TypeError(f"Unsupported type for dereferencing: {var.type}")
def handle_binary_op(rval, module, builder, var_name, local_sym_tab, map_sym_tab, func): def handle_binary_op(rval, module, builder, var_name, local_sym_tab, map_sym_tab, func):
print(module) print(module)
left = rval.left left = rval.left
@ -24,7 +25,7 @@ def handle_binary_op(rval, module, builder, var_name, local_sym_tab, map_sym_tab
# Handle left operand # Handle left operand
if isinstance(left, ast.Name): if isinstance(left, ast.Name):
if left.id in local_sym_tab: if left.id in local_sym_tab:
left = recursive_dereferencer(local_sym_tab[left.id], builder) left = recursive_dereferencer(local_sym_tab[left.id][0], builder)
else: else:
raise SyntaxError(f"Undefined variable: {left.id}") raise SyntaxError(f"Undefined variable: {left.id}")
elif isinstance(left, ast.Constant): elif isinstance(left, ast.Constant):
@ -34,7 +35,7 @@ def handle_binary_op(rval, module, builder, var_name, local_sym_tab, map_sym_tab
if isinstance(right, ast.Name): if isinstance(right, ast.Name):
if right.id in local_sym_tab: if right.id in local_sym_tab:
right = recursive_dereferencer(local_sym_tab[right.id], builder) right = recursive_dereferencer(local_sym_tab[right.id][0], builder)
else: else:
raise SyntaxError(f"Undefined variable: {right.id}") raise SyntaxError(f"Undefined variable: {right.id}")
elif isinstance(right, ast.Constant): elif isinstance(right, ast.Constant):
@ -46,36 +47,36 @@ def handle_binary_op(rval, module, builder, var_name, local_sym_tab, map_sym_tab
if isinstance(op, ast.Add): if isinstance(op, ast.Add):
builder.store(builder.add(left, right), builder.store(builder.add(left, right),
local_sym_tab[var_name]) local_sym_tab[var_name][0])
elif isinstance(op, ast.Sub): elif isinstance(op, ast.Sub):
builder.store(builder.sub(left, right), builder.store(builder.sub(left, right),
local_sym_tab[var_name]) local_sym_tab[var_name][0])
elif isinstance(op, ast.Mult): elif isinstance(op, ast.Mult):
builder.store(builder.mul(left, right), builder.store(builder.mul(left, right),
local_sym_tab[var_name]) local_sym_tab[var_name][0])
elif isinstance(op, ast.Div): elif isinstance(op, ast.Div):
builder.store(builder.sdiv(left, right), builder.store(builder.sdiv(left, right),
local_sym_tab[var_name]) local_sym_tab[var_name][0])
elif isinstance(op, ast.Mod): elif isinstance(op, ast.Mod):
builder.store(builder.srem(left, right), builder.store(builder.srem(left, right),
local_sym_tab[var_name]) local_sym_tab[var_name][0])
elif isinstance(op, ast.LShift): elif isinstance(op, ast.LShift):
builder.store(builder.shl(left, right), builder.store(builder.shl(left, right),
local_sym_tab[var_name]) local_sym_tab[var_name][0])
elif isinstance(op, ast.RShift): elif isinstance(op, ast.RShift):
builder.store(builder.lshr(left, right), builder.store(builder.lshr(left, right),
local_sym_tab[var_name]) local_sym_tab[var_name][0])
elif isinstance(op, ast.BitOr): elif isinstance(op, ast.BitOr):
builder.store(builder.or_(left, right), builder.store(builder.or_(left, right),
local_sym_tab[var_name]) local_sym_tab[var_name][0])
elif isinstance(op, ast.BitXor): elif isinstance(op, ast.BitXor):
builder.store(builder.xor(left, right), builder.store(builder.xor(left, right),
local_sym_tab[var_name]) local_sym_tab[var_name][0])
elif isinstance(op, ast.BitAnd): elif isinstance(op, ast.BitAnd):
builder.store(builder.and_(left, right), builder.store(builder.and_(left, right),
local_sym_tab[var_name]) local_sym_tab[var_name][0])
elif isinstance(op, ast.FloorDiv): elif isinstance(op, ast.FloorDiv):
builder.store(builder.udiv(left, right), builder.store(builder.udiv(left, right),
local_sym_tab[var_name]) local_sym_tab[var_name][0])
else: else:
raise SyntaxError("Unsupported binary operation") raise SyntaxError("Unsupported binary operation")

View File

@ -3,7 +3,7 @@ from llvmlite import ir
from .expr_pass import eval_expr from .expr_pass import eval_expr
def bpf_ktime_get_ns_emitter(call, map_ptr, module, builder, func, local_sym_tab=None): def bpf_ktime_get_ns_emitter(call, map_ptr, module, builder, func, local_sym_tab=None, struct_sym_tab=None, local_var_metadata=None):
""" """
Emit LLVM IR for bpf_ktime_get_ns helper function call. Emit LLVM IR for bpf_ktime_get_ns helper function call.
""" """
@ -13,10 +13,10 @@ def bpf_ktime_get_ns_emitter(call, map_ptr, module, builder, func, local_sym_tab
fn_ptr_type = ir.PointerType(fn_type) fn_ptr_type = ir.PointerType(fn_type)
fn_ptr = builder.inttoptr(helper_id, fn_ptr_type) fn_ptr = builder.inttoptr(helper_id, fn_ptr_type)
result = builder.call(fn_ptr, [], tail=False) result = builder.call(fn_ptr, [], tail=False)
return result return result, ir.IntType(64)
def bpf_map_lookup_elem_emitter(call, map_ptr, module, builder, local_sym_tab=None): def bpf_map_lookup_elem_emitter(call, map_ptr, module, builder, func, local_sym_tab=None, struct_sym_tab=None, local_var_metadata=None):
""" """
Emit LLVM IR for bpf_map_lookup_elem helper function call. Emit LLVM IR for bpf_map_lookup_elem helper function call.
""" """
@ -27,7 +27,7 @@ def bpf_map_lookup_elem_emitter(call, map_ptr, module, builder, local_sym_tab=No
if isinstance(key_arg, ast.Name): if isinstance(key_arg, ast.Name):
key_name = key_arg.id key_name = key_arg.id
if local_sym_tab and key_name in local_sym_tab: if local_sym_tab and key_name in local_sym_tab:
key_ptr = local_sym_tab[key_name] key_ptr = local_sym_tab[key_name][0]
else: else:
raise ValueError( raise ValueError(
f"Key variable {key_name} not found in local symbol table.") f"Key variable {key_name} not found in local symbol table.")
@ -60,10 +60,10 @@ def bpf_map_lookup_elem_emitter(call, map_ptr, module, builder, local_sym_tab=No
result = builder.call(fn_ptr, [map_void_ptr, key_ptr], tail=False) result = builder.call(fn_ptr, [map_void_ptr, key_ptr], tail=False)
return result return result, ir.PointerType()
def bpf_printk_emitter(call, map_ptr, module, builder, func, local_sym_tab=None): def bpf_printk_emitter(call, map_ptr, module, builder, func, local_sym_tab=None, struct_sym_tab=None, local_var_metadata=None):
if not hasattr(func, "_fmt_counter"): if not hasattr(func, "_fmt_counter"):
func._fmt_counter = 0 func._fmt_counter = 0
@ -75,6 +75,7 @@ def bpf_printk_emitter(call, map_ptr, module, builder, func, local_sym_tab=None)
exprs = [] exprs = []
for value in call.args[0].values: for value in call.args[0].values:
print("Value in f-string:", ast.dump(value))
if isinstance(value, ast.Constant): if isinstance(value, ast.Constant):
if isinstance(value.value, str): if isinstance(value.value, str):
fmt_parts.append(value.value) fmt_parts.append(value.value)
@ -85,10 +86,57 @@ def bpf_printk_emitter(call, map_ptr, module, builder, func, local_sym_tab=None)
raise NotImplementedError( raise NotImplementedError(
"Only string and integer constants are supported in f-string.") "Only string and integer constants are supported in f-string.")
elif isinstance(value, ast.FormattedValue): elif isinstance(value, ast.FormattedValue):
# Assume int for now print("Formatted value:", ast.dump(value))
fmt_parts.append("%lld") # TODO: Dirty handling here, only checks for int or str
if isinstance(value.value, ast.Name): if isinstance(value.value, ast.Name):
exprs.append(value.value) if local_sym_tab and value.value.id in local_sym_tab:
var_ptr, var_type = local_sym_tab[value.value.id]
if isinstance(var_type, ir.IntType):
fmt_parts.append("%lld")
exprs.append(value.value)
elif var_type == ir.PointerType(ir.IntType(8)):
# Case with string
fmt_parts.append("%s")
exprs.append(value.value)
else:
raise NotImplementedError(
"Only integer and pointer types are supported in formatted values.")
else:
raise ValueError(
f"Variable {value.value.id} not found in local symbol table.")
elif isinstance(value.value, ast.Attribute):
# object field access from struct
if isinstance(value.value.value, ast.Name) and local_sym_tab and value.value.value.id in local_sym_tab:
var_name = value.value.value.id
field_name = value.value.attr
if local_var_metadata and var_name in local_var_metadata:
var_type = local_var_metadata[var_name]
if var_type in struct_sym_tab:
struct_info = struct_sym_tab[var_type]
if field_name in struct_info.fields:
field_type = struct_info.field_type(
field_name)
if isinstance(field_type, ir.IntType):
fmt_parts.append("%lld")
exprs.append(value.value)
elif field_type == ir.PointerType(ir.IntType(8)):
fmt_parts.append("%s")
exprs.append(value.value)
else:
raise NotImplementedError(
"Only integer and pointer types are supported in formatted values.")
else:
raise ValueError(
f"Field {field_name} not found in struct {var_type}.")
else:
raise ValueError(
f"Struct type {var_type} for variable {var_name} not found in struct symbol table.")
else:
raise ValueError(
f"Metadata for variable {var_name} not found in local variable metadata.")
else:
raise ValueError(
f"Variable {value.value.value.id} not found in local symbol table.")
else: else:
raise NotImplementedError( raise NotImplementedError(
"Only simple variable names are supported in formatted values.") "Only simple variable names are supported in formatted values.")
@ -103,12 +151,12 @@ def bpf_printk_emitter(call, map_ptr, module, builder, func, local_sym_tab=None)
fmt_gvar = ir.GlobalVariable( fmt_gvar = ir.GlobalVariable(
module, ir.ArrayType(ir.IntType(8), len(fmt_str)), name=fmt_name) module, ir.ArrayType(ir.IntType(8), len(fmt_str)), name=fmt_name)
fmt_gvar.global_constant = True fmt_gvar.global_constant = True
fmt_gvar.initializer = ir.Constant( fmt_gvar.initializer = ir.Constant( # type: ignore
ir.ArrayType(ir.IntType(8), len(fmt_str)), ir.ArrayType(ir.IntType(8), len(fmt_str)),
bytearray(fmt_str.encode("utf8")) bytearray(fmt_str.encode("utf8"))
) )
fmt_gvar.linkage = "internal" fmt_gvar.linkage = "internal"
fmt_gvar.align = 1 fmt_gvar.align = 1 # type: ignore
fmt_ptr = builder.bitcast(fmt_gvar, ir.PointerType()) fmt_ptr = builder.bitcast(fmt_gvar, ir.PointerType())
@ -120,7 +168,9 @@ def bpf_printk_emitter(call, map_ptr, module, builder, func, local_sym_tab=None)
"Warning: bpf_printk supports up to 3 arguments, extra arguments will be ignored.") "Warning: bpf_printk supports up to 3 arguments, extra arguments will be ignored.")
for expr in exprs[:3]: for expr in exprs[:3]:
val = eval_expr(func, module, builder, expr, local_sym_tab, None) print(f"{ast.dump(expr)}")
val, _ = eval_expr(func, module, builder,
expr, local_sym_tab, None, struct_sym_tab, local_var_metadata)
if val: if val:
if isinstance(val.type, ir.PointerType): if isinstance(val.type, ir.PointerType):
val = builder.ptrtoint(val, ir.IntType(64)) val = builder.ptrtoint(val, ir.IntType(64))
@ -136,7 +186,6 @@ def bpf_printk_emitter(call, map_ptr, module, builder, func, local_sym_tab=None)
print( print(
"Warning: Failed to evaluate expression for bpf_printk argument. It will be converted to 0.") "Warning: Failed to evaluate expression for bpf_printk argument. It will be converted to 0.")
args.append(ir.Constant(ir.IntType(64), 0)) args.append(ir.Constant(ir.IntType(64), 0))
fn_type = ir.FunctionType(ir.IntType( fn_type = ir.FunctionType(ir.IntType(
64), [ir.PointerType(), ir.IntType(32)], var_arg=True) 64), [ir.PointerType(), ir.IntType(32)], var_arg=True)
fn_ptr_type = ir.PointerType(fn_type) fn_ptr_type = ir.PointerType(fn_type)
@ -172,7 +221,7 @@ def bpf_printk_emitter(call, map_ptr, module, builder, func, local_sym_tab=None)
ir.IntType(32), len(fmt_str))], tail=True) ir.IntType(32), len(fmt_str))], tail=True)
def bpf_map_update_elem_emitter(call, map_ptr, module, builder, local_sym_tab=None): def bpf_map_update_elem_emitter(call, map_ptr, module, builder, func, local_sym_tab=None, struct_sym_tab=None, local_var_metadata=None):
""" """
Emit LLVM IR for bpf_map_update_elem helper function call. Emit LLVM IR for bpf_map_update_elem helper function call.
Expected call signature: map.update(key, value, flags=0) Expected call signature: map.update(key, value, flags=0)
@ -189,7 +238,7 @@ def bpf_map_update_elem_emitter(call, map_ptr, module, builder, local_sym_tab=No
if isinstance(key_arg, ast.Name): if isinstance(key_arg, ast.Name):
key_name = key_arg.id key_name = key_arg.id
if local_sym_tab and key_name in local_sym_tab: if local_sym_tab and key_name in local_sym_tab:
key_ptr = local_sym_tab[key_name] key_ptr = local_sym_tab[key_name][0]
else: else:
raise ValueError( raise ValueError(
f"Key variable {key_name} not found in local symbol table.") f"Key variable {key_name} not found in local symbol table.")
@ -208,7 +257,7 @@ def bpf_map_update_elem_emitter(call, map_ptr, module, builder, local_sym_tab=No
if isinstance(value_arg, ast.Name): if isinstance(value_arg, ast.Name):
value_name = value_arg.id value_name = value_arg.id
if local_sym_tab and value_name in local_sym_tab: if local_sym_tab and value_name in local_sym_tab:
value_ptr = local_sym_tab[value_name] value_ptr = local_sym_tab[value_name][0]
else: else:
raise ValueError( raise ValueError(
f"Value variable {value_name} not found in local symbol table.") f"Value variable {value_name} not found in local symbol table.")
@ -231,7 +280,7 @@ def bpf_map_update_elem_emitter(call, map_ptr, module, builder, local_sym_tab=No
flags_name = flags_arg.id flags_name = flags_arg.id
if local_sym_tab and flags_name in local_sym_tab: if local_sym_tab and flags_name in local_sym_tab:
# Assume it's a stored integer value, load it # Assume it's a stored integer value, load it
flags_ptr = local_sym_tab[flags_name] flags_ptr = local_sym_tab[flags_name][0]
flags_val = builder.load(flags_ptr) flags_val = builder.load(flags_ptr)
else: else:
raise ValueError( raise ValueError(
@ -265,10 +314,10 @@ def bpf_map_update_elem_emitter(call, map_ptr, module, builder, local_sym_tab=No
result = builder.call( result = builder.call(
fn_ptr, [map_void_ptr, key_ptr, value_ptr, flags_const], tail=False) fn_ptr, [map_void_ptr, key_ptr, value_ptr, flags_const], tail=False)
return result return result, None
def bpf_map_delete_elem_emitter(call, map_ptr, module, builder, local_sym_tab=None): def bpf_map_delete_elem_emitter(call, map_ptr, module, builder, func, local_sym_tab=None, struct_sym_tab=None, local_var_metadata=None):
""" """
Emit LLVM IR for bpf_map_delete_elem helper function call. Emit LLVM IR for bpf_map_delete_elem helper function call.
Expected call signature: map.delete(key) Expected call signature: map.delete(key)
@ -284,7 +333,7 @@ def bpf_map_delete_elem_emitter(call, map_ptr, module, builder, local_sym_tab=No
if isinstance(key_arg, ast.Name): if isinstance(key_arg, ast.Name):
key_name = key_arg.id key_name = key_arg.id
if local_sym_tab and key_name in local_sym_tab: if local_sym_tab and key_name in local_sym_tab:
key_ptr = local_sym_tab[key_name] key_ptr = local_sym_tab[key_name][0]
else: else:
raise ValueError( raise ValueError(
f"Key variable {key_name} not found in local symbol table.") f"Key variable {key_name} not found in local symbol table.")
@ -320,10 +369,10 @@ def bpf_map_delete_elem_emitter(call, map_ptr, module, builder, local_sym_tab=No
# Call the helper function # Call the helper function
result = builder.call(fn_ptr, [map_void_ptr, key_ptr], tail=False) result = builder.call(fn_ptr, [map_void_ptr, key_ptr], tail=False)
return result return result, None
def bpf_get_current_pid_tgid_emitter(call, map_ptr, module, builder, func, local_sym_tab=None): def bpf_get_current_pid_tgid_emitter(call, map_ptr, module, builder, func, local_sym_tab=None, struct_sym_tab=None, local_var_metadata=None):
""" """
Emit LLVM IR for bpf_get_current_pid_tgid helper function call. Emit LLVM IR for bpf_get_current_pid_tgid helper function call.
""" """
@ -337,7 +386,59 @@ def bpf_get_current_pid_tgid_emitter(call, map_ptr, module, builder, func, local
# Extract the lower 32 bits (PID) using bitwise AND with 0xFFFFFFFF # Extract the lower 32 bits (PID) using bitwise AND with 0xFFFFFFFF
mask = ir.Constant(ir.IntType(64), 0xFFFFFFFF) mask = ir.Constant(ir.IntType(64), 0xFFFFFFFF)
pid = builder.and_(result, mask) pid = builder.and_(result, mask)
return pid return pid, ir.IntType(64)
def bpf_perf_event_output_handler(call, map_ptr, module, builder, func, local_sym_tab=None, struct_sym_tab=None, local_var_metadata=None):
if len(call.args) != 1:
raise ValueError("Perf event output expects exactly one argument (data), got "
f"{len(call.args)}")
data_arg = call.args[0]
ctx_ptr = func.args[0] # First argument to the function is ctx
if isinstance(data_arg, ast.Name):
data_name = data_arg.id
if local_sym_tab and data_name in local_sym_tab:
data_ptr = local_sym_tab[data_name][0]
else:
raise ValueError(
f"Data variable {data_name} not found in local symbol table.")
# Check is data_name is a struct
if local_var_metadata and data_name in local_var_metadata:
data_type = local_var_metadata[data_name]
if data_type in struct_sym_tab:
struct_info = struct_sym_tab[data_type]
size_val = ir.Constant(ir.IntType(64), struct_info.size)
else:
raise ValueError(
f"Struct type {data_type} for variable {data_name} not found in struct symbol table.")
else:
raise ValueError(
f"Metadata for variable {data_name} not found in local variable metadata.")
# BPF_F_CURRENT_CPU is -1 in 32 bit
flags_val = ir.Constant(ir.IntType(64), 0xFFFFFFFF)
map_void_ptr = builder.bitcast(map_ptr, ir.PointerType())
data_void_ptr = builder.bitcast(data_ptr, ir.PointerType())
fn_type = ir.FunctionType(
ir.IntType(64),
[ir.PointerType(ir.IntType(8)), ir.PointerType(), ir.IntType(64),
ir.PointerType(), ir.IntType(64)],
var_arg=False
)
fn_ptr_type = ir.PointerType(fn_type)
# helper id
fn_addr = ir.Constant(ir.IntType(64), 25)
fn_ptr = builder.inttoptr(fn_addr, fn_ptr_type)
result = builder.call(
fn_ptr, [ctx_ptr, map_void_ptr, flags_val, data_void_ptr, size_val], tail=False)
return result, None
else:
raise NotImplementedError(
"Only simple object names are supported as data in perf event output.")
helper_func_list = { helper_func_list = {
@ -347,15 +448,17 @@ helper_func_list = {
"update": bpf_map_update_elem_emitter, "update": bpf_map_update_elem_emitter,
"delete": bpf_map_delete_elem_emitter, "delete": bpf_map_delete_elem_emitter,
"pid": bpf_get_current_pid_tgid_emitter, "pid": bpf_get_current_pid_tgid_emitter,
"output": bpf_perf_event_output_handler,
} }
def handle_helper_call(call, module, builder, func, local_sym_tab=None, map_sym_tab=None): def handle_helper_call(call, module, builder, func, local_sym_tab=None, map_sym_tab=None, struct_sym_tab=None, local_var_metadata=None):
print(local_var_metadata)
if isinstance(call.func, ast.Name): if isinstance(call.func, ast.Name):
func_name = call.func.id func_name = call.func.id
if func_name in helper_func_list: if func_name in helper_func_list:
# it is not a map method call # it is not a map method call
return helper_func_list[func_name](call, None, module, builder, func, local_sym_tab) return helper_func_list[func_name](call, None, module, builder, func, local_sym_tab, struct_sym_tab, local_var_metadata)
else: else:
raise NotImplementedError( raise NotImplementedError(
f"Function {func_name} is not implemented as a helper function.") f"Function {func_name} is not implemented as a helper function.")
@ -367,14 +470,29 @@ def handle_helper_call(call, module, builder, func, local_sym_tab=None, map_sym_
if map_sym_tab and map_name in map_sym_tab: if map_sym_tab and map_name in map_sym_tab:
map_ptr = map_sym_tab[map_name] map_ptr = map_sym_tab[map_name]
if method_name in helper_func_list: if method_name in helper_func_list:
print(local_var_metadata)
return helper_func_list[method_name]( return helper_func_list[method_name](
call, map_ptr, module, builder, local_sym_tab) call, map_ptr, module, builder, func, local_sym_tab, struct_sym_tab, local_var_metadata)
else: else:
raise NotImplementedError( raise NotImplementedError(
f"Map method {method_name} is not implemented as a helper function.") f"Map method {method_name} is not implemented as a helper function.")
else: else:
raise ValueError( raise ValueError(
f"Map variable {map_name} not found in symbol tables.") f"Map variable {map_name} not found in symbol tables.")
elif isinstance(call.func.value, ast.Name):
obj_name = call.func.value.id
method_name = call.func.attr
if map_sym_tab and obj_name in map_sym_tab:
map_ptr = map_sym_tab[obj_name]
if method_name in helper_func_list:
return helper_func_list[method_name](
call, map_ptr, module, builder, func, local_sym_tab, struct_sym_tab, local_var_metadata)
else:
raise NotImplementedError(
f"Map method {method_name} is not implemented as a helper function.")
else:
raise ValueError(
f"Map variable {obj_name} not found in symbol tables.")
else: else:
raise NotImplementedError( raise NotImplementedError(
"Attribute not supported for map method calls.") "Attribute not supported for map method calls.")

View File

@ -3,12 +3,14 @@ from llvmlite import ir
from .license_pass import license_processing from .license_pass import license_processing
from .functions_pass import func_proc from .functions_pass import func_proc
from .maps_pass import maps_proc from .maps_pass import maps_proc
from .structs_pass import structs_proc from .structs.structs_pass import structs_proc
from .globals_pass import globals_processing from .globals_pass import globals_processing
import os import os
import subprocess import subprocess
import inspect import inspect
from pathlib import Path from pathlib import Path
from pylibbpf import BpfProgram
import tempfile
def find_bpf_chunks(tree): def find_bpf_chunks(tree):
@ -92,6 +94,7 @@ def compile_to_ir(filename: str, output: str):
module.add_named_metadata("llvm.ident", ["llvmlite PythonBPF v0.0.1"]) module.add_named_metadata("llvm.ident", ["llvmlite PythonBPF v0.0.1"])
print(f"IR written to {output}")
with open(output, "w") as f: with open(output, "w") as f:
f.write(f"source_filename = \"{filename}\"\n") f.write(f"source_filename = \"{filename}\"\n")
f.write(str(module)) f.write(str(module))
@ -116,3 +119,21 @@ def compile():
], check=True) ], check=True)
print(f"Object written to {o_file}, {ll_file} can be removed") print(f"Object written to {o_file}, {ll_file} can be removed")
def BPF() -> BpfProgram:
caller_frame = inspect.stack()[1]
src = inspect.getsource(caller_frame.frame)
with tempfile.NamedTemporaryFile(mode="w+", delete=True, suffix=".py") as f, \
tempfile.NamedTemporaryFile(mode="w+", delete=True, suffix=".ll") as inter, \
tempfile.NamedTemporaryFile(mode="w+", delete=False, suffix=".o") as obj_file:
f.write(src)
f.flush()
source = f.name
compile_to_ir(source, str(inter.name))
subprocess.run([
"llc", "-march=bpf", "-filetype=obj", "-O2",
str(inter.name), "-o", str(obj_file.name)
], check=True)
return BpfProgram(str(obj_file.name))

View File

@ -2,21 +2,22 @@ import ast
from llvmlite import ir from llvmlite import ir
def eval_expr(func, module, builder, expr, local_sym_tab, map_sym_tab): def eval_expr(func, module, builder, expr, local_sym_tab, map_sym_tab, structs_sym_tab=None, local_var_metadata=None):
print(f"Evaluating expression: {expr}") print(f"Evaluating expression: {ast.dump(expr)}")
print(local_var_metadata)
if isinstance(expr, ast.Name): if isinstance(expr, ast.Name):
if expr.id in local_sym_tab: if expr.id in local_sym_tab:
var = local_sym_tab[expr.id] var = local_sym_tab[expr.id][0]
val = builder.load(var) val = builder.load(var)
return val return val, local_sym_tab[expr.id][1] # return value and type
else: else:
print(f"Undefined variable {expr.id}") print(f"Undefined variable {expr.id}")
return None return None
elif isinstance(expr, ast.Constant): elif isinstance(expr, ast.Constant):
if isinstance(expr.value, int): if isinstance(expr.value, int):
return ir.Constant(ir.IntType(64), expr.value) return ir.Constant(ir.IntType(64), expr.value), ir.IntType(64)
elif isinstance(expr.value, bool): elif isinstance(expr.value, bool):
return ir.Constant(ir.IntType(1), int(expr.value)) return ir.Constant(ir.IntType(1), int(expr.value)), ir.IntType(1)
else: else:
print("Unsupported constant type") print("Unsupported constant type")
return None return None
@ -37,35 +38,63 @@ def eval_expr(func, module, builder, expr, local_sym_tab, map_sym_tab):
return None return None
if isinstance(arg, ast.Name): if isinstance(arg, ast.Name):
if arg.id in local_sym_tab: if arg.id in local_sym_tab:
arg = local_sym_tab[arg.id] arg = local_sym_tab[arg.id][0]
else: else:
print(f"Undefined variable {arg.id}") print(f"Undefined variable {arg.id}")
return None return None
if arg is None: if arg is None:
print("Failed to evaluate deref argument") print("Failed to evaluate deref argument")
return None return None
# Since we are handling only name case, directly take type from sym tab
val = builder.load(arg) val = builder.load(arg)
return val return val, local_sym_tab[expr.args[0].id][1]
# check for helpers # check for helpers
if expr.func.id in helper_func_list: if expr.func.id in helper_func_list:
return handle_helper_call( return handle_helper_call(
expr, module, builder, func, local_sym_tab, map_sym_tab) expr, module, builder, func, local_sym_tab, map_sym_tab, structs_sym_tab, local_var_metadata)
elif isinstance(expr.func, ast.Attribute): elif isinstance(expr.func, ast.Attribute):
print(f"Handling method call: {ast.dump(expr.func)}")
if isinstance(expr.func.value, ast.Call) and isinstance(expr.func.value.func, ast.Name): if isinstance(expr.func.value, ast.Call) and isinstance(expr.func.value.func, ast.Name):
method_name = expr.func.attr method_name = expr.func.attr
if method_name in helper_func_list: if method_name in helper_func_list:
return handle_helper_call( return handle_helper_call(
expr, module, builder, func, local_sym_tab, map_sym_tab) expr, module, builder, func, local_sym_tab, map_sym_tab, structs_sym_tab, local_var_metadata)
elif isinstance(expr.func.value, ast.Name):
obj_name = expr.func.value.id
method_name = expr.func.attr
if obj_name in map_sym_tab:
if method_name in helper_func_list:
return handle_helper_call(
expr, module, builder, func, local_sym_tab, map_sym_tab, structs_sym_tab, local_var_metadata)
elif isinstance(expr, ast.Attribute):
if isinstance(expr.value, ast.Name):
var_name = expr.value.id
attr_name = expr.attr
if var_name in local_sym_tab:
var_ptr, var_type = local_sym_tab[var_name]
print(f"Loading attribute "
f"{attr_name} from variable {var_name}")
print(f"Variable type: {var_type}, Variable ptr: {var_ptr}")
print(local_var_metadata)
if local_var_metadata and var_name in local_var_metadata:
metadata = structs_sym_tab[local_var_metadata[var_name]]
if attr_name in metadata.fields:
gep = metadata.gep(builder, var_ptr, attr_name)
val = builder.load(gep)
field_type = metadata.field_type(attr_name)
return val, field_type
print("Unsupported expression evaluation") print("Unsupported expression evaluation")
return None return None
def handle_expr(func, module, builder, expr, local_sym_tab, map_sym_tab): def handle_expr(func, module, builder, expr, local_sym_tab, map_sym_tab, structs_sym_tab, local_var_metadata):
"""Handle expression statements in the function body.""" """Handle expression statements in the function body."""
print(f"Handling expression: {ast.dump(expr)}") print(f"Handling expression: {ast.dump(expr)}")
print(local_var_metadata)
call = expr.value call = expr.value
if isinstance(call, ast.Call): if isinstance(call, ast.Call):
eval_expr(func, module, builder, call, local_sym_tab, map_sym_tab) eval_expr(func, module, builder, call, local_sym_tab,
map_sym_tab, structs_sym_tab, local_var_metadata)
else: else:
print("Unsupported expression type") print("Unsupported expression type")

View File

@ -49,38 +49,55 @@ def handle_assign(func, module, builder, stmt, map_sym_tab, local_sym_tab, struc
struct_type = local_var_metadata[var_name] struct_type = local_var_metadata[var_name]
struct_info = structs_sym_tab[struct_type] struct_info = structs_sym_tab[struct_type]
if field_name in struct_info["fields"]: if field_name in struct_info.fields:
field_idx = struct_info["fields"][field_name] field_ptr = struct_info.gep(
struct_ptr = local_sym_tab[var_name] builder, local_sym_tab[var_name][0], field_name)
field_ptr = builder.gep(
struct_ptr, [ir.Constant(ir.IntType(32), 0),
ir.Constant(ir.IntType(32), field_idx)],
inbounds=True)
val = eval_expr(func, module, builder, rval, val = eval_expr(func, module, builder, rval,
local_sym_tab, map_sym_tab) local_sym_tab, map_sym_tab, structs_sym_tab)
if isinstance(struct_info.field_type(field_name), ir.ArrayType) and val[1] == ir.PointerType(ir.IntType(8)):
# TODO: Figure it out, not a priority rn
# Special case for string assignment to char array
# str_len = struct_info["field_types"][field_idx].count
# assign_string_to_array(builder, field_ptr, val[0], str_len)
# print(f"Assigned to struct field {var_name}.{field_name}")
pass
if val is None: if val is None:
print("Failed to evaluate struct field assignment") print("Failed to evaluate struct field assignment")
return return
builder.store(val, field_ptr) print(field_ptr)
builder.store(val[0], field_ptr)
print(f"Assigned to struct field {var_name}.{field_name}") print(f"Assigned to struct field {var_name}.{field_name}")
return return
elif isinstance(rval, ast.Constant): elif isinstance(rval, ast.Constant):
if isinstance(rval.value, bool): if isinstance(rval.value, bool):
if rval.value: if rval.value:
builder.store(ir.Constant(ir.IntType(1), 1), builder.store(ir.Constant(ir.IntType(1), 1),
local_sym_tab[var_name]) local_sym_tab[var_name][0])
else: else:
builder.store(ir.Constant(ir.IntType(1), 0), builder.store(ir.Constant(ir.IntType(1), 0),
local_sym_tab[var_name]) local_sym_tab[var_name][0])
print(f"Assigned constant {rval.value} to {var_name}") print(f"Assigned constant {rval.value} to {var_name}")
elif isinstance(rval.value, int): elif isinstance(rval.value, int):
# Assume c_int64 for now # Assume c_int64 for now
# var = builder.alloca(ir.IntType(64), name=var_name) # var = builder.alloca(ir.IntType(64), name=var_name)
# var.align = 8 # var.align = 8
builder.store(ir.Constant(ir.IntType(64), rval.value), builder.store(ir.Constant(ir.IntType(64), rval.value),
local_sym_tab[var_name]) local_sym_tab[var_name][0])
# local_sym_tab[var_name] = var # local_sym_tab[var_name] = var
print(f"Assigned constant {rval.value} to {var_name}") print(f"Assigned constant {rval.value} to {var_name}")
elif isinstance(rval.value, str):
str_val = rval.value.encode('utf-8') + b'\x00'
str_const = ir.Constant(ir.ArrayType(
ir.IntType(8), len(str_val)), bytearray(str_val))
global_str = ir.GlobalVariable(
module, str_const.type, name=f"{var_name}_str")
global_str.linkage = 'internal'
global_str.global_constant = True
global_str.initializer = str_const
str_ptr = builder.bitcast(
global_str, ir.PointerType(ir.IntType(8)))
builder.store(str_ptr, local_sym_tab[var_name][0])
print(f"Assigned string constant '{rval.value}' to {var_name}")
else: else:
print("Unsupported constant type") print("Unsupported constant type")
elif isinstance(rval, ast.Call): elif isinstance(rval, ast.Call):
@ -92,7 +109,7 @@ def handle_assign(func, module, builder, stmt, map_sym_tab, local_sym_tab, struc
# var = builder.alloca(ir_type, name=var_name) # var = builder.alloca(ir_type, name=var_name)
# var.align = ir_type.width // 8 # var.align = ir_type.width // 8
builder.store(ir.Constant( builder.store(ir.Constant(
ir_type, rval.args[0].value), local_sym_tab[var_name]) ir_type, rval.args[0].value), local_sym_tab[var_name][0])
print(f"Assigned {call_type} constant " print(f"Assigned {call_type} constant "
f"{rval.args[0].value} to {var_name}") f"{rval.args[0].value} to {var_name}")
# local_sym_tab[var_name] = var # local_sym_tab[var_name] = var
@ -100,28 +117,28 @@ def handle_assign(func, module, builder, stmt, map_sym_tab, local_sym_tab, struc
# var = builder.alloca(ir.IntType(64), name=var_name) # var = builder.alloca(ir.IntType(64), name=var_name)
# var.align = 8 # var.align = 8
val = handle_helper_call( val = handle_helper_call(
rval, module, builder, None, local_sym_tab, map_sym_tab) rval, module, builder, func, local_sym_tab, map_sym_tab, structs_sym_tab, local_var_metadata)
builder.store(val, local_sym_tab[var_name]) builder.store(val[0], local_sym_tab[var_name][0])
# local_sym_tab[var_name] = var # local_sym_tab[var_name] = var
print(f"Assigned constant {rval.func.id} to {var_name}") print(f"Assigned constant {rval.func.id} to {var_name}")
elif call_type == "deref" and len(rval.args) == 1: elif call_type == "deref" and len(rval.args) == 1:
print(f"Handling deref assignment {ast.dump(rval)}") print(f"Handling deref assignment {ast.dump(rval)}")
val = eval_expr(func, module, builder, rval, val = eval_expr(func, module, builder, rval,
local_sym_tab, map_sym_tab) local_sym_tab, map_sym_tab, structs_sym_tab)
if val is None: if val is None:
print("Failed to evaluate deref argument") print("Failed to evaluate deref argument")
return return
print(f"Dereferenced value: {val}, storing in {var_name}") print(f"Dereferenced value: {val}, storing in {var_name}")
builder.store(val, local_sym_tab[var_name]) builder.store(val[0], local_sym_tab[var_name][0])
# local_sym_tab[var_name] = var # local_sym_tab[var_name] = var
print(f"Dereferenced and assigned to {var_name}") print(f"Dereferenced and assigned to {var_name}")
elif call_type in structs_sym_tab and len(rval.args) == 0: elif call_type in structs_sym_tab and len(rval.args) == 0:
struct_info = structs_sym_tab[call_type] struct_info = structs_sym_tab[call_type]
ir_type = struct_info["type"] ir_type = struct_info.ir_type
# var = builder.alloca(ir_type, name=var_name) # var = builder.alloca(ir_type, name=var_name)
# Null init # Null init
builder.store(ir.Constant(ir_type, None), builder.store(ir.Constant(ir_type, None),
local_sym_tab[var_name]) local_sym_tab[var_name][0])
local_var_metadata[var_name] = call_type local_var_metadata[var_name] = call_type
print(f"Assigned struct {call_type} to {var_name}") print(f"Assigned struct {call_type} to {var_name}")
# local_sym_tab[var_name] = var # local_sym_tab[var_name] = var
@ -139,10 +156,10 @@ def handle_assign(func, module, builder, stmt, map_sym_tab, local_sym_tab, struc
map_ptr = map_sym_tab[map_name] map_ptr = map_sym_tab[map_name]
if method_name in helper_func_list: if method_name in helper_func_list:
val = handle_helper_call( val = handle_helper_call(
rval, module, builder, func, local_sym_tab, map_sym_tab) rval, module, builder, func, local_sym_tab, map_sym_tab, structs_sym_tab, local_var_metadata)
# var = builder.alloca(ir.IntType(64), name=var_name) # var = builder.alloca(ir.IntType(64), name=var_name)
# var.align = 8 # var.align = 8
builder.store(val, local_sym_tab[var_name]) builder.store(val[0], local_sym_tab[var_name][0])
# local_sym_tab[var_name] = var # local_sym_tab[var_name] = var
else: else:
print("Unsupported assignment call structure") print("Unsupported assignment call structure")
@ -166,7 +183,7 @@ def handle_cond(func, module, builder, cond, local_sym_tab, map_sym_tab):
return None return None
elif isinstance(cond, ast.Name): elif isinstance(cond, ast.Name):
if cond.id in local_sym_tab: if cond.id in local_sym_tab:
var = local_sym_tab[cond.id] var = local_sym_tab[cond.id][0]
val = builder.load(var) val = builder.load(var)
if val.type != ir.IntType(1): if val.type != ir.IntType(1):
# Convert nonzero values to true, zero to false # Convert nonzero values to true, zero to false
@ -183,12 +200,12 @@ def handle_cond(func, module, builder, cond, local_sym_tab, map_sym_tab):
return None return None
elif isinstance(cond, ast.Compare): elif isinstance(cond, ast.Compare):
lhs = eval_expr(func, module, builder, cond.left, lhs = eval_expr(func, module, builder, cond.left,
local_sym_tab, map_sym_tab) local_sym_tab, map_sym_tab)[0]
if len(cond.ops) != 1 or len(cond.comparators) != 1: if len(cond.ops) != 1 or len(cond.comparators) != 1:
print("Unsupported complex comparison") print("Unsupported complex comparison")
return None return None
rhs = eval_expr(func, module, builder, rhs = eval_expr(func, module, builder,
cond.comparators[0], local_sym_tab, map_sym_tab) cond.comparators[0], local_sym_tab, map_sym_tab)[0]
op = cond.ops[0] op = cond.ops[0]
if lhs.type != rhs.type: if lhs.type != rhs.type:
@ -222,7 +239,7 @@ def handle_cond(func, module, builder, cond, local_sym_tab, map_sym_tab):
return None return None
def handle_if(func, module, builder, stmt, map_sym_tab, local_sym_tab): def handle_if(func, module, builder, stmt, map_sym_tab, local_sym_tab, structs_sym_tab=None):
"""Handle if statements in the function body.""" """Handle if statements in the function body."""
print("Handling if statement") print("Handling if statement")
start = builder.block.parent start = builder.block.parent
@ -243,7 +260,7 @@ def handle_if(func, module, builder, stmt, map_sym_tab, local_sym_tab):
builder.position_at_end(then_block) builder.position_at_end(then_block)
for s in stmt.body: for s in stmt.body:
process_stmt(func, module, builder, s, process_stmt(func, module, builder, s,
local_sym_tab, map_sym_tab, False) local_sym_tab, map_sym_tab, structs_sym_tab, False)
if not builder.block.is_terminated: if not builder.block.is_terminated:
builder.branch(merge_block) builder.branch(merge_block)
@ -251,7 +268,7 @@ def handle_if(func, module, builder, stmt, map_sym_tab, local_sym_tab):
builder.position_at_end(else_block) builder.position_at_end(else_block)
for s in stmt.orelse: for s in stmt.orelse:
process_stmt(func, module, builder, s, process_stmt(func, module, builder, s,
local_sym_tab, map_sym_tab, False) local_sym_tab, map_sym_tab, structs_sym_tab, False)
if not builder.block.is_terminated: if not builder.block.is_terminated:
builder.branch(merge_block) builder.branch(merge_block)
@ -261,14 +278,17 @@ def handle_if(func, module, builder, stmt, map_sym_tab, local_sym_tab):
def process_stmt(func, module, builder, stmt, local_sym_tab, map_sym_tab, structs_sym_tab, did_return, ret_type=ir.IntType(64)): def process_stmt(func, module, builder, stmt, local_sym_tab, map_sym_tab, structs_sym_tab, did_return, ret_type=ir.IntType(64)):
print(f"Processing statement: {ast.dump(stmt)}") print(f"Processing statement: {ast.dump(stmt)}")
if isinstance(stmt, ast.Expr): if isinstance(stmt, ast.Expr):
handle_expr(func, module, builder, stmt, local_sym_tab, map_sym_tab) print(local_var_metadata)
handle_expr(func, module, builder, stmt, local_sym_tab,
map_sym_tab, structs_sym_tab, local_var_metadata)
elif isinstance(stmt, ast.Assign): elif isinstance(stmt, ast.Assign):
handle_assign(func, module, builder, stmt, map_sym_tab, handle_assign(func, module, builder, stmt, map_sym_tab,
local_sym_tab, structs_sym_tab) local_sym_tab, structs_sym_tab)
elif isinstance(stmt, ast.AugAssign): elif isinstance(stmt, ast.AugAssign):
raise SyntaxError("Augmented assignment not supported") raise SyntaxError("Augmented assignment not supported")
elif isinstance(stmt, ast.If): elif isinstance(stmt, ast.If):
handle_if(func, module, builder, stmt, map_sym_tab, local_sym_tab) handle_if(func, module, builder, stmt, map_sym_tab,
local_sym_tab, structs_sym_tab)
elif isinstance(stmt, ast.Return): elif isinstance(stmt, ast.Return):
if stmt.value is None: if stmt.value is None:
builder.ret(ir.Constant(ir.IntType(32), 0)) builder.ret(ir.Constant(ir.IntType(32), 0))
@ -340,7 +360,7 @@ def allocate_mem(module, builder, body, func, ret_type, map_sym_tab, local_sym_t
f"Pre-allocated variable {var_name} for deref") f"Pre-allocated variable {var_name} for deref")
elif call_type in structs_sym_tab: elif call_type in structs_sym_tab:
struct_info = structs_sym_tab[call_type] struct_info = structs_sym_tab[call_type]
ir_type = struct_info["type"] ir_type = struct_info.ir_type
var = builder.alloca(ir_type, name=var_name) var = builder.alloca(ir_type, name=var_name)
local_var_metadata[var_name] = call_type local_var_metadata[var_name] = call_type
print( print(
@ -368,8 +388,14 @@ def allocate_mem(module, builder, body, func, ret_type, map_sym_tab, local_sym_t
var.align = ir_type.width // 8 var.align = ir_type.width // 8
print( print(
f"Pre-allocated variable {var_name} of type c_int64") f"Pre-allocated variable {var_name} of type c_int64")
elif isinstance(rval.value, str):
ir_type = ir.PointerType(ir.IntType(8))
var = builder.alloca(ir_type, name=var_name)
var.align = 8
print(
f"Pre-allocated variable {var_name} of type string")
else: else:
print("Unsupported constant type") print(f"Unsupported constant type")
continue continue
elif isinstance(rval, ast.BinOp): elif isinstance(rval, ast.BinOp):
# Assume c_int64 for now # Assume c_int64 for now
@ -381,7 +407,7 @@ def allocate_mem(module, builder, body, func, ret_type, map_sym_tab, local_sym_t
else: else:
print("Unsupported assignment value type") print("Unsupported assignment value type")
continue continue
local_sym_tab[var_name] = var local_sym_tab[var_name] = (var, ir_type)
return local_sym_tab return local_sym_tab
@ -441,7 +467,6 @@ def process_bpf_chunk(func_node, module, return_type, map_sym_tab, structs_sym_t
process_func_body(module, builder, func_node, func, process_func_body(module, builder, func_node, func,
ret_type, map_sym_tab, structs_sym_tab) ret_type, map_sym_tab, structs_sym_tab)
return func return func
@ -517,3 +542,51 @@ def infer_return_type(func_node: ast.FunctionDef):
raise ValueError("Conflicting return types:" raise ValueError("Conflicting return types:"
f"{found_type} vs {t}") f"{found_type} vs {t}")
return found_type or "None" return found_type or "None"
# For string assignment to fixed-size arrays
def assign_string_to_array(builder, target_array_ptr, source_string_ptr, array_length):
"""
Copy a string (i8*) to a fixed-size array ([N x i8]*)
"""
# Create a loop to copy characters one by one
entry_block = builder.block
copy_block = builder.append_basic_block("copy_char")
end_block = builder.append_basic_block("copy_end")
# Create loop counter
i = builder.alloca(ir.IntType(32))
builder.store(ir.Constant(ir.IntType(32), 0), i)
# Start the loop
builder.branch(copy_block)
# Copy loop
builder.position_at_end(copy_block)
idx = builder.load(i)
in_bounds = builder.icmp_unsigned(
'<', idx, ir.Constant(ir.IntType(32), array_length))
builder.cbranch(in_bounds, copy_block, end_block)
with builder.if_then(in_bounds):
# Load character from source
src_ptr = builder.gep(source_string_ptr, [idx])
char = builder.load(src_ptr)
# Store character in target
dst_ptr = builder.gep(
target_array_ptr, [ir.Constant(ir.IntType(32), 0), idx])
builder.store(char, dst_ptr)
# Increment counter
next_idx = builder.add(idx, ir.Constant(ir.IntType(32), 1))
builder.store(next_idx, i)
builder.position_at_end(end_block)
# Ensure null termination
last_idx = ir.Constant(ir.IntType(32), array_length - 1)
null_ptr = builder.gep(
target_array_ptr, [ir.Constant(ir.IntType(32), 0), last_idx])
builder.store(ir.Constant(ir.IntType(8), 0), null_ptr)

View File

@ -30,3 +30,6 @@ class PerfEventArray:
self.key_type = key_size self.key_type = key_size
self.value_type = value_size self.value_type = value_size
self.entries = {} self.entries = {}
def output(self, data):
pass # Placeholder for output method

View File

@ -0,0 +1,31 @@
from llvmlite import ir
class StructType:
def __init__(self, ir_type, fields, size):
self.ir_type = ir_type
self.fields = fields
self.size = size
def field_idx(self, field_name):
return list(self.fields.keys()).index(field_name)
def field_type(self, field_name):
return self.fields[field_name]
def gep(self, builder, ptr, field_name):
idx = self.field_idx(field_name)
return builder.gep(ptr, [ir.Constant(ir.IntType(32), 0),
ir.Constant(ir.IntType(32), idx)],
inbounds=True)
def field_size(self, field_name):
fld = self.fields[field_name]
if isinstance(fld, ir.ArrayType):
return fld.count * (fld.element.width // 8)
elif isinstance(fld, ir.IntType):
return fld.width // 8
elif isinstance(fld, ir.PointerType):
return 8
raise TypeError(f"Unsupported field type: {fld}")

View File

@ -0,0 +1,97 @@
import ast
import logging
from llvmlite import ir
from pythonbpf.type_deducer import ctypes_to_ir
from .struct_type import StructType
logger = logging.getLogger(__name__)
# TODO: Shall we allow the following syntax:
# struct MyStruct:
# field1: int
# field2: str(32)
# Where int is mapped to c_uint64?
# Shall we just int64, int32 and uint32 similarly?
def structs_proc(tree, module, chunks):
""" Process all class definitions to find BPF structs """
structs_sym_tab = {}
for cls_node in chunks:
if is_bpf_struct(cls_node):
print(f"Found BPF struct: {cls_node.name}")
struct_info = process_bpf_struct(cls_node, module)
structs_sym_tab[cls_node.name] = struct_info
return structs_sym_tab
def is_bpf_struct(cls_node):
return any(
isinstance(decorator, ast.Name) and decorator.id == "struct"
for decorator in cls_node.decorator_list
)
def process_bpf_struct(cls_node, module):
""" Process a single BPF struct definition """
fields = parse_struct_fields(cls_node)
field_types = list(fields.values())
total_size = calc_struct_size(field_types)
struct_type = ir.LiteralStructType(field_types)
logger.info(f"Created struct {cls_node.name} with fields {fields.keys()}")
return StructType(struct_type, fields, total_size)
def parse_struct_fields(cls_node):
""" Parse fields of a struct class node """
fields = {}
for item in cls_node.body:
if isinstance(item, ast.AnnAssign) and \
isinstance(item.target, ast.Name):
fields[item.target.id] = get_type_from_ann(item.annotation)
else:
logger.error(f"Unsupported struct field: {ast.dump(item)}")
raise TypeError(f"Unsupported field in {ast.dump(cls_node)}")
return fields
def get_type_from_ann(annotation):
""" Convert an AST annotation node to an LLVM IR type for struct fields"""
if isinstance(annotation, ast.Call) and \
isinstance(annotation.func, ast.Name):
if annotation.func.id == "str":
# Char array
# Assumes constant integer argument
length = annotation.args[0].value
return ir.ArrayType(ir.IntType(8), length)
elif isinstance(annotation, ast.Name):
# Int type, written as c_int64, c_uint32, etc.
return ctypes_to_ir(annotation.id)
raise TypeError(f"Unsupported annotation type: {ast.dump(annotation)}")
def calc_struct_size(field_types):
""" Calculate total size of the struct with alignment and padding """
curr_offset = 0
for ftype in field_types:
if isinstance(ftype, ir.IntType):
fsize = ftype.width // 8
alignment = fsize
elif isinstance(ftype, ir.ArrayType):
fsize = ftype.count * (ftype.element.width // 8)
alignment = ftype.element.width // 8
elif isinstance(ftype, ir.PointerType):
# We won't encounter this rn, but for the future
fsize = 8
alignment = 8
else:
raise TypeError(f"Unsupported field type: {ftype}")
padding = (alignment - (curr_offset % alignment)) % alignment
curr_offset += padding + fsize
final_padding = (8 - (curr_offset % 8)) % 8
return curr_offset + final_padding

View File

@ -1,39 +0,0 @@
import ast
from llvmlite import ir
from .type_deducer import ctypes_to_ir
from . import dwarf_constants as dc
structs_sym_tab = {}
def structs_proc(tree, module, chunks):
for cls_node in chunks:
# Check if this class is a struct
is_struct = False
for decorator in cls_node.decorator_list:
if isinstance(decorator, ast.Name) and decorator.id == "struct":
is_struct = True
break
if is_struct:
print(f"Found BPF struct: {cls_node.name}")
process_bpf_struct(cls_node, module)
continue
return structs_sym_tab
def process_bpf_struct(cls_node, module):
struct_name = cls_node.name
field_names = []
field_types = []
for item in cls_node.body:
if isinstance(item, ast.AnnAssign) and isinstance(item.target, ast.Name):
field_names.append(item.target.id)
field_types.append(ctypes_to_ir(item.annotation.id))
struct_type = ir.LiteralStructType(field_types)
structs_sym_tab[struct_name] = {
"type": struct_type,
"fields": {name: idx for idx, name in enumerate(field_names)}
}
print(f"Created struct {struct_name} with fields {field_names}")