控制文件夹递归深度_“永不”爆栈的递归函数(大雾)

有一天我写了一个牛逼的函数:

static int64_t factorial(int64_t n) {
    if (n == 1) {
        return n;
    }
    return n * factorial(n - 1);
}

由于我相信这个函数足够牛逼所以我打算委以重任,于是我:

factorial(0xffff);

然而当我按下 Command-R 的时候,却看到了这样一个令人失望的景象:

a80a5ae56dc3b1fafab8a509287371b0.png

我们初中二年级就学过,栈空间的长度是很有限的,如此深的递归调用势必会导致爆栈,那么我们的栈到底有多大呢?emmm,这可能跟你用的编译器和操作系统都有关,我大 macOS 下的栈空间是 8 MB 左右,用 vmmap 命令就能看到:

293a7c93c603b7cbf2c294d441b59899.png

那么为啥我们的 app 会挂掉呢?因为鸡贼的操作系统给你每个 Stack 区的上下边界都加了几个 guard 页面,范围很大,你可以看到有 56 MB 那么大,它是不可写的。所以一旦我们把栈打爆,势必会碰到这个 guard 页面,然后你就 EXC_BAD_ACCESS 了~

9be70c3e5418b01ba747e818b1c94231.png

那么,我有个大胆的想法:

8460923fa0994e312d8c5e5ead857952.png

虽然系统给我们的栈区小得可怜,那我们换一个大一点的空间不就得了?比如我们反手跟系统要 100 MB 的页面:

const size_t stack_size = 1024 * 1024 * 100;
vm_address_t addr;
if (vm_allocate(mach_task_self(), &addr, stack_size, YES)) {
    abort();
}

然后移花接木,把栈区换掉,100 MB 的空间足够我们完成所有 reasonable 递归深度函数的调用了。

咋换呢?我们先来复习一下初中二年级就学过的 Calling Convention,当调用一个函数时发生了什么:

12f04c9d0a566995fe5f639b4a5ce24b.png

c2b21c8241a01d77ac7e86e0fb6bd463.png

此时的栈空间长这个样子:

0c1782baab77a5eacb45b1794e3213c7.png

call 指令是一个复合指令,我们来查一手 x86 手册:

a15bfd6180a10ddf5d446110a010d9e0.png

从伪代码可知,call = push rip + mov rip,接下来 callee 函数开始 prologue 阶段,也就是保存 rbp + 重建 rbp + 开辟栈空间。

换栈空间的时机放在 callee 处会由于 prologue 的存在变得比较棘手,需要借助 __attribute__((naked)) 来实现,很是麻烦。显然,放在 caller 那里处理会简单很多:

static void trampoline() {
    const size_t stack_size = 1024 * 1024 * 100;
    vm_address_t addr;
    if (vm_allocate(mach_task_self(), &addr, stack_size, YES)) {
        abort();
    }
    
    // 我们初中二年级就学过栈是向低地址增长的...
    addr += stack_size;
    // 04.06 更新:Darwin 要求栈 16 bytes 对齐
    addr -= 8;
    
    // 先把要调用的函数的地址找出来,PIC 下函数地址是 rip + offset
    void *entry_addr = (void *) &factorial;
    
    __asm__("movq %%rsp, %%r8n"     // 先把原来的 rsp 存到一个寄存器,一会用
            "movq %0, %%rspn"       // 换上我们的豪宅“栈“地址
            "pushq %%r8n"           // 把原来的 rsp 压入新栈
            "movq $0xffff, %%rdin"  // 准备 factorial 参数
            "callq *%1n"            // 一切就绪,调用之!
            "popq %%rspn"           // 调用完了,把 rsp 恢复成原来的蜗居
            :
            : "r"(addr), "r"(entry_addr)
            : "%rdi", "%r8");

    // 忘记释放豪宅了...算了先这样吧
}

就是这么简单,我们再调用一次试试:

188b4422456afd9aae9ca707dfecac63.png

65535 的递归深度,已经超越常规了...

最后总结一下

  1. 栈不一定非要是真的栈,系统给你的不够用就换掉
  2. 一般的协程库在实现时也会在堆区分配空间给新的栈使用
  3. 本文方法切勿直接在实际项目中使用,栈对齐啥的都没做,要用的话自己把这部分做掉
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值