Kamil.S
Kamil.S

Reputation: 5543

How does lldb handle special purpose _swift_runtime_on_report function?

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?

Upvotes: 2

Views: 328

Answers (1)

Jim Ingham
Jim Ingham

Reputation: 27203

I'm not sure I understand what you are asking.

_swift_runtime_on_report is a function that exists so that lldb can set a breakpoint on it, and retrieve some info about the error to show the user. You can see that breakpoint while debugging a swift program by issuing the command:

(lldb) break list -i
Current breakpoints:
Kind: shared-library-event
-1: address = dyld[0x00000000000121ad], locations = 1, resolved = 1, hit count = 1

  -1.1: where = dyld`_dyld_debugger_notification, address = 0x00000001000221ad, resolved, hit count = 1 

Kind: swift-language-runtime-report
-2: address = libswiftCore.dylib[0x00007fff66b14380], locations = 1, resolved = 1, hit count = 1

  -2.1: where = libswiftCore.dylib`_swift_runtime_on_report, address = 0x00007fff689d5380, resolved, hit count = 1 

Swift promises to call this function whenever it's come across a fatal error, before it tears down your program. That allows lldb - in normal debugging - to catch the error and report it to you rather than having the program exit out from under you. It also allows the swift REPL to catch and clean up from any throws that might happen at the top-level of the REPL, since they would otherwise cause the REPL to exit.

When I build and debug the swift file with the one line "assertionFailure()", the debugger stops with:

Fatal error: file errors/errors.swift, line 2
2020-09-29 16:52:40.373185-0700 errors[17428:2120547] Fatal error: file errors/errors.swift, line 2
Process 17428 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = Fatal error
    frame #0: 0x00007fff689d5380 libswiftCore.dylib`_swift_runtime_on_report
libswiftCore.dylib`_swift_runtime_on_report:
->  0x7fff689d5380 <+0>: pushq  %rbp
    0x7fff689d5381 <+1>: movq   %rsp, %rbp
    0x7fff689d5384 <+4>: popq   %rbp
    0x7fff689d5385 <+5>: retq   
Target 0: (errors) stopped.

The only slightly tricky thing here is that if you put a breakpoint on this function as well, lldb will just report the error in the stop reason, since that's a higher priority stop reason. Another thing that might be confusing you is that lldb never shows the breakpoints it has inserted, it always shows the instruction that it overwrote. If you aren't seeing the breakpoints you inserted, that's as designed.

Anyway, I couldn't tell which part of this wasn't working for you? Is the breakpoint not getting set? Or is it not getting hit? Or did you have another question I'm just missing?

Upvotes: 2

Related Questions