linux缺页异常处理--内核空间【转】

简介:

        缺页异常被触发通常有两种情况——

1.程序设计的不当导致访问了非法的地址

2.访问的地址是合法的,但是该地址还未分配物理页框

下面解释一下第二种情况,这是虚拟内存管理的一个特性。尽管每个进程独立拥有3GB的可访问地址空间,但是这些资源都是内核开出的空头支票,也就是说进程手握着和自己相关的一个个虚拟内存区域(vma),但是这些虚拟内存区域并不会在创建的时候就和物理页框挂钩,由于程序的局部性原理,程序在一定时间内所访问的内存往往是有限的,因此内核只会在进程确确实实需要访问物理内存时才会将相应的虚拟内存区域与物理内存进行关联(为相应的地址分配页表项,并将页表项映射到物理内存),也就是说这种缺页异常是正常的,而第一种缺页异常是不正常的,内核要采取各种可行的手段将这种异常带来的破坏减到最小。

       缺页异常的处理函数为do_page_fault(),该函数是和体系结构相关的一个函数,缺页异常的来源可分为两种,一种是内核空间(访问了线性地址空间的第4个GB),一种是用户空间(访问了线性地址空间的0~3GB),以X86架构为例,先来看内核空间异常的处理。

[cpp]  view plain  copy
 
  1. dotraplinkage void __kprobes  
  2. do_page_fault(struct pt_regs *regs, unsigned long error_code)  
  3. {  
  4.     struct vm_area_struct *vma;  
  5.     struct task_struct *tsk;  
  6.     unsigned long address;  
  7.     struct mm_struct *mm;  
  8.     int write;  
  9.     int fault;  
  10.   
  11.     tsk = current; //获取当前进程  
  12.     mm = tsk->mm;  //获取当前进程的地址空间  
  13.   
  14.     /* Get the faulting address: */  
  15.     address = read_cr2(); //读取CR2寄存器获取触发异常的访问地址  
  16.       
  17.     ...  
  18.          ...  
  19.   
  20.     if (unlikely(fault_in_kernel_space(address))) { //判断address是否处于内核线性地址空间  
  21.         if (!(error_code & (PF_RSVD | PF_USER | PF_PROT))) {//判断是否处于内核态  
  22.             if (vmalloc_fault(address) >= 0)//处理vmalloc异常  
  23.                 return;  
  24.   
  25.             if (kmemcheck_fault(regs, address, error_code))  
  26.                 return;  
  27.         }  
  28.   
  29.         /* Can handle a stale RO->RW TLB: */  
  30.         /*异常发生在内核地址空间但不属于上面的情况或上面的方式无法修正, 
  31.           则检查相应的页表项是否存在,权限是否足够*/  
  32.         if (spurious_fault(error_code, address))  
  33.             return;  
  34.   
  35.         /* kprobes don't want to hook the spurious faults: */  
  36.         if (notify_page_fault(regs))  
  37.             return;  
  38.         /* 
  39.          * Don't take the mm semaphore here. If we fixup a prefetch 
  40.          * fault we could otherwise deadlock: 
  41.          */  
  42.         bad_area_nosemaphore(regs, error_code, address);  
  43.   
  44.         return;  
  45.     }  
  46.     ...  
  47.     ...  
  48. }  


该函数传递进来的两个参数--

regs包含了各个寄存器的值

error_code是触发异常的错误类型,它的含义如下

[cpp]  view plain  copy
 
  1. /* 
  2.  * Page fault error code bits: 
  3.  * 
  4.  *   bit 0 ==    0: no page found   1: protection fault 
  5.  *   bit 1 ==    0: read access     1: write access 
  6.  *   bit 2 ==    0: kernel-mode access  1: user-mode access 
  7.  *   bit 3 ==               1: use of reserved bit detected 
  8.  *   bit 4 ==               1: fault was an instruction fetch 
  9.  */  
  10. enum x86_pf_error_code {  
  11.   
  12.     PF_PROT     =       1 << 0,  
  13.     PF_WRITE    =       1 << 1,  
  14.     PF_USER     =       1 << 2,  
  15.     PF_RSVD     =       1 << 3,  
  16.     PF_INSTR    =       1 << 4,  
  17. };  


首先要检查该异常的触发地址是不是位于内核地址空间 也就是address>=TASK_SIZE_MAX,一般为3GB。然后要检查触发异常时是否处于内核态,满足这两个条件就尝试通过vmalloc_fault()来解决这个异常。由于使用vmalloc申请内存时,内核只会更新主内核页表,所以当前使用的进程页表就有可能因为未与主内核页表同步导致这次异常的触发,因此该函数试图将address对应的页表项与主内核页表进行同步

