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

在开发软件的过程中我们经常会遇到错误,如果你用 Google 搜过出错信息,那你多少应该都访问过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);
}

这个程序定义了一个简单的函数 add,接受两个参数 a 和 b,返回值就是 a+b。而 main 函数里则定义了两个变量 x 和 y,然后通过调用这个 add 函数,来计算 u=x+y,最后把 u 的数值打印出来。

$ gcc -g -c function_example.c
$ objdump -d -M intel -S function_example.o

我们把这个程序编译之后,objdump 出来。我们来看一看对应的汇编代码。

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

可以看出来,在这段代码里,main 函数和上一节我们讲的的程序执行区别并不大,它主要是把 jump 指令换成了函数调用的 call 指令。call 指令后面跟着的,仍然是跳转后的程序地址。

这些你理解起来应该不成问题。我们下面来看一个有意思的部分。

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

你有没有发现,函数调用和上一节我们讲的 if…else 和 for/while 循环有点像。它们两个都是在原来顺序执行的指令过程里,执行了一个内存地址的跳转指令,让指令从原来顺序执行的过程里跳开,从新的跳转后的位置开始执行。

但是,这两个跳转有个区别,if…else 和 for/while 的跳转,是跳转走了就不再回来了,就在跳转后的新地址开始顺序地执行指令,就好像徐志摩在《再别康桥》里面写的:“我挥一挥衣袖,不带走一片云彩”,继续进行新的生活了。而函数调用的跳转,在对应函数的指令执行完了之后,还要再回到函数调用的地方,继续执行 call 之后的指令,就好像贺知章在《回乡偶书》里面写的那样:“少小离家老大回,乡音未改鬓毛衰”,不管走多远,最终还是要回来。

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

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

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

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

//未完待续....

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/105159.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

微信小程序生成海报

