Java自学者论坛

 找回密码
 立即注册

手机号码,快捷登录

恭喜Java自学者论坛(https://www.javazxz.com)已经为数万Java学习者服务超过8年了!积累会员资料超过10000G+
成为本站VIP会员,下载本站10000G+会员资源,会员资料板块,购买链接:点击进入购买VIP会员

JAVA高级面试进阶训练营视频教程

Java架构师系统进阶VIP课程

分布式高可用全栈开发微服务教程Go语言视频零基础入门到精通Java架构师3期(课件+源码)
Java开发全终端实战租房项目视频教程SpringBoot2.X入门到高级使用教程大数据培训第六期全套视频教程深度学习(CNN RNN GAN)算法原理Java亿级流量电商系统视频教程
互联网架构师视频教程年薪50万Spark2.0从入门到精通年薪50万!人工智能学习路线教程年薪50万大数据入门到精通学习路线年薪50万机器学习入门到精通教程
仿小米商城类app和小程序视频教程深度学习数据分析基础到实战最新黑马javaEE2.1就业课程从 0到JVM实战高手教程MySQL入门到精通教程
查看: 458|回复: 0

截获Linux操作系统异常处理

[复制链接]
  • TA的每日心情
    奋斗
    2024-4-6 11:05
  • 签到天数: 748 天

    [LV.9]以坛为家II

    2034

    主题

    2092

    帖子

    70万

    积分

    管理员

    Rank: 9Rank: 9Rank: 9

    积分
    705612
    发表于 2021-9-7 10:10:57 | 显示全部楼层 |阅读模式

        在某些情况下,我们可能需要去截获Linux操作系统的一些异常处理,比如截获page fault异常处理。

    可以修改内核的情况下

        如果我们能够修改内核,那么截获page fault异常处理就会非常简单。以linux 3.8.0内核为例,系统中发生page fault之后,会进入page fault异常处理,调用do_page_fault函数。do_page_fault的代码如下:

    1 dotraplinkage void __kprobes
    2 do_page_fault(struct pt_regs *regs, unsigned long error_code)
    3 {
    4     exception_enter(regs);
    5     __do_page_fault(regs, error_code);
    6     exception_exit(regs);
    7 }

        我们把do_page_fault函数的内容提取出来,写成一个新的函数default_do_page_fault。再增加一个函数指针do_page_fault_handler,初始化为default_do_page_fault。将原来的do_page_fault内部改为调用函数指针do_page_fault_handler。修改之后的代码如下:

    void
    default_do_page_fault(struct pt_regs *regs, unsigned long error_code)
    {
        exception_enter(regs);
        __do_page_fault(regs, error_code);
        exception_exit(regs);
    }
    EXPORT_SYMBOL(default_do_page_fault); typedef
    void (*do_page_fault_handler_t)(struct pt_regs *, unsigned long); do_page_fault_handler_t do_page_fault_handler = default_do_page_fault; EXPORT_SYMBOL(do_page_fault_handler); dotraplinkage void __kprobes do_page_fault(struct pt_regs *regs, unsigned long error_code){ do_page_fault_handler(regs, error_code); }

        由于do_page_fault_handler被EXPORT_SYMBOL导出,我们在内核模块中可以很方便地访问它。只要将do_page_fault_handler的值设置为自定义的page fault异常处理函数,就能完成截获功能。如果想要恢复原来的异常处理函数,只需要再次把do_page_fault_handler设置为default_do_page_fault即可。

    不能修改内核的情况下

       但是有些情况下,我们不能直接修改内核代码,需要在已经编译好的内核上完成截获功能。

       开始的时候,我考虑在do_page_fault函数开始处插入跳转代码,跳转到自定义的page fault处理函数中。但是实践的时候发现,内核不允许直接修改do_page_fault的代码。

        经过一番调查,又想到一个新的办法,即通过更改IDT表的方式来截获page fault。

        内核原有的IDT表肯定是不能直接写的,所以我申请了一个页,将原来的IDT表复制过来,再更改页面异常对应的ISR(Interrupt Service Routine)。page fault的ISR名称为page_fault,它将寄存器压栈,将error number压栈,然后调用do_page_fault,待do_page_fault返回之后再恢复寄存器,退出异常处理。

        在Linux内核中,ISR是用汇编写的。例如,x86_64 Linux的ISR源码位于内核源码arch/x86/kernel/entry_64.S中,X86_32的位于arch/x86/kernel/entry_32.S中。如果去读entry_64.S或者entry_32.S,你会发现这两个文件非常复杂,利用了很多的汇编宏和宏定义,无法方便地基于它们写一个自定义的ISR出来。

        我的解决办法是将内核编译出来,反汇编vmlinux.o,然后查找page_fault,找到其汇编代码。下面的汇编代码就是linux-3.8.0 X86_64内核的

     1 ffffffff8136f6f0 <page_fault>:
     2 ffffffff8136f6f0:       66 66 90                data32 xchg %ax,%ax
     3 ffffffff8136f6f3:       ff 15 07 0a 2b 00       callq  *0x2b0a07(%rip)        # ffffffff81620100 <pv_irq_ops+0x30>
     4 ffffffff8136f6f9:       48 83 ec 78             sub    $0x78,%rsp
     5 ffffffff8136f6fd:       e8 ae 01 00 00          callq  ffffffff8136f8b0 <error_entry>
     6 ffffffff8136f702:       48 89 e7                mov    %rsp,%rdi
     7 ffffffff8136f705:       48 8b 74 24 78          mov    0x78(%rsp),%rsi
     8 ffffffff8136f70a:       48 c7 44 24 78 ff ff    movq   $0xffffffffffffffff,0x78(%rsp)
     9 ffffffff8136f711:       ff ff 
    10 ffffffff8136f713:       e8 1f 2e 00 00          callq  ffffffff81372537 <do_page_fault>
    11 ffffffff8136f718:       e9 33 02 00 00          jmpq   ffffffff8136f950 <error_exit>
    12 ffffffff8136f71d:       0f 1f 00                nopl   (%rax)

    我仿照着写了一个,名为my_page_fault

     1 asmlinkage void my_page_fault(void);
     2 asm("   .text");
     3 asm("   .type my_page_fault,@function");
     4 asm("my_page_fault:");
     5 //the first 3 bytes of the routine basically do nothing,
     6 //but I decide to keep them because kernel may rely on them for some special purpose
     7 asm("   .byte 0x66");
     8 asm("   xchg %ax, %ax"); 
     9 asm("   callq *addr_adjust_exception_frame");
    10 asm("   sub $0x78, %rsp");
    11 asm("   callq *addr_error_entry");
    12 asm("   mov %rsp, %rdi");
    13 asm("   mov 0x78(%rsp), %rsi");
    14 asm("   movq $0xffffffffffffffff, 0x78(%rsp)");
    15 asm("   callq my_do_page_fault");
    16 asm("   jmpq *addr_error_exit");
    17 asm("   nopl (%rax)");

    其中第9行addr_adjust_exception_frame是(pv_irq_ops+0x30)地址处存储的值;第11行addr_error_entry是error_entry的地址;第16行addr_error_exit是error_exit的地址。这几个值需要从System.map文件中查询,然后用内核模块参数的形式传入。而my_do_page_fault则是我们自己定义的page fault处理函数。

        如果需要截获X86_32的page fault,可以参考这个C文件。不过需要注意的是,新版内核有所变动,这里的代码需要根据自己的情况做一些调整。

        有了自定义的ISR之后,就可以将这个ISR填到IDT中,加载新的IDT表之后,自定义的page fault处理函数就开始发挥作用了。这个过程主要有以下几个步骤:

    1. 用store_idt(&default_idtr)保存现有的IDT寄存器值
    2. 从default_idtr中读出IDT表首地址和表的大小
    3. 申请一个页面
    4. 将原来的idt表拷贝到新申请的页面中
    5. 利用pack_gate将my_page_fault(注意不是my_do_page_fault)填入到对应的IDT项中
    6. 在idtr中填写新的IDT表地址和大小,用load_idt(&idtr)加载新的IDT表到当前CPU
    7. 利用smp_call_function,将新的IDT表加载到其他CPU上。

        如果想恢复原来的IDT表,则用load(&default_idtr)和smp_call_function加载原来的IDT表,释放申请的页面。

        读完文章之后,可以参考我的github中的代码:https://github.com/RichardUSTC/intercept-page-fault-handler

        本文链接:http://www.cnblogs.com/richardustc/archive/2013/05/03/3057455.html

    哎...今天够累的,签到来了1...
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    QQ|手机版|小黑屋|Java自学者论坛 ( 声明:本站文章及资料整理自互联网,用于Java自学者交流学习使用,对资料版权不负任何法律责任,若有侵权请及时联系客服屏蔽删除 )

    GMT+8, 2024-5-4 02:07 , Processed in 0.063332 second(s), 29 queries .

    Powered by Discuz! X3.4

    Copyright © 2001-2021, Tencent Cloud.

    快速回复 返回顶部 返回列表