Skip to content

Commit a9d82d4

Browse files
Merge pull request #60 from pythonbpf/vmlinux-handler
vmlinux handler with struct support for only int64 and unsigned uint64 type struct fields.
2 parents 22e30f0 + 85a62d6 commit a9d82d4

24 files changed

+686
-121723
lines changed

pythonbpf/allocation_pass.py

Lines changed: 36 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,15 @@
22
import logging
33

44
from llvmlite import ir
5-
from dataclasses import dataclass
6-
from typing import Any
5+
from .local_symbol import LocalSymbol
76
from pythonbpf.helper import HelperHandlerRegistry
7+
from pythonbpf.vmlinux_parser.dependency_node import Field
88
from .expr import VmlinuxHandlerRegistry
99
from pythonbpf.type_deducer import ctypes_to_ir
1010

1111
logger = logging.getLogger(__name__)
1212

1313

14-
@dataclass
15-
class LocalSymbol:
16-
var: ir.AllocaInstr
17-
ir_type: ir.Type
18-
metadata: Any = None
19-
20-
def __iter__(self):
21-
yield self.var
22-
yield self.ir_type
23-
yield self.metadata
24-
25-
2614
def create_targets_and_rvals(stmt):
2715
"""Create lists of targets and right-hand values from an assignment statement."""
2816
if isinstance(stmt.targets[0], ast.Tuple):
@@ -60,21 +48,11 @@ def handle_assign_allocation(builder, stmt, local_sym_tab, structs_sym_tab):
6048
continue
6149

6250
var_name = target.id
63-
6451
# Skip if already allocated
6552
if var_name in local_sym_tab:
6653
logger.debug(f"Variable {var_name} already allocated, skipping")
6754
continue
6855

