Linux源码—异常

(本文部分参考了《Linux内核源代码情景分析》)
内核对页面异常处理时,是从 do_page_fault()开始的。本文主要介绍内核对页面异常的响应过程,也就是从发生异常至 CPU 到达do_page_fault()之间的那一段路程,以及从 do_page_fault()返回之后到 CPU 返回到用户空间这一段路程。
Linux 内核在初始化阶段完成了对页式虚存管理的初始化以后,便调用 trap_int()和 init_IRQ()两个函数进行中断机制的初始化。其中 trap_init()主要是对一些系统保留的中断向量的初始化,而 init_IRQ()则主要是用于外设的中断。在trap_init()中可清楚看到为页面异常设置的中断门指向程序入口 page_fault:

void __init trap_init(void)
 {
...
 set_trap_gate(12,&stack_segment);
 set_trap_gate(13,&general_protection);
 set_trap_gate(14,&page_fault);
 set_trap_gate(15,&spurious_interrupt_bug);
...
}

当发生页面异常时,CPU 穿过中断门以后就直接到达了 page_fault()。 CPU 因异常而穿过中断门的过程,包括堆栈的变化,与因外设中断而引起的过程基本上是一样的。需要注意的是,异常在保护现场时还需要把出错代码压栈(但不是所有异常都产生出错代码)。代码如下:

/* 寻址的页不在内存,相应的页表项为空。或者违反了一种分页保护机制。 */
ENTRY(page_fault)
    pushl $do_page_fault
    jmp error_code

这里的跳转目标 error_code 就好像外设中断处理中的 common_interrupt 一样,是各种异常处理所共用的程序入口。而将服务程序 do_page_fault()的地址压入堆栈,则为具体的服务程序做好了准备。
程序入口 error_code 的代码如下:

error_code:
    /* 保存高级C函数会用到的寄存器到栈中,请参见高级C函数的   */
    pushl %ds
    pushl %eax
    xorl %eax, %eax
    pushl %ebp
    pushl %edi
    pushl %esi
    pushl %edx
    decl %eax           # eax = -1
    pushl %ecx
    pushl %ebx
    cld /* cld指令清eflags方向标志DF,主要用于movs这样的指令。     */

执行到此处,我们把 CPU 此时的堆栈(左边)与 CPU 在外设中断时SAVE_ALL 以后的堆栈(右边)作一比较如下:
这里写图片描述

可是,下面会将堆栈中对应于 ORIG_EAX 位置上的内容转移到寄存器%esi 中,并将其替换成%eax 中的内容。这样一来,出错代码就到了%esi 中,而堆栈中的 ORIG_EAX 就变成了-1。同时,又以寄存器%ecx 的内容替换堆栈中 ES 处的函数指针,而把函数指针转移到寄存器%edi 中。在此之前已经将%es 的内容装入了%ecx,所以在此(movl %ecx, ES(%esp))以后,堆栈的内容于中断或系统调用时就完全一样了,只是 ORIG_EAX 的位置上为-1。这么一来,堆栈就调整好了。将来返回时在 RESTORE_ALL 中会把 ORIG_EAX 跳过去(中断一节中已经讲到)。代码如下:

movl %es, %ecx
    movl ES(%esp), %edi     /* 将esp+32处的高级C函数地址载入edi.*/
    /**
     * 将esp+36处的出错码复制到edx中,将将-1写到栈中。
     * 请注意前面xorl %eax, %eax和decl %eax一句,它将eax中置-1
     * 这样,如果是异常,这个位置就是-1,否则就表示是0x80系统调用。
     */
    movl ORIG_EAX(%esp), %edx   # get the error code
    movl %eax, ORIG_EAX(%esp)
    movl %ecx, ES(%esp)
    /* 把用户数据段选择符复制以ds和es寄存器中。    */
movl $(__USER_DS), %ecx
    movl %ecx, %ds
    movl %ecx, %es
    /**
     * 把内核栈的当前栈顶复制到eax。这是一个内存单元的地址。
     * 它的地址是最后被pushl到栈中的值的地址。通过这个地址就可以访问所有保存的寄存器了。
     * 也就是pt_regs结构。
     */
    movl %esp,%eax          # pt_regs pointer
    /**
     * 调用高级C函数,被调用函数的参数是通过eax和edx传递。
     * 请参见fastcall void do_page_fault(struct pt_regs *regs, unsigned long error_code)
     * 其中的fastcall表示由寄存器传参,regs的值在eax,error_code的值在edx中。
     */
    call *%edi
    jmp ret_from_exception

对于不产生出错代码的异常在进入 error_code 之前会补上一个。请看,同一源文件( entry.S)中因协处理器( coprocessor)出错而导致的异常coprocessor_error:

ENTRY(coprocessor_error)
    pushl $0
    pushl $do_coprocessor_error
    jmp error_code

这里多了一行“ pushl $0”,将0压入堆栈中与出错代码相应的地方,此后就都一样了。

从调用的函数,也就是 do_page_fault()返回以后, CPU 就转入 ret_from_exception。由于do_page_fault()的类型是 void,所以没有返回值。 ret_from_exception 的代码也在 entry.S 中:

ret_from_exception:
    /**
     * 在中断返回前,handle_IRQ_event调用local_irq_disable禁止了中断
     * 所以在中断返回时,不用关中断,但是在异常返回时,需要将中断关闭。
     */
    preempt_stop

如果没有软中断请求需要处理,就直接进入 ret_from_intr:

``ret_from_intr:
    /**
     把当前thread_info半截到ebp中。  */
    GET_THREAD_INFO(%ebp)
    /* 接下来判断EFLAGS和CS,确定是否运行在用户态,是否是VM模式。 */
    movl EFLAGS(%esp), %eax     # mix EFLAGS and CS
    movb CS(%esp), %al
    testl $(VM_MASK | 3), %eax
    /**
     * 如果是运行在内核态,并且不是VM模式,就跳到resume_kernel,
     * 否则跳转到resume_userspace
     */
    jz resume_kernel        # returning to kernel or vm86-space

后面就与中断处理一致了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值