Skip to content

Commit faad355

Browse files
Merge pull request #67 from pythonbpf/32int_support
add i32 support and special support for xdp_md with zext
2 parents a9d82d4 + 5ad33b0 commit faad355

File tree

13 files changed

+316
-34
lines changed

13 files changed

+316
-34
lines changed

pythonbpf/allocation_pass.py

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import ast
22
import logging
3-
3+
import ctypes
44
from llvmlite import ir
55
from .local_symbol import LocalSymbol
66
from pythonbpf.helper import HelperHandlerRegistry
@@ -81,7 +81,7 @@ def _allocate_for_call(builder, var_name, rval, local_sym_tab, structs_sym_tab):
8181
call_type = rval.func.id
8282

8383
# C type constructors
84-
if call_type in ("c_int32", "c_int64", "c_uint32", "c_uint64"):
84+
if call_type in ("c_int32", "c_int64", "c_uint32", "c_uint64", "c_void_p"):
8585
ir_type = ctypes_to_ir(call_type)
8686
var = builder.alloca(ir_type, name=var_name)
8787
var.align = ir_type.width // 8
@@ -249,7 +249,58 @@ def _allocate_for_attribute(builder, var_name, rval, local_sym_tab, structs_sym_
249249
].var = base_ptr # This is repurposing of var to store the pointer of the base type
250250
local_sym_tab[struct_var].ir_type = field_ir
251251

252-
actual_ir_type = ir.IntType(64)
252+
# Determine the actual IR type based on the field's type
253+
actual_ir_type = None
254+
255+
# Check if it's a ctypes primitive
256+
if field.type.__module__ == ctypes.__name__:
257+
try:
258+
field_size_bytes = ctypes.sizeof(field.type)
259+
field_size_bits = field_size_bytes * 8
260+
261+
if field_size_bits in [8, 16, 32, 64]:
262+
# Special case: struct_xdp_md i32 fields should allocate as i64
263+
# because load_ctx_field will zero-extend them to i64
264+
if (
265+
vmlinux_struct_name == "struct_xdp_md"
266+
and field_size_bits == 32
267+
):
268+
actual_ir_type = ir.IntType(64)
269+
logger.info(
270+
f"Allocating {var_name} as i64 for i32 field from struct_xdp_md.{field_name} "
271+
"(will be zero-extended during load)"
272+
)
273+
else:
274+
actual_ir_type = ir.IntType(field_size_bits)
275+
else:
276+
logger.warning(
277+
f"Unusual field size {field_size_bits} bits for {field_name}"
278+
)
279+
actual_ir_type = ir.IntType(64)
280+
except Exception as e:
281+
logger.warning(
282+
f"Could not determine size for ctypes field {field_name}: {e}"
283+
)
284+
actual_ir_type = ir.IntType(64)
285+
286+
# Check if it's a nested vmlinux struct or complex type
287+
elif field.type.__module__ == "vmlinux":
288+
# For pointers to structs, use pointer type (64-bit)
289+
if field.ctype_complex_type is not None and issubclass(
290+
field.ctype_complex_type, ctypes._Pointer
291+
):
292+
actual_ir_type = ir.IntType(64) # Pointer is always 64-bit
293+
# For embedded structs, this is more complex - might need different handling
294+
else:
295+
logger.warning(
296+
f"Field {field_name} is a nested vmlinux struct, using i64 for now"
297+
)
298+
actual_ir_type = ir.IntType(64)
299+
else:
300+
logger.warning(
301+
f"Unknown field type module {field.type.__module__} for {field_name}"
302+
)
303+
actual_ir_type = ir.IntType(64)
253304

254305
# Allocate with the actual IR type, not the GlobalVariable
255306
var = _allocate_with_type(builder, var_name, actual_ir_type)

