在 macOS 上调试

本文档说明了如何使用 Xcode 在 macOS 上调试基于 Gecko 的应用程序,例如 Firefox、Thunderbird 和 SeaMonkey。如果您想从终端进行调试,请参阅 使用 lldb 调试 Mozilla。有关调试挂起方法的具体信息,请参阅 调试 macOS 上的挂起

创建可调试版本

首先,您需要使用以下内容在 .mozconfig 中构建要调试的应用程序

ac_add_options --disable-optimize
ac_add_options --enable-debug-symbols

如果希望编译断言等,您也可以添加此标志

ac_add_options --enable-debug

如果您需要帮助创建自己的版本,请参阅 为 macOS 构建 Firefox

在 macOS 10.14+ 上调试 Firefox

macOS 10.14 引入了公证和强化运行时功能,以提高应用程序安全性。macOS 10.15 进一步加强了此功能,要求应用程序必须启用强化运行时并经过公证才能启动(忽略变通方法)。在较早版本的 macOS 上运行时,公证和强化运行时设置无效。

官方版本

目前,Firefox 69 及更高版本的官方版本已过公证。**因此,在 macOS 10.14+ 上,如果不禁用系统完整性保护 (SIP),则无法将调试器附加到这些官方 Firefox 版本。** 这是因为公证要求启用强化运行时,并且不允许使用 com.apple.security.get-task-allow 授权。**建议使用尝试版本或本地版本进行调试,而不是禁用 SIP(这会带来安全隐患)。以下是两者之间的区别。**

尝试服务器版本

在大多数情况下,需要调试尽可能接近生产环境的版本的开发人员应使用 尝试版本。这些版本启用了强化运行时,并且仅在未经公证方面与生产版本有所不同,这不会影响功能(除了在 macOS 10.15+ 上轻松启动浏览器之外 - 请参阅下面的隔离说明)。目前,开发人员可以通过提交尝试版本并下载“Rpk”可交付构建作业生成的 dmg 来获得允许 com.apple.security.get-task-allow 授权的强化运行时版本。可以将调试器附加到这些版本的 Firefox 进程。尝试版本使用源代码树中的开发人员授权文件(允许调试器附加),而生产版本使用生产版本(必须满足公证要求)。**在 macOS 10.15+ 上,下载的尝试版本默认情况下不会启动,因为需要公证。要解决此问题,请从下载的 Nightly 中删除隔离扩展属性:**

$ xattr -r -d com.apple.quarantine /Path/to/Nightly.app

本地版本

mozilla-central 的本地版本不会启用强化运行时,因此没有调试限制。因此,某些功能在本地版本上将被允许,但在启用了强化运行时的生产版本上会被阻止。已提交 Bug 1522409 以自动对本地版本进行代码签名,以默认启用强化运行时并消除此差异。

禁用系统完整性保护 (SIP)

如果需要调试生产版本,请按照 Apple 的文档步骤禁用系统完整性保护 (SIP)。请注意,禁用 SIP 会绕过强化运行时限制,这可能会掩盖某些仅在启用强化运行时时才会发生的 Bug,因此建议在启用 SIP 的情况下测试修复程序。**禁用 SIP 会带来系统安全隐患,在执行此步骤之前应了解这些隐患。**

创建 Xcode 项目

如果您尝试在现有目录中创建新的 Xcode 项目,则 Xcode 将删除其现有内容(Xcode 将事先警告您)。要解决此问题,以下步骤要求您在 Mozilla 源代码树外部初始化项目,关闭项目,将 .xcodeproj 项目“文件”复制到源代码树中,然后重新打开项目以完成设置。

另请注意,从 Xcode 7.3.1 开始,似乎无法让 Xcode 项目位于源代码树外部。如果您尝试这样做,则 Xcode 将简单地**复制**项目目录下的源文件,而不是链接到它们,这会破坏调试和从 Xcode 内部修改-重新构建-重新启动的可能性。

