DEV Community

bekoo
bekoo

Posted on

Working on Single-Step Breakpoints in a Debugger

Introduction

Single-step breakpoints are fundamental debugging mechanism that allows execution to pause after every single instruction. Unlike hardware breakpoints which trigger at a specific address, single-step mode traces every instruction sequentially.

In this post, we will see these breakpoints in detail with NASM + QEMU.

Single-Step Breakpoints and Trap Flag

A debugger can determine single-step breakpoints with DR6 register. DR6 register holds BS (Single-Step Bit) and a debugger can use this bit. BS is 14 bit of DR6 register:

But that's not all. This bit just shows the breakpoint is triggered in 'single-step progress'. We can't change if we want 'single-step'.

TF (Trap Flag) is the real mechanism behind single-step execution. It lives at bit 8 of the RFLAGS register and cannot be set directly with a MOV instruction. Instead, we manipulate it indirectly through the stack.

When a #DB exception occurs, the CPU pushes the current RFLAGS onto
the stack as part of the exception frame. This saved copy is what IRETQ will restore when returning from the handler. By modifying bit 8 of this saved RFLAGS before executing IRETQ, we control whether single-step continues after the handler returns.

The CPU automatically clears TF before delivering the #DB exception. This is by design — without this behavior, the handler itself would be single-stepped, causing an infinite recursive exception loop. By clearing TF on entry and only restoring it in the saved RFLAGS, we ensure single-step applies only to the code being debugged, not to the handler.

Simple Project in NASM

Here's an example from my NASM Project:

DbHandler:
    push rax
    push rbx
    push rcx
    push rdx
    push rsi
    push rdi
    push rbp
    push r8
    push r9
    push r10
    push r11
    push r12
    push r13
    push r14
    push r15

    mov rbx, dr6

    ; Start single-step progress
    test rbx, (1 << 0)
    jnz .hw_bp_hit

    ; BS set → single-step fired
    ; After the first execution of #DB (with TF=1),
    ; CPU will set BS bit of DR6
    test rbx, (1 << 14)
    jnz .single_step


    jmp Exit

.hw_bp_hit:
    xor  rax, rax
    mov  dr6, rax
    xor  rax, rax
    mov  dr7, rax

    ; Enable TF flag and exit (CPU will generate #DB again)
    or   qword [rsp + 136], (1 << 8)
    jmp  Exit

.single_step:
    ; In each single-step, write the value of RIP 
    mov rax, [rsp + 120]
    call serial_hex64

    dec qword [step_count]
    jz .stop_stepping

    ; Pass 1 to TF (for Single-Step)
    or qword [rsp + 136], (1 << 8)
    jmp Exit

.stop_stepping:
    ; Pass 0 to stop
    and qword [rsp + 136], ~(1 << 8)
    xor rax, rax
    mov dr6, rax
    jmp Exit

Exit:
    pop r15
    pop r14
    pop r13
    pop r12
    pop r11
    pop r10
    pop r9
    pop r8
    pop rbp
    pop rdi
    pop rsi
    pop rdx
    pop rcx
    pop rbx
    pop rax
    iretq
Enter fullscreen mode Exit fullscreen mode

The idea is simple: In the first execution of #DB exception, we set TF flag to 1 and exit from the handler. Then CPU generates #DB exception with BS=1. In the handler, we check this bit:

; After the first execution of #DB (with TF=1),
; CPU will set BS bit of DR6
test rbx, (1 << 14)
jnz .single_step
Enter fullscreen mode Exit fullscreen mode

In the .single-step, we just print the value of RIP, decrease the step_count and pass 1 to TF flag:

.single_step:
    ; In each single-step, write the value of RIP 
    mov rax, [rsp + 120]
    call serial_hex64

    ; step_count = 0x5
    dec qword [step_count]
    jz .stop_stepping

    ; Pass 1 to TF (for Single-Step)
    or qword [rsp + 136], (1 << 8)
    jmp Exit
Enter fullscreen mode Exit fullscreen mode

As we can see, the idea is simple. Here's the result:

I assigned the value 0x5 to the step_count variable, so each step is performed based on this value. Also my breakpoint is based on DebugTarget function:

DebugTarget:
    mov  rax, 0x1
    mov  rbx, 0x2
    add  rax, rbx
    mov  rcx, rax
    mov  rdx, 0x0
    ret
Enter fullscreen mode Exit fullscreen mode

Top comments (0)