Do a bit of API cleanup

This commit is contained in:
Duncan Ogilvie
2026-02-13 00:41:09 +01:00
parent d2e949c291
commit 7cec92243c
10 changed files with 911 additions and 200 deletions
@@ -31,9 +31,9 @@ RESET = "\033[0m"
def section(title: str):
"""Print a section header."""
print(f"\n{BOLD}{'='*60}{RESET}")
print(f"\n{BOLD}{'=' * 60}{RESET}")
print(f"{BOLD}{CYAN}{title}{RESET}")
print(f"{BOLD}{'='*60}{RESET}\n")
print(f"{BOLD}{'=' * 60}{RESET}\n")
def explain(text: str):
@@ -153,13 +153,17 @@ def exercise_2_iteration():
for func in mod.functions:
print(f"Function: {BOLD}{func.name}{RESET}")
print(f" Parameters: {func.num_params}")
print(f" Parameters: {func.param_count}")
print(f" Blocks: {len(list(func.basic_blocks))}")
for bb in func.basic_blocks:
print(f"\n Block: {BOLD}{bb.name}{RESET}")
for inst in bb.instructions:
op_name = inst.opcode.name if hasattr(inst.opcode, 'name') else str(inst.opcode)
op_name = (
inst.opcode.name
if hasattr(inst.opcode, "name")
else str(inst.opcode)
)
is_term = " (TERMINATOR)" if inst.is_terminator_inst else ""
print(f" {op_name}: {inst}{is_term}")
@@ -224,7 +228,10 @@ def exercise_3_simple_transform():
op1 = inst.get_operand(1)
# Try to detect if it's a constant integer
# (This is a simplified check)
if hasattr(op1, 'type') and op1.type.kind == llvm.TypeKind.Integer:
if (
hasattr(op1, "type")
and op1.type.kind == llvm.TypeKind.Integer
):
to_transform.append(inst)
print(f"{CYAN}Found {len(to_transform)} add instructions{RESET}")
@@ -580,9 +587,9 @@ def summary():
def main():
print(f"""
{BOLD}{'='*60}
{BOLD}{"=" * 60}
LLVM-NANOBIND HANDS-ON LEARNING EXERCISES
{'='*60}{RESET}
{"=" * 60}{RESET}
This interactive exercise will walk you through:
+88 -80
View File
@@ -21,7 +21,9 @@ BOLD = "\033[1m"
RESET = "\033[0m"
def ask(question: str, options: list[str], correct: int, explanation: str) -> bool:
def ask(
question: str, options: list[str], correct: int, explanation: str
) -> bool | None:
"""Ask a multiple choice question. Returns True if correct."""
print(f"\n{CYAN}{BOLD}Question:{RESET}")
print(f" {question}\n")
@@ -32,30 +34,32 @@ def ask(question: str, options: list[str], correct: int, explanation: str) -> bo
while True:
try:
answer = input(f"\n{YELLOW}Your answer (1-{len(options)}): {RESET}").strip()
if answer.lower() == 'q':
if answer.lower() == "q":
return None # Signal to quit
choice = int(answer)
if 1 <= choice <= len(options):
break
print(f"Please enter a number between 1 and {len(options)}")
except ValueError:
print(f"Please enter a number between 1 and {len(options)} (or 'q' to quit)")
print(
f"Please enter a number between 1 and {len(options)} (or 'q' to quit)"
)
if choice == correct:
print(f"\n{GREEN}{BOLD}Correct!{RESET}")
print(f"{GREEN}{explanation}{RESET}")
return True
else:
print(f"\n{RED}{BOLD}Not quite.{RESET} The answer is: {options[correct-1]}")
print(f"\n{RED}{BOLD}Not quite.{RESET} The answer is: {options[correct - 1]}")
print(f"{YELLOW}{explanation}{RESET}")
return False
def section_intro(title: str, description: str):
"""Print a section header."""
print(f"\n{'='*60}")
print(f"\n{'=' * 60}")
print(f"{BOLD}{title}{RESET}")
print(f"{'='*60}")
print(f"{'=' * 60}")
print(f"\n{description}\n")
input(f"{YELLOW}Press Enter to begin...{RESET}")
@@ -71,13 +75,13 @@ LLVM_IR_BASICS = [
"A return instruction",
"A terminator instruction (br, ret, switch, etc.)",
"A call instruction",
"Nothing special - it's just the last instruction"
"Nothing special - it's just the last instruction",
],
"correct": 2,
"explanation": "Every basic block must end with exactly one terminator instruction. "
"This is invariant in LLVM IR. Terminators define control flow: where "
"execution can go next. Without a terminator, LLVM wouldn't know what "
"happens after the block."
"This is invariant in LLVM IR. Terminators define control flow: where "
"execution can go next. Without a terminator, LLVM wouldn't know what "
"happens after the block.",
},
{
"question": "What does SSA (Static Single Assignment) mean in practice?",
@@ -85,14 +89,14 @@ LLVM_IR_BASICS = [
"Each variable can only be read once",
"Each value can only be assigned once",
"Each function can only have one return",
"Each block can only have one predecessor"
"Each block can only have one predecessor",
],
"correct": 2,
"explanation": "SSA means every value is defined exactly once. You can't reassign: "
"%x = add i32 1, 2; %x = add i32 %x, 3 is INVALID. "
"Instead, use new names: %x = ...; %y = add i32 %x, 3. "
"This enables powerful optimizations because the compiler knows "
"exactly where each value comes from."
"%x = add i32 1, 2; %x = add i32 %x, 3 is INVALID. "
"Instead, use new names: %x = ...; %y = add i32 %x, 3. "
"This enables powerful optimizations because the compiler knows "
"exactly where each value comes from.",
},
{
"question": "What's a PHI node used for?",
@@ -100,14 +104,14 @@ LLVM_IR_BASICS = [
"Representing function parameters",
"Selecting a value based on which predecessor block we came from",
"Performing floating-point operations",
"Calling external functions"
"Calling external functions",
],
"correct": 2,
"explanation": "PHI nodes exist because of SSA + control flow. When two blocks merge, "
"and each defines a different version of a value, how do we pick? "
"PHI nodes: %result = phi i32 [ %x, %block1 ], [ %y, %block2 ]. "
"The value depends on which predecessor we came from. "
"They're crucial for loops and conditionals."
"and each defines a different version of a value, how do we pick? "
"PHI nodes: %result = phi i32 [ %x, %block1 ], [ %y, %block2 ]. "
"The value depends on which predecessor we came from. "
"They're crucial for loops and conditionals.",
},
{
"question": "Which is NOT a valid LLVM type?",
@@ -115,12 +119,12 @@ LLVM_IR_BASICS = [
"i1 (1-bit integer, boolean)",
"i32 (32-bit integer)",
"ptr (opaque pointer)",
"str (string type)"
"str (string type)",
],
"correct": 4,
"explanation": "LLVM has no native string type. Strings are represented as arrays of i8 "
"([13 x i8]) or pointers to i8 (ptr). i1 is valid (booleans), i32 is valid "
"(common integer), and ptr is the opaque pointer type in modern LLVM."
"([13 x i8]) or pointers to i8 (ptr). i1 is valid (booleans), i32 is valid "
"(common integer), and ptr is the opaque pointer type in modern LLVM.",
},
]
@@ -131,13 +135,13 @@ BINDING_API = [
"Because ptr is more complex",
"It's an API inconsistency that should be fixed",
"Because ptr takes an address space argument",
"Because i32 is a constant, ptr is computed"
"Because i32 is a constant, ptr is computed",
],
"correct": 2,
"explanation": "This is documented as an API inconsistency! The current behavior makes "
"sense because ptr() can take an optional address space argument, but the "
"inconsistency is confusing. The plan proposes making ptr a property that "
"returns the default pointer type, with ptr(addrspace) as an optional method."
"sense because ptr() can take an optional address space argument, but the "
"inconsistency is confusing. The plan proposes making ptr a property that "
"returns the default pointer type, with ptr(addrspace) as an optional method.",
},
{
"question": "To delete an instruction, you must:",
@@ -145,13 +149,13 @@ BINDING_API = [
"Call inst.delete()",
"Call inst.erase_from_parent()",
"Call inst.remove_from_parent() then inst.delete_instruction()",
"Set inst to None"
"Set inst to None",
],
"correct": 3,
"explanation": "The two-step process is error-prone! If you call delete_instruction() "
"while the instruction is still in a block, LLVM will assert/crash. "
"The plan proposes adding erase_from_parent() that does both atomically, "
"matching the C++ API."
"while the instruction is still in a block, LLVM will assert/crash. "
"The plan proposes adding erase_from_parent() that does both atomically, "
"matching the C++ API.",
},
{
"question": "What does replace_all_uses_with() do?",
@@ -159,13 +163,13 @@ BINDING_API = [
"Replaces the instruction with a new one",
"Changes every use of a value to point to a different value",
"Replaces the function containing the value",
"Copies the value to all users"
"Copies the value to all users",
],
"correct": 2,
"explanation": "RAUW is fundamental to SSA-based transformations. When you create a new "
"value that should replace an old one, you use RAUW to update every "
"instruction that uses the old value. This is NOT currently bound - "
"the passes implement it manually, which is error-prone and slow."
"value that should replace an old one, you use RAUW to update every "
"instruction that uses the old value. This is NOT currently bound - "
"the passes implement it manually, which is error-prone and slow.",
},
{
"question": "What happens if you access a Module after exiting its 'with' block?",
@@ -173,13 +177,13 @@ BINDING_API = [
"You get garbage data",
"Python segfaults",
"You get a clean Python exception (due to validity tokens)",
"Nothing - the module is copied"
"Nothing - the module is copied",
],
"correct": 3,
"explanation": "The bindings use 'validity tokens' to track object lifetime. When the "
"context manager exits, the underlying LLVM object is disposed, and "
"all Python wrappers are marked invalid. Accessing them raises a clean "
"exception instead of the undefined behavior you'd get in C++."
"context manager exits, the underlying LLVM object is disposed, and "
"all Python wrappers are marked invalid. Accessing them raises a clean "
"exception instead of the undefined behavior you'd get in C++.",
},
{
"question": "To iterate over an instruction's operands, you use:",
@@ -187,12 +191,12 @@ BINDING_API = [
"for op in inst.operands:",
"for op in inst.get_operands():",
"for i in range(inst.num_operands): op = inst.get_operand(i)",
"for op in inst:"
"for op in inst:",
],
"correct": 3,
"explanation": "There's no .operands iterator! This is listed as a missing convenience. "
"You must use index-based access. The plan proposes adding an operands "
"property that returns an iterator for Pythonic access."
"You must use index-based access. The plan proposes adding an operands "
"property that returns an iterator for Pythonic access.",
},
]
@@ -203,13 +207,13 @@ OBFUSCATION = [
"XOR always equals subtraction",
"It exploits the relationship between arithmetic and bitwise operations in two's complement",
"It's an approximation that's close enough",
"The extra operations cancel out"
"The extra operations cancel out",
],
"correct": 2,
"explanation": "In two's complement representation, there are deep connections between "
"arithmetic and bitwise operations. The identity is exact for all inputs "
"in fixed-width integers. The obfuscation works because decompilers "
"pattern-match 'sub' to '-', but don't recognize this equivalent form."
"arithmetic and bitwise operations. The identity is exact for all inputs "
"in fixed-width integers. The obfuscation works because decompilers "
"pattern-match 'sub' to '-', but don't recognize this equivalent form.",
},
{
"question": "Control flow flattening hides the original CFG by:",
@@ -217,13 +221,13 @@ OBFUSCATION = [
"Encrypting all instructions",
"Introducing a state machine dispatcher that controls block execution",
"Removing all branches",
"Inlining all functions"
"Inlining all functions",
],
"correct": 2,
"explanation": "CFF creates a dispatcher that reads a state variable and branches to "
"the appropriate block. Each block updates the state and jumps back to "
"the dispatcher. The original 'if A then B else C' structure becomes "
"'switch(state) { ... }' - much harder to analyze."
"the appropriate block. Each block updates the state and jumps back to "
"the dispatcher. The original 'if A then B else C' structure becomes "
"'switch(state) { ... }' - much harder to analyze.",
},
{
"question": "Why does the CFF pass demote PHI nodes to stack variables?",
@@ -231,13 +235,13 @@ OBFUSCATION = [
"PHI nodes are too slow",
"PHI nodes encode predecessor information that's lost after flattening",
"The LLVM API doesn't support PHI nodes",
"Stack variables are more secure"
"Stack variables are more secure",
],
"correct": 2,
"explanation": "PHI nodes say 'if we came from block1, use %x; if from block2, use %y'. "
"After flattening, we always come from the dispatcher! The predecessor "
"information is meaningless. By converting to explicit memory operations, "
"we preserve the semantics without relying on control flow."
"After flattening, we always come from the dispatcher! The predecessor "
"information is meaningless. By converting to explicit memory operations, "
"we preserve the semantics without relying on control flow.",
},
{
"question": "The string encryption pass was abandoned because:",
@@ -245,13 +249,13 @@ OBFUSCATION = [
"Strings can't be encrypted",
"The bindings encode strings as UTF-8, corrupting bytes > 127",
"LLVM doesn't support string constants",
"It was too slow"
"It was too slow",
],
"correct": 2,
"explanation": "const_string() and const_data_array() pass strings through UTF-8 encoding. "
"Encrypted bytes often exceed 127, which expand to multi-byte UTF-8 "
"sequences. The resulting array is larger than expected, breaking the "
"decryption logic. This is listed as a critical blocker in the plan."
"Encrypted bytes often exceed 127, which expand to multi-byte UTF-8 "
"sequences. The resulting array is larger than expected, breaking the "
"decryption logic. This is listed as a critical blocker in the plan.",
},
]
@@ -262,13 +266,13 @@ CRITIQUE = [
"Adding documentation for exceptions",
"Making ptr a property instead of method",
"Binding LLVMReplaceAllUsesWith",
"Adding an .operands iterator"
"Adding an .operands iterator",
],
"correct": 3,
"explanation": "The plan categorizes issues by priority. Priority 1 (Critical Blockers) "
"includes RAUW, erase_from_parent, split_basic_block, and raw bytes support. "
"These block real use cases. API consistency issues (like ptr()) are P2, "
"conveniences are P3, documentation is P4."
"includes RAUW, erase_from_parent, split_basic_block, and raw bytes support. "
"These block real use cases. API consistency issues (like ptr()) are P2, "
"conveniences are P3, documentation is P4.",
},
{
"question": "The porting guide rates the API '7/10 for code generation, 5/10 for transforms'. Why the difference?",
@@ -276,13 +280,13 @@ CRITIQUE = [
"Transforms are inherently harder",
"The Builder API is good, but operations like RAUW and block splitting are missing",
"Python is slow for transforms",
"The documentation is better for code generation"
"The documentation is better for code generation",
],
"correct": 2,
"explanation": "Code generation (creating new IR) mainly uses the Builder, which is "
"well-designed (add, sub, br, etc.). Transforms (modifying existing IR) "
"need operations like replace_all_uses_with, erase_from_parent, split_block. "
"These are missing or cumbersome, making transform work painful."
"well-designed (add, sub, br, etc.). Transforms (modifying existing IR) "
"need operations like replace_all_uses_with, erase_from_parent, split_block. "
"These are missing or cumbersome, making transform work painful.",
},
{
"question": "When reviewing API design, 'pit of success' means:",
@@ -290,13 +294,13 @@ CRITIQUE = [
"The API should fail loudly on errors",
"The easy/natural path should be the correct path",
"The API should have extensive documentation",
"The API should be minimal"
"The API should be minimal",
],
"correct": 2,
"explanation": "A 'pit of success' API design makes it hard to do the wrong thing. "
"The current two-step instruction deletion violates this - the natural "
"thing (just call delete) crashes. A good API would make the safe path "
"the obvious path: inst.erase_from_parent() does everything correctly."
"The current two-step instruction deletion violates this - the natural "
"thing (just call delete) crashes. A good API would make the safe path "
"the obvious path: inst.erase_from_parent() does everything correctly.",
},
]
@@ -322,9 +326,9 @@ def run_section(title: str, description: str, questions: list[dict]) -> tuple[in
def main():
print(f"""
{BOLD}{'='*60}
{BOLD}{"=" * 60}
LLVM-NANOBIND SELF-ASSESSMENT QUIZ
{'='*60}{RESET}
{"=" * 60}{RESET}
This quiz tests your understanding of the llvm-nanobind project
and its transformation API. It's designed to:
@@ -352,7 +356,7 @@ Sections:
"Section 1: LLVM IR Fundamentals",
"These questions test your understanding of LLVM's intermediate representation.\n"
"This knowledge is essential for understanding what the bindings expose.",
LLVM_IR_BASICS
LLVM_IR_BASICS,
)
results.append(("LLVM IR Basics", c, t))
@@ -364,7 +368,7 @@ Sections:
"Section 2: The Python Bindings API",
"These questions test your understanding of the llvm-nanobind API design,\n"
"including its strengths, weaknesses, and idiosyncrasies.",
BINDING_API
BINDING_API,
)
results.append(("Binding API", c, t))
@@ -376,7 +380,7 @@ Sections:
"Section 3: Obfuscation Passes",
"These questions test your understanding of the MBA and CFF obfuscation\n"
"passes, which serve as case studies for the transformation API.",
OBFUSCATION
OBFUSCATION,
)
results.append(("Obfuscation", c, t))
@@ -388,14 +392,14 @@ Sections:
"Section 4: Critical Evaluation",
"These questions test your ability to critically evaluate the work,\n"
"including understanding priorities and trade-offs.",
CRITIQUE
CRITIQUE,
)
results.append(("Critical Evaluation", c, t))
# Final results
print(f"\n\n{'='*60}")
print(f"\n\n{'=' * 60}")
print(f"{BOLD}FINAL RESULTS{RESET}")
print(f"{'='*60}\n")
print(f"{'=' * 60}\n")
total_correct = 0
total_questions = 0
@@ -408,7 +412,9 @@ Sections:
total_correct += c
total_questions += t
print(f"\n {'OVERALL':25} {total_correct}/{total_questions} ({total_correct/total_questions*100:.0f}%)")
print(
f"\n {'OVERALL':25} {total_correct}/{total_questions} ({total_correct / total_questions * 100:.0f}%)"
)
# Recommendations
print(f"\n{BOLD}Recommendations:{RESET}")
@@ -427,7 +433,9 @@ Sections:
if total_correct / total_questions >= 0.8:
print(f"\n{GREEN}You're ready to review and critique the work!{RESET}")
else:
print(f"\n{YELLOW}Consider reviewing the learning guide before diving into code review.{RESET}")
print(
f"\n{YELLOW}Consider reviewing the learning guide before diving into code review.{RESET}"
)
if __name__ == "__main__":
+12 -23
View File
@@ -2,14 +2,6 @@ import argparse
import llvm
def first_non_phi(block: llvm.BasicBlock):
instr = block.first_instruction
while instr is not None and instr.opcode == llvm.Opcode.PHI:
instr = instr.next_instruction
assert instr is not None, "bad"
return instr
def main():
parser = argparse.ArgumentParser("bc-profile")
parser.add_argument("ir_in", help="Input LLVM IR to profile")
@@ -40,39 +32,36 @@ def main():
print(f"start.type: {start_fn.type}, function_type: {start_fn.function_type}")
with first_non_phi(main_entry).create_builder() as builder:
# TODO: if you pass a name here LLVM will assert (should be graceful)
builder.call(start_fn.function_type, start_fn, [])
first_inst = main_entry.first_non_phi
assert first_inst is not None, "empty main entry block"
with first_inst.create_builder() as builder:
builder.call(start_fn, [])
for main_block in main_fn.basic_blocks:
terminator = main_block.terminator
if terminator.opcode == llvm.Opcode.Ret:
print(f"return block: '{main_block.name}'") # TODO: how do we print the number if there is no name?
print(main_block) # TODO: this should print the block bitcode?
print(f"return block: '{main_block.name}'")
print(main_block)
with terminator.create_builder() as builder:
builder.call(stop_fn.function_type, stop_fn, [])
builder.call(stop_fn, [])
for function in mod.functions:
if function.name == "main":
print("skipping main")
continue
# TODO: is_ is not a good prefix for vscode discoverability
if function.is_declaration:
print(f"skipping declaration: {function.name}")
continue
print(f"instrumenting function: {function.name}")
name_const = llvm.const_string(ctx, function.name)
# TODO: I guess we need to promote this to a pointer?
name_const = ctx.const_string(function.name)
print(f"{name_const.value_kind=}")
# TODO: should this be optional at all, maybe throw instead?
entry_block = function.entry_block
print("instrumenting entry block")
assert entry_block is not None, "no entry block (bad)"
with first_non_phi(entry_block).create_builder() as builder:
# TODO: varargs instead?
# TODO: this hard crashes
builder.call(enter_fn.function_type, enter_fn, [name_const])
first_inst = entry_block.first_non_phi
assert first_inst is not None, "empty entry block"
with first_inst.create_builder() as builder:
builder.call(enter_fn, [name_const])
with open(args.ir_out, "w", encoding="utf-8") as f:
f.write(str(mod))
+4 -8
View File
@@ -17,16 +17,12 @@ def main():
builder.ret(zero)
print(func)
# The add instruction is used in the xor instruction twice, so we get 2 uses
# TODO: this is a gap in the LLVM C API because we cannot get the operand index
# and we would have to manually iterate the operands (while tracking the previous
# operand index) to find out which operand a use corresponds to. Searching for the
# use.user in the operands would not work if the same value is used multiple times.
# Most likely the approach here is to implement this bookkeeping in llvm-nanobind itself.
print(f"Uses of add:")
# Each use knows its operand index within the user instruction
print("Uses of add:")
for i, use in enumerate(add.uses):
print(f"[{i}] {use.user}")
print(f"[{i}] operand {use.operand_index} of {use.user}")
# But only one user, the xor instruction itself
print(f"Users of add:")
print("Users of add:")
for i, user in enumerate(add.users):
print(f"[{i}] User: {user}")
+4 -4
View File
@@ -742,8 +742,8 @@ class FunCloner:
catch_pad = self.clone_value(src.get_operand(0))
unwind_dest = src.unwind_dest
unwind = self.declare_bb(unwind_dest) if unwind_dest else None
# Note: cleanup_ret accepts None for unwind_bb (stub is incorrect)
dst = builder.cleanup_ret(catch_pad, unwind) # type: ignore[arg-type]
# Note: cleanup_ret accepts None for unwind_bb
dst = builder.cleanup_ret(catch_pad, unwind)
elif op == llvm.Opcode.CatchRet:
catch_pad = self.clone_value(src.get_operand(0))
@@ -771,8 +771,8 @@ class FunCloner:
unwind_dest = src.unwind_dest
unwind_bb = self.declare_bb(unwind_dest) if unwind_dest else None
num_handlers = src.num_handlers
# Note: catch_switch accepts None for unwind_bb (stub is incorrect)
dst = builder.catch_switch(parent_pad, unwind_bb, num_handlers, name) # type: ignore[arg-type]
# Note: catch_switch accepts None for unwind_bb
dst = builder.catch_switch(parent_pad, unwind_bb, num_handlers, name)
if num_handlers > 0:
handlers = src.handlers
for h in handlers:
+778 -32
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -78,7 +78,7 @@ def main():
# Function 5: Function with fastcc calling convention
fastcc_ty = ctx.types.function(i32, [i32], vararg=False)
fastcc_fn = mod.add_function("fastcc_func", fastcc_ty)
fastcc_fn.calling_conv = llvm.CallConv.Fast.value
fastcc_fn.calling_conv = llvm.CallConv.Fast
# Function 6: Will be deleted
delete_ty = ctx.types.function(void_ty, [], vararg=False)
+4 -21
View File
@@ -83,7 +83,6 @@ def demote_phi_to_stack(func: llvm.Function) -> None:
4. Replace uses of PHI with the load
5. Delete the PHI
"""
ctx = func.context
entry_bb = list(func.basic_blocks)[0]
# Collect all PHI nodes first
@@ -126,23 +125,10 @@ def demote_phi_to_stack(func: llvm.Function) -> None:
load = builder.load(phi_type, alloca, phi.name)
# Replace uses of PHI with the load
replace_all_uses_with(phi, load)
phi.replace_all_uses_with(load)
# Delete the PHI
phi.remove_from_parent()
phi.delete_instruction()
def replace_all_uses_with(old_value: llvm.Value, new_value: llvm.Value) -> None:
"""Replace all uses of old_value with new_value."""
uses_to_replace = []
for use in old_value.uses:
uses_to_replace.append(use.user)
for user in uses_to_replace:
for i in range(user.num_operands):
if user.get_operand(i) == old_value:
user.set_operand(i, new_value)
phi.erase_from_parent()
def flatten_function(func: llvm.Function, use_globals: bool, shuffle: bool) -> None:
@@ -160,7 +146,6 @@ def flatten_function(func: llvm.Function, use_globals: bool, shuffle: bool) -> N
# Use i32 for state values (simpler and more compatible)
int_ty = ctx.types.i32
i32_ty = ctx.types.i32
entry_bb = blocks[0]
original_blocks = blocks[1:] # Blocks to flatten (excluding entry)
@@ -263,8 +248,7 @@ def flatten_function(func: llvm.Function, use_globals: bool, shuffle: bool) -> N
int_ty.constant(block_state_map[target]), state_var
)
builder.br(dispatch_bb)
terminator.remove_from_parent()
terminator.delete_instruction()
terminator.erase_from_parent()
elif len(successors) == 2:
# Conditional branch
@@ -295,8 +279,7 @@ def flatten_function(func: llvm.Function, use_globals: bool, shuffle: bool) -> N
builder.position_before(terminator)
builder.cond_br(condition, true_state_bb, false_state_bb)
terminator.remove_from_parent()
terminator.delete_instruction()
terminator.erase_from_parent()
def main():
+2 -19
View File
@@ -24,25 +24,9 @@ Options:
import argparse
import random
import sys
from typing import Callable
import llvm
def replace_all_uses_with(old_value: llvm.Value, new_value: llvm.Value) -> None:
"""Replace all uses of old_value with new_value."""
# Collect uses first to avoid modifying while iterating
uses_to_replace = []
for use in old_value.uses:
uses_to_replace.append((use.user, use))
# Replace each use
for user, use in uses_to_replace:
for i in range(user.num_operands):
if user.get_operand(i) == old_value:
user.set_operand(i, new_value)
# =============================================================================
# MBA Substitution Functions
# =============================================================================
@@ -300,9 +284,8 @@ def run_on_basic_block(bb: llvm.BasicBlock) -> None:
replacement = obfuscator(builder, a, b, inst.name)
# Replace uses and remove original
replace_all_uses_with(inst, replacement)
inst.remove_from_parent()
inst.delete_instruction()
inst.replace_all_uses_with(replacement)
inst.erase_from_parent()
def run_on_function(func: llvm.Function, iterations: int) -> None:
+4 -5
View File
@@ -95,14 +95,14 @@ def transform_branch(
idx0 = builder.gep(
array_ty, jump_table, [i32_ty.constant(0), i32_ty.constant(0)], "sibr.slot0"
)
false_addr = llvm.block_address(func, false_bb)
false_addr = func.block_address(false_bb)
builder.store(false_addr, idx0)
# GEP to table[1] and store true target
idx1 = builder.gep(
array_ty, jump_table, [i32_ty.constant(0), i32_ty.constant(1)], "sibr.slot1"
)
true_addr = llvm.block_address(func, true_bb)
true_addr = func.block_address(true_bb)
builder.store(true_addr, idx1)
# Get condition and invert it
@@ -134,7 +134,7 @@ def transform_branch(
idx0 = builder.gep(
array_ty, jump_table, [i32_ty.constant(0), i32_ty.constant(0)], "sibr.slot0"
)
target_addr = llvm.block_address(func, target_bb)
target_addr = func.block_address(target_bb)
builder.store(target_addr, idx0)
# Load from table[0]
@@ -145,8 +145,7 @@ def transform_branch(
indir_br.add_destination(target_bb)
# Remove original branch
branch.remove_from_parent()
branch.delete_instruction()
branch.erase_from_parent()
return True