有一种情况,越界访问是正常的,那就是用户堆栈过小。
arch/i386/mm/fault.c
do_page_fault()
... ...
if (!(vma->vm_flags & VM_GROWSDOWN))
goto bad_area;
if (error_code & 4) {
if (address + 32 < regs->esp)
goto bad_area;
}
... ...
因堆栈操作而引起的越界访问是作为特殊情况对待的,所以需要检查发生异常时的地址是否紧挨着堆栈的位置。通常,一次压入堆栈的是4个字节,但是i386CPU有一条pusha指令,可以一次将32个字节(8个32位寄存器的内容)压入堆栈,所以检查的标准是%esp-32,超出这个范围就一定是错的了。
但是不超出这个范围的访问也不一定不是非法的,但是linux没有检查这一部分!或许很难在这里知道,这个非法访问是不是由pusha或者其它指令造成的。
堆栈的扩展不是不受限制的,每个进程的task_struct结构中都有个rlim结构数组,规定了各种资源分配使用的限制,其中的RLIMIT_STACK项就是对用户空间堆栈大小的限制。
扩展堆栈的函数expand_stack()如果正常返回,说明改变了堆栈区vm_area_struct结构,但是并未建立起新扩展的页面对物理内存的映射。
虚拟内存管理handle_mm_fault(),定义于mm/memory.c
int handle_mm_fault(struct mm_struct *mm, struct vm_area_struct * vma,
unsigned long address, int write_access)
{
int ret = -1;
pgd_t *pgd;
pmd_t *pmd;
current->state = TASK_RUNNING;
pgd = pgd_offset(mm, address);
spin_lock(&mm->page_table_lock);
pmd = pmd_alloc(mm, pgd, address);
if (pmd) {
pte_t * pte = pte_alloc(mm, pmd, address);
if (pte)
ret = handle_pte_fault(mm, vma, address, write_access, pte);
}
spin_unlock(&mm->page_table_lock);
return ret;
}
先分配(或者找到)一个中间目录项pmd,再找到页面目录,再根据地址找到相应的页面表项,这样,才可以为下面分配物理内存页面并建立映射做好准备。这都是通过pte_alloc()完成的。
分配到一个页面表以后,就通过set_pmd()将其起始地址连同一些属性标志位一起写入中间目录表项中,而对i386却实际上写入到页面目录项pgd中。这样,映射所需的“基础设施”都已经齐全了,但是页面表项pte还是空的。剩下的就是物理内存页面本身了,那是由handle_pte_fault()完成的。
对于堆栈的扩展,一定会进入do_no_page()函数中。而在do_no_page()函数中,vm_ops又不会有nopage()。vm_operations_struct数据结构实际上是一个函数跳转表,结构中通常是一些与文件操作有关的函数指针。这对于可能的文件共享是很有意义的。
之后,系统调用到do_anonymous_page(),
static int do_anonymous_page(struct mm_struct * mm, struct vm_area_struct * vma, pte_t *page_table, int write_access, unsigned long addr)
{
pte_t entry;
entry = pte_wrprotect(mk_pte(ZERO_PAGE(addr), vma->vm_page_prot));
if (write_access) {
struct page *page;
spin_unlock(&mm->page_table_lock);
page = alloc_page(GFP_HIGHUSER);
if (!page)
goto no_mem;
clear_user_highpage(page, addr);
spin_lock(&mm->page_table_lock);
if (!pte_none(*page_table)) {
page_cache_release(page);
return 1;
}
mm->rss++;
flush_page_to_ram(page);
entry = pte_mkwrite(pte_mkdirty(mk_pte(page, vma->vm_page_prot)));
}
set_pte(page_table, entry);
update_mmu_cache(vma, addr, entry);
return 1;
no_mem:
spin_lock(&mm->page_table_lock);
return -1;
}
如果引起页面异常的是一次读操作,那么由mk_pte()构筑的映射表项要通过pte_wrprotect()加以修正;而如果是写操作,则通过pte_mkwrite()加以修正,二者的定义见include/asm-i386/pgtable.h:
static inline pte_t pte_wrprotect(pte_t pte) { (pte).pte_low &= ~_PAGE_RW; return pte; }
static inline pte_t pte_mkwrite(pte_t pte) { (pte).pte_low |= _PAGE_RW; return pte; }
同时,对于读操作,所映射的物理页面总是ZERO_PAGE,这个页面是在include/asm-i386/pgtable.h中定义的:
#define ZERO_PAGE(vaddr) (virt_to_page(empty_zero_page))
就是说,只要是“只读”的页面,开始时都一律映射到同一个物理内存页面empty_zone_page,而不管其虚拟地址是什么。实际上,这个页面的内容都是全0,所以映射之初若从该页面读出就只能读到0。只有可写的页面,才会通过alloc_page()为其分配独立的物理内存。通过set_pte()设置指针page_table所指的页面表项,至此,从虚拟页面到物理页面的映射终于建立了。
这里的update_mmu_cache()对i386CPU是个空函数(见inlcude/asm-i386/pgtable.h),因为i386的MMU是实现在CPU内部,并没有独立的MMU。
最后,特别要指出的,当CPU从一次页面出错异常处理返回到用户空间时,将会先重新执行因映射失败而中途夭折的那条指令,然后才继续往下执行,这是异常出来的特殊性。中断以及自陷(trap指令)发生时,CPU都会将下一条指令,也就是接下去本来应该执行的指令的地址压入堆栈作为中断服务的返回地址。但是异常却不同,当异常发生时,CPU将无法完成(例如除以0,映射失败,等等)而夭折的指令本身的地址压入堆栈。这样可以在从异常出来返回完成未竟的事业。
对用户程序来说,这整个过程都是透明的,就像什么事也没发生过,而堆栈区就仿佛从一开始就已经分配好了足够大的空间一样。
阅读(2196) | 评论(0) | 转发(1) |