How does a debugger break code?

I'm wondering what the mechanism is that actually lets the debugger stop the program being debugged at a per-line or per-instruction basis.

Let's say I'm debugging a program called main.exe, with the necessary debugging symbols. Running a debugger is more than just an emulation of each instruction of main, because my main.exe process still runs as a separate process from gdb.exe. But the debugger must somehow be hijacking the execution of main.exe at a per-instruction basis. How does this happen? Clearly there must be some interprocess communication, but how does the debugger hijack the execution of another program? Are there any helpful links that could explain what is happening here? If not, I suppose I'll just have to start looking at the source code of GDB... that will be.. fun*.

I've tried to search this, but just get a bunch of debugging tutorials, so I'm not sure what keywords to actually use.

This is more for my own curiosity than anything else. I'm trying to write a program which will invoke the debugger and print out every line that the program reaches. I know I can do this already by using gdb as a middle-man**, but I'm just curious as to how it actually controls the execution of the program being debugged.

* https://sourceware.org/git/gitweb.cgi?p=binutils-gdb.git;a=tree
** e.g. debugging a program with "foob" function in it.
(gdb) file main.exe
(gdb) break foob
(gdb) run
Last edited on
the debugger is running the code one assembly statement at a time by sending that to the CPU, similar to the 'emit' assembler statement that is supported by visual studio.
because it is running it one command at a time it can stop when and where you ask it to.

these days, the 'cpu' may be a specialized VM and it may be much more sophisticated. That was the 'old' way that i know of doing it.

compile a smallish program to assembly with comments, and you will see that each c++ statement becomes a block of assembly code. you can use that to track what statement you are executing currently. you would run that whole block if executing 'statement by statement' in a debugger, or use them to know where to 'break point'. Some 'statements' can't be set to break; visual studio has a lot of limits on this or used to. EG (from memory) I don't think int x= 3; can have a breakpoint, and the reason is, that makes no code, it just loads the data segment of the program's memory when looked at it from the machine side.
Last edited on
the debugger is running the code one assembly statement at a time by sending that to the CPU
But how does the debugger program hijack the host program (main.exe) to tell it to stop at a certain instruction/line? That's what I don't get. Is the debugger running the code, or is it telling the host program how to run its own code? When I start gdb.exe, and have run+break main.exe, I can see that main.exe is its own process in the Task Manager, so that's leading me to believe that main.exe is still running mostly on its own, but gdb is somehow hijacking its execution. Maybe that's wrong to assume.

I guess what I'm really asking is how the hijacking (commanding the other program to stop/start its execution) is implemented.

int x = 3; can certainly have a breakpoint, at least when you put on debugging symbols. You can try it yourself by redirecting this to gdb:

1
2
3
4
5
6
int main()
{
    int a = 22;
    int x = 32; // line 4
    int y = 42;
}

g++ -g main.cpp -o main


input to gdb:
file main.exe
break 4
run
info line


gdb output:
(gdb) file main.exe
Reading symbols from main.exe...done.
(gdb) break 4
Breakpoint 1 at 0x401584: file main.cpp, line 4.
(gdb) run
Starting program: {...}\main.exe
[New Thread 20780.0x9538]
[New Thread 20780.0x2444]
[New Thread 20780.0x2b54]
[New Thread 20780.0x2734]

Thread 1 hit Breakpoint 1, main () at main.cpp:4
4           int x = 32; // line 4
(gdb) info line
Line 4 of "main.cpp" starts at address 0x401584 <main()+20> and ends at 0x40158b <main()+27>.
(gdb)

Last edited on
There are several aspects but they are all relatively straightforward. But primarily it is that the caller requests debug privilege on the callee.

What this means is that the OS allows various exceptions and/or signals generated by the callee program to be trapped by the caller. So, for example, if the program tries to execute an illegal instruction the OS stops the callee and tells the caller what it knows about it. This includes register states and location.

It also means that the caller has process read/write privilege on the callee. This allows the caller to examine the callee’s state (memory) and even modify it.

There are also a variety of ways of setting a breakpoint. Most convenient is hardware support, which makes it very easy to step through a program without actually modifying the callee’s binary state.

All of this is easy enough because the caller created the callee, and thus knows everything about it.

This is a very simplified overview (and about all I actually care to know about it myself); implementation details vary for OS, CPU, and compiler.
Ah, that must be what the "Debug programs" privilege in User Rights Management in Windows is... I never connected that. The other part of your post has also given me a few more things to try searching for.

I also found this article, titled How debuggers really work
https://opensource.com/article/18/1/how-debuggers-really-work
which touches on what you just mentioned.

And if I download the source at https://www.gnu.org/software/gdb/download/ and grep it,
grep -r "TOKEN_ADJUST_PRIVILEGES" -C 11 .

I find the set_process_privilege function, and it's being called like
if (set_process_privilege (SE_DEBUG_NAME, TRUE) < 0)

Other things of interest I found are:
/* Implement the supports_stopped_by_hw_breakpoints method.  */
which is also what you mentioned.

So I guess I'll just dive deeper into this, or perhaps just stick with using gdb as an interface, since it seems pretty capable. Eventually I'll also want to do this in C#/.NET.
Last edited on
Topic archived. No new replies allowed.