linux内存管理-越界访问

页式存储管理机制通过页面目录和页面表将每个线性地址(也可以称为虚拟地址)转换成物理地址。如果在这个过程中遇到某种阻碍而使CPU无法最终访问到相应的物理内存单元,映射就失败了。而当前的指令也就不能执行完成。此时CPU会产生一次页面出错(page fault)异常(也称缺页异常中断),进而执行预定的页面异常处理程序,使应用程序得以因映射失败而暂停的指令处开始恢复执行,或进行一些善后处理。这里所说的阻碍可以由以下几种情况:

  • 相应的页面目录项或页面表项为空,也就是该线性地址与物理地址的映射关系尚未建立,或者已经撤销
  • 相应的物理页面不在内存中
  • 指令中规定的访问方式与页面的权限不符,例如企图写一个只读的页面

在这个情景中,我们假定一段用户程序曾经将一个已打开文件通过mmap系统调用映射到内存,然后又已经将映射撤销(通过munmap系统调用)。在撤销一个映射区间时,常常会在虚存地址空间中留下一个孤立的空洞,而相应的地址则不应该继续使用了。但是,在用户程序中往往会有错误,以致在程序中某个地方还再次访问这个已经撤销的区域(程序员们一定会同意,这是不足为奇的)。这时候,一次因越界访问一个无效地址(invalid address)而引起映射失败,从而就产生了一次页面出错异常。中断请求以及异常的响应机制将在中断和异常的博客中集中介绍,我们在那里可以找到从发生异常到进入内核相应服务程序的全过程。这里假定CPU的运行已经到达了页面异常服务程序放的主体do_page_fault的入口处。

函数do_page_fault的代码比较长,我们将随着情景的进展按需要来展示其有关的片段。这里先看开头几行代码:

/*
 * This routine handles page faults.  It determines the address,
 * and the problem, and then passes it off to one of the appropriate
 * routines.
 *
 * error_code:
 *	bit 0 == 0 means no page found, 1 means protection fault
 *	bit 1 == 0 means read, 1 means write
 *	bit 2 == 0 means kernel, 1 means user-mode
 */
