Debugging On macOS
This document explains how to debug Gecko-based applications such as Firefox, Thunderbird, and SeaMonkey on macOS using Xcode. If you want to debug from the terminal see Debugging Mozilla with lldb. For specific information on a way to debug hangs, see Debugging a hang on macOS.
Creating a debuggable build
First, you need to build the application you’re going to debug using this in your .mozconfig
ac_add_options --disable-optimize
ac_add_options --enable-debug-symbols
you can also add this flag if you want assertions etc. compiled in
ac_add_options --enable-debug
See Building Firefox for macOS if you need help creating your own build.
Debugging Firefox on macOS 10.14+
macOS 10.14 introduced Notarization and Hardened Runtime features for improved application security. macOS 10.15 went further, requiring applications to be Notarized with Hardened Runtime enabled in order to launch (ignoring workarounds). When run on earlier macOS versions, Notarization and Hardened Runtime settings have no effect.
Official Builds
At this time, official builds of Firefox 69 and later are Notarized.
As a result, it is not possible to attach a debugger to these official
Firefox releases on macOS 10.14+ without disabling System Integrity
Protection (SIP). This is due to Notarization requiring Hardened
Runtime to be enabled with the com.apple.security.get-task-allow
entitlement disallowed. Rather than disabling SIP (which has security
implications), it is recommended to debug with try builds or local
builds. The differences are explained below.
try Server Builds
In most cases, developers needing to debug a build as close as possible
to the production environment should use a try
build. These
builds enable Hardened Runtime and only differ from production builds in
that they are not Notarized which should not otherwise affect
functionality, (other than the ability to easily launch the browser on
macOS 10.15+ – see quarantine note below). At this time, developers can
obtain a Hardened Runtime build with the
com.apple.security.get-task-allow entitlement allowed by submitting
a try build and downloading the dmg generated by the “Rpk” shippable
build job. A debugger can be attached to Firefox processes of these
builds. try builds use the developer entitlement files from the
source tree (allowing debugger attach) while production builds use
the production versions (which must meet notarization requirements).
On macOS 10.15+, downloaded try builds will not launch by default
because Notarization is required. To workaround this problem, remove the
quarantine extended attribute from the downloaded Nightly:
$ xattr -r -d com.apple.quarantine /Path/to/Nightly.app
Local Builds
Local builds of mozilla-central do not enable Hardened Runtime and hence do not have debugging restrictions. As a result, some functionality will be permitted on local builds, but blocked on production builds which have Hardened Runtime enabled. Bug 1522409 was filed to automate codesigning local builds to enable Hardened Runtime by default and eliminate this discrepancy.
Disabling System Integrity Protection (SIP)
If debugging a production build is required, follow Apple’s documented steps for disabling System Integrity Protection (SIP). Note that disabling SIP bypasses Hardened Runtime restrictions which can mask some bugs that only occur with Hardened Runtime so it is recommended to test fixes with SIP enabled. Disabling SIP has system security implications that should be understood before taking this step.
Creating an Xcode project
Note also that since Xcode 7.3.1 it doesn’t seem to be possible to have the Xcode project live outside the source tree. If you try to do that then Xcode will simply copy the source files under the project directory rather than link to them which breaks debugging and the possibility to modify-rebuild-relaunch from inside Xcode.
These steps were last updated for Xcode 26.5:
Initial setup:
Open Xcode, and create a new Project with File > New > Project. Select the “Other” tab then select the “Empty” project type, then click Next.
Name the project and click Next. Select the root directory of the Mozilla source code, ensure that “Create Git repository” is not checked, then click Next again.
Xcode will create a subdirectory with the name that you gave as the project name. It will contain a single file, again with the same project name along with the .xcodeproj file name extension.
Optional: if you want the .xcodeproj file directly in your source directory, exit Xcode, move the file there, and delete the now empty project directory. Reopen the project by double clicking the .xcodeproj file.
In the left-hand pane in Xcode you should see a tree item where the root item has the project name. Right click on the root item, select ‘Add files to “<project-name>”’, select all the directories in your root source directory (or the ones you care about) and click “Add”.
At the “Choose options for adding these files” window that opens, make sure that “Action” is set to “Reference files in place”, then click “Finish”.
Xcode may take a few minutes to process and add all the files to the project, and may appear to hang, pegged at 100% CPU usage.
Xcode will continue to process files in the background once it becomes responsive again, but while it’s doing that the directories shown in the project tree in the left hand pane may be slow to expand.
You should be able to open any file quickly by hitting Cmd-Shift-O and typing in part of the name of a file.
Setting up debugging:
Make sure you’ve built the source, since you need to point to the
.appin the steps below.In the Product menu, select
Scheme > New Scheme.... When prompted, enter a name for your scheme (for example, “Debug”). If you’re object directory is inside your source directory, then select the.appfile that you want to debug an the Target. If your object directory lives outside your source directory, then for now select a random binary from the Target list (Xcode can’t at this point reference a binary outside the source directory, but we’ll fix that below). Click OK.If Xcode doesn’t open the Scheme Editor window, then open it via
Product > Scheme > Edit Scheme...menu.If you were not able to select the binary you wanted previously:
Select “Run” on the left-hand side of the settings window, then select the “Info” tab.
Expand the Executable field and select
Other.... Navigate to the.appthat you want to debug in your object directory and select it. (The.appfile is typically found inside thedistfolder in your build directory, for exampleFirefox.app.)Select
Profileon the left hand side and repeat this process.Select
Buildon the left hand side and under Targets remove the random dummy target you added previously. Make sure the new one you just added has all its checkboxes checked.
If you are debugging Firefox, Thunderbird, or some other application that supports multiple profiles, using a separate profile for debugging purposes is recommended. See “Having a profile for debugging purposes” below. Select “Run” from the left hand side and in the “Arguments” tab in the scheme editor, and click the ‘+’ below the “Arguments passed on launch” field. Add “-P profilename”, where profilename is the name of a profile you created previously.
Also in the “Arguments” panel, you may want to add an environment variable MOZ_DEBUG_CHILD_PROCESS set to the value 1 to help with debugging.
Click “Close” to close the scheme editor.
At this point you can run and debug the Mozilla application from Xcode (if the application doesn’t appear to open, check whether it opened in the background). When it hits breakpoints, Xcode should open the correct source file at the correct line.
Setting up lldb
lldb is the debugger Xcode provides/uses.
Warning
One important issue that the Mozilla .lldbinit file fixes is that by
default some breakpoints will be listed as “pending”, and Xcode will
not stop at them. If you don’t include the Mozilla’s .lldbinit, you
must at least put
settings set target.inline-breakpoint-strategy always in your
$HOME/.lldbinit as recommended on Debugging Firefox with
lldb.
The
.lldbinit
file in the source tree imports many useful Mozilla specific lldb settings, commands and formatters
into lldb, but you may need to take one of the following steps to
make sure this file is used.
If you are using lldb on the command line (independently of Xcode)
and you will always run it from either the top source directory, the
object directory or else the dist/bin subdirectory of the object
directory, then adding the following setting to your $HOME/.lldbinit
is sufficient:
settings set target.load-cwd-lldbinit true
However, if you will run lldb from a different directory, or if you
will be running it indirectly by debugging in Xcode (Xcode always runs
lldb from “/”), then this setting will not help you. Instead, add the
following to your $HOME/.lldbinit:
# This automatically sources the Mozilla project's .lldbinit as soon as lldb
# starts or attaches to a Mozilla app (that's in an object directory).
#
# This is mainly a workaround for Xcode not providing a way to specify that
# lldb should be run from a given directory. (Xcode always runs lldb from "/",
# regardless of what directory Xcode was started from, and regardless of the
# value of the "Custom working directory" field in the Scheme's Run options.
# Therefore setting `settings set target.load-cwd-lldbinit true` can't help us
# without Xcode providing that functionality.)
#
# The following works by setting a one-shot breakpoint to break on a function
# that we know will both run early (which we want when we start first start the
# app) and run frequently (which we want so that it will trigger ASAP if we
# attach to an already running app). The breakpoint runs some commands to
# figure out the object directory path from the attached target and then
# sources the .lldbinit from there.
#
# NOTE: This scripts actions take a few seconds to complete, so the custom
# formatters, commands etc. that are added may not be immediately available.
#
breakpoint set --name nsThread::ProcessNextEvent --thread-index 1 --auto-continue true --one-shot true
breakpoint command add -s python
# This script that we run does not work if we try to use the global 'lldb'
# object, since it is out of date at the time that the script runs (for
# example, `lldb.target.executable.fullpath` is empty). Therefore we must
# get the following objects from the 'frame' object.
target = frame.GetThread().GetProcess().GetTarget()
debugger = target.GetDebugger()
# Delete our breakpoint (not actually necessary with `--one-shot true`):
target.BreakpointDelete(bp_loc.GetBreakpoint().GetID())
# For completeness, find and delete the dummy breakpoint (the breakpoint
# lldb creates when it can't initially find the method to set the
# breakpoint on):
# BUG WORKAROUND! GetID() on the *dummy* breakpoint appears to be returning
# the breakpoint index instead of its ID. We have to add 1 to correct for
# that! :-(
dummy_bp_list = lldb.SBBreakpointList(target)
debugger.GetDummyTarget().FindBreakpointsByName("nsThread::ProcessNextEvent", dummy_bp_list)
dummy_bp_id = dummy_bp_list.GetBreakpointAtIndex(0).GetID() + 1
debugger.GetDummyTarget().BreakpointDelete(dummy_bp_id)
# "source" the Mozilla project .lldbinit:
os.chdir(target.executable.fullpath.split("/dist/")[0])
debugger.HandleCommand("command source -s true " + os.path.join(os.getcwd(), ".lldbinit"))
DONE
see Debugging Mozilla with lldb. for more information.
Having a profile for debugging purposes
It is recommended to create a separate profile to debug with, whatever your task, so that you don’t lose precious data like Bookmarks, saved passwords, etc. So that you’re not bothered with the profile manager every time you start to debug, expand the “Executables” branch of the “Groups & Files” list and double click on the Executable you added for Mozilla. Click the plus icon under the “Arguments” list and type “-P <profile name>” (e.g. “-P MozillaDebug”). Close the window when you’re done.
Running a debug session
Make sure breakpoints are active (which implies running under the debugger) by opening the Product menu and selecting “Debug / Activate Breakpoints” (also shown by the “Breakpoints” button in the top right section of the main window). Then click the “Run” button or select “Run” from the Product menu.
Setting breakpoints
Setting a breakpoint is easy. Just open the source file you want to debug in Xcode, and click in the margin to the left of the line of code where you want to break.
During the debugging session, each time that line is executed, the debugger will break there, and you will be able to debug it.
Warning
Note that with the default configuration, some breakpoints will be
listed as “pending”, and Xcode will not stop at them. If you don’t
include the Mozilla’s .lldbinit, you must at least put
settings set target.inline-breakpoint-strategy always in your
$HOME/.lldbinit as recommended on Debugging Mozilla with
lldb.
Using Firefox-specific lldb commands
If you included the .lldbinit when Setting up
lldb, you can use Mozilla-specific lldb commands
in the console, located in the Debug area of Xcode. For example, type
js to see the JavaScript stack. For more information, see Debugging
Mozilla with lldb.
Debugging e10s child processes
Using Xcode to debug child processes created by an e10s-enabled browser is a little trickier than debugging a single-process browser, but it can be done. These directions were written using Xcode 6.3.1
Complete all the steps above under “Creating the Project”
From the “Product” menu, ensure the scheme you created is selected under “Scheme”, then choose “Scheme > Edit Scheme”
In the resulting popup, click “Duplicate Scheme”
Give the resulting scheme a more descriptive name than “Copy of Scheme”
Select “Run” on the left-hand side of the settings window, then select the “Info” tab. Set the Executable by clicking on the “Executable” drop-down, and selecting the
plugin-container.appthat is inside the app bundle of the copy of Firefox you want to debug.On the same tab, under “Launch” select “Wait for executable to be launched”
On the “Arguments” tab, remove all arguments passed on launch.
Now you’re ready to start debugging:
From the “Product” menu, ensure the scheme you created above is selected under “Scheme”
Click the “Run” button. The information area at the top of the window will show “Waiting for plugin-container to launch”
From a command line, run your build of Firefox. When that launches a child process (for example, when you start to load a webpage), Xcode will notice and attach to that child process. You can then debug the child process like you would any other process.
When you are done debugging, click the “Stop” button and quit the instance of Firefox that you were debugging in the normal way.
For some help on using lldb see Debugging Mozilla with lldb.
Other resources
Apple has an extensive list of debugging tips and techniques.
Questions? Problems?
Try asking in our Element channels #developers or #macdev.