效果: js1: const cloudHelper = require(../../../helper/cloud_helper.js);async function config1({cover,title,desc,qr,bg = }) {var qr1 ="images/qr.png"var qr2 ="https://636c-cloud1-0gu29f2j63906b7e-1319556650.tcb.qcloud.la/activitycomm/setu…

web入门---tomcat请求响应

Tomcat 介绍 Web 服务器是一个软件程序&#xff0c;对 HTTP协议的操作进行封装&#xff0c;使得不必直接对协议进行操作&#xff0c;让 web 开发更加便捷。主要功能是“提供网上信息浏览服务”。 下载 tomcat 演示说明 这里有一个示例直接双击打开 index.html但是这个“打开…

OK3568 UBUNTU 安装使用I2C-TOOLS

1. 安装 sudo apt-get update sudo apt-get install i2c-tools 使用I2Ctools 参考&#xff1a;https://blog.csdn.net/anyuliuxing/article/details/106382827 i2c-tools 是一组用于在Linux系统中进行I2C&#xff08;Inter-Integrated Circuit&#xff09;总线设备操作和调试…

3.7 Android eBpf Hello World调试(一)

写在前面 tracepoint是Linux内核中预定义的静态探测点,分布于内核各个子系统中。每个tracepoint通常包含开关-enable,过滤器-filter等。如下是/sys/kernel/tracing/events/sched子系统中支持的各种tracepoint事件: coral:/sys/kernel/tracing/events/sched $ ls core_ctl_…

RabbitMQ部署指南

大家好我是苏麟今天安装RabbitMQ. 单机部署 我们在Centos7虚拟机中使用Docker来安装。 下载镜像 在线拉取: 最新版本 docker pull rabbitmq 3-managenment版本 docker pull rabbitmq:3-management这两个下载一个就可以 安装MQ docker run -e RABBITMQ_DEFAULT_USERroo…

Cadence 设计实践笔记-小哥allegro 2层板笔记

本章节主要跟着B站PCB入门首选视频-小哥Cadence Allegro 2层板视频,结合自己的实践一步步完成一个完整的PCB板的设计。 视频链接地址: PCB入门首选视频-小哥Cadence Allegro 2层板视频_哔哩哔哩_bilibili 规范建立文件夹 建立八个文件夹 DATASHEET 主要存放设计项目…

第十二章 磁盘管理

1. 磁盘简介 1.1. 概念 硬盘是由一片或多篇带有磁性的铝合金制的盘片构成&#xff0c;是 一种大容量、永久性的外部存储设备 组成&#xff1a;盘片、马达驱动、缓存、控制电路、接口 图&#xff1a; 1.2. 逻辑结构 磁道&#xff1a;由内到外的同心圆 扇区&#xff1a;半径组成…

JVM基础:初识JVM

IDE&#xff1a;IntelliJ IDEA 2022.1.3 x64 操作系统&#xff1a;win10 x64 位 家庭版 文章目录 一、JVM是什么&#xff1f;二、JVM有哪些功能&#xff1f;2.1 解释和运行2.2 内存管理2.3 即时编译 三、有哪些常见的JVM&#xff1f;3.1 常见JVM3.2 Java虚拟机规范3.3 HotSpot的…

力扣每日一题136:只出现一次的数字

题目描述&#xff1a; 给你一个 非空 整数数组 nums &#xff0c;除了某个元素只出现一次以外&#xff0c;其余每个元素均出现两次。找出那个只出现了一次的元素。 你必须设计并实现线性时间复杂度的算法来解决此问题&#xff0c;且该算法只使用常量额外空间。 示例 1 &#…

C++入门 第一篇(C++关键字, 命名空间,C++输入输出)

目录 1. C关键字 2. 命名空间 2.1 命名空间定义 2.2命名空间的使用 命名空间的使用有三种方式&#xff1a; 1.加命名空间名称及作用域限定符 2.使用using将命名空间中某个成员引入 3.使用using namespace 命名空间名称 引入 3. C输入&输出 4.缺省函数 4.1 缺省参…

记一次Hbase2.1.x历史数据数据迁移方案

查看待迁移的表 list_namespace_tables vaas_dwm2. 制作待迁移表“DWM_TRIP_PART”的快照 snapshot vaas_dwm:DWM_TRIP_PART,dwm_trip_part_snapshot3. 统计待迁移表数据总数 hbase org.apache.hadoop.hbase.mapreduce.RowCounter vaas_dwm:DWM_TRIP_PART

使用postMan调试接口出现 Content type ‘multipart/form-data;charset=UTF-8‘ not supported“

使用postMan调试接口出现 Content type multipart/form-data&#xff1b;charsetUTF-8 not supported" 问题原因解决方案 最近好久没写springboot项目了&#xff0c;然后写了一个添加用户的接口&#xff0c;使用postman测试时出现了问题。如下图&#xff1a; org.springfr…

HttpServletRequest对象与RequestDispatcher对象

一、HttpServletRequest对象 1.介绍 在Servlet API中&#xff0c;定义了一个HttpServletRequest接口&#xff0c;它继承自ServletRequest接口&#xff0c;专门用来封装HTTP请求消息。由于HTTP请求消息分为请求行、请求消息头和请求消息体三部分&#xff0c;因此&#xff0c;在…

Gin 中的 Session(会话控制)

Session 介绍 session和cookie实现的底层目标是一致的,但是从根本而言实现的方法是不同的; session 是另一种记录客户状态的机制, 不同的是 Cookie 保存在客户端浏览器中,而 session保存 在服务器上 ; Session 的工作流程 当客户端浏览器第一次访问服务器并发送请求时,服…

【LVGL】SquareLine Studio入门基础操作

1.SquareLine Studio基础 在这篇文章中将介绍SquareLine Studio的基础操作、解释如何加载一个项目、布局结构。    启动软件后,可以加载之前的项目、创建项目、加载一个示例。    这里以打开示例audio_mixer为例,可以双击该项目打开或者选中该项目点击右下角的【创建】按…

Hadoop3教程(一):Hadoop的定义、组成及全生态概览

文章目录 &#xff08;1&#xff09;定义1.1 发展历史1.2 三大发行版本1.3 Hadoop的优势1.4 Hadoop的组成 &#xff08;13&#xff09;HDFS概述&#xff08;14&#xff09;Yarn架构&#xff08;15&#xff09;MapReduce概述&#xff08;16&#xff09; HDFS、YARN、MapReduce三…

使用 puppeteer 加载 html 文件来运行 js 文件

遇到一个需求, 在浏览器环境下来运行 js sdk 文件, 这个 js 文件是不能运行在 nodejs 环境下的; 所以通过 puppeteer 无头浏览器来运行代码获取对应的结果。 首先是安装插件 puppeteer&#xff0c;然后创建一个项目, 我这里是express&#xff1b; 这里是主要的代码。 const p…

【云原生之Docker实战】使用Docker部署WBO在线协作白板

【云原生之Docker实战】使用Docker部署WBO在线协作白板 一、WBO介绍1.1 WBO简介1.2 WBO特点二、本地环境介绍2.1 本地环境规划2.2 本次实践介绍三、本地环境检查3.1 检查Docker服务状态3.2 检查Docker版本3.3 检查docker compose 版本四、构建WBO镜像4.1 下载WBO源码4.2 构建WB…

【排序算法】详解冒泡排序及其多种优化稳定性分析

文章目录 算法原理细节分析优化1优化2算法复杂度分析稳定性分析总结 算法原理 冒泡排序(Bubble Sort) 就是从序列中的第一个元素开始&#xff0c;依次对相邻的两个元素进行比较&#xff0c;如果前一个元素大于后一个元素则交换它们的位置。如果前一个元素小于或等于后一个元素…

大数据学习(6)-hive底层原理Mapreduce

&&大数据学习&& &#x1f525;系列专栏&#xff1a; &#x1f451;哲学语录: 承认自己的无知&#xff0c;乃是开启智慧的大门 &#x1f496;如果觉得博主的文章还不错的话&#xff0c;请点赞&#x1f44d;收藏⭐️留言&#x1f4dd;支持一下博>主哦&#x…