异常简介
异常是异常控制流的一种形式,它一部分是由硬件实现的,一部分是由操作系统实现的。异常是控制流中的突变,用来响应处理器状态中的某些变化。当处理器状态发生一个重要的变化时,处理器正在执行某个当前指令Icurr。在处理器中,状态被编码为不同的位和信号,状态的变化称之为事件(event)。事件可能和当前指令的执行直接相关。比如,发生虚拟存储器缺页、算术溢出,或者一条指令试图除以零。另一方面,事件也可能和当前指令的执行没有关系。比如,一个系统定时器产生信号或者一个I/O请求完成。在任何情况下,当处理器检测到有事件发生时,它就会通过一张叫做异常表(exception table)的跳转表,进行一个间接过程调用(异常),到一个专门设计用于处理这类事件的操作系统子程序(异常处理程序(exception handler))。
当异常处理程序完成处理后,根据引起异常的事件的类型,会发生一下三种情况中的一种:
- 处理程序将控制返回给当前指令Icurr,即当事件发生时正在执行的指令;
- 处理程序将控制返回给Inext,即如果没有发生异常将会执行的下一条指令;
- 处理程序终止被中断的程序。
异常类似于过程调用,但是有一些重要的不同之处。
- 过程调用时,在跳转到处理程序之前,处理器将返回地址压入栈中。然而,根据异常类型,返回地址要么是当前指令,要么是一下条指令;
- 处理器也把一些额外的处理器状态压到栈里,在处理程序返回时,重新开始被中断的程序会需要这些状态。比如,一个x86-64系统将包含当前条件码和其他内容的EFLAGS寄存器压入栈中;
- 如果控制从一个用户程序转移到内核,那么所有这些项目都被压到内核,那么所有这些项目都被压到内核栈中,而不是压在用户栈中;
- 异常处理程序运行在内核模式下,这意味着它们对所有的系统资源都有完全的访问权限。
一旦硬件触发了异常,剩下的工作就是由异常处理程序在软件中完成。在处理程序处理完事件之后,它通过执行一条特殊的“从中断返回”的指令,可选地返回到被中断程序,该指令将适当的状态弹回到处理器的控制和数据寄存器中,如果异常中断的是一个用户程序,就将状态恢复为用户模式,然后将控制返回给被中断的程序。
异常可以分为四类:中断(interrupt)、陷阱(trap)、故障(fault)和终止(abort)。
类 别 | 原 因 | 异步/同步 | 返回行为 |
中 断 | 来自I/O设备的信号 | 异 步 | 总是返回到下一条指令 |
陷 阱 | 有意的异常 | 同 步 | 总是返回下一条指令 |
故 障 | 潜在可恢复的错误 | 同 步 | 可能返回到当前指令 |
终 止 | 不可恢复的错误 | 同 步 | 不会返回 |
中断
中断是异步发生的,是来自处理器外部的I/O设备的信号的结果。硬件中断不是由任何一条专门的指令造成的,从这个意义上来说它是异步的。硬件中断的异常处理通常称之为中断处理程序(interrupt handler)。例如,网络适配器、磁盘控制器和定时器芯片,通过向处理器芯片上的一个引脚发信号,并将异常号放在系统总线上,以触发中断,这个异常标识了引起中断的设备。在当前指令完成执行之后,处理器注意到中断引脚的电压变高了,就从系统总线读取异常号,然后调用适当的中断处理程序。当处理程序返回时,它就将控制返回给下一条指令(即如果没有发生中断,在控制流中会在当前指令之后的那条指令)。结果是程序继续执行,就好象没有发生过一样。
陷阱和系统调用
陷阱是有意的异常,是执行一条指令的结果。就像中断处理程序一样,陷阱处理程序将控制返回到下一条指令。陷阱最重要的用途是在用户程序和内核之间提供一个象过程调用一样的接口,叫系统调用。用户程序经常需要向内核请求服务,比如读一个文件(read)、创建一个新的进程(fork)、加载一个新的程序(execve),或者终止当前进程(exit)。为了允许对这些内核服务的受控访问,处理器提供了一条特殊的“syscall n”指令,当用户程序想要请求服务n时,可以执行这条指令。执行syscall指令会导致一个到异常处理程序的陷阱,这个处理程序对参数解码,并调用适当的内核程序。从程序员的角度来看,系统调用和普通的函数调用是一样的。然而,他们的实现是非常不同的。普通函数运行在用户模式中,用户模式限制了函数可以执行的指令的类型,而且它们只能方位与调用函数相同的栈。系统调用运行在内核模式中,内核模式允许系统调用执行指令,并访问定义在内核中的栈。
故障
故障由错误情况引起,它可能能够被故障处理程序修正。当故障发生时,处理器将控制转移给故障处理程序。如果处理程序能够修正这个错误情况,它将控制返回到引起故障的指令,从而重新执行它。否则,处理程序返回到内核中abort例程,abort例程会终止引发故障的应用程序。一个经典的故障示例就是缺页异常,当指令引用一个虚拟地址,而与该地址相对应的物理页不再存储器中,因此必须从磁盘中取出,就会发生故障。缺页处理程序从磁盘加载适当的页面,然后将控制返回给引起故障的指令。当指令再次执行时,相应的物理页面已经驻留在存储器中了,指令就可以没有故障地运行了。
终止
终止是不可恢复的致命错误造成的结果,通常是一些硬件错误,比如DRAM或者SRAM位被损坏时发生的奇偶错误。终止处理程序从不将控制返回给应用程序,而是将控制返回给一个abort例程,该例程会终止这个应用程序。
让我们来看看IA32系统定义的一些异常。有高达256种不同的异常类型。0-31的号码对应的是Intel构架师定义的异常,因此对任何IA32系统都是一样的;32-255的号码对应的异常是操作系统定义的中断和陷阱。
异常号 | 描 述 | 异常类型 |
0 | 除法错误 | 故 障 |
13 | 一般保护故障 | 故 障 |
14 | 缺 页 | 故 障 |
18 | 机器检查 | 终 止 |
32-127 | 操作系统定义的异常 | 中断或陷阱 |
128(0x80) | 系统调用 | 陷 阱 |
129-255 | 操作系统定义的异常 | 中断或陷阱 |
一般保护故障。许多原因都会导致不为人知的一般保护故障,通常是因为一个程序引用了一个未定义的虚拟存储区域,或者因为程序试图写一个只读的文本段。Linux不会尝试恢复这类故障。Linux shell通常会把这种一般保护故障报告为“段故障”(Segmentation fault)。缺页是会重新执行产生故障的指令的一个异常示例。机器检查是在导致故障的指令执行中检测到致命的硬件错误时发生的,它从不返回控制给应用程序。
Linux提供上百种系统调用,当引用程序想要请求内核服务时可以使用,每个系统调用都有一个唯一的整数号,对应于一个到内核中跳转表的偏移量。
编 号 | 名 字 | 描 述 |
1 | exit | 结束进程 |
2 | fork | 创建新进程 |
3 | read | 读文件 |
4 | write | 写文件 |
5 | open | 打开文件 |
6 | close | 关闭文件 |
7 | waitpid | 等待子进程结束 |
11 | execve | 加载和运行程序 |
19 | lseek | 定位到文件偏移量处 |
20 | getpid | 获取进程ID |
27 | alarm | 设置传送信号的警告时钟 |
29 | pause | 挂起进程知道信号到达 |
37 | kill | 发送信号到另一个进程 |
48 | signal | 安装一个信号处理程序 |
63 | dup2 | 复制文件描述符 |
64 | getppid | 获得父进程ID |
65 | getpgrp | 获得进程组 |
67 | mmap | 将存储器页映射到文件 |
106 | stat | 获得有关文件的信息 |
1: int main() {
2: // movl $0x4, %eax ; system call number 4 (write syscall)
3: // movl $0x1, %ebx ; stdout has descriptor 1
4: // movl $string, %ecx ; hello world string
5: // movl $0xd, %edx ; string length
5: // int 0x80 ; syscall call code
6: write(1, "hello world\n", 13);
7: exit(0);
8: }