就变成了:
8048578 80484ac 8048530 …………
这样,异常表中的地址对(80484ac,8048530)就诞生了,而对于地址对(80484b0 80484b2)的生成,情况相同,不再赘述。
读到这儿了,有一件事要告诉读者的是,其实例子中异常表的安排在用户空间是不会得到执行的。当运行在用户态的进程访问到标签0处的指令出现缺页异常时,do_page_fault只会将该指令对应的进程页调入内存中,使指令能够重新正确执行,或者直接就杀死该进程,并不会到达函数search_exception_table处。
也许有的读者会问了,既然不执行,前面的例子和围绕例子所展开的讨论又有什么作用呢?大家大可打消这样的疑虑,我们前面的分析并没有白费,因为真正的内核异常表中地址对的生成机制和前面讲述的原理是完全一样的,笔者通过一个运行在用户空间的程序来讲解也是希望让读者能够更加容易的理解异常表的机制,不至于陷入到内核源码的汪洋大海中去。现在,我们可以自己通过objdump工具查看一下内核中的异常表:
$objdump --full-contents --section=__ex_table vmlinux vmlinux: file format elf32-i386 Contents of section __ex_table: c024ac80 e36d10c0 e66d10c0 8b7110c0 6c7821c0 ……………………
做一下转化:
c024ac80 c0106de3 c0106de6 c010718b c021786c
上面的vmlinux就是编译内核所生成的内核可执行程序。和本文给出的例子相比,唯一的不同就是此时的地址对中的异常指令地址和修复地址都是内核空间的虚拟地址。也正是在内核中,异常表才真正发挥着它应有的作用。
总结
下面我对前面所讲述的内容做一个归纳,希望读者能够对内核缺页异常处理有一个清楚的认识:
进程访问内核地址空间的"非法"地址c010718b,存储管理部件(MMU)产生一个缺页异常;CPU调用函数do_page_fault;do_page_fault调用函数search_exception_table(regs->eip == c010718b);search_exception_table在异常表中查找地址c010718b,并返回地址对中的修复地址c021786c; do_page_fault将堆栈中的返回地址eip修改成c021786c并返回;代码按照缺页异常处理程序的返回地址继续执行,也就是从c021786c开始继续执行。
将验证用户空间地址信息"合法"性的工作交给硬件来完成(通过缺页异常的方式)其实就是一种Lazy Computation,也就是等到真正出现缺页异常的时候才进行处理。通过本文的分析可以看出,这种方法与本文前面所提到的通过verify_area来验证的方法相比,较好的避免了系统在无用验证上的开销,能够有效的提高系统的性能。此外,在分析源码的过程中读者会发现,异常表并不仅仅用在缺页异常处理程序中,在通用保护(General Protection)异常等地方,也同样用到了这一技术。
由此可见,异常表是一种广泛应用于Linux内核中的异常处理方法。在系统软件的设计中,异常表也应该成为一种提高系统稳定性的重要手段。
(参考链接: http://os.yesky.com/lin/419/2629419.shtml)
|