本文主要讨论几种概念的定义和区别,力求采用最权威的定义,并给出最清晰的解释。

  • 什么是特殊控制流、什么是系统异常、Mach异常、Unix信号、还有编程语言中的异常和错误,例如 Objective-C 中的 NSException 和 Swift 中的 Error。
  • 他们分别做了什么,区别是什么?
  • 为什么 Swift 捕捉 Error 时的开销仅仅相当于一个 return 语句,而许多其他语言(包括Objective-C)则需要相当大的开销来处理栈展开?

🤦🏻请注意本文中某些名词翻译与你日常见到的可能有所出入

Exceptional Control Flow “特殊控制流”或“非常控制流”

特殊控制流(ECF)和异常(Exception)

软硬件系统为了响应来自程序外部的事件,需要突然改变当前控制流并进行大范围跳转,这种改变被称为非常控制流 exceptional control flow (ECF)。你已经见过了,例如当触摸事件发生时,你程序当前正在执行的指令会被突然地暂时打断,因为处理器要求操作系统先介入,在操作系统处理完触摸信号后,再继续执行你的程序。

异常机制是一种用来实现特殊控制流的机制。特殊控制流用来跳出当前控制流,进入内核代码,执行相应的内核逻辑。系统异常分为四种,既有硬件层面的,也有软件层面的。正是处理器硬件设计者和内核软件设计者共同协作,才使得一切成为可能。

异常机制 (Exception)

Exception 是一种用来实现特殊控制流的机制。特殊控制流用来跳出当前控制流,进入内核代码,执行相应的内核逻辑。除了打断之外,其他类型的异常都是由于执行了当前指令而同步产生的。这个指令被称为“过错指令(faulting instruction)”。

  • 打断 (Interrupt)
    • 产生自I/O设备的信号,异步,总是返回到下一个指令继续执行。
    • 在当前指令执行完之后,处理器注意到打断接点有高位信号,于是从系统总线中读取异常编号,然后执行相应的处理代码。执行完之后返回到下一个指令。
  • 陷入 (Trap)
    • 陷入包括很多种。其中一种用来实现系统调用。系统调用的时候参数被放入寄存器,然后通过汇编指令int n调用陷入trap n,来使用系统调用。系统调用是主动从用户态调用内核提供的功能,同时也会有上下文切换。
  • 错误 (Faults)
    • 遇到可能可以恢复的错误,同步,处理完成后,可能重新执行指令或直接终止。例如缺页错误,系统把缺少的部分从磁盘读取到内存之后,重新执行过错指令,这次不会再遇到缺页错误。
  • 终止 (Abort)
    • 不可恢复的致命错误,例如来自硬件的错误,同步,不返回(返回到abort路线)。处理函数跳转到abort路线直接终止程序。

Signal 与 Mach Exception

Signal 与 Mach Exception 机制,用于在软件层面实现特殊控制流,它让进程或者操作系统能够打断另一个进程。

NeXT Computer: The UNIX Approach to Exception Handling

  1. Executing the signal handler in the same context as the exception makes many registers inaccessible.
  2. The entire concept of signals is predicated on single-threaded applications.

NeXT 的系统设计者认为传统 Unix Signal 机制不够好。实际使用时,信号进入对应进程后,并不会存储在队列中,超过两个时,后续信号会丢失。信号处理函数也运行在同一个进程上下文中,能够调用的函数需要满足异步信号安全重入条件。 macOS 同时提供两种接口。

Unix Signal

一个信号就是一小块信息,告诉进程系统中发生了某个系统事件。 另外,本来硬件层面的异常由操作系统内核异常处理函数处理,用户进程看不到这些事件,但是信号机制则使得这些事件能够暴露给用户进程。例如如果某个进程执行了非法指令,操作系统内核就会发送一个SIGILL信号给进程 (CPU 硬件 –> 系统内核 –> 信号 –> 目标 Process –> 信号处理函数 –> abort() 系统调用 –> 应用程序崩溃)。

Mach Exception