asmlinkage void do_page_fault(struct pt_regs *regs, unsigned long error_code)
{
	struct task_struct *tsk;
	struct mm_struct *mm;
	struct vm_area_struct * vma;
	unsigned long address;
	unsigned long page;
	unsigned long fixup;
	int write;
	siginfo_t info;

	/* get the address */
	__asm__("movl %%cr2,%0":"=r" (address));

	tsk = current;

	/*
	 * We fault-in kernel-space virtual memory on-demand. The
	 * 'reference' page table is init_mm.pgd.
	 *
	 * NOTE! We MUST NOT take any locks for this case. We may
	 * be in an interrupt or a critical region, and should
	 * only copy the information from the master page table,
	 * nothing more.
	 */
	if (address >= TASK_SIZE)
		goto vmalloc_fault;

	mm = tsk->mm;
	info.si_code = SEGV_MAPERR;

	/*
	 * If we're in an interrupt or have no user
	 * context, we must not take the fault..
	 */
	if (in_interrupt() || !mm)
		goto no_context;

	down(&mm->mmap_sem);

	vma = find_vma(mm, address);
	if (!vma)
		goto bad_area;
	if (vma->vm_start <= address)
		goto good_area;
	if (!(vma->vm_flags & VM_GROWSDOWN))
		goto bad_area;

首先是一行汇编代码。为什么要用汇编代码呢?当i386 CPU产生页面出错异常时,CPU将导致映射失败的线性地址放在控制寄存器CR2中,而这显然是相应的服务程序所必需的信息。可是,在c语言中并没有相应的语法可以用来读取CR2的内容,所以只能用汇编代码。这行汇编代码只有输出部分而没有输入部分,它将%0与变量address相结合,并说明该变量应该被分配在一个寄存器中。

同时,内核的中断、异常响应机制还传过来两个参数。一个是pt_regs结构指针regs。它指向例外发生前夕CPU中各寄存器内容的一份副本,这是由内核的中断响应机制保存下来的现场。而error_code则进一步指明映射失败的具体原因。

然后是获取当前进程的task_struct数据结构。在内核中,可以通过一个宏操作current取得当前进程(当前正在运行的进程)的task_struct结构的地址。在每个进程的task_struct结构中有一个指针,指向其mm_struct结构,而跟虚存管理和映射有关的信息都在那个结构中。这里要指出,CPU实际进行的映射并不涉及mm_struct结构,而是向之前讲的那样通过页面目录和页面表进行,但是mm_struct结构反映了,或者说描述了这种映射。

接下来,需要检测两个特殊的情况。一个特殊情况是in_interrupt返回非0,说明映射的失败发生在某个中断服务程序中,因而与当前进程毫无关系。另一个特殊情况是当前进程的mm指针为空,也就是说该进程的映射尚未建立,当然也就不可能与当前进程有关。可是,不跟当前进程有关,in_interrupt又返回0。那这次异常发生在什么地方呢?其实还是在某个中断、异常服务程序中,只不过不在in_interrupt能检测到的范围中而已。如果发生这些特殊情况,控制就通过goto语句转到标号no_context处,不过那与我们这个情景无关,所以我们略去对那段代码的讨论。

以下的操作有互斥的要求,也就是不容许别的进程来打扰,所以要由对信号量的PV操作,即down()/up()操作来保证。为了这个目的,在mm_struct结构中还设置了所需的信号量mmap_sem。这样,从down返回后,就不会有别的进程来打扰了。

可以想象,在知道了发生映射失败的地址以及所属的进程以后,接下来应该要搞清楚的是这个地址是否落在某个已经建立映射的区间,或者进一步具体指出在哪个区间。事实正是如此,这就是find_vma所要做的事情。以前讲过,find_vma试图在一个虚存空间中找出结束地址大于给定地址的第一个区间。如果找不到的话,那本次页面异常就必定是因越界访问而引起。那么,在什么情况下会找不到?回忆一下内核对用户虚存空间的使用,堆栈在用户去的顶部,从上向下伸展,而进程的代码和数据都是自底向上分配空间。如果没有一个区间的结束地址高于给定的地址,那就说明这个地址是在堆栈之上,也就是3G字节以上了。要从用户空间访问属于系统的空间,那当然是越界了,然后就转向bad_area,不过我们这个情景所说的不是这个情况。

如果找到了这么一个区间,而且其起始地址又不高于给定的地址(见148行),那就说明给定的地址恰好落在这个区间。这样,映射肯定已经建立,所以就转向good_area去进一步检查失败的原因。这也不是我们这个情景所要说的。

除了这两种情况,剩下的就是给定地址正落在两个区间当中的空洞里,也就是该地址所在页面的映射尚未建立或已经撤销。在用户虚存空间中,可能有两种不同的空洞。第一种空洞只能有一个,那就是在堆栈区以下的那个大空洞,它代表着供动态分配(通过系统调用brk)而仍未分配出去的空洞。当映射失败的地址是落在这个空洞里呢?请看程序150行。我们知道,堆栈区间是向下伸展的,如果find_vma找到的区间是堆栈区间,那么在它的vm_flags中应该有个标志位VM_GROWSDOWN。要是该标志位为0的话,那就说明空洞上方的区间并非堆栈区,说明这个空洞是因为一个映射区间被撤销而留下的,或者在建立映射时跳过了一块地址。这就是第二种可能,也是我们这个情景所说的情况。所以,我们就随着这里的goto语句转向bad_area,那是在224行:

/*
 * Something tried to access memory that isn't in our memory map..
 * Fix it, but check if it's kernel or user first..
 */
bad_area:
	up(&mm->mmap_sem);

bad_area_nosemaphore:
	/* User mode accesses just cause a SIGSEGV */
	if (error_code & 4) {
		tsk->thread.cr2 = address;
		tsk->thread.error_code = error_code;
		tsk->thread.trap_no = 14;
		info.si_signo = SIGSEGV;
		info.si_errno = 0;
		/* info.si_code has been set above */
		info.si_addr = (void *)address;
		force_sig_info(SIGSEGV, &info, tsk);
		return;
	}

首先,当控制流达到这里时,已经不再需要互斥(因为不再对mm_struct结构进行操作),所以通过up退出临界区。接着,就要进一步考察error_code,看看失败的具体原因。代码的作者为此加了注释:


/*
 * This routine handles page faults.  It determines the address,
 * and the problem, and then passes it off to one of the appropriate
 * routines.
 *
 * error_code:
 *	bit 0 == 0 means no page found, 1 means protection fault
 *	bit 1 == 0 means read, 1 means write
 *	bit 2 == 0 means kernel, 1 means user-mode
 */

当error_code的bit2为1时,表示失败是当CPU处于用户模式时发生的,这正与我们的情景相符,所以控制将进入229行。在那里,对当前进程的task_struct结构内的一些成分进行一些设置以后,就向该进程发出一个强制的信号(或称软中断)SIGSEGV。至此,本次例外服务就结束了。

我们大概会问:就这样完了?是的,完了。接下来的详情,我们在看了有关中断处理和信号的博客以后就会明白。每次从中断、异常处理返回之前,都要检查当前进程是否有悬而未决的信号需要处理,在我们这个情景里当然是有的,其中至少有一个就是SIGSEGV。然后,内核根据这些待处理信号的特质及进程本身的选择决定怎么办。对有些软中断的处理是自愿的,有些则是强制的。而对于SIGSEGV的反应。那是强制的,其后果是在该进程的显示屏上显示程序员们怕见到却又经常见到的“Segment Fault”提示,然后使进程流产(撤销)。至于从异常处理返回用户空间后的地址,在这种情况下并无意义,因为本来就不会回去了。

我们在这里跳过了do_page_fault中的许多代码,因为那些代码与我们眼下这个特定的情景无关。不过,以后在其他的情景里我们还会回到这些代码中来。

Je suppose que tu aimes

Origine blog.csdn.net/guoguangwu/article/details/120615624
conseillé
Classement