Hacker News new | ask | show | jobs
by Decabytes 1203 days ago
I've always been curious about Debuggers. How do they work? How do they connect to a program and step through it. Why can't more compiled languages integrate debuggers inside of them so that you can debug the program without using a separate tool?

And why can't we create interfaces to debuggers so that other text editors can integrate with them, much like how we do we LSPs?

*EDIT*

Thanks for all the responses! I've now heard from multiple sources that debugging on Linux is unpleasant, and it seems like the whole process is challenging regardless of the platform.

4 comments

> How do they work?

Painfully. If you're on Linux, you get to use a mixture of poorly-documented (e.g., ptrace) and undocumented (e.g., r_debug) features to figure out the state of the program. Combine this with the debugging symbols provided by the compiler (DWARF), which is actually a complicated state machine to try to encode sufficient details of the source language, and careful reading of the specification makes you throw it out the window and just rely on doing a well enough job to keep compatibility with gdb.

> Why can't more compiled languages integrate debuggers inside of them so that you can debug the program without using a separate tool?

Because it's really painful to make debugging work properly. At least in the Unix world, the norm is for all of these tools to be developed as separate projects, and the interfaces that the operating system and the standard library and the linker and the compiler and the debugger and the IDE all use to talk to each other are not well-defined enough to make things work well.

> And why can't we create interfaces to debuggers so that other text editors can integrate with them, much like how we do we LSPs?

LSPs have the advantage of needing to communicate relatively little information. At its core, you need to communicate syntax highlighting, autocomplete, typing, and cross-referencing. You can build some fancier stuff on top of that information, but it's easily stuffed in a single box.

Debuggers need to do more things. Fundamentally, they need to be able to precisely correlate the state of a generated build artifact to the original source code of the program. This includes obviously things like knowing what line a given code address refers to (this is an M-N mapping), or what value lives in a given register or stack location (again an M-N mapping). But you also need to be able to understand the ABI of the source level data. This means you can't just box it as "describe language details to me", you also have to have the tools that know how to map language details to binary details. And that's a combinatorial explosion problem.

> Debuggers need to do more things

It's true that coming up with an interface for an abstract debugger is harder, but it's not impossible. Microsoft created Debug Adapter Protocol (https://microsoft.github.io/debug-adapter-protocol/), which is conceptually similar to LSP. It's not perfect, but covers most basic operations pretty well, while leaving to the debugger to deal with the implementation details.

If I'm coming up with a new language, let's call it Drustzig, I can implement the LSP and get support for IDEs (and possibly xref tools and the like) essentially for free. Now to get debugging support for my language... I have to traipse around through every major debugger and beg them to merge Drustzig patches to make it work.

The protocol you've linked (or the similar gdbserver protocol) essentially implements an IDE <-> debugger mapping. Well, most of one: everything is basically being passed as strings, so if you want smart stuff, you kind of have to build in the smart stuff yourself. It doesn't help the other parts of the process; if you want to build a new gdb, you have to do all the parsing of debug info for C, C++, Drustzig, etc. yourself... and you have to integrate the compiler expression stuff yourself. If you want to build a better time-traveling debugger, or a better crash debug format, or something similar, well, the gdb remote protocol lets gdb talk to your tool so all you have to implement is essentially low-level program state (e.g., enumerate registers of a thread, read/write arbitrary memory locations, etc.). But this isn't covered by the thing you've listed either, and it still relies on the debugger supporting a particular protocol.

I agree that language server and debugger are different beasts, but both LSP and DAP serve a purpose of re-using the same server (xrefs or debugger) with different IDEs.

> I can implement the LSP and get support for IDEs essentially for free I mean, technically same is true for DAP... You can implement DAP and get support for IDE for free. But I agree that in general case implementing a good debugger is harder than implementing a good language server.

If Drustzig requires a special debugger (e.g. because it uses acustom format of debug information), then you'd need to implement it, yes. However, existing debuggers can support new languages relatively easy if those follow standard conventions (e.g. use PDB and DWARF). For example, Rust support in LLDB basically comes down to a custom demangler.

Again, I'm not saying that DAP is perfect and solves debugging, but IMO it's a step in the right direction. Make it popular, make it extensible. Debuggers can be mostly language agnostic (within reasonable bounds), but they don't _have_ to be.

Have you tried using vims :Termdebug? By default it's GDB but you can use rr or others inside of it. Just like anything else in vim you can extend it to fit your workflow.

It has some rough edges sure but I'm using it successfully with C and Rust. If you have tried but still don't like it what's your use-case?

On Linux or BSD, the ptrace system call allows one process to take control of another process; it can then observe and control the other process. Breakpoints are inserted by replacing an instruction with a special instruction that traps; the debugger can then take control. Watchpoints (the article calls them data breakpoints) are often implemented by making the containing page read-only, so that a write access traps (this means that there's overhead from other writes to the same page, the debugger has to just resume execution silently for those). A checkpoint can be implemented by forking the process and freezing the fork, so execution can go back to that point.
> How do they work?

A debugger is basically a very complicated exception handler that can, with the help of the kernel, intercept exceptions from other processes and access their memory. (This is for Windows, but I would guess that Linux is about the same.)

When process X wants to debug process Y:

1. Process X calls into the kernel and says it wants to debug process Y.

2. The kernel verifies that process X is allowed to do that. (You wouldn't want a low privileged user to be able to debug a service, right?)

3. The kernel triggers a debug break exception in process Y, usually.

4. Process X goes into a loop where it asks the kernel for the next exception from process Y, which is a blocking call until Y has an exception.

5. The kernel's exception handler catches the exception and passes details about it back to process X.

6. While process X is in control, process Y is suspended and process X can use other kernel calls to read and write process Y's memory.

7. When process X is done doing whatever, it tells the kernel to continue the previous event and asks for the next exception.

Process X will have, of course, loaded some libraries that help it navigate the structures in memory in process Y, starting with something at a well-known address like the PEB. That lets it do things like enumerate threads and loaded modules, find symbols, etc.

Relevant win32 calls are:

WaitForDebugEvent, ContinueDebugEvent, ReadProcessMemory, WriteProcessMemory

(Standard caveat that I haven't actually written one of these things in 20 years applies. Maybe there's new stuff now.)

About the latter, there is an LSP equivalent called DAP (debug adapter protocol).