越界访问
[*]越界访问实例
之前提到过,段式/页式映射管理中,映射关系由Linux内核建立,映射过程由硬件完成。
现假设分配28K内存返回的起始虚拟地址为0x1bf2000,释放后又访问0x1bf2000:
在未释放前,28K虚拟地址,按4K页面为单位,与物理页面一一建立映射,比如以0x1bf2000为起始的虚拟页面,目录、页表中都有关于该虚拟地址映射关系的目录项/表项:
在释放后,内核就会将目录项/表项清零,此时再访问0x1bf2000虚拟地址时,硬件就会顺着页式映射的过程,找到空目录项或空表项,硬件就会产生一次异常,总之,经过一段~!@#$%^&……过程之后(详见《Linux内核源代码情景分析》第三章),会调用到内核中的do_page_fault()函数。
由于仅通过目前已总结的内核知识,根本无法彻底理解这个函数,所以等学习过中断异常后,再回头详细分析这个函数,暂时只需要了解有这样一个函数,并且这个函数内部会通过一个current宏得到当前用户进程的管理结构,并向其发送SIGSEGV信号,也便是让程序员们如痴如醉的“段错误”!!
[*]因祸得福
内核像是一位“严教、随和的老人”,上述的例子,领教了他严教的一面,对于有些栈空间的越界情况,他又会尽量的宽恕,原因是每个进程可以使用的栈空间大小是有上限的(比如2M),但内核在创建进程之初,一般不会一下子分配2M物理页面用于该进程的栈,等真的需要这么多时,才进行扩展。
什么情况表示“需要”?
就是进程在栈空间出现访问越界的时候,可以让esp寄存器移动最多的一条栈操作指令是pusha,可以让esp向下移动32byte,所以如果越界超过32byte长度,仍然是明显的非法操作,内核还是会发个SIGSEGV信号给用户进程。所以说内核对用户进程对栈空间越界访问的容忍,也不排除是容忍了一次真的越界访问,那样的话就说明用户进程有逻辑错误,这种错误也只会影响到该进程本身,早晚由程序员自己发现(内核总不至于帮app纠正“对话框歪了”等等这种bug吧{:qq23:})。
了解了“需要”的判断依据,当do_page_fault()正好面对这种情况时,就会调用expand_stack()对该进程的栈区间进行扩展。对于用户进程来说,一次越界访问,不但没有造成段错误,还得到了更多的内存,其实这种越界操作,对于用户进程往往是透明的,除非故意定义一个超大的局部变量,比如int num。
所谓“扩展”,就是分配内存,主要包括三个事情:分配虚拟内存、分配物理内存、建立映射(建议大致看一下do_page_fault())。
do_page_fault()
|
|--> expand_stack()// 扩展了虚拟内存
|--> handle_mm_fault()
| |
| |--> pte_alloc()
| |--> handle_pte_fault()
| | |
| | |--> do_no_page()
| | | |
| | | |--> do_anonymous_page()
| | | | |
| | | | |--> alloc_page()// 扩展了物理内存
| | | | |--> set_pte() // 建立映射(Linux内核建立映射,需要设置PGD、PMD、PT,所以详见代码)
[*]各个用户进程的内存布局
直接贴上书里的图片(简洁粗略):
页:
[1]