Binary Ninja Blog

Debugger Conditional Breakpoints and the Expression Parser That Backs Them

In Binary Ninja’s debugger, when you set a breakpoint and add a condition like rax == 0x1234, it just works. Let’s take a look into how that works and what sorts of conditions you can use.

This post tells the story of the conditional breakpoint with a side-quest to explore Binary Ninja’s expression parser which is the feature that makes it possible.

It Started with Navigation

If you’ve used Binary Ninja, you’ve probably pressed G to open the navigation dialog and typed in a function name. But did you know that dialog is powered by a full expression parser? This means you can type main + 0x10 to navigate 16 bytes past the start of main, or .text + 0x100 to jump to an offset within a section, or even [.data + 0x20] to dereference a pointer.

The expression parser can do a lot more than you would probably guess. Here’s some examples:

Expression Parser Capabilities

Feature Example Description
Arithmetic main + 0x10 Navigate 16 bytes after main
Sections .text + 0x100 Offset into a section
Symbols data_00005000 Unnamed data variables
Dereference [.data + 0x20] Read pointer at address
Size suffix [.data + 0x20].q Read 8 bytes (quadword)
Special values $here, $start, $end Current address, file boundaries

The supported operators include arithmetic (+, -, *, /, %), bitwise operations (&, |, ^, ~), comparisons (==, !=, >, <, >=, <=), and grouping with parentheses.

For memory dereferences, you can specify the size: [expr].b for a byte, [expr].w for a word, [expr].d for a dword, and [expr].q for a quadword. Without a suffix, it reads an address-sized value.

Numbers default to hexadecimal, but you can use 0n10 for decimal or 010 for octal when needed.

For the complete specification, see the parse_expression API documentation.

Making It Dynamic: Magic Values

The expression parser becomes even more powerful during debugging thanks to “magic values” which are name-value pairs that can be registered at runtime.

When you’re in a debug session, the debugger automatically registers all CPU registers (rax, rbx, rsp, rbp, rip, etc.) and module bases (kernel32, ntdll, libc, etc.) into the expression parser. This enables some useful workflows:

  • Type rbp - 0x20 to navigate directly to a stack variable — no manual calculation needed
  • Type kernel32 + 0x1000 to navigate into a loaded module, even with ASLR
  • Type rsp to jump straight to the stack pointer

Navigate dialog with register expression

A quick note: historically, register names required a $ prefix (e.g., $rax). Register names can now be used directly, as in rax.

Plugin authors can take advantage of this system too. The add_expression_parser_magic_value API lets you register custom values. Imagine registering heap chunk addresses or TLS slots that users can then reference directly in expressions.

The 500-Line Feature

Conditional breakpoint support landed in December 2025, thanks to a PR from community contributor 3rdit. The entire feature (condition evaluation, UI, and API) took about 500 lines of code.

How is it possible to implement such a major feature in just 500 lines? The secret is that the condition evaluation uses the expression parser we just discussed. Here’s a simplified version of how it works:

bool DebuggerController::EvaluateBreakpointCondition(uint64_t address)
{
    const std::string condition = m_state->GetBreakpoints()->GetConditionAbsolute(address);
    if (condition.empty())
        return true;  // No condition means always stop

    // Use the expression parser to evaluate the condition
    uint64_t result = 0;
    std::string error;
    if (!BinaryView::ParseExpression(GetData(), condition, result, address, error))
        return true;  // Parse error, stop to be safe

    return result != 0;  // Non-zero means condition is true
}

The debugger simply calls ParseExpression on the condition string. If the result is non-zero, the condition is true and the debugger stops. That’s it. All the heavy lifting including parsing the expression, reading register values, performing arithmetic and comparisons is handled by the expression parser.

Because the condition is evaluated at the debugger core level rather than the adapter level, the same expression syntax is available across supported debugger adapters. The register and module names in an expression still depend on the target.

But Wait — Comparison Operators?

You might be wondering: how does the expression parser handle conditions like rax == 0x1234? After all, it was originally a navigation feature. What does a comparison even mean in that context?

Back in December 2022, while I was adding the magic value support for register values, I also added comparison operators to the expression parser: ==, !=, >, <, >=, <=. These operators return 1 if the condition is true, 0 otherwise.

I was already thinking ahead for conditional breakpoints. The expression parser already knew how to read register values and dereference memory locations making it a perfect match for evaluating breakpoint conditions. Adding comparison operators made it ready to use.

Then in December 2025, 3rdit reached out asking about adding conditional breakpoint support. I was excited — the foundation I’d laid three years earlier was finally going to be used. I told him that the expression parser was already there to support it so it shouldn’t be hard. He came back with PR #941, which was merged with little modification.

Thanks to 3rdit for the contribution!

How to Use Conditional Breakpoints

Here’s how to add a condition to a breakpoint.

Setting a Condition

  1. Add a breakpoint at the desired location
  2. Right-click the breakpoint in the Breakpoints widget
  3. Select “Edit Condition…”
  4. Enter your condition expression
  5. Click OK

Edit condition dialog

You can also view and edit conditions in the “Condition” column of the Breakpoints widget.

Breakpoint condition in the breakpoint widget

Example Conditions

These examples use x86-64 register names. Use the register names for your target architecture.

Condition When to stop
rax == 0x1234 rax equals a specific value
rax != 0 rax is non-zero
rcx < 0n10 rcx is less than decimal 10
[rsp] == 0 The address-sized value at the stack pointer is zero
rdi == rbp - 0x20 rdi equals a stack address

Remember that unprefixed numbers are hexadecimal: 10 means sixteen; 0n10 means ten.

Using the API

In Binary Ninja’s Python console, dbg is the debugger controller for the current view. These examples assume you have already added breakpoints at the specified locations. Replace the addresses and module name with values from your target.

from binaryninja.debugger import ModuleNameAndOffset

# Set a condition
dbg.set_breakpoint_condition(0x401000, "rax == 0x1234")

# With module-relative address
dbg.set_breakpoint_condition(ModuleNameAndOffset("myprogram", 0x1000), "rdi != 0")

# Get current condition
condition = dbg.get_breakpoint_condition(0x401000)

# Clear condition (set to empty string)
dbg.set_breakpoint_condition(0x401000, "")

For more details, see the conditional breakpoints documentation.

What’s Next

The expression parser continues to evolve. One possible extension would be a string comparison function such as streq. For example, a hypothetical streq(rdi, "password") could stop when rdi points to that string. This is an idea for future work, not syntax you can use today.

We’d love to hear your feedback on what would make conditional breakpoints even more useful for your workflows.

Next time you press G in Binary Ninja, remember that you have a full expression parser at your fingertips. Skip the calculator and just use rbp - 0x20 during your next debugging session.

References

Documentation

Implementation