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
tinysec added a commit that referenced this pull request Jul 21, 2026
The C# MediumLevelILOperation enum was generated against a newer
binaryninja-api (dev branch) and carried 21 extra members the installed
core (5.3.9757 / python api revision aa25bfc) never emits. This shifted
every value from index ~15 onward by +2/+4/+8 and broke sub-instruction
dispatch -- GetOperandAsExpression returned the wrong typed wrapper
(e.g. MLILSysCallUntypedSSA for a native MLIL_CALL_OUTPUT_SSA), so the
cast in the new derived-operand readers threw InvalidCastException.

This PR realigns the enum to the installed core's 140-member ordering
byte-for-byte and closes the PR #42 derived-operand parity gap:

  * Enumeration/BNMediumLevelILOperation.cs -- rewritten to match the
    installed core (MLIL_CALL_OUTPUT at index 56, MLIL_CALL_SSA at 122,
    etc.); MLIL_VAR_OUTPUT is now MLIL_CALL_OUTPUT, and the
    ABS/BSWAP/CLS/CLZ/CTZ/POPCNT/RBIT/MAXS/MAXU/MINS/MINU members the
    installed core never emits are gone.

  * IL/ILOperandKind.cs -- 8 new sub-instruction-derived kinds
    (CallOutputVariables, CallParamExpressions, CallOutputSsaVariables,
    CallOutputSsaMemory, CallParamSsaExpressions, CallParamSsaMemory,
    MemoryIntrinsicOutputSsaVariables, MemoryIntrinsicOutputSsaMemory).
    Each fetches the sub-instruction at RawIndex via
    GetOperandAsExpression and reads one of its existing properties.

  * Struct/BNMediumLevelILInstruction.cs -- 8 new dispatch cases in
    ReadDetailedOperandByKind; FromExpressionIndex / count-table cleaned
    of dead ABS/BSWAP/etc. entries and renamed MLIL_VAR_OUTPUT to
    MLIL_CALL_OUTPUT.

  * MediumLevelIL/MediumLevelILDetailedOperandsTable.cs -- 10 table rows
    updated for the call/syscall/intrinsic/tailcall family so
    output/params/output_dest_memory/params_src_memory are now served
    instead of omitted.

  * MediumLevelIL/MLILTailCallUntyped.cs -- .Output and .Parameters now
    navigate the CallOutput / CallParam sub-instructions.

  * Handle/BNMediumLevelILFunction.cs -- EmitCallOutput uses
    MLIL_CALL_OUTPUT.

All 88 E2E tests pass.
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