编程高手必学的内存知识
海纳
华为编译器高级专家,原 Huawei JDK 团队负责人
20674 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 33 讲
编程高手必学的内存知识
15
15
1.0x
00:00/00:00
登录|注册

04 | 深入理解栈:从CPU和函数的视角看栈的管理

你好,我是海纳。
上节课,我们讲到,栈被操作系统安排在进程的高地址处,它是向下增长的。但这只是对栈相关知识的“浅尝辄止”。那我们今天这节课,就会跟着前面的脉络,让你可以更深刻地理解栈的运行原理。
栈是每一个程序员都很熟悉的话题,但你敢说你真的完全了解它吗?我相信,你在工作中肯定遇到过栈溢出(StackOverflow)的错误,比如在写递归函数的时候,当漏掉退出条件,或者退出条件不小心写错了,就会出现栈溢出错误。我们也经常听说缓冲区溢出带来的严重的安全问题,这在日常的工作中都是要避免的。
所以,今天这节课,我们继续深入探讨一下栈这个话题,我会带你基于“符合人的直观思维”,也就是函数的层面和 CPU 的机器指令层面,多角度来理解栈相关的概念。这样,你以后遇到与栈相关的问题的时候,才知道如何着手进行排查。最后,我们还会通过一个缓冲区溢出攻击栈的案例,看看我们在日常工作中如何提升代码的健壮度和安全性。

函数与栈帧

当我们在调用一个函数的时候,CPU 会在栈空间(这当然是线性空间的一部分)里开辟一小块区域,这个函数的局部变量都在这一小块区域里存活。当函数调用结束的时候,这一小块区域里的局部变量就会被回收。
这一小块区域很像一个框子,所以大家就命名它为 stack frame。frame 本意是框子的意思,在翻译的时候被译为帧,现在它的中文名字就是栈帧了。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