pythonbpf/assign_pass.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -152,15 +152,30 @@ def handle_variable_assignment(
152152
if val_type != var_type:
153153
if isinstance(val_type, Field):
154154
logger.info("Handling assignment to struct field")
155+
# Special handling for struct_xdp_md i32 fields that are zero-extended to i64
156+
# The load_ctx_field already extended them, so val is i64 but val_type.type shows c_uint
157+
if (
158+
hasattr(val_type, "type")
159+
and val_type.type.__name__ == "c_uint"
160+
and isinstance(var_type, ir.IntType)
161+
and var_type.width == 64
162+
):
163+
# This is the struct_xdp_md case - value is already i64
164+
builder.store(val, var_ptr)
165+
logger.info(
166+
f"Assigned zero-extended struct_xdp_md i32 field to {var_name} (i64)"
167+
)
168+
return True
155169
# TODO: handling only ctype struct fields for now. Handle other stuff too later.
156-
if var_type == ctypes_to_ir(val_type.type.__name__):
170+
elif var_type == ctypes_to_ir(val_type.type.__name__):
157171
builder.store(val, var_ptr)
158172
logger.info(f"Assigned ctype struct field to {var_name}")
159173
return True
160-
logger.error(
161-
f"Failed to assign ctype struct field to {var_name}: {val_type} != {var_type}"
162-
)
163-
return False
174+
else:
175+
logger.error(
176+
f"Failed to assign ctype struct field to {var_name}: {val_type} != {var_type}"
177+
)
178+
return False
164179
elif isinstance(val_type, ir.IntType) and isinstance(var_type, ir.IntType):
165180
# Allow implicit int widening
166181
if val_type.width < var_type.width:

pythonbpf/expr/expr_pass.py

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
get_base_type_and_depth,
1313
deref_to_depth,
1414
)
15+
from pythonbpf.vmlinux_parser.assignment_info import Field
1516
from .vmlinux_registry import VmlinuxHandlerRegistry
1617

