Mach 异常处理器

Mike Ash Friday Q&A 中文译文:Mach 异常处理器

作者 TommyWu
封面圖片: Mach 异常处理器

译文 · 原文: Friday Q&A 2013-01-11: Mach Exception Handlers · 作者 Mike Ash

原文:https://www.mikeash.com/pyblog/friday-qa-2013-01-11-mach-exception-handlers.html 发布:2013-01-11 作者:Mike Ash 译者:MiMo(mimo-v2.5-pro);代码块保留英文原样


亲爱的读者们,这是我首次作为客座作者撰写的周五 Q & A 文章,希望能经得起大家的检验。今天的主题是 Mach 异常处理器,这是我在 Mac OS X 和 iOS 平台上为进行崩溃报告而近期深入探索的内容。虽然关于 Mach 异常处理器的可用文档少得惊人,有些人甚至视其为神秘与力量的源泉,但事实上在高层概念上它们相当容易理解 —— 这也正是我希望在此阐明的。遗憾的是,尽管被多项新型崩溃报告方案采用,Mach 异常处理器在 iOS 上仍属于半私有 API,这一点我将在结论部分提及。

信号与异常

在大多数 UNIX 系统中,处理崩溃(例如解引用 NULL 指针、或向不可写内存页写入数据)的唯一可用机制是标准的 UNIX 信号处理程序。当致命的机器异常产生时,内核会捕获该异常,随后在故障进程中执行用户空间的跳板代码,运行该进程先前通过sigaction(3)signal(3)注册的任何函数。

然而,在 OS X 上存在一种功能更为强大的 API:Mach 异常(Mach exceptions)。Mach 异常技术可追溯到阿维・特瓦尼安(Avie Tevanian)在 Mach 操作系统上的工作(没错,就是那位阿维・特瓦尼安),它基于 Mach IPC/RPC(进程间通信/远程过程调用)构建,为 UNIX 信号处理程序 API 提供了一种替代方案。据我所知,Mach 异常处理机制的原始设计首次描述于 1988 年由阿维・特瓦尼安等人合著的一篇论文中。该描述至今仍相当准确,我建议读者参阅该论文以了解更多细节(当然,是在读完本文之后)。

Mach 异常与 UNIX 信号在三个重要方面存在区别:

  • 异常信息是通过一个 Mach IPC 端口(Mach IPC port)作为 Mach 消息(Mach message)传递的,而不是由内核调用用户空间的跳板(trampoline)函数。
  • 任何对目标进程拥有适当 Mach 端口权限(mach port rights)的进程都可以注册异常处理程序。
  • 异常处理程序可以针对特定的线程、特定的任务(task,即进程)或整个主机(host)进行注册。内核将按此顺序搜索处理程序。

这些差异引入了许多在实现调试器和崩溃报告器时很有用的特性,也使得 Mach API 作为 BSD 信号的替代方案而令人感兴趣。

异常是消息
Mach 异常 API 基于 Mach RPC(它本身又基于 Mach IPC(Mach 进程间通信))。围绕 Mach IPC 存在许多困惑,但在高层级上,它与 UNIX socket 或其他允许在进程间读写消息的知名 IPC 机制并无太大不同。Mach IPC 通信通过 mach ports(Mach 端口)发生,而非通过 socket 或其他传统的 UNIX 机制;mach ports 具有唯一的名称,可以与其他进程共享。它们可用于发送和接收包含任意数据的消息。实际使用中涉及的复杂性更多一些,但从概念上讲,您需要了解的基本就是这些。

要使用原始 Mach IPC(Mach 进程间通信)编写 Mach 异常处理程序(Mach exception handler),你需要通过在先前注册为异常处理程序的 Mach 端口(Mach port)上调用 mach_msg () 来等待新的异常消息(exception message)(如何做到这一点将在下文介绍)。对 mach_msg () 的调用将阻塞,直到接收到异常消息或线程被中断。一旦接收到消息,你可以自由地检查它以获取生成异常的线程的状态。如果你愿意在运行时修改寄存器状态,你甚至可以纠正崩溃的原因并重启失败的线程。

