Skip to main content

Overview

Binary Ninja provides powerful debugging capabilities through its Python API and debugger plugin architecture. This guide demonstrates how to work with the debugger, set breakpoints, and automate debugging workflows.

Debugger Architecture

Binary Ninja supports multiple debugger backends:

LLDB

Native debugging on macOS and Linux

GDB

GNU debugger integration

Windows Debugger

Windows kernel and user-mode debugging

Setting Breakpoints

Set breakpoints programmatically at specific addresses or functions.
from binaryninja import load

with load("/path/to/binary") as bv:
    # Get debugger instance
    dbg = bv.debugger

    # Set breakpoint at address
    dbg.add_breakpoint(0x401000)

    # Set breakpoint at function
    func = bv.get_function_at(0x401234)
    if func:
        dbg.add_breakpoint(func.start)

    # Set breakpoint with condition
    dbg.add_breakpoint(0x401567, enabled=True)

Writing Breakpoint Instructions

Modify binary code to insert breakpoint instructions directly.
from binaryninja.plugin import PluginCommand
from binaryninja.log import log_error

def write_breakpoint(view, start, length):
    """
    Fill region with breakpoint instructions for the target architecture.
    Demonstrates cross-architecture breakpoint support.
    """
    # Architecture-specific breakpoint instructions
    bkpt_str = {
        "x86": "int3",
        "x86_64": "int3",
        "armv7": "bkpt",
        "aarch64": "brk #0",
        "mips32": "break"
    }

    if view.arch.name not in bkpt_str:
        log_error(f"Architecture {view.arch.name} not supported")
        return

    # Assemble breakpoint instruction
    bkpt, err = view.arch.assemble(bkpt_str[view.arch.name])
    if bkpt is None:
        log_error(err)
        return

    # Write breakpoint instruction(s) to region
    view.write(start, bkpt * (length // len(bkpt)))

# Register as plugin command for selected ranges
PluginCommand.register_for_range(
    "Convert to breakpoint",
    "Fill region with breakpoint instructions.",
    write_breakpoint
)
Source: ~/workspace/source/python/examples/breakpoint.py:1
This technique modifies the binary directly. Make sure to work on a copy or use the debugger’s software breakpoints instead.

Debugger Control Flow

Control program execution during debugging.
1

Start Debugging Session

from binaryninja import load

with load("/path/to/binary") as bv:
    dbg = bv.debugger

    # Launch target
    dbg.launch()

    # Or attach to running process
    # dbg.attach(pid=1234)
2

Control Execution

# Continue execution
dbg.go()

# Step one instruction
dbg.step_into()

# Step over function calls
dbg.step_over()

# Run until function returns
dbg.step_return()

# Pause execution
dbg.pause()
3

Inspect State

# Get current instruction pointer
rip = dbg.reg_value("rip")
print(f"Current IP: {rip:#x}")

# Read register values
rax = dbg.reg_value("rax")
rbx = dbg.reg_value("rbx")

# Read memory
data = dbg.read_memory(0x404000, 16)

# Get current stack
rsp = dbg.reg_value("rsp")
stack_data = dbg.read_memory(rsp, 0x100)
4

Modify State

# Set register value
dbg.set_reg_value("rax", 0x1234)

# Write memory
dbg.write_memory(0x404000, b"\x41\x42\x43\x44")

# Modify instruction pointer
dbg.set_reg_value("rip", 0x401234)

Automated Analysis During Debugging

Combine debugging with Binary Ninja’s analysis capabilities.
from binaryninja import load, BackgroundTaskThread
from binaryninja.enums import LowLevelILOperation

class AutomatedDebugger(BackgroundTaskThread):
    def __init__(self, bv):
        BackgroundTaskThread.__init__(self, 'Automated Debugging', True)
        self.bv = bv

    def run(self):
        dbg = self.bv.debugger

        # Set breakpoints at all indirect calls
        for func in self.bv.functions:
            for block in func.low_level_il:
                for instr in block:
                    if instr.operation == LowLevelILOperation.LLIL_CALL:
                        # Check if call target is indirect
                        if not isinstance(instr.dest, int):
                            print(f"Indirect call at {instr.address:#x}")
                            dbg.add_breakpoint(instr.address)

        # Launch and run
        dbg.launch()
        dbg.go()

        # When breakpoint hit, log and continue
        while dbg.is_running:
            if dbg.is_stopped:
                rip = dbg.reg_value("rip")
                print(f"Hit breakpoint at {rip:#x}")

                # Log call target
                func = self.bv.get_function_at(rip)
                if func:
                    llil = func.low_level_il.get_instruction_start(rip)
                    if llil is not None:
                        instr = func.low_level_il[llil]
                        target = dbg.reg_value(instr.dest.name)
                        print(f"  Indirect call target: {target:#x}")

                dbg.go()
Use BackgroundTaskThread for long-running debugging tasks to prevent UI freezing.

Debugging Plugin Example

Create a plugin that uses the debugger to collect runtime information.
from binaryninja import PluginCommand, BackgroundTaskThread
from binaryninja.interaction import show_message_box

class RuntimeTracer(BackgroundTaskThread):
    """Trace function calls at runtime"""

    def __init__(self, bv, func):
        BackgroundTaskThread.__init__(self, 'Runtime Tracer', True)
        self.bv = bv
        self.func = func
        self.call_trace = []

    def run(self):
        dbg = self.bv.debugger

        # Set breakpoint at function entry
        dbg.add_breakpoint(self.func.start)

        # Set breakpoints at all call sites
        for ref in self.func.call_sites:
            dbg.add_breakpoint(ref.address)

        dbg.launch()
        dbg.go()

        while dbg.is_running:
            if dbg.is_stopped:
                rip = dbg.reg_value("rip")

                # Log call
                self.call_trace.append({
                    'address': rip,
                    'registers': {
                        'rax': dbg.reg_value("rax"),
                        'rdi': dbg.reg_value("rdi"),
                        'rsi': dbg.reg_value("rsi")
                    }
                })

                dbg.go()

        # Display results
        self.show_results()

    def show_results(self):
        output = "Call Trace:\n\n"
        for entry in self.call_trace:
            output += f"Address: {entry['address']:#x}\n"
            for reg, val in entry['registers'].items():
                output += f"  {reg} = {val:#x}\n"
            output += "\n"

        show_message_box("Runtime Trace Results", output)

def start_tracer(bv, func):
    tracer = RuntimeTracer(bv, func)
    tracer.start()

PluginCommand.register_for_function(
    "Trace Runtime Calls",
    "Trace function calls at runtime",
    start_tracer
)

Memory Snapshots

Capture and compare memory state at different execution points.
class MemorySnapshot:
    def __init__(self, dbg, ranges):
        """
        Take snapshot of memory regions

        Args:
            dbg: Debugger instance
            ranges: List of (address, size) tuples
        """
        self.snapshots = {}
        for addr, size in ranges:
            self.snapshots[addr] = dbg.read_memory(addr, size)

    def diff(self, other):
        """Compare with another snapshot"""
        differences = []

        for addr, data in self.snapshots.items():
            if addr in other.snapshots:
                other_data = other.snapshots[addr]
                for i, (b1, b2) in enumerate(zip(data, other_data)):
                    if b1 != b2:
                        differences.append({
                            'address': addr + i,
                            'before': b1,
                            'after': b2
                        })

        return differences

# Usage
dbg = bv.debugger
dbg.launch()
dbg.go()

# Take snapshot at first breakpoint
snapshot1 = MemorySnapshot(dbg, [(0x404000, 0x1000)])

dbg.go()

# Take snapshot at second breakpoint
snapshot2 = MemorySnapshot(dbg, [(0x404000, 0x1000)])

# Compare
differences = snapshot1.diff(snapshot2)
for diff in differences:
    print(f"{diff['address']:#x}: {diff['before']:#x} -> {diff['after']:#x}")

Scripting Common Debug Tasks

def dump_function_args(dbg, func, arch):
    """Dump function arguments at entry"""
    # Get calling convention
    cc = func.calling_convention
    if cc is None:
        cc = bv.platform.default_calling_convention

    # Read argument registers
    args = []
    for i, reg in enumerate(cc.int_arg_regs):
        value = dbg.reg_value(reg)
        args.append(f"arg{i} ({reg}) = {value:#x}")

    print(f"Function {func.name} called with:")
    for arg in args:
        print(f"  {arg}")

# Set breakpoint at function entry
func = bv.get_functions_by_name("target_function")[0]
dbg.add_breakpoint(func.start)

# When hit, dump args
dump_function_args(dbg, func, bv.arch)

Integration with Analysis

Update Binary Ninja’s analysis based on runtime information.
from binaryninja import BackgroundTaskThread

class AnalysisUpdater(BackgroundTaskThread):
    def __init__(self, bv):
        BackgroundTaskThread.__init__(self, 'Updating Analysis', True)
        self.bv = bv

    def run(self):
        dbg = self.bv.debugger

        # Resolve indirect calls at runtime
        for func in self.bv.functions:
            for ref in func.call_sites:
                # Check if call is indirect
                llil = func.low_level_il.get_instruction_start(ref.address)
                if llil is not None:
                    instr = func.low_level_il[llil]
                    if instr.operation == LowLevelILOperation.LLIL_CALL:
                        if not isinstance(instr.dest, int):
                            # Set breakpoint
                            dbg.add_breakpoint(ref.address)

        dbg.go()

        while dbg.is_running:
            if dbg.is_stopped:
                rip = dbg.reg_value("rip")
                func = self.bv.get_function_at(rip)

                if func:
                    # Get actual call target
                    llil = func.low_level_il.get_instruction_start(rip)
                    instr = func.low_level_il[llil]
                    target = dbg.reg_value(instr.dest.name)

                    # Update analysis with discovered target
                    func.set_user_indirect_branches(
                        rip,
                        [(self.bv.arch, target)]
                    )
                    print(f"Resolved indirect call at {rip:#x} -> {target:#x}")

                dbg.go()

        # Reanalyze with new information
        self.bv.update_analysis_and_wait()
Source: ~/workspace/source/python/examples/update_analysis.py:1

Next Steps

Binary Analysis

Combine debugging with static analysis techniques

Data Flow Analysis

Use runtime information to improve data flow analysis

Build docs developers (and LLMs) love