Debugging
We provide a build of LLDB that is known to work under Darling. It is built from vanilla sources, i.e. without any modifications.
That doesn't mean it works reliably. Fully supporting a debugger is a complex task, so LLDB is known to be buggy under Darling.
Troubleshooting LLDB
If you want to troubleshoot a problem with how LLDB runs under Darling, you need
to make debugserver
produce a log file.
If running debugserver
separately, add -l targetfile
. If using LLDB directly
(which spawns debugserver
as a subprocess automatically), pass an additional
environment variable to LLDB:
LLDB_DEBUGSERVER_LOG_FILE=somelogfile.txt
External DebugServer
If you're having trouble using LLDB normally, you may get luckier by running the
debugserver
separately.
In one terminal, start the debugserver
:
./debugserver 127.0.0.1:12345 /bin/bash
In another terminal, connect to the server in LLDB:
./lldb
(lldb) platform select remote-macosx
Platform: remote-macosx
Connected: no
(lldb) process connect connect://127.0.0.1:12345
Please note that environment variables may be missing by default, if used like this.
Debug with core dump
If you are unable to start your application through lldb, you can generate a core dump and load it through lldb. You will need to enable some options on the Linux side before you are able to generate a core dump. You will to tell Linux that you want to generate a core dump, and that there is no size limit for the core dump.
sudo sysctl -w kernel.core_pattern=core_dump
ulimit -c unlimited
Option #1: Grab Core Dump From Current Working Directory
Note that the core dump will be stored into the current working directory that Linux (not Darling) is pointing to. So you should cd
into the directory you want the core dump to be stored in before you execute darling shell
. From there, you can execute the application.
cd /path/you/want/to/store/core/dump/in
darling shell
/path/to/application/executable
If everything was set up properly, you should find a file called core_dump
. It will be located in the current working directory that Linux is pointing to.
Option #2: Using coredumpctl
To Get The Core Dump.
If your distro uses SystemD, you can use coredumpctl
utility as an alternative to grab the code dump.
To generate the core dump, run the application as normal (ex: darling shell /path/to/application/executable
). Once the program crashes, you should see a (core dumped)
message appear. For example:
Illegal instruction: 4 (core dumped)
After a core dump is generated, you will need to locate the core dump. coredumpctl list -r
will give you a list of core dumps (where the newest entries are listed first).
coredumpctl list -r
TIME PID UID GID SIG COREFILE EXE
Sat 2022-08-13 23:43:20 PDT 812790 1000 1000 SIGILL present /usr/local/libexec/darling/usr/libexec/darling/mldr
Once you figure out the process' core_dump that you want save, you can use the coredumpctl dump
command to dump it, like so:
# 812790 is the PID that you want to dump
coredumpctl dump -o core_dump 812790
For the time being, you will need to use the darling-coredump
command to convert the ELF formatted core dump into a Mach-O core dump.
darling-coredump /path/to/core/dump/file
After the program has finished executing, you should see a darlingcore-core_dump
file. This file will be in the same folder as the core_dump
file. Once you found the file, you can load it up on lldb.
lldb --core /path/to/darlingcore/core/dump/file
Built-in debugging utilities
malloc
libmalloc (the default library that implements malloc()
, free()
and
friends) supports many debug features that can be turned on via environment,
among them:
MallocScribble
(fill freed memory with0x55
)MallocPreScribble
(fill allocated memory with0xaa
)MallocGuardEdges
(guard edges of large allocations)
When this is not enough, you can use libgmalloc, which is (for the most part) a drop-in replacement for libmalloc. This is how you use it:
DYLD_INSERT_LIBRARIES=/usr/lib/libgmalloc.dylib DYLD_FORCE_FLAT_NAMESPACE=1 ./test`
libgmalloc catches memory issues such as use-after-free and buffer overflows,
and attempts to do this the moment they occur, rather than wait for them to mess
up some internal state and manifest elsewhere. It does that by placing each
allocation on its own memory page(s), and adding a guard page next to it (like
MallocGuardEdges
on steroids) — by default, after the allocated buffer (to
catch buffer overruns), or with MALLOC_PROTECT_BEFORE
, before it (to catch
buffer underruns). You're likely want to try it both ways, with and without
MALLOC_PROTECT_BEFORE
. Another useful option is MALLOC_FILL_SPACE
(similar
to MallocPreScribble
from above). See
libgmalloc(3) for more details.
Objective-C and Cocoa
In Objective-C land, you can set NSZombieEnabled=YES
in order to detect
use-after-free of Objective-C objects. This replaces -[NSObject dealloc]
with
a different implementation that does not deallocate the memory, but instead
turns the object into a "zombie". Upon receiving any message (which indicates a
use-after-free), the zombie will log the message and abort the process.
Another useful option is NSObjCMessageLoggingEnabled=YES
, which will instruct
the Objective-C runtime to log all the sent messages to a file in /tmp
.
In AppKit, you can set the NSShowAllViews
default (e.g. with -NSShowAllViews 1
on the command line) to cause it to draw a colorful border around each view.
You can find more tips (not all of which work under Darling) here.
xtrace
You can use xtrace to trace Darwin syscalls a program makes, a lot like
using strace
on Linux:
xtrace vm_stat
[139] fstat64(1, 0x7fffffdfe340) -> 0
[139] host_self_trap() -> port right 2563
[139] mach_msg_trap(0x7fffffdfdfc0, MACH_SEND_MSG|MACH_RCV_MSG, 40, 1072, port 1543, 0, port 0)
[139] {remote = copy send 2563, local = make send-once 1543, id = 219}, 16 bytes of inline data
[139] mach_host::host_statistics64(copy send 2563, 4, 38)
[139] mach_msg_trap() -> KERN_SUCCESS
[139] {local = move send-once 1543, id = 319}, 168 bytes of inline data
[139] mach_host::host_statistics64() -> [75212, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1024, 4159955152, 720014745, 503911965, 0, 4160737656, 2563, 4292863152, 4160733284, 4160733071, 0], 38
[139] write_nocancel(1, 0x7fd456800600, 58)Mach Virtual Memory Statistics: (page size of 4096 bytes)
-> 58
[139] write_nocancel(1, 0x7fd456800600, 49)Pages free: 75212.
-> 49
...
xtrace
can trace both BSD syscalls and Mach traps. For mach_msg()
in
particular, xtrace
additionally displays the message being sent or received,
as well as tries to decode it as a MIG routine call or reply.
Note that xtrace
only traces emulated Darwin syscalls, so any native Linux
syscalls made (usually by native ELF libraries) will not be displayed, which
means information and open file descriptors may appear to come from nowhere in
those cases.
Darlingserver log file
Darlingserver generates a log file called dserver.log
. This file contains useful information on any potentional issue that could of occured in darlingserver
.
By default, darlingserver
only logs errors. You can change this behavior by setting the enviroment vaiable DSERVER_LOG_LEVEL
, like so:
export DSERVER_LOG_LEVEL=warn
export DSERVER_LOG_LEVEL=info
export DSERVER_LOG_LEVEL=debug
You can find this file at ${DARLING_PREFIX}/private/var/log/dserver.log
.
When Darling Is Not Able To Start Up Properly
In some situations, Darling is not able to access the bash shell. Normally, you should never run into this situation if you are building off of master. However, if you are doing any major changes to the source code (ex: updating Apple's open-source code to a new release), it may cause core applications to break in unexpected ways.
If you ever run into this situation, here are some tricks that can help you find the root cause.
Logging When An Executable is Loading.
In src/kernel/emulation/linux/mach/lkm.c
, you can add the following print statements to the mach_driver_init
method, like so:
if (applep != NULL)
{
__simple_printf("applep is not NULL\n");
int i;
for (i = 0; applep[i] != NULL; i++)
{
__simple_printf("applep[%d] = %s\n", i, applep[i]);
if (strncmp(applep[i], "elf_calls=", 10) == 0)
{
uintptr_t table = (uintptr_t) __simple_atoi16(applep[i] + 10, NULL);
_elfcalls = (struct elf_calls*) table;
__simple_printf("_elfcalls = %d\n", _elfcalls);
}
}
}
This will print out values stored in applep
. One benefit of this is that you get to see which programs are being executed.
darling shell
Bootstrapping the container with launchd...
applep is not NULL
applep[0] = executable_path=/usr/libexec/darling/vchroot
applep[1] = kernfd=4
applep[2] = elf_calls=428390
_elfcalls = 4359056
applep is not NULL
applep[0] = executable_path=/sbin/launchd
applep[1] = kernfd=4
applep[2] = elf_calls=428390
_elfcalls = 4359056
applep is not NULL
applep[0] = executable_path=/usr/sbin/memberd
applep[1] = kernfd=4
applep[2] = elf_calls=428390
_elfcalls = 4359056
applep is not NULL
...
Just keep in mind that some scripts can break with this change.