mirror of
https://github.com/varun-r-mallya/Python-BPF.git
synced 2025-12-31 21:06:25 +00:00
Compare commits
21 Commits
symex
...
42b8865a56
| Author | SHA1 | Date | |
|---|---|---|---|
| 42b8865a56 | |||
| 144d9b0ab4 | |||
| 902a52a07d | |||
| 306570953b | |||
| 740eed45e1 | |||
| c8801f4c3e | |||
| e5b3b001ce | |||
| 19b42b9a19 | |||
| 9f5ec62383 | |||
| 7af54df7c0 | |||
| 573bbb350e | |||
| 64679f8072 | |||
| 5667facf23 | |||
| 4f8af16a17 | |||
| b84884162d | |||
| 49740598ea | |||
| 73bbf00e7c | |||
| f7dee329cb | |||
| 5031f90377 | |||
| 95a624044a | |||
| c5bef26b88 |
@ -7,6 +7,7 @@ from pythonbpf.helper import HelperHandlerRegistry
|
||||
from pythonbpf.vmlinux_parser.dependency_node import Field
|
||||
from .expr import VmlinuxHandlerRegistry
|
||||
from pythonbpf.type_deducer import ctypes_to_ir
|
||||
from pythonbpf.maps import BPFMapType
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -25,7 +26,9 @@ def create_targets_and_rvals(stmt):
|
||||
return stmt.targets, [stmt.value]
|
||||
|
||||
|
||||
def handle_assign_allocation(builder, stmt, local_sym_tab, structs_sym_tab):
|
||||
def handle_assign_allocation(
|
||||
builder, stmt, local_sym_tab, map_sym_tab, structs_sym_tab
|
||||
):
|
||||
"""Handle memory allocation for assignment statements."""
|
||||
|
||||
logger.info(f"Handling assignment for allocation: {ast.dump(stmt)}")
|
||||
@ -55,7 +58,9 @@ def handle_assign_allocation(builder, stmt, local_sym_tab, structs_sym_tab):
|
||||
|
||||
# Determine type and allocate based on rval
|
||||
if isinstance(rval, ast.Call):
|
||||
_allocate_for_call(builder, var_name, rval, local_sym_tab, structs_sym_tab)
|
||||
_allocate_for_call(
|
||||
builder, var_name, rval, local_sym_tab, map_sym_tab, structs_sym_tab
|
||||
)
|
||||
elif isinstance(rval, ast.Constant):
|
||||
_allocate_for_constant(builder, var_name, rval, local_sym_tab)
|
||||
elif isinstance(rval, ast.BinOp):
|
||||
@ -74,7 +79,9 @@ def handle_assign_allocation(builder, stmt, local_sym_tab, structs_sym_tab):
|
||||
)
|
||||
|
||||
|
||||
def _allocate_for_call(builder, var_name, rval, local_sym_tab, structs_sym_tab):
|
||||
def _allocate_for_call(
|
||||
builder, var_name, rval, local_sym_tab, map_sym_tab, structs_sym_tab
|
||||
):
|
||||
"""Allocate memory for variable assigned from a call."""
|
||||
|
||||
if isinstance(rval.func, ast.Name):
|
||||
@ -116,15 +123,74 @@ def _allocate_for_call(builder, var_name, rval, local_sym_tab, structs_sym_tab):
|
||||
|
||||
elif isinstance(rval.func, ast.Attribute):
|
||||
# Map method calls - need double allocation for ptr handling
|
||||
_allocate_for_map_method(builder, var_name, local_sym_tab)
|
||||
_allocate_for_map_method(
|
||||
builder, var_name, rval, local_sym_tab, map_sym_tab, structs_sym_tab
|
||||
)
|
||||
|
||||
else:
|
||||
logger.warning(f"Unsupported call function type for {var_name}")
|
||||
|
||||
|
||||
def _allocate_for_map_method(builder, var_name, local_sym_tab):
|
||||
def _allocate_for_map_method(
|
||||
builder, var_name, rval, local_sym_tab, map_sym_tab, structs_sym_tab
|
||||
):
|
||||
"""Allocate memory for variable assigned from map method (double alloc)."""
|
||||
|
||||
map_name = rval.func.value.id
|
||||
method_name = rval.func.attr
|
||||
|
||||
# NOTE: We will have to special case HashMap.lookup which returns a pointer to value type
|
||||
# The value type can be a struct as well, so we need to handle that properly
|
||||
# This special casing is not ideal, as over time other map methods may need similar handling
|
||||
# But for now, we will just handle lookup specifically
|
||||
if map_name not in map_sym_tab:
|
||||
logger.error(f"Map '{map_name}' not found for allocation")
|
||||
return
|
||||
|
||||
if method_name != "lookup":
|
||||
# Fallback allocation for other map methods
|
||||
_allocate_for_map_method_fallback(builder, var_name, local_sym_tab)
|
||||
return
|
||||
|
||||
map_params = map_sym_tab[map_name].params
|
||||
if map_params["type"] != BPFMapType.HASH:
|
||||
logger.warning(
|
||||
"Map method lookup used on non-hash map, using fallback allocation"
|
||||
)
|
||||
_allocate_for_map_method_fallback(builder, var_name, local_sym_tab)
|
||||
return
|
||||
|
||||
value_type = map_params["value"]
|
||||
# Determine IR type for value
|
||||
if isinstance(value_type, str) and value_type in structs_sym_tab:
|
||||
struct_info = structs_sym_tab[value_type]
|
||||
value_ir_type = struct_info.ir_type
|
||||
else:
|
||||
value_ir_type = ctypes_to_ir(value_type)
|
||||
|
||||
if value_ir_type is None:
|
||||
logger.warning(
|
||||
f"Could not determine IR type for map value '{value_type}', using fallback allocation"
|
||||
)
|
||||
_allocate_for_map_method_fallback(builder, var_name, local_sym_tab)
|
||||
return
|
||||
|
||||
# Main variable (pointer to pointer)
|
||||
ir_type = ir.PointerType(ir.IntType(64))
|
||||
var = builder.alloca(ir_type, name=var_name)
|
||||
local_sym_tab[var_name] = LocalSymbol(var, ir_type)
|
||||
# Temporary variable for computed values
|
||||
tmp_ir_type = value_ir_type
|
||||
var_tmp = builder.alloca(tmp_ir_type, name=f"{var_name}_tmp")
|
||||
local_sym_tab[f"{var_name}_tmp"] = LocalSymbol(var_tmp, tmp_ir_type)
|
||||
logger.info(
|
||||
f"Pre-allocated {var_name} and {var_name}_tmp for map method lookup of type {value_ir_type}"
|
||||
)
|
||||
|
||||
|
||||
def _allocate_for_map_method_fallback(builder, var_name, local_sym_tab):
|
||||
"""Fallback allocation for map method variable (i64* and i64**)."""
|
||||
|
||||
# Main variable (pointer to pointer)
|
||||
ir_type = ir.PointerType(ir.IntType(64))
|
||||
var = builder.alloca(ir_type, name=var_name)
|
||||
@ -135,7 +201,9 @@ def _allocate_for_map_method(builder, var_name, local_sym_tab):
|
||||
var_tmp = builder.alloca(tmp_ir_type, name=f"{var_name}_tmp")
|
||||
local_sym_tab[f"{var_name}_tmp"] = LocalSymbol(var_tmp, tmp_ir_type)
|
||||
|
||||
logger.info(f"Pre-allocated {var_name} and {var_name}_tmp for map method")
|
||||
logger.info(
|
||||
f"Pre-allocated {var_name} and {var_name}_tmp for map method (fallback)"
|
||||
)
|
||||
|
||||
|
||||
def _allocate_for_constant(builder, var_name, rval, local_sym_tab):
|
||||
|
||||
@ -49,6 +49,10 @@ class DebugInfoGenerator:
|
||||
)
|
||||
return self._type_cache[key]
|
||||
|
||||
def get_uint8_type(self) -> Any:
|
||||
"""Get debug info for signed 8-bit integer"""
|
||||
return self.get_basic_type("char", 8, dc.DW_ATE_unsigned)
|
||||
|
||||
def get_int32_type(self) -> Any:
|
||||
"""Get debug info for signed 32-bit integer"""
|
||||
return self.get_basic_type("int", 32, dc.DW_ATE_signed)
|
||||
|
||||
@ -147,7 +147,9 @@ def allocate_mem(
|
||||
structs_sym_tab,
|
||||
)
|
||||
elif isinstance(stmt, ast.Assign):
|
||||
handle_assign_allocation(builder, stmt, local_sym_tab, structs_sym_tab)
|
||||
handle_assign_allocation(
|
||||
builder, stmt, local_sym_tab, map_sym_tab, structs_sym_tab
|
||||
)
|
||||
|
||||
allocate_temp_pool(builder, max_temps_needed, local_sym_tab)
|
||||
|
||||
|
||||
@ -1,22 +1,31 @@
|
||||
import logging
|
||||
from llvmlite import ir
|
||||
from pythonbpf.debuginfo import DebugInfoGenerator
|
||||
from .map_types import BPFMapType
|
||||
|
||||
logger: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create_map_debug_info(module, map_global, map_name, map_params, structs_sym_tab):
|
||||
"""Generate debug info metadata for BPF maps HASH and PERF_EVENT_ARRAY"""
|
||||
generator = DebugInfoGenerator(module)
|
||||
|
||||
logger.info(f"Creating debug info for map {map_name} with params {map_params}")
|
||||
uint_type = generator.get_uint32_type()
|
||||
ulong_type = generator.get_uint64_type()
|
||||
array_type = generator.create_array_type(
|
||||
uint_type, map_params.get("type", BPFMapType.UNSPEC).value
|
||||
)
|
||||
type_ptr = generator.create_pointer_type(array_type, 64)
|
||||
key_ptr = generator.create_pointer_type(
|
||||
array_type if "key_size" in map_params else ulong_type, 64
|
||||
array_type
|
||||
if "key_size" in map_params
|
||||
else _get_key_val_dbg_type(map_params.get("key"), generator, structs_sym_tab),
|
||||
64,
|
||||
)
|
||||
value_ptr = generator.create_pointer_type(
|
||||
array_type if "value_size" in map_params else ulong_type, 64
|
||||
array_type
|
||||
if "value_size" in map_params
|
||||
else _get_key_val_dbg_type(map_params.get("value"), generator, structs_sym_tab),
|
||||
64,
|
||||
)
|
||||
|
||||
elements_arr = []
|
||||
@ -97,3 +106,65 @@ def create_ringbuf_debug_info(
|
||||
)
|
||||
map_global.set_metadata("dbg", global_var)
|
||||
return global_var
|
||||
|
||||
|
||||
def _get_key_val_dbg_type(name, generator, structs_sym_tab):
|
||||
"""Get the debug type for key/value based on type object"""
|
||||
|
||||
if not name:
|
||||
logger.warn("No name provided for key/value type, defaulting to uint64")
|
||||
return generator.get_uint64_type()
|
||||
|
||||
type_obj = structs_sym_tab.get(name)
|
||||
if type_obj:
|
||||
return _get_struct_debug_type(type_obj, generator, structs_sym_tab)
|
||||
|
||||
# Fallback to basic types
|
||||
logger.info(f"No struct named {name}, falling back to basic type")
|
||||
|
||||
# NOTE: Only handling int and long for now
|
||||
if name in ["c_int32", "c_uint32"]:
|
||||
return generator.get_uint32_type()
|
||||
|
||||
# Default fallback for now
|
||||
return generator.get_uint64_type()
|
||||
|
||||
|
||||
def _get_struct_debug_type(struct_obj, generator, structs_sym_tab):
|
||||
"""Recursively create debug type for struct"""
|
||||
elements_arr = []
|
||||
for fld in struct_obj.fields.keys():
|
||||
fld_type = struct_obj.field_type(fld)
|
||||
if isinstance(fld_type, ir.IntType):
|
||||
if fld_type.width == 32:
|
||||
fld_dbg_type = generator.get_uint32_type()
|
||||
else:
|
||||
# NOTE: Assuming 64-bit for all other int types
|
||||
fld_dbg_type = generator.get_uint64_type()
|
||||
elif isinstance(fld_type, ir.ArrayType):
|
||||
# NOTE: Array types have u8 elements only for now
|
||||
# Debug info generation should fail for other types
|
||||
elem_type = fld_type.element
|
||||
if isinstance(elem_type, ir.IntType) and elem_type.width == 8:
|
||||
char_type = generator.get_uint8_type()
|
||||
fld_dbg_type = generator.create_array_type(char_type, fld_type.count)
|
||||
else:
|
||||
logger.warning(
|
||||
f"Array element type {str(elem_type)} not supported for debug info, skipping"
|
||||
)
|
||||
continue
|
||||
else:
|
||||
# NOTE: Only handling int and char arrays for now
|
||||
logger.warning(
|
||||
f"Field type {str(fld_type)} not supported for debug info, skipping"
|
||||
)
|
||||
continue
|
||||
|
||||
member = generator.create_struct_member(
|
||||
fld, fld_dbg_type, struct_obj.field_size(fld)
|
||||
)
|
||||
elements_arr.append(member)
|
||||
struct_type = generator.create_struct_type(
|
||||
elements_arr, struct_obj.size, is_distinct=True
|
||||
)
|
||||
return struct_type
|
||||
|
||||
@ -48,7 +48,7 @@ def create_bpf_map(module, map_name, map_params):
|
||||
map_global.align = 8
|
||||
|
||||
logger.info(f"Created BPF map: {map_name} with params {map_params}")
|
||||
return MapSymbol(type=map_params["type"], sym=map_global)
|
||||
return MapSymbol(type=map_params["type"], sym=map_global, params=map_params)
|
||||
|
||||
|
||||
def _parse_map_params(rval, expected_args=None):
|
||||
@ -105,7 +105,9 @@ def process_ringbuf_map(map_name, rval, module, structs_sym_tab):
|
||||
logger.info(f"Ringbuf map parameters: {map_params}")
|
||||
|
||||
map_global = create_bpf_map(module, map_name, map_params)
|
||||
create_ringbuf_debug_info(module, map_global.sym, map_name, map_params)
|
||||
create_ringbuf_debug_info(
|
||||
module, map_global.sym, map_name, map_params, structs_sym_tab
|
||||
)
|
||||
return map_global
|
||||
|
||||
|
||||
@ -119,7 +121,7 @@ def process_hash_map(map_name, rval, module, structs_sym_tab):
|
||||
logger.info(f"Map parameters: {map_params}")
|
||||
map_global = create_bpf_map(module, map_name, map_params)
|
||||
# Generate debug info for BTF
|
||||
create_map_debug_info(module, map_global.sym, map_name, map_params)
|
||||
create_map_debug_info(module, map_global.sym, map_name, map_params, structs_sym_tab)
|
||||
return map_global
|
||||
|
||||
|
||||
|
||||
@ -11,6 +11,7 @@ class MapSymbol:
|
||||
|
||||
type: BPFMapType
|
||||
sym: ir.GlobalVariable
|
||||
params: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class MapProcessorRegistry:
|
||||
|
||||
@ -16,6 +16,33 @@ def get_module_symbols(module_name: str):
|
||||
return [name for name in dir(imported_module)], imported_module
|
||||
|
||||
|
||||
def unwrap_pointer_type(type_obj: Any) -> Any:
|
||||
"""
|
||||
Recursively unwrap all pointer layers to get the base type.
|
||||
|
||||
This handles multiply nested pointers like LP_LP_struct_attribute_group
|
||||
and returns the base type (struct_attribute_group).
|
||||
|
||||
Stops unwrapping when reaching a non-pointer type (one without _type_ attribute).
|
||||
|
||||
Args:
|
||||
type_obj: The type object to unwrap
|
||||
|
||||
Returns:
|
||||
The base type after unwrapping all pointer layers
|
||||
"""
|
||||
current_type = type_obj
|
||||
# Keep unwrapping while it's a pointer/array type (has _type_)
|
||||
# But stop if _type_ is just a string or basic type marker
|
||||
while hasattr(current_type, "_type_"):
|
||||
next_type = current_type._type_
|
||||
# Stop if _type_ is a string (like 'c' for c_char)
|
||||
if isinstance(next_type, str):
|
||||
break
|
||||
current_type = next_type
|
||||
return current_type
|
||||
|
||||
|
||||
def process_vmlinux_class(
|
||||
node,
|
||||
llvm_module,
|
||||
@ -158,13 +185,90 @@ def process_vmlinux_post_ast(
|
||||
if hasattr(elem_type, "_length_") and is_complex_type:
|
||||
type_length = elem_type._length_
|
||||
|
||||
if containing_type.__module__ == "vmlinux":
|
||||
new_dep_node.add_dependent(
|
||||
elem_type._type_.__name__
|
||||
if hasattr(elem_type._type_, "__name__")
|
||||
else str(elem_type._type_)
|
||||
# Unwrap all pointer layers to get the base type for dependency tracking
|
||||
base_type = unwrap_pointer_type(elem_type)
|
||||
base_type_module = getattr(base_type, "__module__", None)
|
||||
|
||||
if base_type_module == "vmlinux":
|
||||
base_type_name = (
|
||||
base_type.__name__
|
||||
if hasattr(base_type, "__name__")
|
||||
else str(base_type)
|
||||
)
|
||||
# ONLY add vmlinux types as dependencies
|
||||
new_dep_node.add_dependent(base_type_name)
|
||||
|
||||
logger.debug(
|
||||
f"{containing_type} containing type of parent {elem_name} with {elem_type} and ctype {ctype_complex_type} and length {type_length}"
|
||||
)
|
||||
new_dep_node.set_field_containing_type(
|
||||
elem_name, containing_type
|
||||
)
|
||||
new_dep_node.set_field_type_size(elem_name, type_length)
|
||||
new_dep_node.set_field_ctype_complex_type(
|
||||
elem_name, ctype_complex_type
|
||||
)
|
||||
new_dep_node.set_field_type(elem_name, elem_type)
|
||||
|
||||
# Check the containing_type module to decide whether to recurse
|
||||
containing_type_module = getattr(
|
||||
containing_type, "__module__", None
|
||||
)
|
||||
if containing_type_module == "vmlinux":
|
||||
# Also unwrap containing_type to get base type name
|
||||
base_containing_type = unwrap_pointer_type(
|
||||
containing_type
|
||||
)
|
||||
containing_type_name = (
|
||||
base_containing_type.__name__
|
||||
if hasattr(base_containing_type, "__name__")
|
||||
else str(base_containing_type)
|
||||
)
|
||||
|
||||
# Check for self-reference or already processed
|
||||
if containing_type_name == current_symbol_name:
|
||||
# Self-referential pointer
|
||||
logger.debug(
|
||||
f"Self-referential pointer in {current_symbol_name}.{elem_name}"
|
||||
)
|
||||
new_dep_node.set_field_ready(elem_name, True)
|
||||
elif handler.has_node(containing_type_name):
|
||||
# Already processed
|
||||
logger.debug(
|
||||
f"Reusing already processed {containing_type_name}"
|
||||
)
|
||||
new_dep_node.set_field_ready(elem_name, True)
|
||||
else:
|
||||
# Process recursively - use base containing type, not the pointer wrapper
|
||||
new_dep_node.add_dependent(containing_type_name)
|
||||
process_vmlinux_post_ast(
|
||||
base_containing_type,
|
||||
llvm_handler,
|
||||
handler,
|
||||
processing_stack,
|
||||
)
|
||||
new_dep_node.set_field_ready(elem_name, True)
|
||||
elif (
|
||||
containing_type_module == ctypes.__name__
|
||||
or containing_type_module is None
|
||||
):
|
||||
logger.debug(
|
||||
f"Processing ctype internal{containing_type}"
|
||||
)
|
||||
new_dep_node.set_field_ready(elem_name, True)
|
||||
else:
|
||||
raise TypeError(
|
||||
f"Module not supported in recursive resolution: {containing_type_module}"
|
||||
)
|
||||
elif (
|
||||
base_type_module == ctypes.__name__
|
||||
or base_type_module is None
|
||||
):
|
||||
# Handle ctypes or types with no module (like some internal ctypes types)
|
||||
# DO NOT add ctypes as dependencies - just set field metadata and mark ready
|
||||
logger.debug(
|
||||
f"Base type {base_type} is ctypes - NOT adding as dependency, just processing field"
|
||||
)
|
||||
elif containing_type.__module__ == ctypes.__name__:
|
||||
if isinstance(elem_type, type):
|
||||
if issubclass(elem_type, ctypes.Array):
|
||||
ctype_complex_type = ctypes.Array
|
||||
@ -176,57 +280,20 @@ def process_vmlinux_post_ast(
|
||||
)
|
||||
else:
|
||||
raise TypeError("Unsupported ctypes subclass")
|
||||
else:
|
||||
raise ImportError(
|
||||
f"Unsupported module of {containing_type}"
|
||||
)
|
||||
logger.debug(
|
||||
f"{containing_type} containing type of parent {elem_name} with {elem_type} and ctype {ctype_complex_type} and length {type_length}"
|
||||
)
|
||||
new_dep_node.set_field_containing_type(
|
||||
elem_name, containing_type
|
||||
)
|
||||
new_dep_node.set_field_type_size(elem_name, type_length)
|
||||
new_dep_node.set_field_ctype_complex_type(
|
||||
elem_name, ctype_complex_type
|
||||
)
|
||||
new_dep_node.set_field_type(elem_name, elem_type)
|
||||
if containing_type.__module__ == "vmlinux":
|
||||
containing_type_name = (
|
||||
containing_type.__name__
|
||||
if hasattr(containing_type, "__name__")
|
||||
else str(containing_type)
|
||||
)
|
||||
|
||||
# Check for self-reference or already processed
|
||||
if containing_type_name == current_symbol_name:
|
||||
# Self-referential pointer
|
||||
logger.debug(
|
||||
f"Self-referential pointer in {current_symbol_name}.{elem_name}"
|
||||
)
|
||||
new_dep_node.set_field_ready(elem_name, True)
|
||||
elif handler.has_node(containing_type_name):
|
||||
# Already processed
|
||||
logger.debug(
|
||||
f"Reusing already processed {containing_type_name}"
|
||||
)
|
||||
new_dep_node.set_field_ready(elem_name, True)
|
||||
else:
|
||||
# Process recursively - THIS WAS MISSING
|
||||
new_dep_node.add_dependent(containing_type_name)
|
||||
process_vmlinux_post_ast(
|
||||
containing_type,
|
||||
llvm_handler,
|
||||
handler,
|
||||
processing_stack,
|
||||
)
|
||||
new_dep_node.set_field_ready(elem_name, True)
|
||||
elif containing_type.__module__ == ctypes.__name__:
|
||||
logger.debug(f"Processing ctype internal{containing_type}")
|
||||
# Set field metadata but DO NOT add dependency or recurse
|
||||
new_dep_node.set_field_containing_type(
|
||||
elem_name, containing_type
|
||||
)
|
||||
new_dep_node.set_field_type_size(elem_name, type_length)
|
||||
new_dep_node.set_field_ctype_complex_type(
|
||||
elem_name, ctype_complex_type
|
||||
)
|
||||
new_dep_node.set_field_type(elem_name, elem_type)
|
||||
new_dep_node.set_field_ready(elem_name, True)
|
||||
else:
|
||||
raise TypeError(
|
||||
"Module not supported in recursive resolution"
|
||||
raise ImportError(
|
||||
f"Unsupported module of {base_type}: {base_type_module}"
|
||||
)
|
||||
else:
|
||||
new_dep_node.add_dependent(
|
||||
@ -245,9 +312,12 @@ def process_vmlinux_post_ast(
|
||||
raise ValueError(
|
||||
f"{elem_name} with type {elem_type} from module {module_name} not supported in recursive resolver"
|
||||
)
|
||||
|
||||
elif module_name == ctypes.__name__ or module_name is None:
|
||||
# Handle ctypes types - these don't need processing, just return
|
||||
logger.debug(f"Skipping ctypes type {current_symbol_name}")
|
||||
return True
|
||||
else:
|
||||
raise ImportError("UNSUPPORTED Module")
|
||||
raise ImportError(f"UNSUPPORTED Module {module_name}")
|
||||
|
||||
logger.info(
|
||||
f"{current_symbol_name} processed and handler readiness {handler.is_ready}"
|
||||
|
||||
@ -11,7 +11,9 @@ from .class_handler import process_vmlinux_class
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def detect_import_statement(tree: ast.AST) -> list[tuple[str, ast.ImportFrom]]:
|
||||
def detect_import_statement(
|
||||
tree: ast.AST,
|
||||
) -> list[tuple[str, ast.ImportFrom, str, str]]:
|
||||
"""
|
||||
Parse AST and detect import statements from vmlinux.
|
||||
|
||||
@ -25,7 +27,7 @@ def detect_import_statement(tree: ast.AST) -> list[tuple[str, ast.ImportFrom]]:
|
||||
List of tuples containing (module_name, imported_item) for each vmlinux import
|
||||
|
||||
Raises:
|
||||
SyntaxError: If multiple imports from vmlinux are attempted or import * is used
|
||||
SyntaxError: If import * is used
|
||||
"""
|
||||
vmlinux_imports = []
|
||||
|
||||
@ -40,28 +42,19 @@ def detect_import_statement(tree: ast.AST) -> list[tuple[str, ast.ImportFrom]]:
|
||||
"Please import specific types explicitly."
|
||||
)
|
||||
|
||||
# Check for multiple imports: from vmlinux import A, B, C
|
||||
if len(node.names) > 1:
|
||||
imported_names = [alias.name for alias in node.names]
|
||||
raise SyntaxError(
|
||||
f"Multiple imports from vmlinux are not supported. "
|
||||
f"Found: {', '.join(imported_names)}. "
|
||||
f"Please use separate import statements for each type."
|
||||
)
|
||||
|
||||
# Check if no specific import is specified (should not happen with valid Python)
|
||||
if len(node.names) == 0:
|
||||
raise SyntaxError(
|
||||
"Import from vmlinux must specify at least one type."
|
||||
)
|
||||
|
||||
# Valid single import
|
||||
# Support multiple imports: from vmlinux import A, B, C
|
||||
for alias in node.names:
|
||||
import_name = alias.name
|
||||
# Use alias if provided, otherwise use the original name (commented)
|
||||
# as_name = alias.asname if alias.asname else alias.name
|
||||
vmlinux_imports.append(("vmlinux", node))
|
||||
logger.info(f"Found vmlinux import: {import_name}")
|
||||
# Use alias if provided, otherwise use the original name
|
||||
as_name = alias.asname if alias.asname else alias.name
|
||||
vmlinux_imports.append(("vmlinux", node, import_name, as_name))
|
||||
logger.info(f"Found vmlinux import: {import_name} as {as_name}")
|
||||
|
||||
# Handle "import vmlinux" statements (not typical but should be rejected)
|
||||
elif isinstance(node, ast.Import):
|
||||
@ -103,40 +96,37 @@ def vmlinux_proc(tree: ast.AST, module):
|
||||
with open(source_file, "r") as f:
|
||||
mod_ast = ast.parse(f.read(), filename=source_file)
|
||||
|
||||
for import_mod, import_node in import_statements:
|
||||
for alias in import_node.names:
|
||||
imported_name = alias.name
|
||||
found = False
|
||||
for mod_node in mod_ast.body:
|
||||
if (
|
||||
isinstance(mod_node, ast.ClassDef)
|
||||
and mod_node.name == imported_name
|
||||
):
|
||||
process_vmlinux_class(mod_node, module, handler)
|
||||
found = True
|
||||
break
|
||||
if isinstance(mod_node, ast.Assign):
|
||||
for target in mod_node.targets:
|
||||
if isinstance(target, ast.Name) and target.id == imported_name:
|
||||
process_vmlinux_assign(mod_node, module, assignments)
|
||||
found = True
|
||||
break
|
||||
if found:
|
||||
break
|
||||
if not found:
|
||||
logger.info(
|
||||
f"{imported_name} not found as ClassDef or Assign in vmlinux"
|
||||
)
|
||||
for import_mod, import_node, imported_name, as_name in import_statements:
|
||||
found = False
|
||||
for mod_node in mod_ast.body:
|
||||
if isinstance(mod_node, ast.ClassDef) and mod_node.name == imported_name:
|
||||
process_vmlinux_class(mod_node, module, handler)
|
||||
found = True
|
||||
break
|
||||
if isinstance(mod_node, ast.Assign):
|
||||
for target in mod_node.targets:
|
||||
if isinstance(target, ast.Name) and target.id == imported_name:
|
||||
process_vmlinux_assign(mod_node, module, assignments, as_name)
|
||||
found = True
|
||||
break
|
||||
if found:
|
||||
break
|
||||
if not found:
|
||||
logger.info(f"{imported_name} not found as ClassDef or Assign in vmlinux")
|
||||
|
||||
IRGenerator(module, handler, assignments)
|
||||
return assignments
|
||||
|
||||
|
||||
def process_vmlinux_assign(node, module, assignments: dict[str, AssignmentInfo]):
|
||||
def process_vmlinux_assign(
|
||||
node, module, assignments: dict[str, AssignmentInfo], target_name=None
|
||||
):
|
||||
"""Process assignments from vmlinux module."""
|
||||
# Only handle single-target assignments
|
||||
if len(node.targets) == 1 and isinstance(node.targets[0], ast.Name):
|
||||
target_name = node.targets[0].id
|
||||
# Use provided target_name (for aliased imports) or fall back to original name
|
||||
if target_name is None:
|
||||
target_name = node.targets[0].id
|
||||
|
||||
# Handle constant value assignments
|
||||
if isinstance(node.value, ast.Constant):
|
||||
|
||||
@ -21,7 +21,7 @@ def debug_info_generation(
|
||||
generated_debug_info: List of tuples (struct, debug_info) to track generated debug info
|
||||
|
||||
Returns:
|
||||
The generated global variable debug info
|
||||
The generated global variable debug info, or None for unsupported types
|
||||
"""
|
||||
# Set up debug info generator
|
||||
generator = DebugInfoGenerator(llvm_module)
|
||||
@ -31,23 +31,39 @@ def debug_info_generation(
|
||||
if existing_struct.name == struct.name:
|
||||
return debug_info
|
||||
|
||||
# Check if this is a union (not supported yet)
|
||||
if not struct.name.startswith("struct_"):
|
||||
logger.warning(f"Skipping debug info generation for union: {struct.name}")
|
||||
# Create a minimal forward declaration for unions
|
||||
union_type = generator.create_struct_type(
|
||||
[], struct.__sizeof__() * 8, is_distinct=True
|
||||
)
|
||||
return union_type
|
||||
|
||||
# Process all fields and create members for the struct
|
||||
members = []
|
||||
for field_name, field in struct.fields.items():
|
||||
# Get appropriate debug type for this field
|
||||
field_type = _get_field_debug_type(
|
||||
field_name, field, generator, struct, generated_debug_info
|
||||
)
|
||||
# Create struct member with proper offset
|
||||
member = generator.create_struct_member_vmlinux(
|
||||
field_name, field_type, field.offset * 8
|
||||
)
|
||||
members.append(member)
|
||||
try:
|
||||
# Get appropriate debug type for this field
|
||||
field_type = _get_field_debug_type(
|
||||
field_name, field, generator, struct, generated_debug_info
|
||||
)
|
||||
|
||||
if struct.name.startswith("struct_"):
|
||||
struct_name = struct.name.removeprefix("struct_")
|
||||
else:
|
||||
raise ValueError("Unions are not supported in the current version")
|
||||
# Ensure field_type is a tuple
|
||||
if not isinstance(field_type, tuple) or len(field_type) != 2:
|
||||
logger.error(f"Invalid field_type for {field_name}: {field_type}")
|
||||
continue
|
||||
|
||||
# Create struct member with proper offset
|
||||
member = generator.create_struct_member_vmlinux(
|
||||
field_name, field_type, field.offset * 8
|
||||
)
|
||||
members.append(member)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to process field {field_name} in {struct.name}: {e}")
|
||||
continue
|
||||
|
||||
struct_name = struct.name.removeprefix("struct_")
|
||||
# Create struct type with all members
|
||||
struct_type = generator.create_struct_type_with_name(
|
||||
struct_name, members, struct.__sizeof__() * 8, is_distinct=True
|
||||
@ -74,11 +90,17 @@ def _get_field_debug_type(
|
||||
generated_debug_info: List of already generated debug info
|
||||
|
||||
Returns:
|
||||
The debug info type for this field
|
||||
A tuple of (debug_type, size_in_bits)
|
||||
"""
|
||||
# Handle complex types (arrays, pointers)
|
||||
# Handle complex types (arrays, pointers, function pointers)
|
||||
if field.ctype_complex_type is not None:
|
||||
if issubclass(field.ctype_complex_type, ctypes.Array):
|
||||
# Handle function pointer types (CFUNCTYPE)
|
||||
if callable(field.ctype_complex_type):
|
||||
# Function pointers are represented as void pointers
|
||||
logger.info(f"Field {field_name} is a function pointer, using void pointer")
|
||||
void_ptr = generator.create_pointer_type(None, 64)
|
||||
return void_ptr, 64
|
||||
elif issubclass(field.ctype_complex_type, ctypes.Array):
|
||||
# Handle array types
|
||||
element_type, base_type_size = _get_basic_debug_type(
|
||||
field.containing_type, generator
|
||||
@ -100,11 +122,13 @@ def _get_field_debug_type(
|
||||
for existing_struct, debug_info in generated_debug_info:
|
||||
if existing_struct.name == struct_name:
|
||||
# Use existing debug info
|
||||
return debug_info, existing_struct.__sizeof__()
|
||||
return debug_info, existing_struct.__sizeof__() * 8
|
||||
|
||||
# If not found, create a forward declaration
|
||||
# This will be completed when the actual struct is processed
|
||||
logger.warning("Forward declaration in struct created")
|
||||
logger.warning(
|
||||
f"Forward declaration created for {struct_name} in {parent_struct.name}"
|
||||
)
|
||||
forward_type = generator.create_struct_type([], 0, is_distinct=True)
|
||||
return forward_type, 0
|
||||
|
||||
|
||||
@ -11,6 +11,10 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class IRGenerator:
|
||||
# This field keeps track of the non_struct names to avoid duplicate name errors.
|
||||
type_number = 0
|
||||
unprocessed_store: list[str] = []
|
||||
|
||||
# get the assignments dict and add this stuff to it.
|
||||
def __init__(self, llvm_module, handler: DependencyHandler, assignments):
|
||||
self.llvm_module = llvm_module
|
||||
@ -129,7 +133,19 @@ class IRGenerator:
|
||||
|
||||
for field_name, field in struct.fields.items():
|
||||
# does not take arrays and similar types into consideration yet.
|
||||
if field.ctype_complex_type is not None and issubclass(
|
||||
if callable(field.ctype_complex_type):
|
||||
# Function pointer case - generate a simple field accessor
|
||||
field_co_re_name, returned = self._struct_name_generator(
|
||||
struct, field, field_index
|
||||
)
|
||||
field_index += 1
|
||||
globvar = ir.GlobalVariable(
|
||||
self.llvm_module, ir.IntType(64), name=field_co_re_name
|
||||
)
|
||||
globvar.linkage = "external"
|
||||
globvar.set_metadata("llvm.preserve.access.index", debug_info)
|
||||
self.generated_field_names[struct.name][field_name] = globvar
|
||||
elif field.ctype_complex_type is not None and issubclass(
|
||||
field.ctype_complex_type, ctypes.Array
|
||||
):
|
||||
array_size = field.type_size
|
||||
@ -137,7 +153,7 @@ class IRGenerator:
|
||||
if containing_type.__module__ == ctypes.__name__:
|
||||
containing_type_size = ctypes.sizeof(containing_type)
|
||||
if array_size == 0:
|
||||
field_co_re_name = self._struct_name_generator(
|
||||
field_co_re_name, returned = self._struct_name_generator(
|
||||
struct, field, field_index, True, 0, containing_type_size
|
||||
)
|
||||
globvar = ir.GlobalVariable(
|
||||
@ -149,7 +165,7 @@ class IRGenerator:
|
||||
field_index += 1
|
||||
continue
|
||||
for i in range(0, array_size):
|
||||
field_co_re_name = self._struct_name_generator(
|
||||
field_co_re_name, returned = self._struct_name_generator(
|
||||
struct, field, field_index, True, i, containing_type_size
|
||||
)
|
||||
globvar = ir.GlobalVariable(
|
||||
@ -163,12 +179,28 @@ class IRGenerator:
|
||||
array_size = field.type_size
|
||||
containing_type = field.containing_type
|
||||
if containing_type.__module__ == "vmlinux":
|
||||
containing_type_size = self.handler[
|
||||
containing_type.__name__
|
||||
].current_offset
|
||||
for i in range(0, array_size):
|
||||
field_co_re_name = self._struct_name_generator(
|
||||
struct, field, field_index, True, i, containing_type_size
|
||||
# Unwrap all pointer layers to get the base struct type
|
||||
base_containing_type = containing_type
|
||||
while hasattr(base_containing_type, "_type_"):
|
||||
next_type = base_containing_type._type_
|
||||
# Stop if _type_ is a string (like 'c' for c_char)
|
||||
# TODO: stacked pointers not handl;ing ctypes check here as well
|
||||
if isinstance(next_type, str):
|
||||
break
|
||||
base_containing_type = next_type
|
||||
|
||||
# Get the base struct name
|
||||
base_struct_name = (
|
||||
base_containing_type.__name__
|
||||
if hasattr(base_containing_type, "__name__")
|
||||
else str(base_containing_type)
|
||||
)
|
||||
|
||||
# Look up the size using the base struct name
|
||||
containing_type_size = self.handler[base_struct_name].current_offset
|
||||
if array_size == 0:
|
||||
field_co_re_name, returned = self._struct_name_generator(
|
||||
struct, field, field_index, True, 0, containing_type_size
|
||||
)
|
||||
globvar = ir.GlobalVariable(
|
||||
self.llvm_module, ir.IntType(64), name=field_co_re_name
|
||||
@ -176,9 +208,30 @@ class IRGenerator:
|
||||
globvar.linkage = "external"
|
||||
globvar.set_metadata("llvm.preserve.access.index", debug_info)
|
||||
self.generated_field_names[struct.name][field_name] = globvar
|
||||
field_index += 1
|
||||
field_index += 1
|
||||
else:
|
||||
for i in range(0, array_size):
|
||||
field_co_re_name, returned = self._struct_name_generator(
|
||||
struct,
|
||||
field,
|
||||
field_index,
|
||||
True,
|
||||
i,
|
||||
containing_type_size,
|
||||
)
|
||||
globvar = ir.GlobalVariable(
|
||||
self.llvm_module, ir.IntType(64), name=field_co_re_name
|
||||
)
|
||||
globvar.linkage = "external"
|
||||
globvar.set_metadata(
|
||||
"llvm.preserve.access.index", debug_info
|
||||
)
|
||||
self.generated_field_names[struct.name][field_name] = (
|
||||
globvar
|
||||
)
|
||||
field_index += 1
|
||||
else:
|
||||
field_co_re_name = self._struct_name_generator(
|
||||
field_co_re_name, returned = self._struct_name_generator(
|
||||
struct, field, field_index
|
||||
)
|
||||
field_index += 1
|
||||
@ -198,7 +251,7 @@ class IRGenerator:
|
||||
is_indexed: bool = False,
|
||||
index: int = 0,
|
||||
containing_type_size: int = 0,
|
||||
) -> str:
|
||||
) -> tuple[str, bool]:
|
||||
# TODO: Does not support Unions as well as recursive pointer and array type naming
|
||||
if is_indexed:
|
||||
name = (
|
||||
@ -208,7 +261,7 @@ class IRGenerator:
|
||||
+ "$"
|
||||
+ f"0:{field_index}:{index}"
|
||||
)
|
||||
return name
|
||||
return name, True
|
||||
elif struct.name.startswith("struct_"):
|
||||
name = (
|
||||
"llvm."
|
||||
@ -217,9 +270,18 @@ class IRGenerator:
|
||||
+ "$"
|
||||
+ f"0:{field_index}"
|
||||
)
|
||||
return name
|
||||
return name, True
|
||||
else:
|
||||
print(self.handler[struct.name])
|
||||
raise TypeError(
|
||||
"Name generation cannot occur due to type name not starting with struct"
|
||||
logger.warning(
|
||||
"Blindly handling non-struct type to avoid type errors in vmlinux IR generation. Possibly a union."
|
||||
)
|
||||
self.type_number += 1
|
||||
unprocessed_type = "unprocessed_type_" + str(self.handler[struct.name].name)
|
||||
if self.unprocessed_store.__contains__(unprocessed_type):
|
||||
return unprocessed_type + "_" + str(self.type_number), False
|
||||
else:
|
||||
self.unprocessed_store.append(unprocessed_type)
|
||||
return unprocessed_type, False
|
||||
# raise TypeError(
|
||||
# "Name generation cannot occur due to type name not starting with struct"
|
||||
# )
|
||||
|
||||
@ -3,21 +3,20 @@ CFLAGS := -emit-llvm -target bpf -c
|
||||
|
||||
SRC := $(wildcard *.bpf.c)
|
||||
LL := $(SRC:.bpf.c=.bpf.ll)
|
||||
LL2 := $(SRC:.bpf.c=.bpf.o2.ll)
|
||||
OBJ := $(SRC:.bpf.c=.bpf.o)
|
||||
|
||||
LL0 := $(SRC:.bpf.c=.bpf.o0.ll)
|
||||
.PHONY: all clean
|
||||
|
||||
all: $(LL) $(OBJ) $(LL2)
|
||||
all: $(LL) $(OBJ) $(LL0)
|
||||
|
||||
%.bpf.o: %.bpf.c
|
||||
$(BPF_CLANG) -O2 -g -target bpf -c $< -o $@
|
||||
|
||||
%.bpf.ll: %.bpf.c
|
||||
$(BPF_CLANG) -O0 $(CFLAGS) -g -S $< -o $@
|
||||
$(BPF_CLANG) $(CFLAGS) -O2 -g -S $< -o $@
|
||||
|
||||
%.bpf.o2.ll: %.bpf.c
|
||||
$(BPF_CLANG) -O2 $(CFLAGS) -g -S $< -o $@
|
||||
%.bpf.o0.ll: %.bpf.c
|
||||
$(BPF_CLANG) $(CFLAGS) -O0 -g -S $< -o $@
|
||||
|
||||
clean:
|
||||
rm -f $(LL) $(OBJ) $(LL2)
|
||||
rm -f $(LL) $(OBJ) $(LL0)
|
||||
|
||||
15
tests/c-form/requests.bpf.c
Normal file
15
tests/c-form/requests.bpf.c
Normal file
@ -0,0 +1,15 @@
|
||||
#include "vmlinux.h"
|
||||
#include <bpf/bpf_helpers.h>
|
||||
#include <bpf/bpf_tracing.h>
|
||||
|
||||
char LICENSE[] SEC("license") = "GPL";
|
||||
|
||||
SEC("kprobe/blk_mq_start_request")
|
||||
int example(struct pt_regs *ctx)
|
||||
{
|
||||
struct request *req = (struct request *)(ctx->di);
|
||||
u32 data_len = req->__data_len;
|
||||
bpf_printk("data length %u\n", data_len);
|
||||
|
||||
return 0;
|
||||
}
|
||||
22
tests/failing_tests/vmlinux/assignment_handling.py
Normal file
22
tests/failing_tests/vmlinux/assignment_handling.py
Normal file
@ -0,0 +1,22 @@
|
||||
from vmlinux import XDP_PASS
|
||||
from pythonbpf import bpf, section, bpfglobal, compile_to_ir
|
||||
import logging
|
||||
from ctypes import c_int64, c_void_p
|
||||
|
||||
|
||||
@bpf
|
||||
@section("kprobe/blk_mq_start_request")
|
||||
def example(ctx: c_void_p) -> c_int64:
|
||||
d = XDP_PASS # This gives an error, but
|
||||
e = XDP_PASS + 0 # this does not
|
||||
print(f"test1 {e} test2 {d}")
|
||||
return c_int64(0)
|
||||
|
||||
|
||||
@bpf
|
||||
@bpfglobal
|
||||
def LICENSE() -> str:
|
||||
return "GPL"
|
||||
|
||||
|
||||
compile_to_ir("assignment_handling.py", "assignment_handling.ll", loglevel=logging.INFO)
|
||||
22
tests/failing_tests/vmlinux/requests.py
Normal file
22
tests/failing_tests/vmlinux/requests.py
Normal file
@ -0,0 +1,22 @@
|
||||
from vmlinux import struct_request, struct_pt_regs
|
||||
from pythonbpf import bpf, section, bpfglobal, compile_to_ir
|
||||
import logging
|
||||
from ctypes import c_int64
|
||||
|
||||
|
||||
@bpf
|
||||
@section("kprobe/blk_mq_start_request")
|
||||
def example(ctx: struct_pt_regs) -> c_int64:
|
||||
req = struct_request(ctx.di)
|
||||
c = req.__data_len
|
||||
print(f"data length {c}")
|
||||
return c_int64(0)
|
||||
|
||||
|
||||
@bpf
|
||||
@bpfglobal
|
||||
def LICENSE() -> str:
|
||||
return "GPL"
|
||||
|
||||
|
||||
compile_to_ir("requests.py", "requests.ll", loglevel=logging.INFO)
|
||||
42
tests/passing_tests/hash_map_struct.py
Normal file
42
tests/passing_tests/hash_map_struct.py
Normal file
@ -0,0 +1,42 @@
|
||||
from pythonbpf import bpf, section, struct, bpfglobal, compile, map
|
||||
from pythonbpf.maps import HashMap
|
||||
from pythonbpf.helper import pid
|
||||
from ctypes import c_void_p, c_int64
|
||||
|
||||
|
||||
@bpf
|
||||
@struct
|
||||
class val_type:
|
||||
counter: c_int64
|
||||
shizzle: c_int64
|
||||
|
||||
|
||||
@bpf
|
||||
@map
|
||||
def last() -> HashMap:
|
||||
return HashMap(key=val_type, value=c_int64, max_entries=16)
|
||||
|
||||
|
||||
@bpf
|
||||
@section("tracepoint/syscalls/sys_enter_clone")
|
||||
def hello_world(ctx: c_void_p) -> c_int64:
|
||||
obj = val_type()
|
||||
obj.counter, obj.shizzle = 42, 96
|
||||
t = last.lookup(obj)
|
||||
if t:
|
||||
print(f"Found existing entry: counter={obj.counter}, pid={t}")
|
||||
last.delete(obj)
|
||||
return 0 # type: ignore [return-value]
|
||||
val = pid()
|
||||
last.update(obj, val)
|
||||
print(f"Map updated!, {obj.counter}, {obj.shizzle}, {val}")
|
||||
return 0 # type: ignore [return-value]
|
||||
|
||||
|
||||
@bpf
|
||||
@bpfglobal
|
||||
def LICENSE() -> str:
|
||||
return "GPL"
|
||||
|
||||
|
||||
compile()
|
||||
21
tests/passing_tests/vmlinux/requests2.py
Normal file
21
tests/passing_tests/vmlinux/requests2.py
Normal file
@ -0,0 +1,21 @@
|
||||
from vmlinux import struct_pt_regs
|
||||
from pythonbpf import bpf, section, bpfglobal, compile_to_ir
|
||||
import logging
|
||||
from ctypes import c_int64
|
||||
|
||||
|
||||
@bpf
|
||||
@section("kprobe/blk_mq_start_request")
|
||||
def example(ctx: struct_pt_regs) -> c_int64:
|
||||
req = ctx.di
|
||||
print(f"data length {req}")
|
||||
return c_int64(0)
|
||||
|
||||
|
||||
@bpf
|
||||
@bpfglobal
|
||||
def LICENSE() -> str:
|
||||
return "GPL"
|
||||
|
||||
|
||||
compile_to_ir("requests2.py", "requests2.ll", loglevel=logging.INFO)
|
||||
Reference in New Issue
Block a user