分类: LINUX
2012-05-27 18:58:19
80x86处理器发布了大约20种不同的异常。内核必须为每种异常提供一个专门的异常处理程序。对于某些异常,CPU控制单元在开始执行异常处理程序前会产生一个硬件的错误码(hardware error code),并且压入内核态堆栈。
下面列表给出了在80x86处理器中可以找到的异常的向量、名字、类型及其简单描述。更多信息可以在Intel的技术文挡中找到。
0 - "Divide error"(故障)
当一个程序试图执行整数被0除操作时产生。
1 - "Debug"(陷阱或故障)
产生于:①设置eflags的TF标志时(对于实现调试程序的单步执行是相当有用的),②一条指令或操作数的地址落在一个活动debug寄存器的范围之内(参见第三章的“硬件上下文”一节)。
2 - 未用
为非屏蔽中断保留(利用NMI引脚的那些中断)。
3 - "Breakpoint"(陷阱)
由int3(断点)指令(通常由debugger插入)引起。
4 - "Overflow" (陷阱)
当eflags的OF (overflow)标志被设置时,into (检查溢出)指令被执行。
5 - "Bounds check" (故障)
对于有效地址范围之外的操作数,bound(检查地址边界)指令被执行。
6 - "Invalid opcode" (故障)
CPU执行单元检测到一个无效的操作码(决定执行操作的机器指令部分)
7 - "Device not available" (故障)
随着cr0的TS标志被设置,ESCAPE, MMX, 或 SSE/SSE2 指令被执行(参见第三章的“保存和加载FPU, MMX及 XMM寄存器”一节)。
8 - "Double fault" (异常终止)
正常情况下,当CPU正试图为前一个异常调用处理程序时,同时又检测到一个异常,两个异常能被串行处理。然而,在少数情况下,处理器不能串行地处理它们,因而产生这种异常。
9 - "Coprocessor segment overrun" (异常终止)
因为外部数学协处理器引起的问题(仅用于80386微处理器)。
10 - "Invalid TSS" (故障)
CPU试图让一个上下文切换到有无效的TSS的进程。
11 - "Segment not present" (故障)
引用一个不存在的内存段(段描述符的Segment-Present标志被清0)。
12 - "Stack segment fault" (故障)
试图超过栈段界限的指令,或者由ss标识的段不在内存。
13 - "General protection" (故障)
违反了80x86保护模式下的保护规则之一。
14 - "Page Fault" (故障)
寻址的页不在内存,相应的页表项为空,或者违反了一种分页保护机制。
15 – 由Intel保留
16 - "Floating-point error" (故障)
集成到CPU芯片中的浮点单元用信号通知一个错误情形,如数字溢出,或被0除。
17 - "Alignment check" (故障)
操作数的地址没有正确地对齐(例如,一个长整数的地址不是4的倍数)。
18 - "Machine check" (异常终止)
机器检查机制检测到一个CPU错误或总线错误。
19 - "SIMD floating point exception" (故障)
集成到CPU芯片中的SSE或SSE2单元对浮点操作信号通知一个错误情形。
20~31这些值由Intel保留做将来开发。如下表所示,每个异常都由专门的异常处理程序来处理,它们通常把一个Unix信号发送到引起异常的进程。在后面的博文中,我们要专门讨论进程间的通信。
编号 |
异常 |
异常处理程序 |
信号 |
0 |
Divide error |
divide_error( ) |
SIGFPE |
1 |
Debug |
debug( ) |
SIGTRAP |
2 |
NMI |
nmi( ) |
None |
3 |
Breakpoint |
int3( ) |
SIGTRAP |
4 |
Overflow |
overflow( ) |
SIGSEGV |
5 |
Bounds check |
bounds( ) |
SIGSEGV |
6 |
Invalid opcode |
invalid_op( ) |
SIGILL |
7 |
Device not available |
device_not_available( ) |
None |
8 |
Double fault |
doublefault_fn( ) |
None |
9 |
Coprocessor segment overrun |
coprocessor_segment_overrun( ) |
SIGFPE |
10 |
Invalid TSS |
invalid_TSS( ) |
SIGSEGV |
11 |
Segment not present |
segment_not_present( ) |
SIGBUS |
12 |
Stack segment fault |
stack_segment( ) |
SIGBUS |
13 |
General protection |
general_protection( ) |
SIGSEGV |
14 |
Page Fault |
page_fault( ) |
SIGSEGV |
15 |
Intel-reserved |
None |
None |
16 |
Floating-point error |
coprocessor_error( ) |
SIGFPE |
17 |
Alignment check |
alignment_check( ) |
SIGBUS |
18 |
Machine check |
machine_check( ) |
None |
19 |
SIMD floating point |
simd_coprocessor_error( ) |
SIGFPE |
CPU产生的
大部分异常都由Linux解释为出错条件。当其中一个异常发生时,内核就向引起异常的进程发送一个信号向它通知一个反常条件。这个过程一定不要含糊,我们
举个例子,例如,如果进程执行了一条被0除的指令,刚执行完毕,CPU就产生一个“Divide
error”异常,并由相应的异常处理程序向当前进程发送一个SIGFPE信号,这个进程将采取若干必要的步骤来(调用相应的信号处理程序来从出错中)恢
复或者中止运行(如果没有为这个信号设置处理程序的话)。
但是,在两种情况下,Linux没有解释为出错条件,而是直接利用CPU异常更有效地管理硬件资源。第一种情况是保存和加载FPU、MMX及XMM寄存
器,“Device not
availeble"异常与cr0寄存器的TS标志一起用来把新值装入浮点寄存器,有兴趣的同志可以查查相关资料,这里不在话下。第二种情况指的是
“Page
Fault"异常,该异常推迟给进程分配新的页框,直到不能再推迟为止。相应的处理程序比较复杂,因为异常可能表示一个错误条件,也可能不表示一个错误条
件,我们将在后面的博文中详细讨论。
在理清了异常处理的过程之后,我们就来具体分析一下异常处理程序:
异常处理程序有一个标准的结构,由以下三部分组成:
1. 在内核堆栈中保存大多数寄存器的内容(这部分用汇编语言实现)。
2. 用高级的C函数处理异常。
3. 通过ret_from_exception()函数从异常处理程序退出。
为了利用异常,必须对IDT进行适当的初始化,使得每个被确认的异常都有一个异常处理程序。trap_init()函数的工作是将一些最终值(即处理异常
的函数)插入到IDT的非屏蔽中断及异常表项中。这是由函数set_trap_gate()、set_intr_gate()、
set_system_gate()、set_system_intr_gate()和set_task_gate(
)来完成的。如果有不清楚的请参考一下上一篇博文。
set_trap_gate(0,÷_error);
set_trap_gate(1,&debug);
set_intr_gate(2,&nmi);
set_system_intr_gate(3,&int3);
set_system_gate(4,&overflow);
set_system_gate(5,&bounds);
set_trap_gate(6,&invalid_op);
set_trap_gate(7,&device_not_available);
set_task_gate(8,31);
set_trap_gate(9,&coprocessor_segment_overrun);
set_trap_gate(10,&invalid_TSS);
set_trap_gate(11,&segment_not_present);
set_trap_gate(12,&stack_segment);
set_trap_gate(13,&general_protection);
set_intr_gate(14,&page_fault);
set_trap_gate(16,&coprocessor_error);
set_trap_gate(17,&alignment_check);
set_trap_gate(18,&machine_check);
set_trap_gate(19,&simd_coprocessor_error);
set_system_gate(128,&system_call);
由于“Double
fault”异常表示内核有严重的非法操作,其处理是通过任务门而不是陷阱门或系统门来完成的,因而,试图显示寄存器值的异常处理程序并不确定esp寄存
器的值是否正确。产生这种异常的时候,CPU取出存放在IDT第8项中的任务门描述符,该描述符指向存放在GDT表第32项中的TSS段描述符。然
后,CPU用TSS段中的相关值装载eip和esp寄存器,结果是:处理器在自己的私有栈上执行doublefault_fn()异常处理函数。
现在我们要考察一旦一个典型的异常处理程序被调用,它会做些什么。
1 为异常处理程序保存寄存器的值
让我们用handler_name来表示一个通用的异常处理程序的名字(如page_fault)。(所有异常处理程序的实际名字都出现在前一部分的宏列表中。)每一个异常处理程序都以下列的汇编指令开始:
handler_name:
pushl $0 /* only for some exceptions */
pushl $do_handler_name
jmp error_code
当异常发生时,如果控制单元没有自动地把一个硬件出错代码插入到栈中,相应的汇编语言片段会包含一条pushl $0指令,在栈中垫上一个空值。然后,把高级C函数的地址压进栈中,它的名字由异常处理程序名与do_ 前缀组成。
标号为error_code的汇编语言片段对所有的异常处理程序都是相同的,除了“Device not available”这一个异常。这段代码执行以下步骤:
1. 把高级C函数可能用到的寄存器保存在栈中。
2. 产生一条cld指令来清eflags的方向标志DF,以确保调用字符串指令时会自动增加edi和esi寄存器的值。
3. 把栈中位于esp+36处的硬件出错码拷贝到edx中,给栈中这一位置存上值-1,这个值用来把0x80异常与其他异常隔离开。
4. 把保存在栈中esp+32位置的do_handler_name()高级C函数的地址装入edi寄存器中,然后,在栈的这个位置写入es的值。
5. 把内核栈的当前栈顶拷贝到eax寄存器。这个地址表示内存单元的地址,在这个单元中存放的是第1步所保存的最后一个寄存器的值。
6. 把用户数据段的选择符拷贝到ds和es寄存器中。
7. 调用地址在edi中的高级C函数。
被调用的函数从eax和edx寄存器而不是从栈中接收参数。执行进程切换的主要函数__switch_to()就是一个从CPU寄存器获取参数的函数。
如前所述,执行异常处理程序的C函数名总是由do_前缀和处理程序名组成。其中的大部分函数把硬件出错码和异常向量保存在当前进程的描述符中,然后,向当前进程发送一个适当的信号。用代码描述如下:
current->thread.error_code = error_code;
current->thread.trap_no = vector;
force_sig(sig_number, current);
异常处理程序刚一终止,当前进程就关注这个信号。该信号要么在用户态由进程自己的信号处理程序(如果存在的话)来处理,要么由内核来处理。在后面这种情况下,内核一般会杀死这个进程。
异常处理程序总是检查异常是发生在用户态还是在内核态,在后一种情况下,还要检查是否由系统调用的无效参数引起。出现在内核态的任何其他异常都是由于内核
的bug引起的。在这种情况下,异常处理程序认为是内核行为失常了。为了避免硬盘上的数据崩溃,处理程序调用die()函数,该函数在控制台上打印出所有
CPU寄存器的内容(这种转储就叫做kernel oops),并调用do-exit()来终止当前进程。
当执行异常处理的C函数终止时,程序执行一条jmp指令以跳转到ret_from_exception()函数。