【计算机组成原理】学习笔记——总目录

【07】函数调用:为什么会发生stack overflow?

  • 引言
  • 一、为什么我们需要程序栈?
  • 二、如何构造一个 stack overflow?
  • 三、如何利用函数内联进行性能优化?
  • 总结【个人总结的重点】

引言

作为全球最大的程序员问答网站,Stack Overflow 的名字来自于一个常见的报错,就是栈溢出(stack overflow)。今天,我们就从程序的函数调用开始,讲讲函数间的相互调用,在计算机指令层面是怎么实现的,以及什么情况下会发生栈溢出这个错误。

一、为什么我们需要程序栈?

简单的 C 程序 function_example.c

// function_example.c
#include <stdio.h>
int static add(int a, int b)
{return a+b;
}int main()
{int x = 5;int y = 10;int u = add(x, y);
}

对应的汇编代码:

int static add(int a, int b)
{0:   55                      push   rbp1:   48 89 e5                mov    rbp,rsp4:   89 7d fc                mov    DWORD PTR [rbp-0x4],edi7:   89 75 f8                mov    DWORD PTR [rbp-0x8],esireturn a+b;a:   8b 55 fc                mov    edx,DWORD PTR [rbp-0x4]d:   8b 45 f8                mov    eax,DWORD PTR [rbp-0x8]10:   01 d0                   add    eax,edx
}12:   5d                      pop    rbp13:   c3                      ret
0000000000000014 <main>:
int main()
{14:   55                      push   rbp15:   48 89 e5                mov    rbp,rsp18:   48 83 ec 10             sub    rsp,0x10int x = 5;1c:   c7 45 fc 05 00 00 00    mov    DWORD PTR [rbp-0x4],0x5int y = 10;23:   c7 45 f8 0a 00 00 00    mov    DWORD PTR [rbp-0x8],0xaint u = add(x, y);2a:   8b 55 f8                mov    edx,DWORD PTR [rbp-0x8]2d:   8b 45 fc                mov    eax,DWORD PTR [rbp-0x4]30:   89 d6                   mov    esi,edx32:   89 c7                   mov    edi,eax34:   e8 c7 ff ff ff          call   0 <add>39:   89 45 f4                mov    DWORD PTR [rbp-0xc],eax3c:   b8 00 00 00 00          mov    eax,0x0
}41:   c9                      leave  42:   c3                      ret

函数调用的call 指令:call 指令后面跟着的,仍然是跳转后的程序地址

1、add 函数
可以看到,add 函数编译之后,代码先执行了一条 push 指令和一条 mov 指令;在函数执行结束的时候,又执行了一条 pop 和一条 ret 指令。这四条指令的执行,其实就是在进行我们接下来要讲==压栈(Push)和出栈(Pop)==操作。

2、if…else 和 for/while 的跳转和函数调用的区别

  • if…else 和 for/while 的跳转,是跳转走了就不再回来了,就在跳转后的新地址开始顺序地执行指令。
  • 函数调用的跳转,在对应函数的指令执行完了之后,还要再回到函数调用的地方,继续执行 call 之后的指令

3、进一步思考
那我们有没有一个可以不跳转回到原来开始的地方,来实现函数的调用呢?直觉上似乎有这么一个解决办法。你可以把调用的函数指令,直接插入在调用函数的地方,替换掉对应的 call 指令,然后在编译器编译代码的时候,直接就把函数调用变成对应的指令替换掉。

不过,仔细琢磨一下,你会发现这个方法有些问题。如果函数 A 调用了函数 B,然后函数 B 再调用函数 A,我们就得面临在 A 里面插入 B 的指令,然后在 B 里面插入 A 的指令,这样就会产生无穷无尽地替换。就好像两面镜子面对面放在一块儿,任何一面镜子里面都会看到无穷多面镜子。

Infinite Mirror Effect,如果函数 A 调用 B,B 再调用 A,那么代码会无限展开

