计算机组成原理(七)

函数调用:为什么会发生 stackoverflow?

为什么我们需要程序栈?

  • 函数调用和 if…else 和 for/while 循环有点像。
    • 它们两个都是在原来顺序执行的指令过程里,执行了一个内存地址的跳转指令,让指令从原来顺序执行的过程里跳开,从新的跳转后的位置开始执行。
    • 但是,这两个跳转有个区别,if…else 和 for/while 的跳转,是跳转走了就不再回来了,就在跳转后的新地址开始顺序地执行指令。
    • 函数调用的跳转,在对应函数的指令执行完了之后,还要再回到函数调用的地方,继续执行 call 之后的指令。
  • 我们在内存里面开辟一段空间,用栈这个后进先出(LIFO,Last In First Out)的数据结构。
    • 栈就像一个乒乓球桶,每次程序调用函数之前,我们都把调用返回后的地址写在一个乒乓球上,然后塞进这个球桶,这个操作其实就是我们常说的压栈。
    • 如果函数执行完了,我们就从球桶里取出最上面的那个乒乓球,很显然,这就是出栈。
    • 拿到出栈的乒乓球,找到上面的地址,把程序跳转过去,就返回到了函数调用后的下一条指令了。
    • 如果函数 A 在执行完成之前又调用了函数 B,那么在取出乒乓球之前,我们需要往球桶里塞一个乒乓球。
    • 而我们从球桶最上面拿乒乓球的时候,拿的也一定是最近一次的,也就是最下面一层的函数调用完成后的地址。
    • 乒乓球桶的底部,就是栈底,最上面的乒乓球所在的位置,就是栈顶。
      在这里插入图片描述
  • 在真实的程序里,压栈的不只有函数调用完成后的返回地址。
    • 比如函数A在调用B的时候,需要传输一些参数数据,这些参数数据在寄存器不够用的时候也会被压入栈中。
    • 整个函数A所占用的所有内存空间,就是函数A的栈帧(Stack Frame)。
  • 实际的程序栈布局,顶和底与我们的乒乓球桶相比是倒过来的。
    • 底在最上面,顶在最下面,这样的布局是因为栈底的内存地址是在一开始就固定的。
    • 而一层层压栈之后,栈顶的内存地址是在逐渐变小而不是变大

如何构造一个 stack overflow?

  • 通过引入栈,无论有多少层的函数调用,或者在函数 A 里调用函数 B,再在函数 B 里调用 A,这样的递归调用,我们都只需要通过维持 rbp 和 rsp,这两个维护栈顶所在地址的寄存器,就能管理好不同函数之间的跳转。不过,栈的大小也是有限的。如果函数调用层数太多,我们往栈里压入它存不下的内容,程序在执行的过程中就会遇到栈溢出的错误,这就是大名鼎鼎的“stack overflow”。

如何利用函数内联进行性能优化?

  • 把一个实际调用的函数产生的指令,直接插入到相应的位置,来替换对应的函数调用指令。
    • 如果被调用的函数里,没有调用其他函数,这个方法还是可以行得通的。
    • 事实上,这就是一个常见的编译器进行自动优化的场景,我们通常叫函数内联(Inline)。
    • 我们只要在 GCC 编译的时候,加上对应的一个让编译器自动优化的参数 -O,编译器就会在可行的情况下,进行这样的指令替换。
  • 除了依靠编译器的自动优化,你还可以在定义函数的地方,加上 inline 的关键字,来提示编译器对函数进行内联。
    • 内联带来的优化是,CPU 需要执行的指令数变少了,根据地址跳转的过程不需要了,压栈和出栈的过程也不用了。
    • 不过内联并不是没有代价,内联意味着,我们把可以复用的程序指令在调用它的地方完全展开了。
    • 如果一个函数在很多地方都被调用了,那么就会展开很多次,整个程序占用的空间就会变大了。
    • 这样没有调用其他函数,只会被调用的函数,我们一般称之为叶子函数(或叶子过程)。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值