前几天我在chinaunix上发现了这么一个贴子,帖子有些久远是2007年的,楼主贴了一段C代码如下:
int main( int argc, char *argv[] )
{
int i;
int *p;
p = (int*)&p - 1;
for( i = 1; 1; i++ )
{
printf( "%d\t", i );
fflush( stdout );
*p-- = 1;
}
return 0;
}
楼主说这段代码出错,是p越下栈区时被内核检测到报错,不是资源耗尽错误。他想请教一下,在内存报错时,是否执行到do_page_fault函数。如果执行到这个函数,他觉的应该在下面这段代码中执行栈扩展expand_stack,直到资源耗尽错误。
由于最近我刚好在看缺页异常处理部分,所以对这个帖子比较感兴趣,就研究了一下。先简单列一下do_page_fault的代码如下(省略了很多注释和错误处理的代码):
dotraplinkage void __kprobes do_page_fault(struct pt_regs *regs, unsigned long error_code)
{
struct vm_area_struct *vma;
struct task_struct *tsk;
unsigned long address;
struct mm_struct *mm;
int write;
int fault;
tsk = current;
mm = tsk->mm;
/* <span style="color:#FF0000;">Get the faulting address</span>: */
address = read_cr2();
if (unlikely(fault_in_kernel_space(address))) {//处理kernel中的缺页异常,这里不关注
......
......
return;
}
......
......
vma = find_vma(mm, address);<span style="color:#FF0000;">//查找结束地址大于address的第一个vma</span>
if (unlikely(!vma)) {
bad_area(regs, error_code, address);
return;
}
if (likely(vma->vm_start <= address))<span style="color:#FF0000;">//如果导致缺页的地址属于该vma</span>
goto good_area;
if (unlikely(!(<span style="color:#FF0000;">vma->vm_flags & VM_GROWSDOWN</span>))) {<span style="color:#FF0000;">//如果该vma的flags中有VM_GROWSDOWN,则说明导致缺页的地址在栈中</span>
bad_area(regs, error_code, address);
return;
}
if (error_code & PF_USER) {
/*
* Accessing the stack below %sp is always a bug.
* The large cushion allows instructions like enter
* and pusha to work. ("<span style="color:#FF0000;">enter $65535, $31</span>" pushes
* 32 pointers and then decrements %sp by 65535.)
*/
if (unlikely(address + 65536 + 32 * sizeof(unsigned long) < regs->sp)) {<span style="color:#FF0000;">//关于这个分支读者可以参考一下enter指令的用法。</span>
bad_area(regs, error_code, address);
return;
}
}
if (unlikely(<span style="color:#FF0000;">expand_stack(vma, address)</span>)) {<span style="color:#FF0000;">//扩展栈</span>
bad_area(regs, error_code, address);
return;
}
/*
* Ok, we have a good vm_area for this memory access, so
* we can handle it..
*/
good_area:
write = error_code & PF_WRITE;
if (unlikely(access_error(error_code, write, vma))) {
bad_area_access_error(regs, error_code, address);
return;
}
fault = handle_mm_fault(mm, vma, address, write ? FAULT_FLAG_WRITE : 0);<span style="color:#FF0000;">//真正的用户空间处理取缺页异常的函数。</span>
......
}
那段C代码出错的原因肯定是p指针的一直向下移动,导致栈不停的向下扩展,最后超出了栈的最大限制。
刚开始我以为在这行代码执行过后vma = find_vma(mm, address);找到的vma一定不包含导致缺页的地址address,这样分支if (likely(vma->vm_start <= address))就不会成立,然后就会走到if (unlikely(expand_stack(vma, address))) ,进入expand_stack函数,最终在这个函数中检查栈扩展后是否超出栈的最大限制时出错。
但是后来经过调试发现,事实不是这样的,出错的位置并不是expand_stack函数。这里先简单介绍下handle_mm_fault函数向下调用的流程。handle_mm_fault会调用handle_pte_fault,在handle_pte_fault中调用分几种情况,如果导致缺页的地址属于文件映射的话会调用do_linear_fault函数,如果属于匿名映射的话会调用do_anonymous_page,如果属于非线性映射的话会调用do_nonlinear_fault,还有几种情况,比如所缺的页已经换出,写时复制等。这里是栈缺页导致,属于匿名映射,会调用do_anonymous_page函数。在do_anonymous_page的刚开始的位置有这么一行代码:
if (check_stack_guard_page(vma, address) < 0)
下面看下check_stack_guard_page的代码:
static inline int check_stack_guard_page(struct vm_area_struct *vma, unsigned long address)
{
address &= PAGE_MASK;<span style="color:#FF0000;">//缺页地址按页大小对齐</span>
if ((vma->vm_flags & VM_GROWSDOWN) && <span style="color:#FF0000;">address == vma->vm_start</span>) {<span style="color:#FF0000;">//如果缺页地址所在的页是栈目前最下面的那一页,必然会进这个分支。</span>
struct vm_area_struct *prev = vma->vm_prev;
/*
* Is there a mapping abutting this one below?
*
* That's only ok if it's the same stack mapping
* that has gotten split..
*/
if (prev && prev->vm_end == address)
return prev->vm_flags & VM_GROWSDOWN ? 0 : -ENOMEM;
expand_stack(vma, address - PAGE_SIZE);<span style="color:#FF0000;">//提前向下扩展1页,应该是</span><span style="color:#FF0000;">作为guard_page</span>
}
if ((vma->vm_flags & VM_GROWSUP) && address + PAGE_SIZE == vma->vm_end) {
struct vm_area_struct *next = vma->vm_next;
/* As VM_GROWSDOWN but s/below/above/ */
if (next && next->vm_start == address + PAGE_SIZE)
return next->vm_flags & VM_GROWSUP ? 0 : -ENOMEM;
expand_upwards(vma, address + PAGE_SIZE);
}
return 0;
}
也就是说在每次缺页执行到这个函数的时候,如果当前缺页的地址address属于表示栈的vma的时候,栈都会向下扩展1页,这个动作先于为栈分配物理页面。所以上面的执行流程出了最后一次栈缺页之外,其他的栈缺页执行do_page_fault函数时都会在分支if (likely(vma->vm_start <= address))处成立,然后跳转至handle_mm_fault,然后在分配物理页之前可能(缺页的地址在栈的最下面的那一页时)会先将栈向下扩展一页,作为guard page,所以下次缺页时if (likely(vma->vm_start <= address))分支一定是成立的,直到最后一次,expand_stack在扩展栈的时候检查到栈目前的大小已经超出了栈的大小限制,导致栈的扩展失败。然后为这次缺页分配物理页,当这个物理页上的内存耗尽之后,会再次引发一次缺页,这次if (likely(vma->vm_start <= address))分支是不会成立的,因为上次栈扩展失败,这次address会比vma->vm_start小4个字节,然后继续向下走会执行到分支if (unlikely(address + 65536 + 32 * sizeof(unsigned long) < regs->sp)) ,因为那段C代码中的p指针一直在栈上向下移动,并没有进栈的操作,因此regs->sp是一直没有变的,所以缺页的地址address是远远小于regs->sp的,大概小于8M,是在我的64机器上(8G内存)栈的最大长度,此分支是成立的,然后调用bad_area(regs, error_code, address);最终出错。