一段导致栈耗尽的C代码

前几天我在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);最终出错。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值