[cpp]  view plain  copy
 
  1. static noinline int vmalloc_fault(unsigned long address)  
  2. {  
  3.     unsigned long pgd_paddr;  
  4.     pmd_t *pmd_k;  
  5.     pte_t *pte_k;  
  6.   
  7.     /* 确定触发异常的地址是否处于VMALLOC区域*/  
  8.     if (!(address >= VMALLOC_START && address < VMALLOC_END))  
  9.         return -1;  
  10.   
  11.     /* 
  12.      * Synchronize this task's top level page-table 
  13.      * with the 'reference' page table. 
  14.      * 
  15.      * Do _not_ use "current" here. We might be inside 
  16.      * an interrupt in the middle of a task switch.. 
  17.      */  
  18.     pgd_paddr = read_cr3();//获取当前的PGD地址  
  19.     pmd_k = vmalloc_sync_one(__va(pgd_paddr), address);//将当前使用的页表和内核页表同步  
  20.     if (!pmd_k)  
  21.         return -1;  
  22.   
  23.     /*到这里已经获取了内核页表对应于address的pmd,并且将该值设置给了当前使用页表的pmd, 
  24.       最后一步就是判断pmd对应的pte项是否存在*/  
  25.     pte_k = pte_offset_kernel(pmd_k, address);//获取pmd对应address的pte项  
  26.     if (!pte_present(*pte_k))//判断pte项是否存在,不存在则失败  
  27.         return -1;  
  28.   
  29.     return 0;  
  30. }  

同步处理:

[cpp]  view plain  copy
 
  1. static inline pmd_t *vmalloc_sync_one(pgd_t *pgd, unsigned long address)  
  2. {  
  3.     unsigned index = pgd_index(address);  
  4.     pgd_t *pgd_k;  
  5.     pud_t *pud, *pud_k;  
  6.     pmd_t *pmd, *pmd_k;  
  7.   
  8.     pgd += index; //记录当前页表pgd对应address的偏移  
  9.     pgd_k = init_mm.pgd + index;//记录内核页表对应address的偏移  
  10.   
  11.     if (!pgd_present(*pgd_k))//内核PGD页表对应的项不存在,则无法进行下一步,返回NULL  
  12.         return NULL;  
  13.   
  14.     /* 
  15.      * set_pgd(pgd, *pgd_k); here would be useless on PAE 
  16.      * and redundant with the set_pmd() on non-PAE. As would 
  17.      * set_pud. 
  18.      */  
  19.        
  20.     /*获取当前页表对应address的PUD地址和内核页表对应address的地址,并判断pud_k对应的项是否存在*/  
  21.     pud = pud_offset(pgd, address);  
  22.     pud_k = pud_offset(pgd_k, address);  
  23.     if (!pud_present(*pud_k))  
  24.         return NULL;  
  25.   
  26.     /*对pmd进行和上面类似的操作*/  
  27.     pmd = pmd_offset(pud, address);  
  28.     pmd_k = pmd_offset(pud_k, address);  
  29.     if (!pmd_present(*pmd_k))  
  30.         return NULL;  
  31.   
  32.     if (!pmd_present(*pmd))//当前使用页表对应的pmd项不存在,则修正pmd项使其和内核页表的pmd_k项相同  
  33.         set_pmd(pmd, *pmd_k);  
  34.     else  
  35.         BUG_ON(pmd_page(*pmd) != pmd_page(*pmd_k));  
  36.   
  37.     return pmd_k;  
  38. }  



 如果do_page_fault()函数执行到了bad_area_nosemaphore(),那么就表明这次异常是由于对非法的地址访问造成的。在内核中产生这样的结果的情况一般有两种:

1.内核通过用户空间传递的系统调用参数,访问了无效的地址

2.内核的程序设计缺陷

第一种情况内核尚且能通过异常修正机制来进行修复,而第二种情况就会导致OOPS错误了,内核将强制用SIGKILL结束当前进程。

内核态的bad_area_nosemaphore()的实际处理函数为bad_area_nosemaphore()-->__bad_area_nosemaphore()-->no_context()