69-
# When allocating a variable, check if it's a vmlinux struct type
70-
if isinstance(
71-
stmt.value, ast.Name
72-
) and VmlinuxHandlerRegistry.is_vmlinux_struct(stmt.value.id):
73-
# Handle vmlinux struct allocation
74-
# This requires more implementation
75-
print(stmt.value)
76-
pass
77-
7856
# Determine type and allocate based on rval
7957
if isinstance(rval, ast.Call):
8058
_allocate_for_call(builder, var_name, rval, local_sym_tab, structs_sym_tab)
@@ -248,9 +226,41 @@ def _allocate_for_attribute(builder, var_name, rval, local_sym_tab, structs_sym_
248226
logger.error(f"Struct variable '{struct_var}' not found")
249227
return
250228

251-
struct_type = local_sym_tab[struct_var].metadata
229+
struct_type: type = local_sym_tab[struct_var].metadata
252230
if not struct_type or struct_type not in structs_sym_tab:
253-
logger.error(f"Struct type '{struct_type}' not found")
231+
if VmlinuxHandlerRegistry.is_vmlinux_struct(struct_type.__name__):
232+
# Handle vmlinux struct field access
233+
vmlinux_struct_name = struct_type.__name__
234+
if not VmlinuxHandlerRegistry.has_field(vmlinux_struct_name, field_name):
235+
logger.error(
236+
f"Field '{field_name}' not found in vmlinux struct '{vmlinux_struct_name}'"
237+
)
238+
return
239+
240+
field_type: tuple[ir.GlobalVariable, Field] = (
241+
VmlinuxHandlerRegistry.get_field_type(vmlinux_struct_name, field_name)
242+
)
243+
field_ir, field = field_type
244+
# TODO: For now, we only support integer type allocations.
245+
# This always assumes first argument of function to be the context struct
246+
base_ptr = builder.function.args[0]
247+
local_sym_tab[
248+
struct_var
249+
].var = base_ptr # This is repurposing of var to store the pointer of the base type
250+
local_sym_tab[struct_var].ir_type = field_ir
251+
252+
actual_ir_type = ir.IntType(64)
253+
254+
# Allocate with the actual IR type, not the GlobalVariable
255+
var = _allocate_with_type(builder, var_name, actual_ir_type)
256+
local_sym_tab[var_name] = LocalSymbol(var, actual_ir_type, field)
257+
258+
logger.info(
259+
f"Pre-allocated {var_name} from vmlinux struct {vmlinux_struct_name}.{field_name}"
260+
)
261+
return
262+
else:
263+
logger.error(f"Struct type '{struct_type}' not found")
254264
return
255265

256266
struct_info = structs_sym_tab[struct_type]

pythonbpf/assign_pass.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from llvmlite import ir
44
from pythonbpf.expr import eval_expr
55
from pythonbpf.helper import emit_probe_read_kernel_str_call
6+
from pythonbpf.type_deducer import ctypes_to_ir
7+
from pythonbpf.vmlinux_parser.dependency_node import Field
68

79
logger = logging.getLogger(__name__)
810

@@ -148,7 +150,18 @@ def handle_variable_assignment(
148150
val, val_type = val_result
149151
logger.info(f"Evaluated value for {var_name}: {val} of type {val_type}, {var_type}")
150152
if val_type != var_type:
151-
if isinstance(val_type, ir.IntType) and isinstance(var_type, ir.IntType):
153+
if isinstance(val_type, Field):
154+
logger.info("Handling assignment to struct field")
155+
# TODO: handling only ctype struct fields for now. Handle other stuff too later.
156+
if var_type == ctypes_to_ir(val_type.type.__name__):
157+
builder.store(val, var_ptr)
158+
logger.info(f"Assigned ctype struct field to {var_name}")
159+
return True
160+
logger.error(
161+
f"Failed to assign ctype struct field to {var_name}: {val_type} != {var_type}"
162+
)
163+
return False
164+
elif isinstance(val_type, ir.IntType) and isinstance(var_type, ir.IntType):
152165
# Allow implicit int widening
153166
if val_type.width < var_type.width:
154167
val = builder.sext(val, var_type)

pythonbpf/codegen.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,24 @@ def finalize_module(original_str):
3737
return re.sub(pattern, replacement, original_str)
3838

3939

40+
def bpf_passthrough_gen(module):
41+
i32_ty = ir.IntType(32)
42+
ptr_ty = ir.PointerType(ir.IntType(8))
43+
fnty = ir.FunctionType(ptr_ty, [i32_ty, ptr_ty])
44+
45+
# Declare the intrinsic
46+
passthrough = ir.Function(module, fnty, "llvm.bpf.passthrough.p0.p0")
47+
48+
# Set function attributes
49+
# TODO: the ones commented are supposed to be there but cannot be added due to llvmlite limitations at the moment
50+
# passthrough.attributes.add("nofree")
51+
# passthrough.attributes.add("nosync")
52+
passthrough.attributes.add("nounwind")
53+
# passthrough.attributes.add("memory(none)")
54+
55+
return passthrough
56+
57+
4058
def find_bpf_chunks(tree):
4159
"""Find all functions decorated with @bpf in the AST."""
4260
bpf_functions = []
@@ -57,6 +75,8 @@ def processor(source_code, filename, module):
5775
for func_node in bpf_chunks:
5876
logger.info(f"Found BPF function/struct: {func_node.name}")
5977

78+
bpf_passthrough_gen(module)
79+
6080
vmlinux_symtab = vmlinux_proc(tree, module)
6181
if vmlinux_symtab:
6282
handler = VmlinuxHandler.initialize(vmlinux_symtab)
@@ -137,7 +157,7 @@ def compile_to_ir(filename: str, output: str, loglevel=logging.INFO):
137157

138158
module.add_named_metadata("llvm.ident", [f"PythonBPF {VERSION}"])
139159

140-
module_string = finalize_module(str(module))
160+
module_string: str = finalize_module(str(module))
141161

142162
logger.info(f"IR written to {output}")
143163
with open(output, "w") as f:

pythonbpf/debuginfo/debug_info_generator.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,3 +184,83 @@ def create_global_var_debug_info(
184184
"DIGlobalVariableExpression",
185185
{"var": global_var, "expr": self.module.add_debug_info("DIExpression", {})},
186186
)
187+
188+
def get_int64_type(self):
189+
return self.get_basic_type("long", 64, dc.DW_ATE_signed)
190+
191+
def create_subroutine_type(self, return_type, param_types):
192+
"""
193+
Create a DISubroutineType given return type and list of parameter types.
194+
Equivalent to: !DISubroutineType(types: !{ret, args...})
195+
"""
196+
type_array = [return_type]
197+
if isinstance(param_types, (list, tuple)):
198+
type_array.extend(param_types)
199+
else:
200+
type_array.append(param_types)
201+
return self.module.add_debug_info("DISubroutineType", {"types": type_array})
202+
203+
def create_local_variable_debug_info(
204+
self, name: str, arg: int, var_type: Any
205+
) -> Any:
206+
"""
207+
Create debug info for a local variable (DILocalVariable) without scope.
208+
Example:
209+
!DILocalVariable(name: "ctx", arg: 1, file: !3, line: 20, type: !7)
210+
"""
211+
return self.module.add_debug_info(
212+
"DILocalVariable",
213+
{
214+
"name": name,
215+
"arg": arg,
216+
"file": self.module._file_metadata,
217+
"type": var_type,
218+
},
219+
)
220+
221+
def add_scope_to_local_variable(self, local_variable_debug_info, scope_value):
222+
"""
223+
Add scope information to an existing local variable debug info object.
224+
"""
225+
# TODO: this is a workaround a flaw in the debug info generation. Fix this if possible in the future.
226+
# We should not be touching llvmlite's internals like this.
227+
if hasattr(local_variable_debug_info, "operands"):
228+
# LLVM metadata operands is a tuple, so we need to rebuild it
229+
existing_operands = local_variable_debug_info.operands
230+
231+
# Convert tuple to list, add scope, convert back to tuple
232+
operands_list = list(existing_operands)
233+
operands_list.append(("scope", scope_value))
234+
235+
# Reassign the new tuple
236+
local_variable_debug_info.operands = tuple(operands_list)
237+
238+
def create_subprogram(
239+
self, name: str, subroutine_type: Any, retained_nodes: List[Any]
240+
) -> Any:
241+
"""
242+
Create a DISubprogram for a function.
243+
244+
Args:
245+
name: Function name
246+
subroutine_type: DISubroutineType for the function signature
247+
retained_nodes: List of DILocalVariable nodes for function parameters/variables
248+
249+
Returns:
250+
DISubprogram metadata
251+
"""
252+
return self.module.add_debug_info(
253+
"DISubprogram",
254+
{
255+
"name": name,
256+
"scope": self.module._file_metadata,
257+
"file": self.module._file_metadata,
258+
"type": subroutine_type,
259+
# TODO: the following flags do not exist at the moment in our dwarf constants file. We need to add them.
260+
# "flags": dc.DW_FLAG_Prototyped | dc.DW_FLAG_AllCallsDescribed,
261+
# "spFlags": dc.DW_SPFLAG_Definition | dc.DW_SPFLAG_Optimized,
262+
"unit": self.module._debug_compile_unit,
263+
"retainedNodes": retained_nodes,
264+
},
265+
is_distinct=True,
266+
)

pythonbpf/expr/expr_pass.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -72,20 +72,28 @@ def _handle_attribute_expr(
7272
if var_name in local_sym_tab:
7373
var_ptr, var_type, var_metadata = local_sym_tab[var_name]
7474
logger.info(f"Loading attribute {attr_name} from variable {var_name}")
75-
logger.info(f"Variable type: {var_type}, Variable ptr: {var_ptr}")
75+
logger.info(
76+
f"Variable type: {var_type}, Variable ptr: {var_ptr}, Variable Metadata: {var_metadata}"
77+
)
78+
if (
79+
hasattr(var_metadata, "__module__")
80+
and var_metadata.__module__ == "vmlinux"
81+
):
82+
# Try vmlinux handler when var_metadata is not a string, but has a module attribute.
83+
# This has been done to keep everything separate in vmlinux struct handling.
84+
vmlinux_result = VmlinuxHandlerRegistry.handle_attribute(
85+
expr, local_sym_tab, None, builder
86+
)
87+
if vmlinux_result is not None:
88+
return vmlinux_result
89+
else:
90+
raise RuntimeError("Vmlinux struct did not process successfully")
7691
metadata = structs_sym_tab[var_metadata]
7792
if attr_name in metadata.fields:
7893
gep = metadata.gep(builder, var_ptr, attr_name)
7994
val = builder.load(gep)
8095
field_type = metadata.field_type(attr_name)
8196
return val, field_type
82-
83-
# Try vmlinux handler as fallback
84-
vmlinux_result = VmlinuxHandlerRegistry.handle_attribute(
85-
expr, local_sym_tab, None, builder
86-
)
87-
if vmlinux_result is not None:
88-
return vmlinux_result
8997
return None
9098

9199

pythonbpf/expr/vmlinux_registry.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import ast
22

3+
from pythonbpf.vmlinux_parser.vmlinux_exports_handler import VmlinuxHandler
4+
35

46
class VmlinuxHandlerRegistry:
57
"""Registry for vmlinux handler operations"""
68

79
_handler = None
810

911
@classmethod
10-
def set_handler(cls, handler):
12+
def set_handler(cls, handler: VmlinuxHandler):
1113
"""Set the vmlinux handler"""
1214
cls._handler = handler
1315

@@ -37,9 +39,37 @@ def handle_attribute(cls, expr, local_sym_tab, module, builder):
3739
)
3840
return None
3941

42+
@classmethod
43+
def get_struct_debug_info(cls, name):
44+
if cls._handler is None:
45+
return False
46+
return cls._handler.get_struct_debug_info(name)
47+
4048
@classmethod
4149
def is_vmlinux_struct(cls, name):
4250
"""Check if a name refers to a vmlinux struct"""
4351
if cls._handler is None:
4452
return False
4553
return cls._handler.is_vmlinux_struct(name)
54+
55+
@classmethod
56+
def get_struct_type(cls, name):
57+
"""Try to handle a struct name as vmlinux struct"""
58+
if cls._handler is None:
59+
return None
60+
return cls._handler.get_vmlinux_struct_type(name)
61+
62+
@classmethod
63+
def has_field(cls, vmlinux_struct_name, field_name):
64+
"""Check if a vmlinux struct has a specific field"""
65+
if cls._handler is None:
66+
return False
67+
return cls._handler.has_field(vmlinux_struct_name, field_name)
68+
69+
@classmethod
70+
def get_field_type(cls, vmlinux_struct_name, field_name):
71+
"""Get the type of a field in a vmlinux struct"""
72+
if cls._handler is None:
73+
return None
74+
assert isinstance(cls._handler, VmlinuxHandler)
75+
return cls._handler.get_field_type(vmlinux_struct_name, field_name)

0 commit comments

Comments
 (0)