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, so by the principle of process isolation, it shouldn't be able to tamper with another running process, right?
Stepping back a bit, recall that a process is just executing the copy of the code that is mapped in its virtual memory. Therefore, anything that can modify a process's memory, by extension, controls the code that is being executed by that process. It turns out that, on Linux, a parent process can usually modify a child process's memory using a system call called ptrace. What's more, ptrace can even modify write-protected memory regions; perfect for writing to the code region.
To sum up, here's how a simple x86 Linux userspace debugger might work:
This may raise some questions in your mind, such as:
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:
man 2 waitman 5 proc_pid_mapsman 2 personality