越界访问

本文深入探讨了页式存储管理机制中页面错误处理的过程,包括如何通过页面目录和页面表将线性地址转换为物理地址,以及在转换过程中可能遇到的问题。详细介绍了do_page_fault()函数的工作原理,解释了在不同情况下如何处理页面错误,特别是如何通过扩展堆栈区域解决常见的映射失败问题。


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

1) 相应的页面目录或页面表项为空,也就是该线性地址与物理地址的映射尚未建立,或者已撤销。

2)相应的物理页面不在内存中。

3)指令中规定的访问方式与页面权限不符,例如企图写一个“只读”页面。

当CPU的运行已经到达了页面异常服务程序的主体do_page_fault()的入口处。

fastcall void do_page_fault(struct pt_regs *regs, unsigned long error_code)
{
/*pt_regs结构指针regs,它指向CPU发生前夕的各寄存器内容的一个副本;这是由内核的中断响应机制保存下来的"现场"
error_code则进一步指明映射失败的具体原因。*/

	struct task_struct *tsk;
	struct mm_struct *mm;
	struct vm_area_struct * vma;
	unsigned long address;
	unsigned long page;
	int write;
	siginfo_t info;


	/* get the address */
	/**
	 * 读取引起异常的线性地址。CPU控制单元把这个值存放在cr2控制寄存器中。
	 */
	__asm__("movl %%cr2,%0":"=r" (address));

	if (notify_die(DIE_PAGE_FAULT, "page fault", regs, error_code, 14,SIGSEGV) == NOTIFY_STOP)
		return;
	/* It's safe to allow irq's after cr2 has been saved */
	/**
	 * 只在保存了cr2就可以打开中断了。
	 * 如果中断发生前是允许中断的,或者运行在虚拟8086模式,就打开中断。
	 */
	if (regs->eflags & (X86_EFLAGS_IF|VM_MASK))
		local_irq_enable();
	tsk = current;  //取得当前进程的task_struct结构的地址
	info.si_code = SEGV_MAPERR;
	if (in_atomic() || !mm)
		goto bad_area_nosemaphore;
	vma = find_vma(mm, address);
	/**
	 * 如果vma为空,说明在出错地址后面没有线性区了,说明错误的地址肯定是无效的。
	 */ 
	if (!vma)
		goto bad_area;
	/**
	 * vma在address后面,并且它的起始地址在address前面,说明线性区包含了这个地址。
	 * 谢天谢地,这很可能不是真的错误,可能是COW机制起作用了,也可能是需要调页了。
	 */
	if (vma->vm_start <= address)//表示映射已经建立
		goto good_area;
	if (!(vma->vm_flags & VM_GROWSDOWN)) 当VM_GROWSDOWN为0的话。表示此区间不是堆栈区。

		goto bad_area;
	 * 运行到此,说明address地址后面的vma有VM_GROWSDOWN标志,表示它是一个堆栈区
	 * 请注意,如果是内核态访问用户态的堆栈空间,就应该直接扩展堆栈,而不判断if (address + 32 < regs->esp)
	 */
	if (error_code & 4) {
		/*
		 * accessing the stack below %esp is always a bug.
		 * The "+ 32" is there due to some instructions (like
		 * pusha) doing post-decrement on the stack and that
		 * doesn't show up until later..
		 */
		/**
		 * 虽然下一个线性区是堆栈,可是离非法地址太远了,不可能是操作堆栈引起的错误
		 * xie.baoyou注:32而不是4是考虑到pusha的存在。
		 */
		if (address + 32 < regs->esp)
			goto bad_area;
	}
	/**
	 * 线程堆栈空间不足,就扩展一下,一般会成功的,不会运行到bad_area.
	 * 注意:如果异常发生在内核态,说明内核正在访问用户态的栈,就直接扩展用户栈。
	 */
	if (expand_stack(vma, address))
		goto bad_area;

当运行到expand_stack()时,表示属于正常的堆栈扩展请求,那就从缺页的地方开始分配若干页面并建立映射,并将其并入堆栈区间,使其得以扩展。
expand_stack()中的操作(不粘源码了):
将地址按页面边界对齐,并计算需要增长几个页面才能把给定的地址包括进去(通常不是一个)。
在rlim结构数组,规定对各种资源分配使用的限制,所以需要进行检查;如果扩展后以后的空间大小超过了可用于堆栈的资源,或者使动态分配的页面总量超过了可用于该进程的资源限制,那就不能扩展,就会返回一个负的出错代码-ENOMEME,在do_page_fault()中也会返回bad_area;
在正常的请求下(改变了堆栈的结构数据),将转交给good_area完成(完成对新扩展的页面对物理内存的映射)。
good_area:先检查权限问题(堆栈可写),然后采用相应函数进行页面的分配。再分配页面表的时候,先看缓冲池(内核将释放的页面表会先保存在内存池中)中是否为空;
空了通过handle_pte_fault()来分配。-》do_no_page()-》do_anonymous_page()-》alloc_pages()为其分配一个的物理内存页面,通过set_pte()将分配到物理页面连同所有状态及标志位设置进page_table所指的页面表项。至此映射成功。
(中间有一部分代码因为在此情景不涉及没有说明)
特别指出,当CPU从一次页面错误异常处理返回到用户空间时,将会先重新执行因映射失败而中途夭折的那条指令,然后才继续往下执行。(不同于中断)


查看失败的具体原因。(bad_area)

bad_area_nosemaphore:
	/* User mode accesses just cause a SIGSEGV */
	/**
	 * 发生在用户态的错误地址。
	 * 就发生一个SIGSEGV信号给current进程,并结束函数。
	 */
	if (error_code & 4) {
		/* 
		 * Valid to do another page fault here because this one came 
		 * from user space.
		 */
		if (is_prefetch(regs, address, error_code))
			return;

		tsk->thread.cr2 = address;
		/* Kernel addresses are always protection faults */
		tsk->thread.error_code = error_code | (address >= TASK_SIZE);
		tsk->thread.trap_no = 14;
		info.si_signo = SIGSEGV;
		info.si_errno = 0;
		/* info.si_code has been set above */
		info.si_addr = (void __user *)address;
		/**
		 * force_sig_info确信进程不忽略或阻塞SIGSEGV信号
		 * SEGV_MAPERR或SEGV_ACCERR已经被设置在info.si_code中。
		 */
		force_sig_info(SIGSEGV, &info, tsk);
		return;
	}

当error_code的bit0为0,表示没有物理页面;bit1为1表示写操作;bit2为1时,表示失败是当CPU处于用户模式发生的。(VM_GROWSDOWN为1属于特殊情况)
对当前进程的task_struct结构内的一些成分进行设置后,就向该进程发出一个强制的信号“SIGSEGV”,(显示屏显示"Segment Fault"),然后使进程撤销。本次例外任务结束。
在每次从中断/异常返回之前,都要检查当前进程是否有悬而未决的信号需要处理。

### 关于野指针越界访问的原因分析 在C/C++编程中,野指针是指未初始化或已经释放但仍指向原来位置的指针。当程序试图通过这样的指针对内存进行读写操作时,就可能发生越界访问的情况。 #### 1. 野指针形成原因 - **未初始化指针**:声明了一个指针变量但没有为其分配有效的内存空间,在此情况下使用该指针可能导致不可预测的行为。 - **已删除对象上的悬空指针**:如果一个动态分配的对象被`delete`之后,对应的指针并没有设置成`NULL`,那么这个指针就成了所谓的“悬空指针”,继续使用它可能会引发错误[^1]。 #### 2. 越界访问的风险 一旦发生越界访问,不仅可能破坏其他数据结构的内容,还可能导致应用程序崩溃甚至安全漏洞。具体来说: - 访问超出数组边界的数据; - 修改不属于当前进程地址空间内的区域; - 对已经被回收重用的堆区内存执行非法的操作; 这些行为都违反了语言的安全性和稳定性原则,因此必须严格避免。 ### 解决方案 为了防止野指针引起的越界访问问题,建议采取如下措施: #### 使用智能指针替代原始指针 现代C++推荐采用标准库中的智能指针(如`std::unique_ptr`, `std::shared_ptr`),它们能够自动管理资源生命周期并有效减少手动管理带来的风险[^2]。 ```cpp #include <memory> int main() { std::unique_ptr<int> safePtr(new int(42)); // 不需要显式调用 delete, 析构函数会处理好一切 return 0; } ``` #### 初始化所有指针 确保每一个新创建出来的指针都被赋予合理的初始值(通常是`nullptr`)。这有助于后续逻辑判断以及调试期间发现问题所在。 ```cpp int* ptr = nullptr; // 正确做法 // 或者更简洁的方式 int *ptr{}; // C++11及以上版本支持大括号初始化语法 ``` #### 及时清理不再使用的指针 每当完成对某个由`new`关键字开辟出来的新对象的操作后,应该立即销毁之并将关联的裸指针置零以标记其为空闲状态。 ```cpp if (p != nullptr){ delete p; p = nullptr; } ``` #### 静态代码分析工具的应用 利用静态分析器可以在编译前发现潜在缺陷,比如PVS-Studio、Cppcheck等开源软件可以帮助开发者识别那些容易忽略却极其危险的地方。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值