alexyang.dev

How do breakpoints work?

The debugger is a tool designed to inspect programs as they're running. To this end, one of the debugger's most important capabilities is being able to pause a program's execution at any point so that its state can be observed. In debugger parlance, the point at which the program is paused is called a "breakpoint."

In this post, I want to answer the following question: how exactly does the debugger pause the execution of a program? In other words, how do breakpoints work?

Breakpoints are implemented differently across different CPU architectures and operating systems, so in this post I'll just talk about Linux on x86 CPUs. The basic ideas can be extrapolated to other environments and architectures.

The x86 instruction set has an interesting instruction called INT3. It's just a single byte: 0xCC.

When a process executes INT3, it causes the CPU to emit an interrupt. All interrupts are handled synchronously by the kernel, so execution proceeds into kernel code. At this point the kernel is able to set the status of the interrupted process to "stopped" and eventually yield execution to other processes that are still running.

In other words, INT3 basically says to the kernel, "stop running me now."

When run normally, a program probably won't contain an INT3. But a debugger can inject an INT3 into a program at runtime to effectively pause the program's execution at any point the debugger chooses.

How is the debugger able to just modify the code of another running process? The debugger is itself a process, and by the principle of process isolation, it shouldn't be able to read or write another process's memory, right? It turns out that a process is usually permitted to read or write the memory of its child process. On Linux, this is done using a system call called ptrace.

Just being able to write to the memory of another process is sufficient to inject an INT3 into that process's code, because a process's code lives in its virtual memory. In practice, the debugger does this by overwriting the target instruction with an INT3, waiting for the process to hit the INT3, then putting the original instruction back before continuing.

To sum up, here's how a simple Linux x86 debugger might work:

  1. Spawn a child process with the target program
  2. Overwrite the target instruction with INT3
  3. Set the child process to "running"
  4. Sleep until the child process enters the "stopped" state
  5. Wake up and inspect the execution state of the child process

This may raise some questions in your mind, such as:

  1. How does the debugger get notified (i.e. wake up) when the execution of the traced process has been paused?
  2. How do you find out where in memory to write the INT3?
  3. How do you find the instruction that corresponds to a line of source code?
  4. ASLR makes this hard. Can it be turned off?

I won't answer these questions in detail here; I just wanted to go over some basic concepts. If you want a comprehensive resource, I highly recommend the 2025 book Building a Debugger by Sy Brand.

Hints for the above questions:

  1. man 2 wait
  2. man 5 proc_pid_maps
  3. DWARF
  4. man 2 personality