_nosay 发表于 2016-09-19 11:50

越界访问


[*]越界访问实例
    之前提到过,段式/页式映射管理中,映射关系由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]
查看完整版本: 越界访问