本文从函数的层面和CPU的机器指令层面多角度解析了栈的相关概念。首先介绍了函数与栈帧的关系,指出当调用一个函数时,CPU会在栈空间开辟一小块区域,称为栈帧,用于存储函数的局部变量。通过示例代码和递归函数的执行过程,读者能更深刻地理解栈帧与函数的关系。文章通过实际案例和技术原理,帮助读者理解栈的重要性和运行机制,以及如何避免栈溢出和缓冲区溢出带来的安全问题。通过深入理解栈的管理,读者可以更好地排查与栈相关的问题,并提升代码的健壮度和安全性。整体而言,本文通过深入浅出的方式,从CPU和函数的视角全面解析了栈的管理,为读者提供了全面的栈相关知识,使读者能够更深入地理解栈的运行原理和相关概念。 文章还介绍了栈溢出的概念,并通过一个精心构造的例子展示了缓冲区溢出攻击的原理。作者提出了对缓冲区溢出攻击进行防御的两种常见手段,并解释了gcc自带的栈保护机制。此外,文章还通过汉诺塔程序的求解过程分析了栈帧的创建和销毁的过程,以此揭示了函数和栈帧的关系。最后,文章强调了编写健壮而安全的代码的重要性,以及每个程序员都应该关注安全问题。 总的来说,本文通过多个角度深入浅出地解析了栈的管理和相关安全问题,为读者提供了全面的栈相关知识,使读者能够更深入地理解栈的运行原理和相关概念。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《编程高手必学的内存知识》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(27)

  • 最新
  • 精选
  • 🐮
    老师,出堆后栈空间里的数据还是保留的啊,是不是叫栈空间扩展和收缩形象点

    作者回复: 是的。可以这么说。不过我们在说expand这个词的时候,往往用于栈空间不足了,需要对栈进行动态扩展这种场景。你说的很对,rsp指针的上移并没有真的把栈上的数据清空掉,所以我们在使用局部变量的时候一定要初始化,否则就有可能访问到上次释放的栈内存。你掌握的很好!

    2021-11-01
    14
  • 老师好,栈溢出的例子,在栈桢上不是先保存基地址rbp,然后分配rsp保存参数和局部变量吗?所以在参数的栈高位应该还有rbp,然后才是rip。但是代码本地运行一下是可以的,通过objdump看,发现没有push %rbp,mov %rsp,%rbp了。这是因为gcc加了-o1的优化参数。这个是不是有点类似方法内敛呢?不加-o1,就还会先保存rbp了,在执行即使段错误。 然后,思考题答案,通过objdump看,发现参数寄存器rdi和rsi保存的不再是值了,而是通过lea把参数的栈地址传递过去了。因此修改就等于是修改了main的栈桢上的值。

    作者回复: 对的。这说明你真的动手实践过了。这就掌握得很好了。你的看法都是对的。

    2021-11-03
    2
    6
  • keepgoing
    又看了一遍老师这一课,没太看懂栈溢出攻击这一块的细节,想多请教一下: 执行test函数后,字符串数组s中从0元素到15元素在栈中存储地址是从高向低的吗?随后call copy方法后,会压栈下一条指令地址到栈上,这条指令存储地址更低。所以最后让栈溢出时多拷贝地址是把地址数值的低位放在内存地址高位、数值的高位放在内存地址低位满足小端序顺利解析到这条地址的数值。 不知道这样理解对吗,因为对老师举的这个例子比较感兴趣,所以想把细节搞清楚,如果没理解对不知道能不能辛苦老师多讲一下,感谢!

    作者回复: 是的。是这样的。就是通过把bad函数的地址写到栈上,然后就使得ret指令跑进bad函数里面运行了。两个要素:一是越界读写,一是覆盖栈上的返回地址。

    2021-11-04
    2
    3
  • GL
    swap函数在C传入指针或C++的引用 是拿到了操作数的存放地址 所以可以改变对应的值,Java语言的入参如果基本数据类型是没法改变外部变量的值,如果是引用类型是可以改变引用对象内的属性值。

    作者回复: Right! Java没有指向栈上的指针,这个设计很重要。

    2021-11-02
    2
    3
  • 老师您好,缓冲区溢出的Segment Fault,是指一个栈桢里溢出,还是栈桢之间的溢出。我理解是按照文章说的保护机制,应该是溢出到了别的栈桢,才会出现Segment Fault。不知道这样的理解正不正确呢?

    作者回复: 往往无意识的缓冲区溢出,因为会拿随机值覆盖有效值,所以会带来segment fault,覆盖了本栈帧的关键数据,或者覆盖了其他栈帧的数据都有可能造成segment fault。但是精心构造的缓冲区溢出,就像课程里对test函数的攻击,是可以把控制流导向恶意代码的。保护机制其实是在关键位置,比如栈帧开始处,编译器自动插入变量,函数结束时再检查一下,如果变化了就主动触发fault,以增强安全性,所以不一定是溢出到别的栈帧,两者之间没有必然联系

    2021-11-01
    1
  • thomas
    第 3 行的作用呢,是把栈向下增长 0x10,这是为了给局部变量预留空间。从这里,你可以看出来运行 fac 函数要是消耗栈空间的。 ==========================> 请问栈增长多少是如何预估出来的?

    作者回复: 编译器在做编译优化的过程中会计算的。具体地说就是寄存器分配这一步就能统计出来需要多大的栈空间。

    2021-12-25
  • 拭心
    看的有点晕,尤其是各个汇编指令和他们操作的寄存器的作用,不知道您是怎么记忆这些晦涩的内容呢?

    作者回复: 其实这是本科阶段的三节课:汇编原理,计算机组成和计算机体系结构的内容。我们学了三个学期的呀。一下子记不住很正常。慢慢来。

    2021-11-16
    2
  • keepgoing
    老师想提个小建议,能不能把汇编代码也贴上来比较方便理解,n*(n-1)那个例子因为示例代码只有机器码,只能看着您的文字理解,我们这种刚开始入门的同学看着可能比较抽象,不过这一课又把栈更深入地理解了一遍,谢谢老师

    作者回复: 汇编代码往右拖😂,我也看不懂机器码,哈哈

    2021-11-04
    2
  • 老师,我觉得你一下用python,一下用c语言,不太好。

    作者回复: 其实基本上都是C语言。python呢就当伪代码看吧,你看最后结尾的吊打面试官里,其实也是伪代码。自己转换成可执行的C或者Java代码是个很好的练习哦。

    2021-11-03
  • Rovebiy
    老师,是不是曾经在知乎写过专栏进击的Java新人?

    作者回复: 嗯,是我。

    2021-11-02
收起评论
显示
设置
留言
27
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部