科技行者

行者学院 转型私董会 科技行者专题报道 网红大战科技行者

知识库

知识库 安全导航

至顶网软件频道利用异常表处理Linux内核态缺页异常 (5)

利用异常表处理Linux内核态缺页异常 (5)

  • 扫一扫
    分享文章到微信

  • 扫一扫
    关注官方公众号
    至顶头条

将.fixup段和.text段独立开来的目的是为了提高CPU流水线的利用率。熟悉体系结构的读者应该知道,当前的CPU引入了流水线技术来加快指令的执行,即在执行当前指令的同时,要将下面的一条甚至多条指令预取到流水线中。

作者:asdkj 来源:赛迪网技术社区 2007年10月28日

关键字: 异常 内核 Linux

  • 评论
  • 分享微博
  • 分享邮件
 

将.fixup段和.text段独立开来的目的是为了提高CPU流水线的利用率。熟悉体系结构的读者应该知道,当前的CPU引入了流水线技术来加快指令的执行,即在执行当前指令的同时,要将下面的一条甚至多条指令预取到流水线中。这种技术在面对程序执行分支的时候遇到了问题:如果预取的指令并不是程序下一步要执行的分支,那么流水线中的所有指令都要被排空,这对系统的性能会产生一定的影响。在我们的这个程序中,如果将.fixup段的指令安排在正常执行的.text段中,当程序执行到前面的指令时,这几条很少执行的指令会被预取到流水线中,正常的执行必然会引起流水线的排空操作,这显然会降低整个系统的性能。

下面我们就可以看到异常表是如何形成的了:

$objdump --full-contents --section=__ex_table hello
  hello:   file format elf32-i386
  
  Contents of section __ex_table:
  8048578 ac840408 30850408 b0840408 b2840408 ....0...........
  

由于x86使用小尾端的编址方式,上面的这段数据比较凌乱。让我把上面的__ex_table中的内容转变成大家通常看到的样子,相信会更容易理解一些:

8048578 80484ac 8048530 80484b0 80484b2 ....0...........
  

上面的红色部分就是我们最感兴趣的地方,而这段数据是如何形成的呢?将前面objdump生成的可执行程序中的汇编语句和hello.c中的源程序结合起来看,就可以发现一些有趣的东西了!

先让我们回头看看hello.c中__ex_table段的语句 .long 0b,3b。其中标签0b(b代表backward,即往回的标签0)是可能出现异常的指令的地址。结合objdump生成的可执行程序.text段的汇编语句可以知道标签0就是80484ac:

原始的汇编语句:

0: rep; movsl

链接到可执行程序后:

80484ac: f3 a5 repz movsl %ds:(%esi),%es:(%edi)

而标签3就是处理异常的指令的地址,在我们的这个例子中就是80484b0。

原始的汇编语句:

3: lea 0(%eax,%ecx,4),%ecx

链接到可执行程序后:

8048530: 8d 4c 88 00 lea 0x0(%eax,%ecx,4),%ecx

因此,相应的汇编语句:

.section __ex_table,"a"
   .align 4
   .long 0b,3b

就变成了:

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内核中的异常处理方法。在系统软件的设计中,异常表也应该成为一种提高系统稳定性的重要手段。

    • 评论
    • 分享微博
    • 分享邮件
    邮件订阅

    如果您非常迫切的想了解IT领域最新产品与技术信息,那么订阅至顶网技术邮件将是您的最佳途径之一。

    重磅专题
    往期文章
    最新文章