While analysing how Swift assertionFailure() works under the hood I noticed the actual fatal error is reported through _swift_runtime_on_report function. Its implementation is defined here: https://github.com/apple/swift/blob/master/stdlib/public/runtime/Errors.cpp as
SWIFT_NOINLINE SWIFT_RUNTIME_EXPORT void
_swift_runtime_on_report(uintptr_t flags, const char *message,
RuntimeErrorDetails *details) {
// Do nothing. This function is meant to be used by the debugger.
// The following is necessary to avoid calls from being optimized out.
asm volatile("" // Do nothing.
: // Output list, empty.
: "r" (flags), "r" (message), "r" (details) // Input list.
: // Clobber list, empty.
);
}
Clearly it's just a fancy write up for a function that literally does nothing. It's just sitting there waiting for lldb to be treated specially. What I mean by this is:
libswiftCore.dylib`_swift_runtime_on_report:
-> 0x7fff64d1cd50 <+0>: push rbp
0x7fff64d1cd51 <+1>: mov rbp, rsp
0x7fff64d1cd54 <+4>: pop rbp
0x7fff64d1cd55 <+5>: ret
0x7fff64d1cd56 <+6>: nop word ptr cs:[rax + rax]
The x86-64 isn't really that relevant here. The fact the first line (with the arrow ->) fatal errors (whatever that exactly means) in Xcode is. Interestingly even setting a breakpoint at memory address 0x7fff64d1cd50 beforehand won't trigger it, it will fatal error anyway without hitting the breakpoint.
When I tinker with the program counter (rip) I can skip to 0x7fff64d1cd51 and catch breakpoint at 0x7fff64d1cd54 without triggering the fatal error. So it seems plausible reading program memory at 0x7fff64d1cd50 is caught by lldb which eventually fatal errors in a graceful way.
And now comes the most confusing part of the riddle. In my minimalistic XCode project I have main.swift consisting of
assertionFailure()
But if I add intentionally a C function defined like this (body being irrelevant here):
#include <stdint.h>
_swift_runtime_on_report(uintptr_t flags, const char *message,
void *details) {
int i = 0;
i++;
return i;
}
it will confuse lldb enough to lift the memory restriction on vanilla _swift_runtime_on_report function (at this point I can now catch breakpoints at 0x7fff64d1cd50). Eventually it omits the "graceful" fatal error step and it will fail on ud2 (i.e. x86 deliberate bad instruction)
Interestingly it's perfectly legal to call my local "imposter" function from Swift too, everything works like it's a perfectly normal C function.
So my question is how lldb is exactly failing in _swift_runtime_on_report / 0x7fff64d1cd50 ? And why I was able to break this mechanism with this local C function that isn't even called. Obviously through symbol / definition clash, but what is really happening?