异常处理

CPU产生的大部分异常都是由Linux解释为出错条件。当其中一个异常发生时,内核就向引起异常的进程发送一个信号向它通知一个反常条件。例如,如果进程执行了一个被0除的操作,CPU就产生了一个”Divideerror”异常,并由相应的异常处理程序向当前进程发送一个SIGFPE信号,这个进程将采取若干必要的步骤来恢复或者中止运行。

但是,在两种情况下,Linux利用CPU异常更有效地管理硬件资源。第一种情况已经在第三章”保存和加载FPUMMXXMM寄存器”一节描述过,”Devicenot availeble”异常与cr0寄存器的TS标志一起用来把新值装入浮点寄存器。第二种情况指的是”PageFault”异常,该异常推迟给进程分配新的页框,直到不能再推迟为止。相应的处理程序比较复杂,因为异常可能表示一个错误条件,也可能不表示一个错误条件。

异常处理程序有一个标准的结构,由以下三部分组成:

  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()来完成的。


由于”Doublefault”异常表示内核有严重的非法操作,其处理是通过任务门而不是陷阱门或系统门来完成的,因而,试图显示寄存器值的异常处理程序并不确定esp寄存器的值是否正确。产生这种异常的时候,CPU取出存放在IDT8项中的任务门描述符,该描述符指向存放在GDT表第32项中TSS段描述符。然后,CPUTSS段中的相关值装载eipesp寄存器,结果是:处理器在自己的私有栈上执行doublefault_fn()异常处理函数。

现在我们要考察一旦一个典型的异常处理程序被调用,它会做些什么。由于篇幅所限,我们对异常处理仅做粗略的描述,尤其是我们不涉及下面的内容:

  1. 由一些处理函数发送给用户态进程的信号码。

  2. 内核运行在MSDOS虚拟模式时产生的异常,它们的处理是不同的。

  3. Debug”异常


为异常处理程序保存寄存器的值


让我们用handler_name来表示一个通用的异常处理程序的名字。每一个异常处理程序都以下列的汇编指令开始:

handle_name:

pushl$0

pushl$do_handler_name

jmperror_code

当异常发生时,如果控制单元没有自动地把一个硬件出错代码插入到栈中,相应的汇编语言片段会包含一条pushl$0指令,在栈中垫上一个空值。然后,把高级C函数的地址压栈中,它的名字由异常处理程序名与do_前缀组成。

标号为error_code的汇编语言片段对所有的异常处理程序都是相同的。除了”Devicenot available”这一个异常。这段代码执行以下步骤:

  1. 把高级C函数可能用到的寄存器保存在栈中。

  2. 产生一条cld指令来清eflags的方向标志DF,以确保调用字符串指令时会自动增加ediesi寄存器的值。

  3. 把栈中位于esp+36处的硬件出错码拷贝到edx中,给栈中这一位置存上值-1,正如我们将在第十一章的”系统调用的重新执行”一节中所看到的哪样,这个值用来把0x80异常与其它异常隔离开。

  4. 把保存在栈中esp32位置的do_handler_name()高级C函数的地址装入edi寄存器中,然后,在栈的这个位置写入es的值。

  5. 把内核栈的当前栈顶拷贝到eax寄存器。这个地址表示内存单元的地址,在这个单元中存放的是第1步所保存的最后一个寄存器的值。

  6. 把用户数据段的选择符拷贝到dses寄存器中。

  7. 调用地址在edi中的高级C函数。

被调用的函数从eaxedx寄存器而不是从栈中接收参数。我们已经遇见过一个从CPU寄存器获取参数的函数__switch_to(),在第三章”执行进程切换”一节我们讨论过这个函数。


进入和离开异常处理程序

如前所述,执行异常处理程序的C函数名总是由do_前缀和处理程序名组成。其中的大部分函数把硬件出错码和异常向量保存在当前进程的描述符中,然后,向当前进程发送一个适当的信号。用代码描述如下:

current->thread.error_code= error_code

current->thread.trap_no= vector;

force_sig(sig_number,current);

异常处理程序刚一终止,当前进程就关注这个信号。该信号要么在用户态由进程自己的信号处理程序来处理,要么由内核来处理。在后面这种情况下,内核一般会杀死这个进程。异常处理程序发送的信号已在表4-1中列出。

异常处理程序总是检查异常是发生在用户态还是在内核态,在后一种情况下,还要检查是否由系统调用的无效参数引起,我们将在第十章“动态地址检查:修正代码”一节描述内核如何防御自己受无效的系统调用参数攻击。出现在内核态的任何其它异常都是由于内核的bug引起的。在这种情况下,异常处理程序认为是内核行为失常了。为了避免硬盘上的数据崩溃,处理程序调用die()函数,该函数在控制台上打印出所有CPU寄存器的内容,并调用do_exit()来终止当前进程。

当执行异常处理的C函数终止时,程序执行一条jmp指令以跳转到ret_from_exception()函数。这个函数将在后面的”从中断和异常返回”一节中进行描述。

相关文章
相关标签/搜索
每日一句
    每一个你不满意的现在,都有一个你没有努力的曾经。