由于异常是作为消息提供的,而不是通过调用本地函数,异常消息可以转发到先前注册的 Mach 异常处理程序,即使该现有处理程序完全在进程外。这意味着你可以插入一个异常处理程序而不干扰现有的处理程序,无论是调试器还是 Apple 的崩溃报告器。要将消息转发到现有处理程序,你同样使用 mach_msg () 将原始消息发送到先前注册的处理程序的 mach 端口,使用 MACH_SEND_MSG 标志。

然而,如果你希望自行处理该 Mach RPC 请求(而非将其转发),则需要回复该消息,告知发送方你是否已处理了此异常。若崩溃线程的状态已修正到可恢复执行的程度,Mach 便认为该异常已被处理。此时内核不会尝试寻找其他异常处理程序(exception handler),并视为问题已解决。但若你在回复 RPC 请求时告知发送方(通常是内核)该异常未被处理,发送方将继续寻找下一个适用的 Mach 异常处理程序。请注意,内核会依次尝试向线程特定(thread-specific)、任务特定(task-specific)及主机全局(host-global)异常处理程序发送异常。

异常请求本身需要接收回复这一特性,可用于实现有趣的用途。例如,当调试器在断点命中时其异常处理程序被调用,它可以简单地等待 —— 直到(且仅当)你要求调试器继续执行时 —— 再回复 Mach 异常消息。

(译注:以上描述基于早期 Mach 内核异常处理机制,现代 macOS / iOS 系统的实现细节可能已发生演变。)

Mach RPC,而非 IPC

虽然我在上文中描述了如何使用 raw Mach IPC(原始 Mach 进程间通信)实现 mach exception handling(Mach 异常处理),但事实上,Mach 中的接口并不是这样定义的。相反,Mach RPC(Mach 远程过程调用)使用一种接口描述语言(在最初的 1989 年论文中称为 matchmaker),来描述 Mach RPC 请求(及其回复)的格式,并自动生成代码以处理接收到的消息并生成回复。

在 OS X 上,用于异常处理的 Mach RPC 接口描述文件 —— mach_exc.defsexc.defs —— 可通过 /usr/include/mach 路径访问。如果你在 Xcode 项目中包含这些文件,它将自动运行 mig(1) 工具(Mach Interface Generator,Mach 接口生成器),生成接收和处理 Mach 异常消息所必需的头文件和 C 源文件。exc.defs 文件提供用于处理 32 位异常的 API,而 mach_exc.defs 文件提供用于处理 64 位异常的 API。不幸的是,iOS 上未提供 Mach RPC defs,且只提供了部分必要的生成头文件。因此,在 iOS 上,不依赖未文档化的功能就无法实现完全正确的 Mach exception handler(Mach 异常处理程序)。

MIG 生成的代码主要处理两件事:

  • 解读传入的 RPC 消息,并以解码后的数据调用现有的处理函数(handler function)。
  • 使用处理函数的返回值来初始化对 RPC 消息的响应(response)。

生成的代码并不负责注册 Mach 异常处理程序(Mach exception handler)、接收 Mach 消息,或者实际发送回复。这些是实现者的责任。此外,存在多种支持的异常 “行为”(behaviors),它们提供关于异常的不同信息集;为所有这些行为提供回调函数是实现者的责任。

以下通过一个 64 位安全的代码示例可以最好地说明这一点,该代码旨在配合由 mach_exc.defs 生成的 RPC 代码工作(为简洁起见,此处省略了错误处理):