Mach Exception 是 Mach 内核发出的一种消息,告诉相应的Mach Task (Process)有内核事件发生,需要处理。处理器在处理某些错误时,会通过特殊控制流机制调用内核代码,然后内核生成对应的 Mach Exception 并通过 Mach IPC 机制报告给对应的进程。 Mach Exception 还被用来实现 out-of-process debugging:

https://www.mikeash.com/pyblog/friday-qa-2013-01-11-mach-exception-handlers.html 例如LLDB可以通过ptrace系统调用(使用PT_ATTACHEXC选项)来attach到另一个进程,并通过mach_vm_write来修改__TEXT区,将目标指令修改为断点(系统调用的一种),当程序运行到对应指令时,就会通过系统调用进入系统内核,并触发异常机制,相关的Mach消息就会被发送给对应的调试器进程。

实际上,还有许多其他步骤:
> [https://bugs.openjdk.java.net/browse/JDK-8189429]

The high level steps for attaching to a target process are somewhat as follows: 

* Allocate an exception port for receiving exception messages on behalf of the target process. 
* Add the ability to send messages on this exception port. 
* Save the list of exception ports of the target process (so that we restore it later while detaching from the process). 
* Register the newly allocated exception port with the target process. 
* Invoke ptrace with PT_ATTACHEXC on the target process. 
* Wait for the exception message from the kernel with the mach_msg() call by listening in on the newly created exception port. 
* Once the reply is obtained, call mach_exc_server() to parse and decode the kernel exception message, and to prepare a reply which would need to be sent by SA to the target process while detaching. Else the target would continue to remain suspended in the kernel. 
* mach_exc_server() resides in code generated by running the mig(1) command. The generation of this code is included in the build. mach_exc_server() invokes the catch_mach_exception_raise() routine which has to be implemented in SA. 
* Implement the catch_mach_exception_raise() routine to check that the message denotes a Unix soft signal, and return a "KERN_SUCCESS" (which would cause the generated mach_exc_server() routine to create a reply message indicating that this exception is being handled). 
* Suspend all the threads in the target task with task_suspend(). 

The steps to be followed while detaching: 

* Restore the pre-saved original exception ports registered with the target process. 
* Invoke ptrace() with the "PT_DETACH" request on the target process. 
* Reply to the previous "mach soft signal" exception, since unless this acknowledgement is sent, the thread raising the exception (in the target) remains suspended. The reply message to be sent is obtained from the previous call to mach_exc_server(). 
* Release the exception port allocated while attaching to the target using mach_port_deallocate(). 

NSException

语言层面的异常。当发生这种异常时,Runtime 需要进行stack unwinding以找出一个可以捕获和处理这个问题的函数。如果最终没有捕获,语言运行时会自动调用abort系统调用,此时进程会收到来自系统的SIGABRT信号。

apple / swift 源码中的另外一个例子是,在Swift中数组越界会因为Swift Runtime对下标检查而崩溃在_checkValidSubscript,这时候会 trap 到系统内部,从而触发信号。

另一方面,Swift平时就付出了一定的运行时开销来记录和检查每次可 throw 调用的结果。因为每次都有确定的 Error 处理者,因此不需要通过栈展开来寻找可以处理问题的函数。

参考资料

  1. NeXT Computer:  The UNIX Approach to Exception Handling 
  2. Computer Systems: A Programmer’s Perspective
  3. Mac OS X and iOS Internals: To the Apple’s Core
  4. https://bugs.openjdk.java.net/browse/JDK-8189429
  5. https://www.mikeash.com/pyblog/friday-qa-2017-08-25-swift-error-handling-implementation.html
  6. https://www.mikeash.com/pyblog/friday-qa-2013-01-11-mach-exception-handlers.html

最后关于几个名词 routine, method, function, procedure

按照个人理解,这几个词有细微的含义差别。在未出现面向对象编程时,经常把系统或库提供的接口称为routine,将自己写的可复用的命名代码块称为procedure。function为全局函数,method为对象所拥有的函数。这几个词似乎经常可以互换。