4、引出“程序栈”【本节重点!】
看来,把被调用函数的指令直接插入在调用处的方法行不通。那我们就换一个思路,能不能把后面要跳回来执行的指令地址给记录下来呢?就像前面讲 PC 寄存器一样,我们可以专门设立一个“程序调用寄存器”,来存储接下来要跳转回来执行的指令地址。等到函数调用结束,从这个寄存器里取出地址,再跳转到这个记录的地址,继续执行就好了。

但是在多层函数调用里,简单只记录一个地址也是不够的。我们在调用函数 A 之后,A 还可以调用函数 B,B 还能调用函数 C。这一层又一层的调用并没有数量上的限制。在所有函数调用返回之前,每一次调用的返回地址都要记录下来,但是我们 CPU 里的寄存器数量并不多。像我们一般使用的 Intel i7 CPU 只有 16 个 64 位寄存器,调用的层数一多就存不下了


结合以下C#程序理解上图圈中的内容【在学习时文字部分会产生歧义(本人读的有点绕)】:

using System;namespace ConsoleApp3
{class Program{public static void A(){Console.WriteLine("A");B();}public static void B(){Console.WriteLine("B");}static void Main(string[] args){A();Console.ReadLine();}}
}

在真实的程序里,压栈的不只有函数调用完成后的返回地址。比如函数 A 在调用 B 的时候,需要传输一些参数数据,这些参数数据在寄存器不够用的时候也会被压入栈中。整个函数 A 所占用的所有内存空间,就是函数 A 的栈帧(Stack Frame)。Frame 在中文里也有“相框”的意思,所以,每次到这里,我都有种感觉,整个函数 A 所需要的内存空间就像是被这么一个“相框”给框了起来,放在了栈里面。

【重要!!!】
而实际的程序栈布局,顶和底与我们的乒乓球桶相比是倒过来的。底在最上面,顶在最下面,这样的布局是因为栈底的内存地址是在一开始就固定的。而一层层压栈之后,栈顶的内存地址是在逐渐变小而不是变大。【栈底的内存地址大!】

图中,rbp 是 register base pointer 栈基址寄存器(栈帧指针),指向当前栈帧的栈底地址。rsp 是 register stack pointer 栈顶寄存器(栈指针),指向栈顶元素。

对应上面函数 add 的汇编代码,我们来仔细看看,main 函数调用 add 函数时,add 函数入口在 0~1 行,add 函数结束之后在 12~13 行。

【重点理解】
我们在调用第 34 行的 call 指令时,会把当前的 PC 寄存器里的下一条指令的地址压栈,保留函数调用结束后要执行的指令地址。而 add 函数的第 0 行,push rbp 这个指令,就是在进行压栈。这里的 rbp 又叫栈帧指针(Frame Pointer),是一个存放了当前栈帧位置的寄存器。push rbp 就把之前调用函数,也就是 main 函数的栈帧的栈底地址,压到栈顶。

接着,第 1 行的一条命令 mov rbp, rsp 里,则是把 rsp 这个栈指针(Stack Pointer)的值复制到 rbp 里,而 rsp 始终会指向栈顶。这个命令意味着,rbp 这个栈帧指针指向的地址,变成当前最新的栈顶,也就是 add 函数的栈帧的栈底地址了。

而在函数 add 执行完成之后,又会分别调用第 12 行的 pop rbp 来将当前的栈顶出栈,这部分操作维护好了我们整个栈帧。然后,我们可以调用第 13 行的 ret 指令,这时候同时要把 call 调用的时候【压入的 PC 寄存器里的下一条指令】出栈,更新到 PC 寄存器中,将程序的控制权返回到出栈后的栈顶。

二、如何构造一个 stack overflow?

【记住!】
通过引入栈,我们可以看到,无论有多少层的函数调用,或者在函数 A 里调用函数 B,再在函数 B 里调用 A,这样的递归调用,我们都只需要通过维持 rbp 和 rsp,这两个维护栈顶所在地址的寄存器,就能管理好不同函数之间的跳转。

