linux 内核源代码情景分析——越界访问
页式存储管理机制通过页面目录和页面表将每个线性地址转换成物理地址,当遇到下面几种情况就会使CPU产生一次缺页中断,从而执行预定的页面异常处理程序:
① 相应的页面目录或页表项为空,也就是该线性地址与物理地址的关系还没建立或者已经撤销
② 相应的物理页面不在内存中
③ 指令规定的访问方式与页面的权限不符
我们假设一个情景,当我们的用户程序将一个打开的文件通过mmap()映射到内存,然后又通过munmap()撤销映射。在撤销一个映射区间时,常常会在虚存地址空间中留下一个空洞,而相应的地址则不应继续使用了,但是,程序中可能会有错误继续访问这个已经撤销的区域,这时候,一次因越界访问一个无效地址而引起映射失败,从而产生了一次页面出错异常。 我们假设CPU已经运行到页面异常服务程序的主体函数do_page_fault()的入口处
1 asmlinkage void do_page_fault(struct pt_regs *regs, unsigned long error_code)
2 {
3 struct task_struct *tsk;
4 struct mm_struct *mm;
5 struct vm_area_struct * vma;
6 unsigned long address;
7 unsigned long page;
8 unsigned long fixup;
9 int write;
10 siginfo_t info;
11
12 /* get the address */
13 __asm__("movl %%cr2,%0":"=r" (address));
14
15 tsk = current;
16
17 /*
18 * We fault-in kernel-space virtual memory on-demand. The
19 * 'reference' page table is init_mm.pgd.
20 *
21 * NOTE! We MUST NOT take any locks for this case. We may
22 * be in an interrupt or a critical region, and should
23 * only copy the information from the master page table,
24 * nothing more.
25 */
26 if (address >= TASK_SIZE)
27 goto vmalloc_fault;
28
29 mm = tsk->mm;
30 info.si_code = SEGV_MAPERR;
31
32 /*
33 * If we're in an interrupt or have no user
34 * context, we must not take the fault..
35 */
36 if (in_interrupt() || !mm)
37 goto no_context;
38
39 down(&mm->mmap_sem);
40
41 vma = find_vma(mm, address);
42 if (!vma)
43 goto bad_area;
44 if (vma->vm_start <= address)
45 goto good_area;
46 if (!(vma->vm_flags & VM_GROWSDOWN))
47 goto bad_area;
此函数参数,一个是pt_regs 结构指针regs,它指向异常发生前夕 CPU 中各寄存器内容的一份副本,这是由内核的中断响应机制保存下来的“现场”,另一个参数error_code则指明映射失败的具体原因。
首先是一行汇编代码,引文当i386 CPU 产生“页面错”异常时,CPU 将导致映射失败的线性地址放在控制寄存器CR2中,这行代码的作用就是读取CR2的内容到变量address。
然后是通过宏操作current来取得当前进程的task_struct结构的地址。
接下来,需要检测两个特殊情况,一个特殊情况是in_interrupt()返回非0,说明映射的失败发生在某个中断服务程序中,因而与当前进程毫无关系,另一个特殊情况是当前进程的mm指针为空,也就是该进程尚未建立映射,也就不可能与当前进程有关。若mm指针为空,且in_interrupt返回非0,,那这次异常发生在什么地方呢?其实还是在某个中断/异常的服务程序中,只不过不在in_interrupt()能检测到的范围而已。
以下有互斥的要求了,从down()返回后就不会有别的进程来打扰了。
在知道了发生映射失败的地址以及所属的进程以后,接下来应该要搞清楚的是这个地址是否落在某个已建立起映射的区间,或者进一步具体指出在哪个区间。事实正是这样,这就是find_vma()所做的事。以前讲过,find_vma()试图在一个虚存空间中找出结束地址大于给定地址的第一个区间,如果找不到的话,那么本次页面异常就必定是因越界访问而引起,那么,在什么情况下会找不到呢?回忆一下内核对用户虚存空间的使用,堆栈在用户区的顶部,从上而下扩展,而进程的代码和数据都是自底向上分配空间。如果没有一个区间的结束地址高于给定的地址,那就说明这个地址是在堆栈之上,也就是3G字节以上了,要从用户空间访问内核空间,当然是越界了,然后就转向bad_area,发生映射失败的地址对应下图的①
如果找到了这么一个区间,而且其起始地址又不高于给定的地址,那就说明给定的地址恰好落在这个区间,这样,映射肯定已经建立,所以就转向good_area去进一步检查失败的原因,发生映射失败的地址对应下图的②或⑤
剩下的情况就是给定地址正好落在两个区间当中的空洞里,也就是该地址所在页面的映射尚未建立或已经撤销,在用户虚存空间中,可能有两种不同的空洞,第一种空洞只能有一个,那就是堆栈区以下的那个大空洞,它代表着工动态分配而仍未分配出去的空间。但是怎样知道这个地址是落在这个空洞里呢?我们知道,堆栈是向下扩展的,如果find_vma()找到的区间是堆栈区间,那么它的vm_flags中应该有标志位VM_GROWSDOWN。要是该标志位为0的话,那就说明空洞上方的区间并非堆栈区,说明这个空洞是因为一个映射区间被撤销而留下的,或者是在建立映射时跳过了一块地址,发生映射失败的地址对应下图的④

下面,我们就随着goto语句转向bad_area,
【do_page_fault】
1 bad_area:
2 up(&mm->mmap_sem);
3
4 bad_area_nosemaphore:
5 /* User mode accesses just cause a SIGSEGV */
6 if (error_code & 4) {
7 tsk->thread.cr2 = address;
8 tsk->thread.error_code = error_code;
9 tsk->thread.trap_no = 14;
10 info.si_signo = SIGSEGV;
11 info.si_errno = 0;
12 /* info.si_code has been set above */
13 info.si_addr = (void *)address;
14 force_sig_info(SIGSEGV, &info, tsk);
15 return;
16 }
当error_code的bit2为1时,表示失败是当CPU处于用户模式时发生的,这与我们的情景相符,所以控制将进入第6行,在那里,对当前进程的task_struct结构内的一些成员进行一些设置以后,就向该进程发出一个强制的“信号”SIGSEGV,至此,本次例外服务程序就结束了。
每次中断/异常返回之前,都要检查当前进程是否有悬而未决的信号需要处理,然后,内核根据这些待处理的信号的性质以及进程本身的选择决定怎么办,而对于SIGSEGV的处理结果是在该进程的显示屏上显示“Segment Fault”提示,然后结束进程
linux 内核源代码情景分析——越界访问的更多相关文章
- Linux内核源代码情景分析系列
http://blog.sina.com.cn/s/blog_6b94d5680101vfqv.html Linux内核源代码情景分析---第五章 文件系统 5.1 概述 构成一个操作系统最重要的就 ...
- Linux内核源代码情景分析-fork()
父进程fork子进程: child = fork() fork经过系统调用.来到了sys_fork.具体过程请參考Linux内核源码情景分析-系统调用. asmlinkage int sys_fork ...
- linux 内核源代码情景分析——用户堆栈的扩展
上一节中,我们浏览了一次因越界访问而造成映射失败从而引起进程流产的过程,不过有时候,越界访问时正常的.现在我们就来看看当用户堆栈过小,但是因越界访问而"因祸得福"得以伸展的情景. ...
- linux 内核源代码情景分析——地址映射的全过程
linux 内核采用页式存储管理.虚拟地址空间划分成固定大小的"页面",由MMU在运行时将虚拟地址映射成某个物理内存页面中的地址.页式内存管理比段式内存管理有很多好处,但是由于In ...
- linux 内核源代码情景分析——linux 内存管理的基本框架
386 CPU中的页式存管的基本思路是:通过页面目录和页面表分两个层次实现从线性地址到物理地址的映射.这种映射模式在大多数情况下可以节省页面表所占用的空间.因为大多数进程不会用到整个虚存空间,在虚存空 ...
- linux 内核源代码情景分析——linux 内核源代码中的C语言代码
linux 内核的主体是以GNU的C语言编写的,GNU为此提供了编译工具gcc.GNU对C语言本身作了不少扩充. 1) gcc 从 C++ 语言中吸收了"inline"和" ...
- Linux内核源代码情景分析-中断半
一.中断初始化 1.中断向量表IDT初始化 void __init init_IRQ(void) { int i; #ifndef CONFIG_X86_VISWS_APIC init_ISA_irq ...
- linux 内核源代码情景分析——几个重要的数据结构和函数
页面目录PGD.中间目录PMD和页面表PT分别是由表项pgd_t.pmd_t和pte_t构成的数组,而这些表项都是数据结构 1 /* 2 * These are used to make use of ...
- linux 内核源代码情景分析——linux 内核源码中的汇编语言代码
1. 用汇编语言编写部分核心代码的原因: ① 操作系统内核中的底层程序直接与硬件打交道,需要用到一些专用的指令,而这些指令在C语言中并无对应的语言成分: ② CPU中的一些特殊指令也没有对应的C语言成 ...
随机推荐
- PHP中DirectIO直操作文件扩展的使用
关于 PHP 的文件操作,我们也将是通过一系列的文章来进行学习.今天我们先学习的是一个很少人使用过,甚至很多人根本不知道的扩展,它与我们日常的文件操作有些许的不同.不过这些差别并不是我们肉眼所能直观看 ...
- 深入学习Composer原理(四)
本系列第四篇文章,也是最后一篇 首先,我们先看看Composer的源码从哪里看起.当然,请您先准备好源码. composer init或者直接install之后,自动生成了一个vendor目录,这时您 ...
- Java基础系列(34)- 什么是数组
数组的定义 数组是相同类型数据的有序集合 数组描述的是相同类型的若干个数据,按照一定的先后次序排列组合而成 其中,每一个数据称作一个数组元素,每个数组元素可以通过一个下标来访问它们
- Java基础系列(22)- For循环详解
For循环 虽然所有循环结构都可以用while和dowhile表示,但是Java提供了另外一种语句for循环,使一些循环结构变动更加简单 for循环语句是支持迭代的一种通用结构,是最有效.最灵活的循环 ...
- maven编译打包
sonar扫描java项目,需要使用maven 来到maven项目下第一件事情编译打包,注意代码扫描是在编译之后的:https://blog.csdn.net/qq_34556414/article/ ...
- 重新嫁接rm命令
### 重定义rm命令 #### 定义回收站目录trash_path='~/.trash'# 判断 $trash_path 定义的文件是否存在,如果不存在,那么就创建 $trash_path.if [ ...
- pyqt5读取文本框内容,输出到日志框(QTextBrowser)
import sys from PyQt5.QtWidgets import QApplication, QMainWindow, QAction,QLabel,QLineEdit,QPushButt ...
- MyBatis 面试复习整理
MyBatis MyBatis 是一款优秀的ORM(对象关系映射)框架,可以通过对象和数据库之间的映射,将程序中的对象自动存储到数据库中.它内部封装了 JDBC ,使开发者只需要关注 SQL语句本身, ...
- 1.1 jvm核心类加载器--jdk源码剖析
目录 前提: 运行环境 1. 类加载的过程 1.1 类加载器初始化的过程 1.2 类加载的过程 1.3 类的懒加载 2. jvm核心类加载器 3. 双亲委派机制 4. 自定义类加载器 5. tomca ...
- 简单Tab切换
延迟Tab切换,使用css中的flex布局,原生js实现.(京东首页菜单也有此延迟功能哦!) 每天进步一丢丢~~ 1.延迟Tab切换 <!DOCTYPE html> <html la ...