1718
logger: Logger = logging.getLogger(__name__)
@@ -279,16 +280,45 @@ def _handle_ctypes_call(
279280
call_type = expr.func.id
280281
expected_type = ctypes_to_ir(call_type)
281282

282-
if val[1] != expected_type:
283+
# Extract the actual IR value and type
284+
# val could be (value, ir_type) or (value, Field)
285+
value, val_type = val
286+
287+
# If val_type is a Field object (from vmlinux struct), get the actual IR type of the value
288+
if isinstance(val_type, Field):
289+
# The value is already the correct IR value (potentially zero-extended)
290+
# Get the IR type from the value itself
291+
actual_ir_type = value.type
292+
logger.info(
293+
f"Converting vmlinux field {val_type.name} (IR type: {actual_ir_type}) to {call_type}"
294+
)
295+
else:
296+
actual_ir_type = val_type
297+
298+
if actual_ir_type != expected_type:
283299
# NOTE: We are only considering casting to and from int types for now
284-
if isinstance(val[1], ir.IntType) and isinstance(expected_type, ir.IntType):
285-
if val[1].width < expected_type.width:
286-
val = (builder.sext(val[0], expected_type), expected_type)
300+
if isinstance(actual_ir_type, ir.IntType) and isinstance(
301+
expected_type, ir.IntType
302+
):
303+
if actual_ir_type.width < expected_type.width:
304+
value = builder.sext(value, expected_type)
305+
logger.info(
306+
f"Sign-extended from i{actual_ir_type.width} to i{expected_type.width}"
307+
)
308+
elif actual_ir_type.width > expected_type.width:
309+
value = builder.trunc(value, expected_type)
310+
logger.info(
311+
f"Truncated from i{actual_ir_type.width} to i{expected_type.width}"
312+
)
287313
else:
288-
val = (builder.trunc(val[0], expected_type), expected_type)
314+
# Same width, just use as-is (e.g., both i64)
315+
pass
289316
else:
290-
raise ValueError(f"Type mismatch: expected {expected_type}, got {val[1]}")
291-
return val
317+
raise ValueError(
318+
f"Type mismatch: expected {expected_type}, got {actual_ir_type} (original type: {val_type})"
319+
)
320+
321+
return value, expected_type
292322

293323

294324
def _handle_compare(

pythonbpf/functions/function_debug_info.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,17 +49,27 @@ def generate_function_debug_info(
4949
"The first argument should always be a pointer to a struct or a void pointer"
5050
)
5151
context_debug_info = VmlinuxHandlerRegistry.get_struct_debug_info(annotation.id)
52+
53+
# Create pointer to context this must be created fresh for each function
54+
# to avoid circular reference issues when the same struct is used in multiple functions
5255
pointer_to_context_debug_info = generator.create_pointer_type(
5356
context_debug_info, 64
5457
)
58+
59+
# Create subroutine type - also fresh for each function
5560
subroutine_type = generator.create_subroutine_type(
5661
return_type, pointer_to_context_debug_info
5762
)
63+
64+
# Create local variable - fresh for each function with unique name
5865
context_local_variable = generator.create_local_variable_debug_info(
5966
leading_argument_name, 1, pointer_to_context_debug_info
6067
)
68+
6169
retained_nodes = [context_local_variable]
62-
print("function name", func_node.name)
70+
logger.info(f"Generating debug info for function {func_node.name}")
71+
72+
# Create subprogram with is_distinct=True to ensure each function gets unique debug info
6373
subprogram_debug_info = generator.create_subprogram(
6474
func_node.name, subroutine_type, retained_nodes
6575
)

pythonbpf/type_deducer.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
"c_long": ir.IntType(64),
1717
"c_ulong": ir.IntType(64),
1818
"c_longlong": ir.IntType(64),
19+
"c_uint": ir.IntType(32),
20+
"c_int": ir.IntType(32),
1921
# Not so sure about this one
2022
"str": ir.PointerType(ir.IntType(8)),
2123
}

pythonbpf/vmlinux_parser/vmlinux_exports_handler.py

Lines changed: 59 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import logging
22
from typing import Any
3-
3+
import ctypes
44
from llvmlite import ir
55

66
from pythonbpf.local_symbol import LocalSymbol
@@ -94,32 +94,30 @@ def handle_vmlinux_struct_field(
9494
f"Attempting to access field {field_name} of possible vmlinux struct {struct_var_name}"
9595
)
9696
python_type: type = var_info.metadata
97-
globvar_ir, field_data = self.get_field_type(
98-
python_type.__name__, field_name
99-
)
97+
struct_name = python_type.__name__
98+
globvar_ir, field_data = self.get_field_type(struct_name, field_name)
10099
builder.function.args[0].type = ir.PointerType(ir.IntType(8))
101-
print(builder.function.args[0])
102100
field_ptr = self.load_ctx_field(
103-
builder, builder.function.args[0], globvar_ir
101+
builder, builder.function.args[0], globvar_ir, field_data, struct_name
104102
)
105-
print(field_ptr)
106103
# Return pointer to field and field type
107104
return field_ptr, field_data
108105
else:
109106
raise RuntimeError("Variable accessed not found in symbol table")
110107

111108
@staticmethod
112-
def load_ctx_field(builder, ctx_arg, offset_global):
109+
def load_ctx_field(builder, ctx_arg, offset_global, field_data, struct_name=None):
113110
"""
114111
Generate LLVM IR to load a field from BPF context using offset.
115112
116113
Args:
117114
builder: llvmlite IRBuilder instance
118115
ctx_arg: The context pointer argument (ptr/i8*)
119116
offset_global: Global variable containing the field offset (i64)
120-
117+
field_data: contains data about the field
118+
struct_name: Name of the struct being accessed (optional)
121119
Returns:
122-
The loaded value (i64 register)
120+
The loaded value (i64 register or appropriately sized)
123121
"""
124122

125123
# Load the offset value
@@ -164,13 +162,61 @@ def load_ctx_field(builder, ctx_arg, offset_global):
164162
passthrough_fn, [ir.Constant(ir.IntType(32), 0), field_ptr], tail=True
165163
)
166164

167-
# Bitcast to i64* (assuming field is 64-bit, adjust if needed)
168-
i64_ptr_type = ir.PointerType(ir.IntType(64))
169-
typed_ptr = builder.bitcast(verified_ptr, i64_ptr_type)
165+
# Determine the appropriate IR type based on field information
166+
int_width = 64 # Default to 64-bit
167+
needs_zext = False # Track if we need zero-extension for xdp_md
168+
169+
if field_data is not None:
170+
# Try to determine the size from field metadata
171+
if field_data.type.__module__ == ctypes.__name__:
172+
try:
173+
field_size_bytes = ctypes.sizeof(field_data.type)
174+
field_size_bits = field_size_bytes * 8
175+
176+
if field_size_bits in [8, 16, 32, 64]:
177+
int_width = field_size_bits
178+
logger.info(f"Determined field size: {int_width} bits")
179+
180+
# Special handling for struct_xdp_md i32 fields
181+
# Load as i32 but extend to i64 before storing
182+
if struct_name == "struct_xdp_md" and int_width == 32:
183+
needs_zext = True
184+
logger.info(
185+
"struct_xdp_md i32 field detected, will zero-extend to i64"
186+
)
187+
else:
188+
logger.warning(
189+
f"Unusual field size {field_size_bits} bits, using default 64"
190+
)
191+
except Exception as e:
192+
logger.warning(
193+
f"Could not determine field size: {e}, using default 64"
194+
)
195+
196+
elif field_data.type.__module__ == "vmlinux":
197+
# For pointers to structs or complex vmlinux types
198+
if field_data.ctype_complex_type is not None and issubclass(
199+
field_data.ctype_complex_type, ctypes._Pointer
200+
):
201+
int_width = 64 # Pointers are always 64-bit
202+
logger.info("Field is a pointer type, using 64 bits")
203+
# TODO: Add handling for other complex types (arrays, embedded structs, etc.)
204+
else:
205+
logger.warning("Complex vmlinux field type, using default 64 bits")
206+
207+
# Bitcast to appropriate pointer type based on determined width
208+
ptr_type = ir.PointerType(ir.IntType(int_width))
209+
210+
typed_ptr = builder.bitcast(verified_ptr, ptr_type)
170211