// Handle EXCEPTION_DEFAULT behavior
kern_return_t catch_mach_exception_raise (mach_port_t exception_port,
mach_port_t thread,
mach_port_t task,
exception_type_t exception,
mach_exception_data_t code,
mach_msg_type_number_t codeCnt)
{
// Do smart stuff here.
fprintf(stderr, "My exception handler was called by exception_raise()\n");
// Inform the kernel that we haven't handled the exception, and the
// next handler should be called.
return KERN_FAILURE;
}
extern boolean_t mach_exc_server (mach_msg_header_t *msg, mach_msg_header_t *reply);
static void exception_server (mach_port_t exceptionPort) {
mach_msg_return_t rt;
mach_msg_header_t *msg;
mach_msg_header_t *reply;
msg = malloc(sizeof(union __RequestUnion__mach_exc_subsystem));
reply = malloc(sizeof(union __ReplyUnion__mach_exc_subsystem));
while (1) {
rt = mach_msg(msg, MACH_RCV_MSG, 0, sizeof(union __RequestUnion__mach_exc_subsystem), exceptionPort, 0, MACH_PORT_NULL);
assert(rt == MACH_MSG_SUCCESS);
// Call out to the mach_exc_server generated by mig and mach_exc.defs.
// This will in turn invoke one of:
// mach_catch_exception_raise()
// mach_catch_exception_raise_state()
// mach_catch_exception_raise_state_identity()
// .. depending on the behavior specified when registering the Mach exception port.
mach_exc_server(msg, reply);
// Send the now-initialized reply
rt = mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL);
assert(rt == MACH_MSG_SUCCESS);
}
}

从示例代码中你会注意到,我们的异常处理器被称为服务端(server)。在 Mach RPC 术语中,内核(kernel)是客户端(client):它向我们的异常服务器发出 RPC 请求,并等待我们的回复。

异常行为类型 如上所述,异常消息有多种格式,包含不同类型的数据。实现者需要注册正确的行为类型;mig 生成的 RPC 代码会解析消息,并将其传递给针对特定类型的用户定义函数。Mach 异常 API 定义了三种基本行为:

  • EXCEPTION_DEFAULT:异常消息将包含触发异常的引用线程。由catch_exception_raise()处理。

  • EXCEPTION_STATE:异常消息将包含触发线程的寄存器状态,但不包含对该线程本身的引用。由catch_exception_raise_state()处理。

  • EXCEPTION_STATE_IDENTITY:异常消息将包含触发线程的寄存器状态以及对触发线程的引用。由catch_exception_raise_state_identity()处理。

除了上述行为之外,在后来的 OS X 版本中,为了支持 64 位安全性,增加了一个额外的变体。MACH_EXCEPTION_CODES 标志可以通过与所列的任何行为进行 OR 运算来设置。设置后,系统将提供 64 位安全的异常消息。即使目标是 32 位进程,LLDB / GDB 也会使用此标志。当使用 MACH_EXCEPTION_CODES 标志时,还必须使用由 mach_exc.defs 生成的 RPC 函数;这些函数和类型都使用 mach_ 前缀。

一般而言,EXCEPTION_DEFAULTEXCEPTION_STATE_IDENTITY 对于大多数用途来说已经足够。由于 EXCEPTION_DEFAULT 行为提供了对触发线程的引用,因此您也可以通过 Mach 的 thread_state() API 来获取通常由 EXCEPTION_STATE_IDENTITY 提供的线程状态。

在注册您的异常处理程序时,您有责任请求与您打算使用的 RPC 实现(exc.defsmach_exc.defs)相匹配的 MACH_EXCEPTION_CODES 行为。

整合运用 现在该落到实处了:实际注册一个 Mach 端口以接收异常消息。如前所文所述,可以为线程、任务和主机注册处理器,并且针对每一类都有一套功能相同但名称不同的 API:

  • (thread|task|host)_get_exception_ports:返回当前已注册的异常端口集合。
  • (thread|task|host)_set_exception_ports:设置将用于所有未来异常的异常端口。
  • (thread|task|host)_swap_exception_ports:原子地设置新的异常端口,并返回旧的端口集合。此函数可用于避免因多个处理器并发注册而可能产生的竞态条件。

要注册你的处理器,你需要首先分配一个 Mach 端口来接收消息,然后插入一个” 发送权限” 以便能够发送响应,最后调用上述的 set()swap() 函数之一,将其注册为异常消息的接收端。

例如(为简洁起见,再次省略错误处理):

mach_port_t server_port;
kern_return_t kr = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &server_port);
assert(kr == KERN_SUCCESS);
kr = mach_port_insert_right(mach_task_self(), &server_port, &server_port, MACH_MSG_TYPE_MAKE_SEND);
assert(kr == KERN_SUCCESS);
kr = task_set_exception_ports(task, EXC_MASK_BAD_ACCESS, server_port, EXCEPTION_DEFAULT|MACH_EXCEPTION_CODES, THREAD_STATE_NONE);

如果你希望保留先前的异常处理器,则应使用 task_swap_exception_ports() 来替代 task_set_exception_ports()

总结 Mach 异常处理器是一个非常有用的工具,虽然使用它需要处理不少环节,但希望它们看起来并非令人生畏地复杂。归根结底,Mach 异常(mach exceptions)只是一个简单的异常消息,附带一个回复,通过 Mach 端口(Mach ports)发送。

Mach API 相比信号处理器(signal handlers)有一些显著优势,包括能够将异常转发到进程外(forward exceptions out-of-process),以及在完全不同的栈上处理所有异常 —— 这在处理由目标线程栈溢出触发的异常时可能非常有用。

如果你计划实现自己的 Mach 异常处理器,无疑有更多细节值得进一步研究:

  • 当转发 Mach 异常时,你需要发送一个与先前注册的处理器异常类型(exception flavor)相匹配的异常消息。这可能意味着需要用额外的线程状态来填充一条新的 Mach 异常消息。

  • 使用 MIG 生成的 exc_server()mach_exc_server() 函数来解释 Mach 消息并非严格必要(尽管这可能是更好的做法)。由于 mig(1) 生成的结构体可用于直接解释 Mach 异常(Mach exception)消息,你可以直接这样做。

  • 如果你转发自己进程中发生的异常消息,需要确保回复的目标不是你自己的进程。单步调试器(single-stepping debuggers)只会恢复它们想要单步执行的线程;这意味着它们不会恢复你的异常处理线程,你将永远不会收到回复,而被中断的线程也将永远不会恢复。

