Lemme preface this by saying that writing a debugger is really hard. It requires you to understand a lot of stuff:
-
CPU architecture — The techniques you use for Apple silicon will be quite different from the techniques you use for, say, Intel.
-
Runtime architecture — Even within the same CPU architecture, different platforms have different calling conventions, different ways to invoke system functionality, and so on.
-
Binary format — Technically this is part of the runtime architecture. Apple platforms use the Mach-O binary format. You’ll be able to find base information about this online, but Apple’s recent changes are not well documented. I have some notes about this in An Apple Library Primer.
-
Platform debugging infrastructure — Each platform has its own unique features here. That’s true even for closely related platforms. Notably, the macOS APIs for this are very different than the APIs on other Unix-y platforms. More on that below.
-
Compiler — To implement a source-level debugging you have to understand how the compiler maps source code to machine code. A compiler usually outputs this mapping as it builds the code. For Apple compilers that means understanding DWARF, which is ridiculously complicated in and of itself. If you want to support some other compiler, consult that compiler’s documentation.
I’m happy to help you with this stuff but you’ll end up doing all the heavy lifting. Given the time I have to spend here on DevForums, the best I’ll be able to do is offer pointers to other resources.
Let’s start with some breakpoint basics. The standard approach here is to replace the target instruction with a trap instruction [1]. The question, as you’ve noticed, is how to continue after the trap. There are two common techniques:
-
Emulate the instruction and then resume the thread.
-
Restore the original instruction, single step the thread, re-insert the breakpoint, and then resume. Note that you have to single step the thread and leave all the other threads suspended. Otherwise there’s a small chance that some other thread could miss your breakpoint.
Both of these require you to manipulate the thread’s state. On macOS you do this with Mach APIs [2]. Specifically:
-
Install yourself as a task exception handler. If you’re launching the inferior [3] that means posix_spawnattr_setspecialport_np
.
-
That means writing a Mach exception handler, which means working with Mach messaging directly. This is one of the things that I specifically advise folks not to do O-:
-
Suspend and resume the task using task_suspend
and task_resume
.
-
Enumerate threads using task_threads
.
-
Suspend and resume threads with thread_suspend
and thread_resume
.
-
Set registers with thread_get_state
and thread_set_state
.
None of these APIs are documented in the usual place. See Availability of Low-Level APIs for links to the documentation we do have. However, for Mach specifically you should search the ’net for conceptual docs. I also recommend:
Both are wildly out of date, but they cover background that you’ll need to know.
Good luck!
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
[1] This is and of itself is tricky in a world of read-only code, which prevents you from modifying code, and code signing, where the system gets grumpy if a page’s hash doesn’t match the on-disk value.
[2] On other Unix-y systems you typically do this using ptrace
. macOS does support ptrace
but that’s mostly as a compatibility measure.
[3] If you’re trying to attach to an existing process things get even more complex.