Without hardware support, there is no way for a debugger to protect itself from the program being debugged. A debugger needs to protect its code and working state, as well as any hooks it's set up for its debugging (e.g. single-stepping interrupts); but in typical "retro" systems (8-bit machines), a debugger would just insert itself into memory and hope for the best.
Debuggers hosted on the same system as the code being debugged, and implemented entirely in software, would only be able to rely on the cooperation of the program being debugged and the person doing the debugging. Most debugging isn't adversarial: the code being debugged isn't actively taking measures against the debugger, so the debugger doesn't need to actively protect itself. (If a bug destroys the debugger as well as the program being debugged, well...) The debugger can offer configuration options to the end-user to facilitate things of course, but there will always be limitations that the end-user will have to live with (using a debugger leaves less RAM for the program being debugged, it might use some interrupts that the program would want for itself, etc.) — all told though the gain from having a hosted debugger makes it worthwhile.
Some early debuggers used extra hardware, without special CPU support, to reduce the level of compromise:
- hosting the debugger in a cartridge means the code can't be touched
- a cartridge can also provide extra RAM, which means the debugger's working state doesn't reduce the amount of memory available (at least, on systems with less RAM than their address space)
but, without a MMU, all this still happens in the same address space (even if banking is involved) so it's only reliable in non-adversarial debugging.
Adversarial debugging, or any form of complex debugging really (kernel debugging etc.) requires hardware support. This can take a number of forms:
- a MMU to provide memory and I/O protection
- CPU support for some level of virtualisation (e.g. V86-mode on the 386)
- outside hardware, using a second computer for debugging — this was very common, in a variety of forms (a small debugging shim running on the host and communicating with the debugger via a serial port, the network etc.; ICEs clipped to the CPU, or replacing the CPU...)
Hosted debuggers, especially for adversarial debugging, only really took off with the arrival of MMUs and CPU-based protection; e.g. Turbo Debugger 32, SoftICE... Multi-host debugging is still used in many cases, e.g. embedded development, development on mobile phones (using emulators or real devices), kernel debugging...
There were some systems where the whole operating environment was constantly debuggable, e.g. Lisp Machines, but again that's non-adversarial debugging in most cases.