最后,我需要强调,在 iOS 上实现正确的 Mach 异常处理器所需的头文件和 Mach 接口是不可用的(尽管它们在 Mac OS X 上是可用且公开的)。我提交了一个雷达(radar)请求添加它们(rdar://12939497),以及一个 Apple DTS 支持事件来澄清情况。该雷达仍然开放,但 DTS 提供了以下指导:

我们的工程师已经审核了你的请求,认为将此作为你已提交的 bug 报告来处理最为合适。目前没有文档记录的方法能够实现此功能,也没有可行的替代方案。

与此同时,根据我自己的工作以及 DTS(Developer Technical Support,开发者技术支持)的反馈,在 iOS 上仅使用公开 API 实现 Mach 异常处理(Mach exception handling)是不可行的。希望这一问题能在未来的 iOS 版本中得到解决,以便我们能够安全地采用 Mach 异常机制。

至此,我为 Friday Q & A 栏目贡献的首篇文章就结束了。如有任何问题,欢迎发送邮件与我交流。如果我有什么地方大错特错,也欢迎在评论区批评指正。


#Original (English)

Source: https://www.mikeash.com/pyblog/friday-qa-2013-01-11-mach-exception-handlers.html

This is my first guest Friday Q&A article, dear readers, and I hope it will withstand your scrutiny. Today’s topic is on Mach exception handlers, something I’ve recently spent some time exploring on Mac OS X and iOS for the purpose of crash reporting. While there is surprisingly little documentation available about Mach exception handlers, and they’re considered by some to be a mystical source of mystery and power, the fact is that they’re actually pretty simple to understand at a high level - something I hope to elucidate here. Unfortunately, they’re also partially private API on iOS, despite being used in a number of new crash reporting solutions - something I’ll touch on in the conclusion.

Signals vs. ExceptionsOn most UNIX systems, the only mechanism available for handling crashes (such as dereferencing NULL, or writing to an unwritable page) are the standard UNIX signal handlers. When a fatal machine exception is generated, it is caught by the kernel, which then executes a user-space trampoline within the failing process, executing any function previously registered by that process via sigaction(3) or signal(3).

On OS X, however, a much more versatile API exists: Mach exceptions. Dating back to Avie Tevanian’s work on the Mach OS (yes, that Avie Tevanian), Mach exceptions build on Mach IPC/RPC to provide an alternative to the UNIX signal handler API. The original design of the Mach exception handling facility was first described, as far as I’m aware, in a 1988 paper authored by Avie Tevanian, among others. It remains fairly accurate to this day, and I’d recommend reading it for more details (after finishing this post, of course).

Mach exceptions differ from UNIX signals in three significant ways:

  • Exception information is delivered as a Mach message via a Mach IPC port, rather than by the kernel calling into a userspace trampoline.

  • Exception handlers may be registered by any process that has the appropriate mach port rights for the target process.

  • Exception handlers may be registered for a specific thread, a specific task (process), or for the entire host. The kernel will search for handlers in that order.

These differences introduce a number of properties that can be useful when implementing debuggers and crash reporters, and are what make the Mach API interesting as an alternative to BSD signals.

Exceptions are MessagesThe Mach exception API is based on Mach RPC (which is, in itself, based on Mach IPC). There’s a lot of confusion around Mach IPC, but at a high-level, it’s not too dissimilar to UNIX sockets or other well-known IPC mechanisms that allow one to read/write messages between processes. Mach IPC communication occurs over mach ports, rather than via socket or other traditional UNIX mechanism; mach ports have unique names, and can be shared with other processes. They can be used to send and receive messages containing arbitrary data. There’s a bit more complexity involved in their actual use, but conceptually, that’s about all you need to know.

To write a Mach exception handler using raw Mach IPC, you would need to wait for a new exception message by calling mach_msg() on a Mach port previously registered as an exception handler (how to do this is covered below). The call to mach_msg() will block until an exception message is received, or the thread is interrupted. Once a message is received, you are free to introspect it for the state of the thread that generated the exception. You can even correct the cause of the crash and restart the failing thread, if you feel like hacking register state at runtime.

Since exceptions are provided as messages, rather than by calling a local function, exception messages can be forwarded to the previously registered Mach exception handler, even if that existing handler is completely out-of-process. This means that you can insert an exception handler without disturbing an existing one, whether it’s the debugger or Apple’s crash reporter. To forward the message to an existing handler, you also use mach_msg() to send the original message to a previously registered handler’s mach port, using the MACH_SEND_MSG flag.

However, if you wish to respond the Mach RPC request yourself, rather than forwarding it, you would need to reply to the message, informing the sender whether or not you handled the exception. Mach considers an exception handled if the crashing thread’s state has been corrected such that its execution can be resumed. In this case, the kernel does not attempt to find any other exception handler, and considers the matter settled. However, if you reply to the RPC request informing the sender (usually the kernel) that the exception has not been handled, the sender will then try to find the next applicable Mach exception handler. Remember that the kernel attempts to send exceptions to thread-specific, task-specific, and host-global exception handlers, in that order.

The fact that a reply is expected from the exception request can be used for interesting purposes. For example, if a debugger has its exception handler called when a breakpoint is hit, it can simply wait to reply to the Mach exception message until (and only if) you request that the debugger continue execution.

Mach RPC, not IPCWhile above I described how one might implement mach exception handling with raw Mach IPC, the fact is that this is not how the interfaces are defined in Mach. Instead, Mach RPC uses an interface description language (called matchmaker in the original 1989 paper), to describe the format of Mach RPC requests (and their replies), and automatically generate code to handle received messages and generate a reply.

On OS X, the Mach RPC interface descriptions for exception handling - mach_exc.defs and exc.defs - are available via /usr/include/mach. If you include these files in your Xcode project, it will automatically run the mig(1) tool (Mach Interface Generator), generating headers and C source files necessary to receive and handle Mach exception messages. The exc.defs file provides an API for working with 32-bit exceptions, whereas the mach_exc.defs file provides an API for working with 64-bit exceptions. Unfortunately, the Mach RPC defs are not provided on iOS, and only a subset of the necessary generated headers are provided. As a result, it’s not possible to implement a fully correct Mach exception handler on iOS without relying on undocumented functionality.

The code generated by MIG handles two things:

  • Interpreting incoming RPC messages and calling out to an existing handler function with the decoded data.

  • Initialize a response to the RPC messages using the return values from the handler function.

The generated code does not handle registering a Mach exception handler, receiving the Mach message, or actually sending the reply. That is the implementor’s responsibility. In addition, there are multiple supported exception “behaviors” that provide different sets of information about an exception; it is the implementor’s responsibility to provide callback functions for all of them.

This is best illustrated in the following 64-bit safe code, intended to work with RPC code generated by mach_exc.defs (I’ve left out error handling for simplicity):

// Handle EXCEPTION_DEFAULT behavior
kern_return_t catch_mach_exception_raise (mach_port_t exception_port,
mach_port_t thread,
mach_port_t task,
exception_type_t exception,
mach_exception_data_t code,
mach_msg_type_number_t codeCnt)
{
// Do smart stuff here.
fprintf(stderr, "My exception handler was called by exception_raise()\n");
// Inform the kernel that we haven't handled the exception, and the
// next handler should be called.
return KERN_FAILURE;
}
extern boolean_t mach_exc_server (mach_msg_header_t *msg, mach_msg_header_t *reply);
static void exception_server (mach_port_t exceptionPort) {
mach_msg_return_t rt;
mach_msg_header_t *msg;
mach_msg_header_t *reply;
msg = malloc(sizeof(union __RequestUnion__mach_exc_subsystem));
reply = malloc(sizeof(union __ReplyUnion__mach_exc_subsystem));
while (1) {
rt = mach_msg(msg, MACH_RCV_MSG, 0, sizeof(union __RequestUnion__mach_exc_subsystem), exceptionPort, 0, MACH_PORT_NULL);
assert(rt == MACH_MSG_SUCCESS);
// Call out to the mach_exc_server generated by mig and mach_exc.defs.
// This will in turn invoke one of:
// mach_catch_exception_raise()
// mach_catch_exception_raise_state()
// mach_catch_exception_raise_state_identity()
// .. depending on the behavior specified when registering the Mach exception port.
mach_exc_server(msg, reply);
// Send the now-initialized reply
rt = mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL);
assert(rt == MACH_MSG_SUCCESS);
}
}

You’ll note from the example code that our exception handler is called a server. In Mach RPC parlance, the kernel would be the client: it issues RPC requests to our exception server, and waits for our reply.

Exception BehaviorsAs described above, exception messages come in multiple formats, containing varying types of data. It’s the implementor’s responsibility to register for the correct behavior; the mig-generated RPC code will interpret the messages and hand it off to a user-defined function for the specific type. There are three basic behaviors defined by the Mach Exception API:

  • EXCEPTION_DEFAULT: Exception messages will contain a reference thread that triggered it. Handled by catch_exception_raise().

  • EXCEPTION_STATE: Exception messages will contain the register state of the triggering thread, but not a reference to the thread itself. Handled by catch_exception_raise_state().

  • EXCEPTION_STATE_IDENTITY: Exception messages will contain the register state of the triggering thread, as well as a reference to the triggering thread. Handled by catch_exception_raise_state_identity().

In addition to the above behaviors, an additional variant was added in later OS X releases to support 64-bit safety. The MACH_EXCEPTION_CODES flag may be set by OR’ing it with any of the listed behaviors, in which case 64-bit safe exception messages will be provided. This flag is used by LLDB/GDB even when targeting 32-bit processes. When using the MACH_EXCEPTION_CODES flag, one must also use the RPC functions generated by mach_exc.defs; these use the mach_ prefix for all functions and types.

Generally speaking, EXCEPTION_DEFAULT or EXCEPTION_STATE_IDENTITY are sufficient for most purposes. Since EXCEPTION_DEFAULT behavior provides a reference to the triggering thread, you can also fetch the thread state that would normally be provided via EXCEPTION_STATE_IDENTITY via the Mach thread_state() API.

When registering your exception handler, you are responsible for requesting the MACH_EXCEPTION_CODES behavior that matches the RPC implementation (exc.defs or mach_exc.defs) that you intend to use.

Putting it TogetherIt’s time to get down to brass tacks: actually registering an mach port to receive exception messages. As noted above, handlers can be registered for threads, tasks, and the host, and there are different sets of identical APIs for each:

  • (thread|task|host)_get_exception_ports: Returns the currently registered set of exception ports.

  • (thread|task|host)_set_exception_ports: Sets the exception port that will be used for all future exceptions.

  • (thread|task|host)_swap_exception_ports: Atomically set a new exception port, and return the current ports. This can be used to avoid race conditions that could otherwise occur if multiple handlers are registered concurrently.

To register your handler, you’ll need to first allocate a mach port to receive the messages, insert a “send right” to permit sending responses, and then call one of the exception port set() or swap() functions to register it as a receiver of exception messages.

For example (error handling again elided for conciseness):

mach_port_t server_port;
kern_return_t kr = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &server_port);
assert(kr == KERN_SUCCESS);
kr = mach_port_insert_right(mach_task_self(), &server_port, &server_port, MACH_MSG_TYPE_MAKE_SEND);
assert(kr == KERN_SUCCESS);
kr = task_set_exception_ports(task, EXC_MASK_BAD_ACCESS, server_port, EXCEPTION_DEFAULT|MACH_EXCEPTION_CODES, THREAD_STATE_NONE);

If you wish to preserve the previous exception handlers, task_swap_exception_ports() should be used in place of task_set_exception_ports().

ConclusionMach exception handlers are a very useful tool, and using them requires a fair bit of moving pieces, but hopefully they don’t seem dauntingly complex. At the end of the day, mach exceptions are just a simple exception message, coupled with a reply, sent over Mach ports.

There are some signficiant advantages of the Mach API over signal handlers, including the ability to forward exceptions out-of-process, and handle all exceptions on a completely different stack - something that can be useful when handling an exception triggered by a stack overflow on the target thread.

If you plan on implementing your own mach exception handler, there are certainly more details worth further investigation:

  • When forwarding mach exceptions, you need to send an exception message that matches the previous registered handler’s exception flavor. This may mean populating a new Mach exception message with additional thread state.

  • It’s not strictly necessary to use the MIG-generated exc_server() or mach_exc_server() functions for interpreting Mach messages (though it is probably a good idea). Since mig(1) generates structures that may be used to directly interpret the Mach exception messages, you can do so directly.

  • If you forward exception messages for exceptions that occur in your own process, you need to be sure that the target for the reply is not also your own process. Single-stepping debuggers will only resume the thread they wish to step; that means that they won’t resume your exception handler’s thread, you’ll never receive the reply, and the interrupted thread will never resume.

Lastly, I should highlight that the headers and mach interfaces required to implement a correct mach exception handler on iOS are not available (though they are available and public on Mac OS X). I filed a radar requesting their addition (rdar://12939497), as well as an Apple DTS support incident to clarify the situation. The radar is still open, but DTS provided the following guidance:

Our engineers have reviewed your request and have determined that this would be best handled as a bug report, which you have already filed. There is no documented way of accomplishing this, nor is there a workaround possible.

Our engineers have reviewed your request and have determined that this would be best handled as a bug report, which you have already filed. There is no documented way of accomplishing this, nor is there a workaround possible.

In the meantime, as far as I can determine through my own work, and as per DTS’s feedback, it’s not possible to implement Mach exception handling on iOS using only public API. Hopefully this will be resolved in a future release of iOS, such that we can safely adopt Mach exceptions.

Thus concludes my first contribution to Friday Q&A. If you have any questions, feel free to drop me an e-mail. If I got anything terrible wrong, feel free to roast me in the comments.