Skip to content

feat: MLIL DetailedOperands/Operands/Traverse + shared IL operand types#42

Merged
tinysec merged 1 commit into
masterfrom
feat/mlil-navigation
Jul 9, 2026
Merged

feat: MLIL DetailedOperands/Operands/Traverse + shared IL operand types#42
tinysec merged 1 commit into
masterfrom
feat/mlil-navigation

Conversation

@tinysec

@tinysec tinysec commented Jul 9, 2026

Copy link
Copy Markdown
Owner

Summary

Adds MediumLevelILInstruction.DetailedOperands / Operands / Traverse, mirroring Python mediumlevelil.py, driven by a static descriptor table — the same pattern already used for HighLevelIL. Extracts the shared ILOperand / ILOperandKind / ILOperandDescriptor types so both ILs share one source of truth.

Changes

  • IL/ shared typesILOperand, ILOperandKind, ILOperandDescriptor extracted from the HLIL-specific copies. ILOperandDescriptor gains SecondaryRawIndex for the MLIL aliased dest/prev operands (e.g. SET_VAR_ALIASED = var@0, dest-version@1, prev-version@2 — prev shares the var slot with dest but reads version@2).
  • HLIL refactored onto the shared types; its reader threads the secondary index through the SSAVariable / ConstantData cases.
  • MediumLevelIL/MediumLevelILDetailedOperandsTable.cs — 140 rows generated from the official Python overrides by reading each property's actual _get_*(N) raw index (not accumulated slot widths — the core's advance=0 rule desyncs accumulation for sub-instruction-derived operands). Cross-checked against the live core (5.3.9757).
  • Struct/BNMediumLevelILInstruction.csDetailedOperands / Operands / Traverse + GetOperandAsInteger reader.

Fixes that surfaced

  • OperationOperands counts too low for five ops whose copied RawOperands array was shorter than the descriptors read → IndexOutOfRange:
    MLIL_CALL_UNTYPED 3→4, MLIL_RET_HINT 0→1, MLIL_STORE_STRUCT_SSA 4→5, MLIL_SYSCALL_UNTYPED 2→3, MLIL_TAILCALL_UNTYPED 2→4.
  • GetOperandAsIntegerArray passed the slot value to BNMediumLevelILGetOperandList instead of the operand index → garbage list; now mirrors GetOperandAsIntegerMap.

Known parity gap (documented)

The call/syscall/intrinsic family lists an output/params/output_dest_memory operand reached by unwrapping a CallOutput/CallParam sub-instruction; BNMediumLevelILGetOperandList does not flatten across that boundary, so those derived operands are omitted (flat-readable operands included). Sub-instruction navigation to close the gap is a tracked follow-up.

Version note

The C# MediumLevelILOperation enum was generated against a newer binaryninja-api than the installed core (5.3.9757 / aa25bfc). The table matches the installed core (140 ops); 11 enum members the core never emits have no row (empty DetailedOperands); the one rename (installed MLIL_CALL_OUTPUT == C# MLIL_VAR_OUTPUT) is handled in the generator.

Tests

E2E + reflection-consistency tests live in the separate sharp-binaryninja-tests harness (kept out of this repo per project convention). All 85 tests pass, including 9 new MLIL DetailedOperands tests.

… types (#42)

Mirror Python MediumLevelILInstruction.detailed_operands / operands / traverse
(mediumlevelil.py) by driving them from a static descriptor table, the same
pattern already used for HighLevelIL.

Shared types (IL/): extract ILOperand, ILOperandKind, ILOperandDescriptor out
of the HLIL-specific copies so both ILs share one source of truth.
ILOperandDescriptor gains a SecondaryRawIndex for the MLIL "aliased" dest/prev
operands, where prev shares the variable slot with dest but reads a different
version slot (e.g. SET_VAR_ALIASED is var@0, dest-version@1, prev-version@2).
HLIL is refactored onto the shared types; its DetailedOperands reader now takes
a descriptor and threads the secondary index through the SSAVariable /
ConstantData cases.

MLIL table (MediumLevelIL/MediumLevelILDetailedOperandsTable.cs): 140 rows
generated from the official Python overrides by reading each property's actual
_get_*(N) raw index (not accumulated slot widths -- the core's advance=0 rule
desynchronises accumulation for sub-instruction-derived operands). Empirically
cross-checked against the live core (5.3.9757): detailed_operands succeeds with
zero exceptions across cat/ls/true, and the operand indices match.

Fixes that surfaced:
- OperationOperands counts were too low for five ops whose copied RawOperands
  array was shorter than the descriptors read, which would have thrown
  IndexOutOfRange: MLIL_CALL_UNTYPED 3->4, MLIL_RET_HINT 0->1,
  MLIL_STORE_STRUCT_SSA 4->5, MLIL_SYSCALL_UNTYPED 2->3,
  MLIL_TAILCALL_UNTYPED 2->4.
- GetOperandAsIntegerArray passed the slot VALUE to
  BNMediumLevelILGetOperandList instead of the operand index, returning a
  garbage list; now mirrors GetOperandAsIntegerMap.

KNOWN PARITY GAP (documented in the table header): the call/syscall/intrinsic
family lists an output/params/output_dest_memory operand whose value is reached
by unwrapping a CallOutput/CallParam sub-instruction expression
(BNMediumLevelILGetOperandList does not flatten across the parent->sub
boundary). Those derived operands are omitted; the flat-readable operands are
included. Sub-instruction navigation to close the gap is tracked as a follow-up.

VERSION NOTE: the C# MediumLevelILOperation enum was generated against a newer
binaryninja-api than the installed core (5.3.9757 / aa25bfc). The table matches
the installed core (140 ops). 11 enum members the core never emits have no row
(empty DetailedOperands); the one rename (installed MLIL_CALL_OUTPUT == C#
MLIL_VAR_OUTPUT) is handled in the generator.

E2E + consistency tests live in the separate sharp-binaryninja-tests harness.
@tinysec tinysec merged commit 42e1e9e into master Jul 9, 2026
1 check passed
@tinysec tinysec deleted the feat/mlil-navigation branch July 9, 2026 06:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant