深入浅出计算机组成原理
徐文浩
bothub 创始人
70432 人已学习
新⼈⾸单¥68
登录后,你可以任选4讲全文学习
课程目录
已完结/共 62 讲
深入浅出计算机组成原理
15
15
1.0x
00:00/00:00
登录|注册

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

栈溢出错误
程序栈大小有限
创建占内存的变量
递归层数过深
无限递归
叶子函数
优化
栈底和栈顶
压栈和出栈操作
分享和讨论
程序栈中需要保留的信息
深入了解程序栈、栈帧、寄存器、指令集
《深入理解计算机系统(第三版)》
指令执行流程的灵活性
程序开发中的抽象
函数内联和栈溢出的关系
程序栈的作用
问题解决
构造栈溢出
函数内联
程序栈
课后思考
推荐阅读
总结延伸
栈溢出
函数调用
函数调用和栈溢出

该思维导图由 AI 生成,仅供参考

在开发软件的过程中我们经常会遇到错误,如果你用 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 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 函数和上一节我们讲的的程序执行区别并不大,它主要是把 jump 指令换成了函数调用的 call 指令。call 指令后面跟着的,仍然是跳转后的程序地址。
这些你理解起来应该不成问题。我们下面来看一个有意思的部分。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

本文深入探讨了函数调用在计算机指令层面的实现,以及栈溢出错误的发生情况。通过分析函数调用的内部实现和栈溢出的原因,读者可以更好地理解这一常见错误的产生机制。文章以简洁清晰的语言解释了栈溢出错误的技术特点,对于想要深入了解程序运行机制的读者来说,是一篇值得阅读的文章。 文章通过分析简单的C程序function_example.c,介绍了函数调用和栈溢出错误的相关内容。在讲解函数调用的内部实现时,文章以汇编代码的形式展示了函数调用和返回的过程,深入剖析了函数调用时的压栈和出栈操作。同时,文章还探讨了函数调用中的跳转指令和程序调用寄存器的使用,以及栈的数据结构和布局。通过这些详细的分析,读者可以更好地理解函数调用和栈溢出错误的产生机制。 此外,文章还介绍了如何构造栈溢出错误以及如何利用函数内联进行性能优化。通过讲解这些内容,读者可以了解到在CPU指令层面程序的函数间调用是如何执行的,以及函数内联和栈溢出的优化方案和常见Bug。这些内容为读者提供了更深入的技术理解和启发。 总的来说,本文通过深入浅出的方式解释了函数调用和栈溢出错误的技术特点,为读者提供了一篇有价值的技术文章。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《深入浅出计算机组成原理》
新⼈⾸单¥68
立即购买
登录 后留言

全部留言(140)

  • 最新
  • 精选
  • kdb_reboot
    倒数第二图比较好 补充一下寄存器说明 rbp - register base pointer (start of stack) rsp - register stack pointer (current location in stack, growing downwards) 建议将图编号这样评论的时候也能有所指代

    作者回复: kdb_reboot,谢谢建议。这个建议不错,我麻烦编辑稍后加上。

    2019-05-10
    8
    57
  • chengzise
    老师这里需要补冲一下,函数调用call指令时,(PC)指令地址寄存器会自动压栈,即返回地址压栈,函数返回ret指令时会自动弹栈,即返回地址赋值给PC寄存器,把之前。图片有显示压栈,没有文字说明,其他同学可以不太理解。

    作者回复: 👍,谢谢 chengzise 同学,谢谢补充。call 在调用的时候会做push eip的操作,而在ret的时候会做pop eip的操作。

    2019-05-10
    2
    46
  • 秋天
    现在有点模糊的是栈只是用来做函数调用,记录跳转地址的?它和寄存器的本质区别吗?这两者能给解释一下吗?谢谢!

    作者回复: 秋天同学你好, 我们在这里先要分清楚 抽象概念 和 实际的硬件实现部分。 寄存器 和 内存,是在硬件层面就是放在不同的位置,使用不同的物理硬件来实现的。 而栈是一个抽象概念,实际是存放在内存里面的。栈是用来管理函数调用的“现场”的。确保函数调用完成后,还能回到调用者那里。

    2019-05-27
    2
    30
  • Better me
    push rbp; mov rbp rsp; 老师,想问这两句是如何控制函数调用的

    作者回复: 这两个是在维护函数调用的栈帧。 指令地址本身的压栈和出栈是在 call 和 ret 的部分进行的。 你可以认为 call 的同时进行了一次 push rip 把PC寄存器里面的内容压栈了,而在 ret 的时候 pop 把这部分数据出栈写回到PC寄存器里面了。

    2019-05-10
    5
    20
  • 蚂蚁内推+v
    老师 巨大数组为什么是分配在栈空间的呢?(java里面是分配到堆上的 c预约和java不同吗)

    作者回复: 如果是函数作用域内的临时变量,就是分配在栈上的啊。 首先Java运行时候的JVM自己就是一个应用程序,和C编译出来的机器码就不一样。 Java通过New出来的对象是在堆上,但是函数作用域里面的临时变量,以及对应的引用都是放在栈上的。

    2019-06-06
    2
    17
  • Allen
    int main() { d: 55 push ebp e: 89 e5 mov ebp,esp 10: 83 ec 18 sub esp,0x18 int x = 5; 13: c7 45 f4 05 00 00 00 mov DWORD PTR [ebp-0xc],0x5 int y = 10; 1a: c7 45 f8 0a 00 00 00 mov DWORD PTR [ebp-0x8],0xa 老师,请教下: sub esp,0x18 的目的是干什么? 0x18 是怎么计算的?

    作者回复: 这个是在维护栈帧,因为后面有两个临时变量需要在调用其他函数之前保留到栈里面。0x18是16进制的24 两个int各需要8 bit,一共16bit,然后ebp本来就要8bit,一共只有24bit,考虑对齐到16bit的整数倍还要额外的8bit一共24bit

    2019-05-16
    2
    16
  • once
    老师 call指令已经将pc寄存器里的下一个指令(add函数执行完的跳转地址)压栈了 那 add函数里面的 push rbp压的又是什么栈 还有把main函数从栈底压到栈顶这个是什么意思 没有图看了好几遍也懵懵的 help老师

    作者回复: 这两个是在维护函数调用的栈帧。 指令地址本身的压栈和出栈是在 call 和 ret 的部分进行的。 你可以认为 call 的同时进行了一次 push rip 把PC寄存器里面的内容压栈了,而在 ret 的时候 pop 把这部分数据出栈写回到PC寄存器里面了。 这一部分的确是有不少同学表示写得不够清楚,我晚点看单独会在FAQ里面更详细地写一下这个过程。也再修订一下这一讲希望能讲解地更清楚一些。

    2019-08-28
    4
    13
  • 秋天
    java程序应该不是那种分页的形式,在虚机起动的时候我们根据配置或者是起动参数指定需要的内存大小,应该是预先分配好一大段连续的内存供程序使用,所以在程序运行过程中如果超出啦,预分配大小的内存就会出现内存溢出的错误

    作者回复: java虚拟机其实是一个应用层的程序,java虚拟机的内部内存分配其实是在虚拟内存地址层面的分配。的确不涉及到操作系统和硬件层面的分页问题。

    2019-05-27
    9
  • 小猪
    老师,我觉得用goto就可以实现函数调用,起先跳转到函数,运行完,在用goto跳回来就行了

    作者回复: 小猪同学你好, 那么被调用的函数运行完之后,怎么知道要跳回到哪一个地址呢?

    2019-05-22
    8
    9
  • Alphalin
    请问地址34后面的地址怎么直接到39了? 35地址在哪呢

    作者回复: 34的整个指令有长度啊,你数一数这条指令对应的机器码需要多少空间呢?

    2019-05-15
    2
    7
收起评论
显示
设置
留言
99+
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部