linux内核-页面异常的进入返回

我们在内存管理中介绍内对页面异常处理时,是从do_page_fault开始的。当时因为尚未介绍CPU的中断和异常机制,所以暂时跳过了对页面异常的响应过程,也就是从发生异常至CPU到达do_page_fault之间的那一段路程,以及do_page_fault返回之后到CPU返回到用户空间这一段路程。我们可以来补上这个缺口了。

与外设中断不同,各种异常都有为其保留的专用中断向量,因此相应的初始化也是直截了当的,这一点我们已经在初始化的博客中看到了。

为页面异常设置的中断门指向程序入口page_fault(见IDT初始化博客中所引用trap_init中的970行),所以当发生页面异常时,CPU穿过中断门以后就直接到达了page_fault。CPU因异常而穿过中断门的过程,包括堆栈的变化,与因外设中断而引起的过程基本上是一样的,读者可以参阅外设中断博客。但是,有一点很重要的不同。当中断发生时,CPU将寄存器EFLAGS的内容,以及代表着返回地址的CS和EIP两个寄存器的内容压入堆栈。如果CPU的运行级别发生变化,则在此之前还要发生堆栈的切换,并且要把代表老堆栈指针的SS和ESP的内容压入堆栈。这一点,我们已经在前面介绍过了。当异常发生时,在上述这些操作之后,还要加上附加的操作。那就是:如果所发生的的异常产生出错代码的话,就把这个出错码也压入堆栈。并非所有的异常都产生出错代码,有关详情可参考Intel的技术资料或相关专著,但是绝大多数异常,包括我们这里所关心的野蛮异常时会产生出错代码的,而且,实际上我们在内存管理中已经看到do_page_fault如何通过这个出错代码识别发生异常的原因。可是,CPU只是在进入异常时才知道是否应该把出错代码压入堆栈,而从异常处理通过iret指令返回时已经时过境迁,CPU已经无从知道当初发生异常的原因,因此不会自动跳过堆栈中的这一项,而要靠相应的异常处理程序对堆栈加以调整,使得在CPU开始执行iret指令时堆栈顶部是返回地址。由于这个不同,对异常的处理和对中断的处理在代码中也要有所不同。

页面异常处理的入口page_fault是在arch/i386/entry.S中定义的:

ENTRY(page_fault)
	pushl $ SYMBOL_NAME(do_page_fault)
	jmp error_code

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

error_code:
	pushl %ds
	pushl %eax
	xorl %eax,%eax
	pushl %ebp
	pushl %edi
	pushl %esi
	pushl %edx
	decl %eax			# eax = -1
	pushl %ecx
	pushl %ebx
	cld
	movl %es,%ecx
	movl ORIG_EAX(%esp), %esi	# get the error code
	movl ES(%esp), %edi		# get the function address
	movl %eax, ORIG_EAX(%esp)
	movl %ecx, ES(%esp)
	movl %esp,%edx
	pushl %esi			# push the error code
	pushl %edx			# push the pt_regs pointer
	movl $(__KERNEL_DS),%edx
	movl %edx,%ds
	movl %edx,%es
	GET_CURRENT(%ebx)
	call *%edi
	addl $8,%esp
	jmp ret_from_exception

读者也许注意到了,这里并不像进入中断响应时那样引用SAVE_ALL。让我们来看看有什么区别,以及为什么。观察下图,我们把CPU执行到这里的307行时的堆栈(左边)与CPU在外设中断时SAVE_ALL以后的堆栈(右边)做一比较。

顺便提一下,系统调用时的堆栈在执行完SAVE_ALL以后与下图的右边(中断)几乎完全一样,只是在ORIG_EAX位置上时系统调用号而不是中断请求号。

比较之后,可以看到其实也只有在两个位置上不同。一个是ORIG_EAX对应的位置上,现在是CPU在发生异常时压入堆栈的出错代码。另一个是在与ES相应的位置上,现在是do_page_fault的入口地址。其他就都一样了。可是,下面会将堆栈中对应于ORIG_EAX位置上的内容转移到寄存器esi中,并将其替换成%eax中的内容。这样一来,出错代码就到了%esi中,而堆栈中的ORIG_EAX就变成了-1(见298行和303行)。同时,又以寄存器%ecx的内容替换堆栈中ES处的函数指针,而把函数指针转移到寄存器%edi中。在此之前的307行已经将%es的内容装入了%ecx,所以在311行以后函数指针do_page_fault在%edi中,而堆栈中变成了寄存器%es的副本。至此,也就是在311行之后,堆栈的内容与中断或系统调用时就完全一样了。只是ORIG_EAX的位置上为-1。这么一来,堆栈就调整好了。我们在中断的博客中看到将来返回时在RESTORE_ALL中会把ORIG_EAX跳过去。

读者也许会问:那么,对于不产生出错代码的异常又怎么处理呢?很简单,在进入error_code之前补上一个就是了。请看,同一源文件(entry.S)中因协处理器(coprocessor)出错而导致的异常coprocessor_error:

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

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

回到前面error_code的代码中,第313行和314行先后把%esih和%edx的内容压入堆栈。我们知道,%esi中时出错代码,而312行已经把堆栈指针的当前内容拷贝到%edx中。在中断的博客中我么那已经讲过,内核将SAVE_ALL以后堆栈中的内容视同一个pt_regs数据结构,而当时的堆栈指针指向该数据结构的起点。所以,这二者一项是出错代码而另一项便是pt_regs结构指针,这正是do_page_fault的两个参数。把调用参数压栈以后,就为319行的函数调用做好了准备。其他一些准备工作读者在中断响应中都已看到过,这里就不重复了。

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

page_fault=>error_code=>...=>ret_from_exception

ret_from_exception:
#ifdef CONFIG_SMP
	GET_CURRENT(%ebx)
	movl processor(%ebx),%eax
	shll $CONFIG_X86_L1_CACHE_SHIFT,%eax
	movl SYMBOL_NAME(irq_stat)(,%eax),%ecx		# softirq_active
	testl SYMBOL_NAME(irq_stat)+4(,%eax),%ecx	# softirq_mask
#else
	movl SYMBOL_NAME(irq_stat),%ecx		# softirq_active
	testl SYMBOL_NAME(irq_stat)+4,%ecx	# softirq_mask
#endif
	jne   handle_softirq

ENTRY(ret_from_intr)
	GET_CURRENT(%ebx)
	movl EFLAGS(%esp),%eax		# mix EFLAGS and CS
	movb CS(%esp),%al
	testl $(VM_MASK | 3),%eax	# return to VM86 mode or non-supervisor?
	jne ret_with_reschedule
	jmp restore_all

如果没有软中断请求需要处理,就直接进入ret_from_intr。后面这些代码读者已经很熟悉了,要是还有困难可以回到前几篇博客中再看看。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值