core files on Darwin/macOS

Posted on

Core dumps are seldom overseen as valuable debugging tool, and so I thought to write a very brief refresher.

What actually are core dumps? Core dumps are snapshots of a process image (virtual memory, registers state, ...), usually created when the process terminates abnormally.

When a process misbehaves on macOS, its bad deeds end up being handled by the private CrashReport framework; this is part of the service that manages to show the "Application Whatever has crashed" dialog box.

Among other things, CrashReport generates a - surprise surprise - Crash Report that can be easily accessed via Console.app. By default, system related crash reports are stored in /Library/Logs/DiagnosticReports/ while user/application reports are stored in /Users/<USERNAME>/Library/Logs/DiagnosticReports/.

All this, again, is the default behavior, and the generated crash reports are meant to be human readable.

What if we need something different? Something that could be consumed by a debugger like lldb so that the conditions that led to the crash can be easily investigated?

In such a case we need to enable the generation of the cores by calling ulimit -c unlimited from the terminal. By default core dumps' size is capped to 0, and hence completely disabled.

By default core dumps are saved in /cores which may not be accessible by the user. This brings us to another couple of interesting sysctl tunables: kern.coredump and kern.corefile.

kern.coredump can be either 1 or 0 to enable or disable the generation of core dumps. It defaults to 1.

kern.corefile contains the path of the core file to generate. It defaults to /cores/core.%P (%P is replaced with the PID).

Let's go over this with an example, from the Terminal:

  • ulimit -c unlimited to remove any limit to the core dumps
  • mkdir $HOME/cores to create a suitable directory for the cores
  • sudo sysctl kern.corefile=$HOME/cores/core.%P to set the location of the cores

All these changes are ephemeral.

Now we're ready to experiment with a small test program. For this example I've decided to use Swift and call the source fatal.swift:

import Foundation

func do_something(_ with: Int) {
    guard with == 1 else {
        fatalError("Oops, something nasty happened")
    }

    // do something...
    print("...")
}

do_something(0)

Let's compile it with swiftc -g fatal.swift and then run it: ./fatal. You'll see something like this:

Fatal error: Oops, something nasty happened: file fatal.swift, line 5
zsh: illegal hardware instruction (core dumped)  ./fatal

Of course, the important part here is "core dumped": the core file will be generated in $HOME/cores, ready to be loaded by lldb.