[cpp]  view plain  copy
 
  1. <span style="font-size:12px;">static noinline void  
  2. no_context(struct pt_regs *regs, unsigned long error_code,  
  3.        unsigned long address)  
  4. {  
  5.     struct task_struct *tsk = current;  
  6.     unsigned long *stackend;  
  7.     unsigned long flags;  
  8.     int sig;  
  9.   
  10.     /* Are we prepared to handle this kernel fault? */  
  11.     /*fixup_exception()用于搜索异常表,并试图找到一个对应该异常的例程来进行修正, 
  12.       这个例程在fixup_exception()返回后执行*/  
  13.     if (fixup_exception(regs))  
  14.         return;  
  15.   
  16.     /* 
  17.      * 32-bit: 
  18.      * 
  19.      *   Valid to do another page fault here, because if this fault 
  20.      *   had been triggered by is_prefetch fixup_exception would have 
  21.      *   handled it. 
  22.      * 
  23.      * 64-bit: 
  24.      * 
  25.      *   Hall of shame of CPU/BIOS bugs. 
  26.      */  
  27.     if (is_prefetch(regs, error_code, address))  
  28.         return;  
  29.   
  30.     if (is_errata93(regs, address))  
  31.         return;  
  32.   
  33.     /* 
  34.      * Oops. The kernel tried to access some bad page. We'll have to 
  35.      * terminate things with extreme prejudice: 
  36.      */  
  37.     /* 走到这里就说明异常确实是由于内核的程序设计缺陷导致的了,内核将 
  38.        产生一个oops,下面的工作就是打印CPU寄存器和内核态堆栈的信息到控制台并 
  39.        终结当前的进程*/  
  40.     flags = oops_begin();  
  41.   
  42.     show_fault_oops(regs, error_code, address);  
  43.   
  44.     stackend = end_of_stack(tsk);  
  45.     if (*stackend != STACK_END_MAGIC)  
  46.         printk(KERN_ALERT "Thread overran stack, or stack corrupted\n");  
  47.   
  48.     tsk->thread.cr2      = address;  
  49.     tsk->thread.trap_no  = 14;  
  50.     tsk->thread.error_code   = error_code;  
  51.   
  52.     sig = SIGKILL;  
  53.     if (__die("Oops", regs, error_code))  
  54.         sig = 0;  
  55.   
  56.     /* Executive summary in case the body of the oops scrolled away */  
  57.     printk(KERN_EMERG "CR2: %016lx\n", address);  
  58.   
  59.     oops_end(flags, regs, sig);  
  60. }  
  61. </span>  


















本文转自张昺华-sky博客园博客,原文链接:http://www.cnblogs.com/sky-heaven/p/5663385.html,如需转载请自行联系原作者

相关文章
|
10月前
|
安全 网络协议 Linux
深入理解Linux内核模块:加载机制、参数传递与实战开发
本文深入解析了Linux内核模块的加载机制、参数传递方式及实战开发技巧。内容涵盖模块基础概念、加载与卸载流程、生命周期管理、参数配置方法,并通过“Hello World”模块和字符设备驱动实例,带领读者逐步掌握模块开发技能。同时,介绍了调试手段、常见问题排查、开发规范及高级特性,如内核线程、模块间通信与性能优化策略。适合希望深入理解Linux内核机制、提升系统编程能力的技术人员阅读与实践。
916 1
|
10月前
|
Ubuntu Linux
Ubuntu 23.04 用上 Linux 6.2 内核,预计下放到 22.04 LTS 版本
Linux 6.2 带来了多项内容更新,修复了 AMD 锐龙处理器设备在启用 fTPM 后的运行卡顿问题,还增强了文件系统。
|
10月前
|
Ubuntu Linux
Ubuntu 23.10 现在由Linux内核6.3提供支持
如果你想在你的个人电脑上测试一下Ubuntu 23.10的最新开发快照,你可以从官方下载服务器下载最新的每日构建ISO。然而,请记住,这是一个预发布版本,所以不要在生产机器上使用或安装它。
|
10月前
|
传感器 监控 Ubuntu
10 月发布,Ubuntu 23.10 已升级到 Linux Kernel 6.3 内核
硬件方面,Linux 6.3 引入了在 HID 中引入了原生的 Steam Deck 控制器接口,允许罗技 G923 Xbox 版赛车方向盘在 Linux 上运行;改善 8BitDo Pro 2 有线控制器的行为;并为一系列华硕 Ryzen 主板添加传感器监控。
|
10月前
|
Ubuntu Linux
Ubuntu24.04LTS默认采用Linux 6.8内核,实验性版本可通过PPA获得
IT之家提醒,当下的 Ubuntu 23.10 也是一个“短期支持版本”,该版本将在今年 7 月终止支持,而今年 4 月推出的 Ubuntu 24.04 LTS 长期支持版本将获得 5 年的更新支持。
|
10月前
|
监控 Ubuntu Linux
什么Linux,Linux内核及Linux操作系统
上面只是简单的介绍了一下Linux操作系统的几个核心组件,其实Linux的整体架构要复杂的多。单纯从Linux内核的角度,它要管理CPU、内存、网卡、硬盘和输入输出等设备,因此内核本身分为进程调度,内存管理,虚拟文件系统,网络接口等4个核心子系统。
1038 0
|
10月前
|
Web App开发 缓存 Rust
|
10月前
|
Ubuntu 安全 Linux
Ubuntu 发行版更新 Linux 内核,修复 17 个安全漏洞
本地攻击者可以利用上述漏洞,攻击 Ubuntu 22.10、Ubuntu 22.04、Ubuntu 20.04 LTS 发行版,导致拒绝服务(系统崩溃)或执行任意代码。
|
10月前
|
Ubuntu 机器人 物联网
Linux Ubuntu 22.04 LTS 测试版实时内核已可申请
请注意,在启用实时内核后您需要手动配置 grub 以恢复到原始内核。更多内容请参考: