Overview
Binary Ninja’s Intermediate Language (IL) representations provide architecture-independent views of code. This guide demonstrates how to parse, visit, and analyze IL operations using real examples from the Binary Ninja API.
IL Levels
Binary Ninja provides multiple IL representations, each with different levels of abstraction:
LLIL Low Level IL - Close to assembly, architecture-specific details normalized
MLIL Medium Level IL - Variables instead of registers, simplified expressions
HLIL High Level IL - C-like representation with control flow structures
Accessing IL Functions
Low Level IL
Medium Level IL
High Level IL
func = bv.get_function_at( 0x 401000 )
llil = func.low_level_il
# Iterate through basic blocks
for block in llil.basic_blocks:
for instr in block:
print ( f " { instr.instr_index } @ { instr.address :#x} : { instr } " )
Parsing LLIL Instructions
The Low Level IL parser example from Binary Ninja source demonstrates how to recursively parse IL expression trees.
Access IL Instruction
Ref < LowLevelILFunction > il = func -> GetLowLevelIL ();
if ( ! il) {
printf ( "Function does not have LLIL \n " );
return ;
}
// Loop through basic blocks
for ( auto & block : il -> GetBasicBlocks ()) {
for ( size_t instrIndex = block -> GetStart ();
instrIndex < block -> GetEnd (); instrIndex ++ ) {
LowLevelILInstruction instr = ( * il)[instrIndex];
// Process instruction
}
}
Display Instruction Operation
// Get the formatted text representation
vector < InstructionTextToken > tokens;
il -> GetInstructionText (func, func -> GetArchitecture (), instrIndex, tokens);
printf ( " %zu @ 0x %llx " , instrIndex, instr . address );
for ( auto & token : tokens)
printf ( " %s " , token . text . c_str ());
printf ( " \n " );
Parse Operands Recursively
void PrintILExpr ( const LowLevelILInstruction & instr , size_t indent ) {
PrintIndent (indent);
PrintOperation ( instr . operation );
printf ( " \n " );
indent ++ ;
for ( auto & operand : instr . GetOperands ()) {
switch ( operand . GetType ()) {
case IntegerLowLevelOperand:
PrintIndent (indent);
printf ( "int 0x %llx \n " , operand . GetInteger ());
break ;
case RegisterLowLevelOperand:
PrintIndent (indent);
printf ( "reg " );
PrintRegister ( instr . function , operand . GetRegister ());
printf ( " \n " );
break ;
case ExprLowLevelOperand:
// Recursively parse subexpression
PrintILExpr ( operand . GetExpr (), indent);
break ;
case SSARegisterLowLevelOperand:
PrintIndent (indent);
printf ( "ssa reg " );
PrintRegister ( instr . function , operand . GetSSARegister (). reg );
printf ( "# %zu \n " , operand . GetSSARegister (). version );
break ;
// ... handle other operand types
}
}
}
LLIL Operation Types
Common LLIL operations you’ll encounter:
Operation Description Example LLIL_SET_REGRegister assignment rax = 0x1234LLIL_LOADMemory read rax = [rbp - 8]LLIL_STOREMemory write [rsp] = raxLLIL_CALLFunction call call(0x401000)LLIL_SYSCALLSystem call syscallLLIL_ADDAddition rax + rbxLLIL_CMP_EEquality comparison rax == 0LLIL_IFConditional branch if (condition)
Using IL Visitors
Visitors provide a powerful way to find specific patterns in IL code without manual recursion.
Finding Constants
Finding Memory Loads
Python Visitor
// Example: Find all constants in an instruction
instr . VisitExprs ([ & ]( const LowLevelILInstruction & expr ) {
switch ( expr . operation ) {
case LLIL_CONST :
case LLIL_CONST_PTR :
case LLIL_EXTERN_PTR :
printf ( "Found constant 0x %llx \n " , expr . GetConstant ());
return false ; // Stop visiting this branch
default :
break ;
}
return true ; // Continue visiting subexpressions
});
Return true to continue visiting child expressions, false to stop.
// Example: Find loads from constant addresses
instr . VisitExprs ([ & ]( const LowLevelILInstruction & expr ) {
if ( expr . operation == LLIL_LOAD) {
auto src = expr . GetSourceExpr < LLIL_LOAD > ();
if ( src . operation == LLIL_CONST_PTR) {
printf ( "Loading from address 0x %llx \n " ,
src . GetConstant < LLIL_CONST_PTR > ());
return false ;
}
else if ( src . operation == LLIL_EXTERN_PTR) {
printf ( "Loading from address 0x %llx \n " ,
src . GetConstant < LLIL_EXTERN_PTR > ());
return false ;
}
}
return true ;
});
from binaryninja.enums import LowLevelILOperation
def find_constants ( instr ):
constants = []
def visitor ( expr ):
if expr.operation in [
LowLevelILOperation. LLIL_CONST ,
LowLevelILOperation. LLIL_CONST_PTR
]:
constants.append(expr.constant)
return False
return True
instr.visit(visitor)
return constants
# Usage
for instr in func.low_level_il:
consts = find_constants(instr)
if consts:
print ( f " { instr.address :#x} : { consts } " )
Parsing MLIL Instructions
Medium Level IL uses variables instead of registers and simplifies expressions.
Access MLIL Function
Ref < MediumLevelILFunction > il = func -> GetMediumLevelIL ();
if ( ! il) {
printf ( "Function does not have MLIL \n " );
return ;
}
Iterate Instructions
for ( auto & block : il -> GetBasicBlocks ()) {
for ( size_t instrIndex = block -> GetStart ();
instrIndex < block -> GetEnd (); instrIndex ++ ) {
MediumLevelILInstruction instr = ( * il)[instrIndex];
// Display instruction
vector < InstructionTextToken > tokens;
il -> GetInstructionText (func, func -> GetArchitecture (),
instrIndex, tokens);
printf ( " %zu @ 0x %llx " , instrIndex, instr . address );
for ( auto & token : tokens)
printf ( " %s " , token . text . c_str ());
printf ( " \n " );
}
}
Parse MLIL Operands
void PrintILExpr ( const MediumLevelILInstruction & instr , size_t indent ) {
for ( auto & operand : instr . GetOperands ()) {
switch ( operand . GetType ()) {
case IntegerMediumLevelOperand:
printf ( "int 0x %llx \n " , operand . GetInteger ());
break ;
case VariableMediumLevelOperand:
printf ( "var " );
PrintVariable ( instr . function , operand . GetVariable ());
printf ( " \n " );
break ;
case SSAVariableMediumLevelOperand:
printf ( "ssa var " );
PrintVariable ( instr . function ,
operand . GetSSAVariable (). var );
printf ( "# %zu \n " , operand . GetSSAVariable (). version );
break ;
case ExprMediumLevelOperand:
PrintILExpr ( operand . GetExpr (), indent);
break ;
}
}
}
MLIL Operation Types
Operation Description Example MLIL_SET_VARVariable assignment var_10 = 0x1234MLIL_LOADMemory read var_8 = [rbp - 0x10]MLIL_STOREMemory write [var_10] = var_8MLIL_CALLFunction call call(sub_401000)MLIL_VARVariable reference var_10MLIL_VAR_SSASSA variable var_10#1MLIL_IFConditional if (var_8 == 0)
Templated Accessors
Binary Ninja provides type-safe templated accessors for efficiently parsing specific IL operations.
// Type-safe access to LOAD instruction components
if ( expr . operation == MLIL_LOAD) {
// GetSourceExpr is templated on the operation type
auto src = expr . GetSourceExpr < MLIL_LOAD > ();
if ( src . operation == MLIL_CONST_PTR) {
uint64_t addr = src . GetConstant < MLIL_CONST_PTR > ();
printf ( "Loading from constant address 0x %llx \n " , addr);
}
}
// Type-safe access to binary operations
if ( expr . operation == MLIL_ADD) {
auto left = expr . GetLeftExpr < MLIL_ADD > ();
auto right = expr . GetRightExpr < MLIL_ADD > ();
// Process addition operands
}
Templated accessors require you to specify the operation type. Make sure the operation matches before calling templated methods.
Both LLIL and MLIL have Static Single Assignment (SSA) forms that make data flow analysis easier.
# Access SSA form
llil_ssa = func.low_level_il.ssa_form
mlil_ssa = func.medium_level_il.ssa_form
# SSA instructions have version numbers
for instr in mlil_ssa:
if instr.operation == MediumLevelILOperation. MLIL_SET_VAR_SSA :
var = instr.dest
print ( f " { var.var.name } # { var.version } = { instr.src } " )
SSA Phi Nodes
from binaryninja.enums import MediumLevelILOperation
for instr in mlil_ssa:
if instr.operation == MediumLevelILOperation. MLIL_VAR_PHI :
dest = instr.dest
sources = instr.src
print ( f " { dest.var.name } # { dest.version } = φ( { sources } )" )
Complete LLIL Parser Example
Here’s the complete LLIL parser from Binary Ninja source:
~/workspace/source/examples/llil_parser/src/llil_parser.cpp
Ref < BinaryView > bv = BinaryNinja :: Load ( argv [ 1 ]);
if ( ! bv || bv -> GetTypeName () == "Raw" ) {
fprintf (stderr, "Input file does not appear to be an executable \n " );
return - 1 ;
}
// Go through all functions in the binary
for ( auto & func : bv -> GetAnalysisFunctionList ()) {
Ref < Symbol > sym = func -> GetSymbol ();
if (sym)
printf ( "Function %s : \n " , sym -> GetFullName (). c_str ());
else
printf ( "Function at 0x %llx : \n " , func -> GetStart ());
Ref < LowLevelILFunction > il = func -> GetLowLevelIL ();
if ( ! il) {
printf ( " Does not have LLIL \n\n " );
continue ;
}
for ( auto & block : il -> GetBasicBlocks ()) {
for ( size_t instrIndex = block -> GetStart ();
instrIndex < block -> GetEnd (); instrIndex ++ ) {
LowLevelILInstruction instr = ( * il)[instrIndex];
// Display instruction
vector < InstructionTextToken > tokens;
il -> GetInstructionText (func, func -> GetArchitecture (),
instrIndex, tokens);
printf ( " %zu @ 0x %llx " , instrIndex, instr . address );
for ( auto & token : tokens)
printf ( " %s " , token . text . c_str ());
printf ( " \n " );
// Parse IL tree
PrintILExpr (instr, 2 );
// Find constants using visitor
instr . VisitExprs ([ & ]( const LowLevelILInstruction & expr ) {
switch ( expr . operation ) {
case LLIL_CONST :
case LLIL_CONST_PTR :
case LLIL_EXTERN_PTR :
printf ( " Found constant 0x %llx \n " ,
expr . GetConstant ());
return false ;
default :
break ;
}
return true ;
});
}
}
printf ( " \n " );
}
Next Steps
Data Flow Analysis Use IL to track data flow and perform advanced analysis
Binary Analysis Learn fundamental binary analysis workflows