【引出stack overflow】
不过,栈的大小也是有限的。如果函数调用层数太多,我们往栈里压入它存不下的内容,程序在执行的过程中就会遇到栈溢出的错误,这就是大名鼎鼎的“stack overflow”。

栈溢出实例:

int a()
{return a();
}int main()
{a();return 0;
}

除了无限递归,递归层数过深,在栈空间里面创建非常占内存的变量(比如一个巨大的数组),这些情况都很可能给你带来 stack overflow

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

上面我们提到一个方法,把一个实际调用的函数产生的指令,直接插入主程序中,来替换对应的函数调用指令。尽管这个通用的函数调用方案,被我们否决了,但是如果被调用的函数里,没有调用其他函数,这个方法还是可以行得通的。

事实上,这就是一个常见的编译器进行自动优化的场景,我们通常叫函数内联(Inline)

内联带来的优化是,CPU 需要执行的指令数变少了,根据地址跳转的过程不需要了,压栈和出栈的过程也不用了

不过内联并不是没有代价,内联意味着,我们把可以复用的程序指令在调用它的地方完全展开了。如果一个函数在很多地方都被调用了,那么就会展开很多次,整个程序占用的空间就会变大了


这样没有调用其他函数,只会被调用的函数,我们一般称之为叶子函数(或叶子过程)


这一节,我们讲了一个程序的函数间调用,在 CPU 指令层面是怎么执行的。其中一定需要你牢记的,就是程序栈这个新概念。

通过加入了程序栈,我们相当于在指令跳转的过程中,加入了一个“记忆”的功能,能在跳转去运行新的指令之后,再回到跳出去的位置,能够实现更加丰富和灵活的指令执行流程。这个也为我们在程序开发的过程中,提供了“函数”这样一个抽象,使得我们在软件开发的过程中,可以复用代码和指令,而不是只能简单粗暴地复制、粘贴代码和指令

总结【个人总结的重点】

  • 本节内容比较难理解,我大约看了3遍才完全看懂,能明白作者想表达的。【必须死磕】

  • 程序栈:压栈【push rbp;move rbp,rsp】,出栈【pop rbp;ret】。这四条指令实现了压栈和出栈,要牢记。【请结合下边rbp,rsp的概念进行理解这几条指令!这个理解非常重要!】

  • rbp:是 register base pointer 栈基址寄存器(栈帧指针),指向当前栈帧的栈底地址。

  • rsp: 是 register stack pointer 栈顶寄存器(栈指针),指向栈顶元素。

  • push rbp:在当前rbp指向的指针上压栈新的地址。【个人重要理解,rbp有点类似代码中常出现的temp,是】

  • move rbp,rsp:将栈顶指针的地址赋给rbp,否则rbp还是指向压栈前的栈顶指针地址。【个人重要理解】

  • rbp和rsp本质上是两个寄存器,存储了相应的指针地址。

  • 下图理解透彻了,这节就真懂了

  • 栈溢出(stack overflow)的原因:函数无限递归;函数调用层数过深;在栈空间里面创建非常占内存的变量(比如一个巨大的数组)【函数调用在CPU指令层面使用的是入栈,出栈。栈指针的地址存放在栈寄存器中,但由于CPU的寄存器是有限的,在无限递归调用函数的时候,就会在栈中存放很多地址,栈无法放下时,就会引发stack overflow】。

  • 函数内联(Inline):将函数直接放到主程序中,主程序顺序执行,不再发生压栈出栈。当函数只有单次调用时,可以采用此方法进行性能优化。但是多次调用同一个函数时,这种方法会使程序占用内存增大。

【计算机组成原理】学习笔记——总目录

