Skip to content

Commit 3179549

Browse files
Add LLVM optimizer (#104)
* Add LLVM optimizer * expr_pass: add bpf_passthrough attributes to make tests pass
1 parent 8290fec commit 3179549

5 files changed

Lines changed: 93 additions & 12 deletions

File tree

‎pythonbpf/codegen.py‎

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
)
1616
from .debuginfo import DW_LANG_C11, DwarfBehaviorEnum, DebugInfoGenerator
1717
import os
18+
import shutil
1819
import subprocess
1920
import inspect
2021
from pathlib import Path
@@ -175,8 +176,66 @@ def compile_to_ir(filename: str, output: str, loglevel=logging.INFO):
175176
return output, structs_sym_tab, maps_sym_tab
176177

177178

179+
MIN_OPT_VERSION = 15
180+
_OPT_VERSIONED_RE = re.compile(r"opt-(\d+)")
181+
182+
183+
def _find_opt():
184+
"""Locate an LLVM opt binary.
185+
186+
Prefers a plain ``opt`` on PATH. Otherwise falls back to the newest
187+
versioned ``opt-N`` (N >= MIN_OPT_VERSION) that distros such as
188+
Debian/Ubuntu install, e.g. ``opt-18``. Returns None if none is found.
189+
"""
190+
opt = shutil.which("opt")
191+
if opt:
192+
return opt
193+
194+
best_version, best_path = -1, None
195+
for directory in os.environ.get("PATH", "").split(os.pathsep):
196+
try:
197+
entries = os.listdir(directory or ".")
198+
except OSError:
199+
continue
200+
for entry in entries:
201+
match = _OPT_VERSIONED_RE.fullmatch(entry)
202+
if not match:
203+
continue
204+
version = int(match.group(1))
205+
if version < MIN_OPT_VERSION or version <= best_version:
206+
continue
207+
path = shutil.which(entry, path=directory or ".")
208+
if path:
209+
best_version, best_path = version, path
210+
return best_path
211+
212+
213+
def _run_opt(ll_file):
214+
"""Run the O2 pipeline over the IR and return it as bitcode, or None if
215+
no opt binary is available."""
216+
217+
opt = _find_opt()
218+
if opt is None:
219+
logger.warning(
220+
f"No LLVM 'opt' (or 'opt-N', N >= {MIN_OPT_VERSION}) found on PATH; "
221+
"skipping IR optimization."
222+
)
223+
return None
224+
225+
logger.info(f"Optimizing IR with {opt} -O2: {ll_file}")
226+
result = subprocess.run(
227+
[opt, "-O2", str(ll_file), "-o", "-"],
228+
check=True,
229+
capture_output=True,
230+
)
231+
return result.stdout
232+
233+
178234
def _run_llc(ll_file, obj_file):
179-
"""Compile LLVM IR to BPF object file using llc."""
235+
"""Optimize LLVM IR with opt (when available) and compile it to a BPF
236+
object file using llc."""
237+
238+
bitcode = _run_opt(ll_file)
180239

181240
logger.info(f"Compiling IR to object: {ll_file} -> {obj_file}")
182241
result = subprocess.run(
@@ -185,20 +244,20 @@ def _run_llc(ll_file, obj_file):
185244
"-march=bpf",
186245
"-filetype=obj",
187246
"-O2",
188-
str(ll_file),
247+
"-" if bitcode is not None else str(ll_file),
189248
"-o",
190249
str(obj_file),
191250
],
251+
input=bitcode,
192252
check=True,
193253
capture_output=True,
194-
text=True,
195254
)
196255

197256
if result.returncode == 0:
198257
logger.info(f"Object file written to {obj_file}")
199258
return True
200259
else:
201-
logger.error(f"llc compilation failed: {result.stderr}")
260+
logger.error(f"llc compilation failed: {result.stderr.decode()}")
202261
return False
203262

204263

