(八)函数调用为何会发生“Stack Overflow”
时间:2020-04-28
本文章向大家介绍(八)函数调用为何会发生“Stack Overflow”,主要包括(八)函数调用为何会发生“Stack Overflow”使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。
一、一次函数调用分析
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); }
编译并objdump:
$ gcc -g -c function_example.c
$ objdump -d -M intel -S function_example.o
int static add(int a, int b) { 0: 55 push rbp 1: 48 89 e5 mov rbp,rsp 4: 89 7d fc mov DWORD PTR [rbp-0x4],edi 7: 89 75 f8 mov DWORD PTR [rbp-0x8],esi return 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 rbp 13: c3 ret 0000000000000014 <main>: int main() { 14: 55 push rbp 15: 48 89 e5 mov rbp,rsp 18: 48 83 ec 10 sub rsp,0x10 int x = 5; 1c: c7 45 fc 05 00 00 00 mov DWORD PTR [rbp-0x4],0x5 int y = 10; 23: c7 45 f8 0a 00 00 00 mov DWORD PTR [rbp-0x8],0xa int 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,edx 32: 89 c7 mov edi,eax 34: e8 c7 ff ff ff call 0 <add> 39: 89 45 f4 mov DWORD PTR [rbp-0xc],eax 3c: b8 00 00 00 00 mov eax,0x0 } 41: c9 leave 42: c3 ret
分析:
- 可以看到main方法和add方法的入口处14行和0行都有一个push操作,紧接着是一个mov操作将rsp赋值给rbp,rbp正是上一步push的参数;
- 同时可以看到在add方法的结尾有一个pop指令,参数正是函数开头push的参数rbp;
- add函数和main函数结尾都有一个ret指令;
- main方法中1c行到32行
- 首先对两个内存地址分别赋值5和10
- 将刚刚赋值过的两个内存地址的值再次赋值给edx和eda
- 将dex和eda的值分别赋给esi和edi
- main函数34行执行了call指令,参数是add函数;
实际上PUSH指令就表示压栈,POP就表示出栈。其实上述的调用过程和if/else的指令都进行了指令地址跳转,区别在于通过if/else跳转后,指令顺序执行到下一行,而call命令执行的跳转函数完成后还会调回call命令下一行,这是如何实现的呢?
程序栈说明:
我们自己思考这个功能该如何实现呢?
第一种方法,我们可以将add方法的指令直接拼接到main方法指令行的对应位置,这个方法的问题是,假如main方法和add相互调用(循环调用)那么不断拼接无穷无尽,显然不行。
第二种方法,我们可以单独设置一个程序执行寄存器,每次函数执行都记录该函数执行需要返回的位置,这样等该函数执行完成,直接返回到记录的返回位置即可,但是在多级函数调用情况下,只记录一个跳转机制远远不够,因此计算机科学家实际使用了一种更好的办法;
实际栈结构:
- 在内存中开辟一块区域保存先进后出的栈结构来记录函数调用的返回地址
- A函数调用B函数时,在进入B函数后,首先将A函数调用B函数的下一行指令地址放入栈中,此之谓压栈,假若B函数又调用了C函数,就又在C函数一开始将B调用C的下一行指令地址压入栈顶
- C函数执行完成,从栈顶拿出要返回的地址并将指令跳转回该地址,此之谓出栈,随之B函数执行完成,此时又从栈中取出栈顶存放的指令地址并跳转执行;
- 如此,调用层级越深的函数所记录的跳转地址越接近于栈顶,如此弹栈过程遍类似于反向遍历;
汇编代码的表现:
- add方法0行,rbp表示的是当前栈的指针,此时push rbp表示将main函数栈帧的栈底地址压入了程序栈顶;
- 紧接着1行,mov rbp,rsp 将rsp的值赋给rbp,而rsp是一个始终指向栈顶的参数,此时rbp指向栈顶;
- add末尾pop rbp表示弹栈,将当前的栈顶出栈,由此维护好了栈;
- 由于call调用时PC寄存器将返回地址压栈,最后执行ret指令时将call指令压入栈顶的返回地址弹栈并替换到PC寄存器中,以此完成指令跳转。
二、构造一次Stack Overflow
由于栈的大小有限,因此当调用层级太多时,会由于压栈造成栈空间不足以致溢出,典型场景为不加限制的递归调用:
int a() { return a(); } int main() { a(); return 0; }
三、使用函数内联进行性能优化
程序编译时将实际函数调用产生的指令直接插入到调用位置,来替换对应的函数调用指令,称为函数内联。
这样做的好处是:减少了指令数、减少了函数调用时的压栈弹栈开销;
但是当目标函数被引用很多次时会多次展开,使得程序占用空间变得很大。
原文地址:https://www.cnblogs.com/rxmind/p/12793554.html
- Nodejs学习笔记(十一)--- 数据采集器示例(request和cheerio)
- 厚土Go学习笔记 | 02. 打印当前时间time.Now()时不我待
- 厚土Go学习笔记 | 01. Hello World开篇
- Golang精编100题
- IntelliJ idea配置Go开发环境
- 仰望PHPSHE1.5漏洞
- Golang负载均衡
- 构建Docker镜像两种方式的比较-Dockerfile方式和S2I方式
- Golang单例模式
- 2018,我要Axublog。
- 厚土Go学习笔记 | 16. go语言有指针 没有指针运算
- 嗤!给你来点fiyocms漏洞喷雾
- 厚土Go学习笔记 | 15. defer语句延迟函数的执行
- Nodejs学习笔记(九)--- 与Redis的交互(mranney/node_redis)入门
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- markdown转为pdf文件
- [已解决]报错:Required request body is missing
- jupyter notebook修改默认路径和浏览器
- python selenium while 循环
- implicitly_wait()隐式等待
- [已解决]python FileNotFoundError: [WinError 3] for getsize(filepath)
- [已解决]ValueError: row index was 65536, not allowed by .xls format
- 记一次由Redis分布式锁造成的重大事故,避免以后踩坑!
- ES6部分源码重写 -1(ES5-构造函数解析)
- ES6部分源码重写 -2(ES6-构造函数初步解析)
- ES6部分源码重写 -3(ES6-ES5的方式来手写部分源码)
- ES6部分源码重写 -4(1-classES7中的扩展)
- ES6- Set-1之使用方法
- ES6- Set-2之去重—并集—交集—差集
- ES6- Map-1介绍及常用api