【07】函数调用:为什么会发生stack overflow?相关推荐

  1. 知识点讲解四:栈溢出(stack overflow)问题解决方案

    在爬取某个网页的时候遇到了这个问题: Fatal Python error: Cannot recover from stack overflow 我问题所在:使用函数时递归调用次数过多(800左右会 ...

  2. VS或VC编译正常,但运行时出现Stack overflow

    以下内容为网络资源结合自身实践的总结,在此首先感谢提供资源的各位网友. 大家都知道,Windows程序的内存机制大概是这样的,全局变量(局部的静态变量本质也属于此范围)存储于堆内存,该段内存较大,一般 ...

  3. 基于用户投票的排名算法(三):Stack Overflow

    作者: 阮一峰 日期: 2012年3月11日 上一篇文章,我介绍了Reddit的排名算法. 它的特点是,用户可以投赞成票,也可以投反对票.也就是说,除了时间因素以外,只要考虑两个变量就够了. 但是,还 ...

  4. 全球最大编程问答社区 Stack Overflow 宣布裁员 15%!

    作者 | 唐小引 头图 | Stack Overflow 首页 出品 | CSDN(ID:CSDNnews) 受全球疫情的影响,科技圈的裁员正在持续增加,继 Uber.Airbnb 接连裁员14%.2 ...

  5. [转载] log4j-over-slf4j与slf4j-log4j12共存stack overflow异常分析

    参考链接: log4j-示例程序 注:下文中的"桥接"."转调"."绑定"等词基本都是同一个概念. log4j-over-slf4j和slf ...

  6. ecw2c在工作中非常糟糕的一天教会了我关于建立Stack Overflow社区的知识

    Hi, my name is Sara Chipps, first time Stack blogger, long time Stacker (I've always wanted to say t ...

  7. Stack Overflow是如何做应用缓存的

    首先要说下缓存是什么?缓存,就是在取出数据结果后,暂时将数据存储在某些可以快速存取的位置(例如各种NoSQL如Redis,HBase,又或MemoryCache等等),于是就可以让这些耗时的数据结果多 ...

  8. 【C++错误】VS调试出现0xC00000FD:Stack overflow溢出

    Debug出现 0xC00000FD:Stack overflow溢出 错误: 0xC00000FD:Stack overflow        出现这样情况的原因究竟是什么?根据错误可以直观看到这是 ...

  9. log4j-over-slf4j与slf4j-log4j12共存stack overflow异常分析

    转自:https://blog.csdn.net/kxcfzyk/article/details/38613861 注:下文中的"桥接"."转调"." ...

最新文章

  1. php数据库录入和输出,PHP数据库之CURD操作
  2. C++ vector容器类型
  3. 喜讯,Asp.net Ajax 文档提供下载
  4. 操作系统知识点大总结【管程与死锁】
  5. ElasticSearch ---- 查询
  6. linux访问db2数据库操作命令行,DB2数据库基本操作指令30条
  7. 网管员的最爱!解密六款低成本RADIUS
  8. java 设置session超时_Java设置session超时(失效)的时间
  9. ajax登录返回token,AJAX安全-Session做Token
  10. 开源软路由和防火墙pfSense
  11. 乌龙钻白玉 白虎卧沙滩
  12. 程序员,停止你的焦虑
  13. 利用Lambda表达式从实体集合中筛选出符合条件的实体集合
  14. win10 windows文件查找通配符
  15. M-LAG—跨设备链路聚合组
  16. c语言中while中的判断语句为感叹号x时是什么意思?
  17. html语言简介 ppt,网页制作与HTML语言基本结构简介.ppt
  18. 人脸识别之三检测视频流(摄像头)中的人脸
  19. 《机器学习》慕课版课后习题-第5章
  20. Win7上.bat文件打开方式变成了文本文档,怎么修改和恢复

热门文章

  1. 1.6Excel--查找和引用函数
  2. API调用,淘宝天猫、1688、京东、拼多多商品页面APP端原数据获取
  3. cb4cle计数器如何设计九分频电路
  4. hadoop 结合zookeeper 高可用 优化新特性
  5. 惠普打印机墨盒更换教程_惠普打印机加墨教程:老司机教你
  6. 什么是 COB 灯?
  7. 《uniapp遇到的问题》 详情 ------ 编号:001
  8. NLP自然语言处理 集束搜索(beam search)和贪心搜索(greedy search)
  9. Redux 的基本使用
  10. G.7xx 音频压缩标准