‎pythonbpf/context.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ def __init__(self, module: ir.Module):
7272
# Helper management
7373
self.scratch_pool = ScratchPoolManager()
7474

75+
# Sequence numbers for llvm.bpf.passthrough barriers; each call needs a
76+
# distinct one so the optimizer cannot CSE two barriers together.
77+
self.passthrough_seq = 0
78+
7579
# Vmlinux handling (optional, specialized)
7680
self.vmlinux_handler = None # Can be VmlinuxHandler instance
7781

‎pythonbpf/expr/expr_pass.py‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,28 @@ def _handle_unary_op(
407407
# ============================================================================
408408

409409

410+
def _bpf_passthrough(builder, compilation_context, val):
411+
"""Wrap an i1 in llvm.bpf.passthrough, an optimization barrier the BPF
412+
backend strips before instruction selection.
413+
414+
Without it, opt's SimplifyCFG speculates a short-circuit operand's compare
415+
into the preceding block, and SelectionDAG then folds the two null checks
416+
of `p1 or p2` into `(p1 | p2) != 0`. The verifier rejects that with
417+
"pointer |= pointer prohibited". BPFAdjustOpt uses the same intrinsic to
418+
keep compares serialized for the verifier.
419+
"""
420+
module = compilation_context.module
421+
name = "llvm.bpf.passthrough.i1.i1"
422+
fn = module.globals.get(name)
423+
if fn is None:
424+
fn_type = ir.FunctionType(ir.IntType(1), [ir.IntType(32), ir.IntType(1)])
425+
fn = ir.Function(module, fn_type, name=name)
426+
427+
seq = ir.Constant(ir.IntType(32), compilation_context.passthrough_seq)
428+
compilation_context.passthrough_seq += 1
429+
return builder.call(fn, [seq, val])
430+
431+
410432
def _handle_and_op(func, builder, expr, local_sym_tab, compilation_context):
411433
"""Handle `and` boolean operations."""
412434

@@ -432,6 +454,8 @@ def _handle_and_op(func, builder, expr, local_sym_tab, compilation_context):
432454

433455
# Convert to boolean if needed
434456
operand_bool = convert_to_bool(builder, operand_val)
457+
if i > 0:
458+
operand_bool = _bpf_passthrough(builder, compilation_context, operand_bool)
435459
current_block = builder.block
436460

437461
if is_last:
@@ -485,6 +509,8 @@ def _handle_or_op(func, builder, expr, local_sym_tab, compilation_context):
485509

486510
# Convert to boolean if needed
487511
operand_bool = convert_to_bool(builder, operand_val)
512+
if i > 0:
513+
operand_bool = _bpf_passthrough(builder, compilation_context, operand_bool)
488514
current_block = builder.block
489515

490516
if is_last:

‎tests/test_config.toml‎

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,6 @@
1515

1616
"failing_tests/conditionals/struct_ptr.py" = {reason = "Struct pointer used directly as boolean condition not supported", level = "ir"}
1717

18-
# Compiles cleanly; rejected by the kernel. The `data + 34 < data_end` guard is
19-
# emitted as a signed compare over values round-tripped through the stack, so the
20-
# verifier never narrows the packet range (it stays r=0) and the later
21-
# `iph.saddr` read fails with "invalid access to packet, off=26 size=4" /
22-
# "R1 offset is outside of the packet". Direct packet access needs bounds checks
23-
# in a form the verifier can follow.
24-
"failing_tests/xdp/xdp_test_1.py" = {reason = "XDP direct packet access: the data/data_end guard does not establish a packet range the verifier can use", level = "verifier"}
25-
2618
"failing_tests/license.py" = {reason = "Missing LICENSE global produces IR that llc rejects — should be caught earlier with a clear error message", level = "llc"}
2719

2820
"failing_tests/undeclared_values.py" = {reason = "Undeclared variable used in f-string — should raise SyntaxError (correct behaviour, test documents it)", level = "ir"}

0 commit comments

Comments
 (0)