这些步骤上次更新于 Xcode 15

  1. 打开 Xcode,并使用“文件”>“新建”>“项目”创建新项目。选择“其他”选项卡,然后选择“空”项目类型,然后单击“下一步”。命名项目并再次单击“下一步”。Xcode 将拒绝在非空目录中创建新项目,因此创建/选择一个包含项目文件的临时目录,取消选中“创建 Git 存储库”,然后单击“创建”。

  2. 在继续操作之前,请关闭项目(“文件”>“关闭项目”)。现在打开 Finder,在临时目录中找到 *.xcodejproj 目录,将其移动到 Mozilla 源代码树的根目录中,然后双击它以重新打开。(您现在可以删除临时目录。)

  3. 在 Xcode 中的左侧窗格中,您应该会看到一个树形项目,其中根项目具有项目名称。现在,右键单击根项目,选择“将文件添加到“<project-name>””,选择源目录中的所有文件和目录,取消选中“如果需要,复制项目”,然后单击“添加”。(然后,这些项目将逐步添加到左侧窗格中的根项目 <project-name> 下。请注意,子目录最初可能显示为空,但它们也会随着 Xcode 处理源文件而逐步填充。完成后,您可以通过按 Cmd-Shift-O 并键入文件名来快速打开任何文件。)

  4. 在“产品”菜单中,选择“方案”>“新建方案”,并为您的方案命名(例如,“调试”。在您单击“确定”后,Xcode 应该会打开新方案的设置窗口。(如果没有,则从“产品”>“编辑方案”菜单中打开其设置。)

  5. 在设置窗口的左侧选择“运行”,然后选择“信息”选项卡。通过单击“无”并选择“其他...”来设置可执行文件。将弹出一个名为“选择要启动的可执行文件”的新对话框。浏览到要调试的 .app 文件(Firefox.appNightlyDebug.app 等)。.app 文件通常位于构建目录中的 dist 文件夹内。

  6. 如果您正在调试 Firefox、Thunderbird 或其他支持多个配置文件的应用程序,建议使用单独的配置文件进行调试。请参阅下面的“为调试目的创建配置文件”。在方案编辑器中选择“参数”选项卡,然后单击“启动时传递的参数”字段下方的“+”。添加“ -P profilename”,其中 profilename 是您之前创建的配置文件的名称。

  7. 同样在“参数”面板中,您可能希望添加一个环境变量 MOZ_DEBUG_CHILD_PROCESS 并将其值设置为 1,以帮助调试 e10s。

  8. 从方案编辑器窗口的左侧选择“构建”,并检查“目标”下是否没有任何内容列出(否则,当您尝试运行可执行文件进行调试时可能会导致问题,因为您将收到构建错误)。

  9. 单击“关闭”以关闭方案编辑器。

此时,您可以从 Xcode 运行和调试 Mozilla 应用程序(如果应用程序似乎没有打开,请检查它是否在后台打开)。当它到达断点时,Xcode 应该会在正确的行打开正确的源文件。

设置 lldb

lldb 是 Xcode 提供/使用的调试器。

警告

Mozilla 的 .lldbinit 文件解决的一个重要问题是,默认情况下,某些断点将被列为“挂起”,并且 Xcode 不会在这些断点处停止。如果您不包含 Mozilla 的 .lldbinit,则至少必须在您的 $HOME/.lldbinit 中放置 settings set target.inline-breakpoint-strategy always,如 使用 lldb 调试 Firefox 中建议的那样。

源代码树中的 .lldbinit 文件将许多有用的 Mozilla 特定的 lldb 设置、命令和格式化程序 导入到 lldb 中,但您可能需要执行以下步骤之一以确保使用此文件。

如果您在命令行上使用 lldb(独立于 Xcode),并且您将始终从顶级源代码目录、对象目录或对象目录的 dist/bin 子目录运行它,则将以下设置添加到您的 $HOME/.lldbinit 中就足够了

settings set target.load-cwd-lldbinit true

但是,如果您将从其他目录运行 lldb,或者如果您将通过 Xcode 进行调试间接运行它(Xcode 始终从“/”运行 lldb),则此设置将无法帮助您。相反,请将以下内容添加到您的 $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

请参阅 使用 lldb 调试 Mozilla。以获取更多信息。

为调试目的创建配置文件

建议创建一个单独的配置文件进行调试,无论您的任务是什么,这样您就不会丢失宝贵的数据,例如书签、保存的密码等。为了避免每次开始调试时都受到配置文件管理器的干扰,请展开“组和文件”列表中的“可执行文件”分支,然后双击您为 Mozilla 添加的可执行文件。点击“参数”列表下的加号图标,并输入“-P <profile name>” (例如,“-P MozillaDebug”)。完成后关闭窗口。

运行调试会话

通过打开“产品”菜单并选择“调试/激活断点”(在主窗口右上角的“断点”按钮中也显示)来确保断点处于活动状态(这意味着在调试器下运行)。然后点击“运行”按钮或从“产品”菜单中选择“运行”。

设置断点

设置断点很容易。只需在 Xcode 中打开您要调试的源文件,然后点击代码行左侧的边距即可设置断点。

在调试会话期间,每次执行该行时,调试器都会在此处中断,您可以对其进行调试。

警告

请注意,使用默认配置,一些断点将被列为“挂起”,Xcode 不会在这些断点处停止。如果您不包含 Mozilla 的 .lldbinit,则至少需要在您的 $HOME/.lldbinit 中添加 settings set target.inline-breakpoint-strategy always,如 使用 lldb 调试 Mozilla 中建议的那样。

使用 Firefox 特定的 lldb 命令

如果您在 设置 lldb 时包含了 .lldbinit,则可以在 Xcode 的调试区域中的控制台中使用 Mozilla 特定的 lldb 命令。例如,键入 js 以查看 JavaScript 堆栈。有关更多信息,请参阅 使用 lldb 调试 Mozilla

调试 e10s 子进程

使用 Xcode 调试由启用 e10s 的浏览器创建的子进程比调试单进程浏览器稍微复杂一些,但可以实现。这些说明是在 Xcode 6.3.1 中编写的。

  1. 完成“创建项目”下所有上述步骤。

  2. 从“产品”菜单中,确保您创建的方案在“方案”下被选中,然后选择“方案 > 编辑方案”。

  3. 在弹出的窗口中,点击“复制方案”。

  4. 为生成的方案提供一个比“方案副本”更具描述性的名称。

  5. 在设置窗口的左侧选择“运行”,然后选择“信息”选项卡。通过点击“可执行文件”下拉菜单并选择您要调试的 Firefox 副本的应用程序包内的 plugin-container.app 来设置可执行文件。

  6. 在同一选项卡上的“启动”下,选择“等待可执行文件启动”。

  7. 在“参数”选项卡上,删除启动时传递的所有参数。

现在您已准备好开始调试。

  1. 从“产品”菜单中,确保您在上面创建的方案在“方案”下被选中。

  2. 点击“运行”按钮。窗口顶部的信息区域将显示“等待 plugin-container 启动”。

  3. 从命令行运行您的 Firefox 版本。当它启动子进程时(例如,当您开始加载网页时),Xcode 会注意到并附加到该子进程。然后,您可以像调试任何其他进程一样调试子进程。

  4. 调试完成后,点击“停止”按钮并以正常方式退出您正在调试的 Firefox 实例。

有关使用 lldb 的帮助,请参阅 使用 lldb 调试 Mozilla

其他资源

Apple 提供了一个广泛的 调试技巧和技术列表

问题?故障?

尝试在我们的 Element 频道 #developers#macdev 中提问。