171212
# Load and return the value
172213
value = builder.load(typed_ptr)
173214

215+
# Zero-extend i32 to i64 for struct_xdp_md fields
216+
if needs_zext:
217+
value = builder.zext(value, ir.IntType(64))
218+
logger.info("Zero-extended i32 value to i64 for struct_xdp_md field")
219+
174220
return value
175221

176222
def has_field(self, struct_name, field_name):

tests/c-form/Makefile

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
11
BPF_CLANG := clang
2-
CFLAGS := -O0 -emit-llvm -target bpf -c
2+
CFLAGS := -emit-llvm -target bpf -c
33

44
SRC := $(wildcard *.bpf.c)
55
LL := $(SRC:.bpf.c=.bpf.ll)
6+
LL2 := $(SRC:.bpf.c=.bpf.o2.ll)
67
OBJ := $(SRC:.bpf.c=.bpf.o)
78

89
.PHONY: all clean
910

10-
all: $(LL) $(OBJ)
11+
all: $(LL) $(OBJ) $(LL2)
1112

1213
%.bpf.o: %.bpf.c
1314
$(BPF_CLANG) -O2 -g -target bpf -c $< -o $@
1415

1516
%.bpf.ll: %.bpf.c
16-
$(BPF_CLANG) $(CFLAGS) -g -S $< -o $@
17+
$(BPF_CLANG) -O0 $(CFLAGS) -g -S $< -o $@
18+
19+
%.bpf.o2.ll: %.bpf.c
20+
$(BPF_CLANG) -O2 $(CFLAGS) -g -S $< -o $@
1721

1822
clean:
19-
rm -f $(LL) $(OBJ)
23+
rm -f $(LL) $(OBJ) $(LL2)

tests/c-form/i32test.bpf.c

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#include <linux/bpf.h>
2+
#include <bpf/bpf_helpers.h>
3+
4+
SEC("xdp")
5+
int print_xdp_data(struct xdp_md *ctx)
6+
{
7+
// 'data' is a pointer to the start of packet data
8+
long data = (long)ctx->data;
9+
10+
bpf_printk("ctx->data = %lld\n", data);
11+
12+
return XDP_PASS;
13+
}
14+
15+
char LICENSE[] SEC("license") = "GPL";
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import logging
2+
3+
from pythonbpf import bpf, section, bpfglobal, compile_to_ir
4+
from pythonbpf import compile # noqa: F401
5+
from vmlinux import TASK_COMM_LEN # noqa: F401
6+
from vmlinux import struct_trace_event_raw_sys_enter # noqa: F401
7+
from ctypes import c_int64, c_int32, c_void_p # noqa: F401
8+
9+
10+
# from vmlinux import struct_uinput_device
11+
# from vmlinux import struct_blk_integrity_iter
12+
13+
14+
@bpf
15+
@section("tracepoint/syscalls/sys_enter_execve")
16+
def hello_world(ctx: struct_trace_event_raw_sys_enter) -> c_int64:
17+
b = ctx.args
18+
c = b[0]
19+
print(f"This is context args field {c}")
20+
return c_int64(0)
21+
22+
23+
@bpf
24+
@bpfglobal
25+
def LICENSE() -> str:
26+
return "GPL"
27+
28+
29+
compile_to_ir("args_test.py", "args_test.ll", loglevel=logging.INFO)
30+
compile()

0 commit comments

Comments
 (0)