【GCC编译优化系列】究竟什么样的代码会导致函数调用的栈溢出呢?

一段看似铁定栈溢出的函数代码,它一定会溢出吗?

文章目录

  • 1 问题现场
  • 2 简单分析
  • 3 深入分析
    • 3.1 假如不考虑编译优化的情况
    • 3.2 如果编译器执行了编译优化
  • 4 经验总结
  • 5 更多分享

1 问题现场

事情是这样的,最近我们在考虑招收一批新鲜血液,首次尝试大规模招收应届生和大三实习生。也是受领导所托,让我出几道笔试题,刚好我最近在项目中遇到一个栈溢出的问题,于是简单写了这么一段函数:

__attribute__ ((noinline)) int test_handler(void)
{uint8_t msg[16] = {0};memcpy(msg, "hello6666666666666666666666666666", 100);return msg[0];
}

笔试的题目是:请问这段代码有没有问题?如果有,请指出其问题所在,并尽可能地阐述其风险所在。

2 简单分析

朋友,要是你看到这样一道笔试题,你可能会第一时间大笑起来,这不就是一道典型的 函数栈溢出 的代码场景题吗?教科书都偶讲过啊!
但是,真的仅仅是这样吗?
我们先来简单分析一下这个函数代码:

1)函数先定义了一个16字节长度的msg数组;
2)仅接着使用memcpy对msg数组进行操作赋值;从字符串 “hello6666666666666666666666666666” 中拷贝100个字节到msg数组中;
3)由于msg数组仅有16字节的长度,且这部分内存是位于栈中,而第2)中拷贝的长度却是100字节,大于了16个字节,这样msg数组就不够空间来存储了,因此触发了很典型的 栈溢出 问题。

以上是很常规的教科书式的分析,如果我在笔试场上遇到这道题,我肯定也是这么描述。
但是这些年,随着我对编译优化有了更多的认知后,我开始有不同的看法。

3 深入分析

本文先不考虑其他编译器的情况,仅以ARM-GCC编译器最为讨论,准确地说,编译器版本是:

arm-none-eabi-gcc -v
gcc version 9.2.1 20191025 (release) [ARM/arm-9-branch revision 277599] (GNU Tools for Arm Embedded Processors 9-2019-q4-major)

根据问题代码,下面按两种情况深入分析:

3.1 假如不考虑编译优化的情况

在GCC编译器中,如果想让代码编译不执行任何优化,可以考虑将CFLAGS中加入 -O0 这个优化级别。
同时,上一小节中,我们仅仅是从C代码(高级语言代码)的角度分析,得出栈溢出的结论,而我们如果想知道这段代码真正在经过编译器编译后,得到什么样的执行代码,还得看最后生成的汇编代码才行。
于是,我在配置优化级别为 O0 后,得到以下汇编代码,它对应的就是test_handler函数。

.LC48:.ascii  "hello6666666666666666666666666666\000".section        .text.test_handler,"ax",%progbits.align  1.global test_handler.syntax unified.code   16.thumb_func.fpu softvfp.type   test_handler, %function
test_handler:
.LFB36:.loc 1 299 1.cfi_startproc@ args = 0, pretend = 0, frame = 16@ frame_needed = 1, uses_anonymous_args = 0push    {r7, lr}.cfi_def_cfa_offset 8.cfi_offset 7, -8.cfi_offset 14, -4sub     sp, sp, #16   //定义msg数组,长度为16个字节.cfi_def_cfa_offset 24add     r7, sp, #0.cfi_def_cfa_register 7.loc 1 300 13movs    r3, r7movs    r2, #0str     r2, [r3]adds    r3, r3, #4movs    r2, #12movs    r1, #0movs    r0, r3bl      memset     //调用memset函数对msg数组进行清0操作,因为我定义msg数组时,加了 {0} 做初始化.loc 1 302 5ldr     r1, .L77movs    r3, r7movs    r2, #100movs    r0, r3bl      memcpy     //调用memcpy函数对msg数组进行拷贝赋值.loc 1 304 15movs    r3, r7ldrb    r3, [r3].loc 1 305 1movs    r0, r3     //把msg[0]放到r0寄存器,准备退出函数mov     sp, r7add     sp, sp, #16@ sp neededpop     {r7, pc}   //函数退出
.L78:.align  2
.L77:.word   .LC48.cfi_endproc

简单分析一下这段汇编代码:
1)一开始定义了一个字符串,内容是"hello6666666666666666666666666666\000",它的标签号是 .LC48
2)接下来就是test_handler的定义,它的起始地址标签是 .LFB36
3)其他的汇编代码,见代码中的注释

从上面的分析可以看出,基本上就是跟我们分析C代码是一致的;也就是说,这种情况下,这个函数发生栈溢出是一定的。

感兴趣的朋友也可以把这段代码集成编译到你的代码工程中,看看会发生什么事。

3.2 如果编译器执行了编译优化

以上分析只是一种常规的场景,在实际的嵌入式编译中,往往我们采用的是 Os 级别的优化,在这个级别的优化下,启用了很多编译优化选项,并尽可能地为 缩小镜像的size 而服务,这也是 Os 名称的含义来源。

我们把优化级别配置成 Os 后,我们来看下得到的汇编代码是怎么样的:

        .section        .text.test_handler,"ax",%progbits.align  1.global test_handler.syntax unified.code   16.thumb_func.fpu softvfp.type   test_handler, %function
test_handler:
.LFB36:.loc 1 299 1 is_stmt 1 view -0.cfi_startproc@ args = 0, pretend = 0, frame = 0@ frame_needed = 0, uses_anonymous_args = 0@ link register save eliminated..loc 1 300 5 view .LVU194.loc 1 302 5 view .LVU195.loc 1 304 5 view .LVU196.loc 1 305 1 is_stmt 0 view .LVU197movs    r0, #104   //整数104赋值给r0寄存器,准备函数退出@ sp neededbx      lr         //函数退出.cfi_endproc

简单一看,哗,这个汇编代码精简了许多,至少从代码行数上就下降了不少。
细细一看,多了些门道。

1)首先,“hello6666666666666666666666666666\000” 字符串没有再被定义了;
2)其次,msg数组也没有被定义了;
3)这段汇编代码,有效的代码其实就只有2句: movs r0, #104bx lr

啥意思?就这么简单?
原来的在编译优化级别Os的帮助下,这个函数直接就知道了最后返回的ret,其实就是字符串"hello6666666666666666666666666666\000"的第一个字符,即’h’,而这个’h’字符的ASCII值,正好就是 104(10进制数)。
所以这个函数简单地不能再简单,就变成了类似这样的伪代码:

int test_handler(void)
{return 104;
}

所以,你跟我说,这样的函数代码会发生 栈溢出 吗?这显然是不会的啊!

另外一点,也值得注意的是,细心的朋友可能看到我在 test_handler 函数前加了一个 __attribute__ ((noinline));这个是GCC的扩展功能,目的就是告诉编译器,这个函数需要帮忙保留,不要变成 内联函数

倘若,我把这个修饰去掉后,会是怎么样呢?显然会把这个函数编译成 内联函数,具体的汇编代码是怎么样的,感兴趣的朋友可以自己去实践一下。

4 经验总结

这个小小的代码片段,告诉我们的道理就是:

  • 别总是盯着眼前的 ”苟且“(代码),必要的时候,还需要看得更深,看得更远,别被眼前的一切所蒙蔽了;
  • 纸上得来终觉浅,绝知此事要躬行;教科书上面的讲解并没有错,但是放到实际的应用场景下,不见得一定会100%争取;
  • 努力提升自己 汇编、反汇编 的技术能力,关键时候也许你助你一臂之力。

5 更多分享

架构师李肯

架构师李肯全网同名),一个专注于嵌入式IoT领域的架构师。有着近10年的嵌入式一线开发经验,深耕IoT领域多年,熟知IoT领域的业务发展,深度掌握IoT领域的相关技术栈,包括但不限于主流RTOS内核的实现及其移植、硬件驱动移植开发、网络通讯协议开发、编译构建原理及其实现、底层汇编及编译原理、编译优化及代码重构、主流IoT云平台的对接、嵌入式IoT系统的架构设计等等。拥有多项IoT领域的发明专利,热衷于技术分享,有多年撰写技术博客的经验积累,连续多月获得RT-Thread官方技术社区原创技术博文优秀奖,荣获CSDN博客专家、CSDN物联网领域优质创作者、2021年度CSDN&RT-Thread技术社区之星、2022年RT-Thread全球技术大会讲师、RT-Thread官方嵌入式开源社区认证专家、RT-Thread 2021年度论坛之星TOP4、华为云云享专家(嵌入式物联网架构设计师)等荣誉。坚信【知识改变命运,技术改变世界】!


我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=2qp6214asoow0

【GCC编译优化系列】究竟什么样的代码会导致函数调用的栈溢出呢?相关推荐

  1. 【GCC编译优化系列】宏定义名称与函数同名是一种什么骚操作?

    作者简介 *架构师李肯(全网同名)**,一个专注于嵌入式IoT领域的架构师.有着近10年的嵌入式一线开发经验,深耕IoT领域多年,熟知IoT领域的业务发展,深度掌握IoT领域的相关技术栈,包括但不限于 ...

  2. 【GCC编译优化系列】GCC链接失败的错误提示 undefined reference to ‘xxx‘ 可能还有一种情况你没注意到?

    文章目录 1 写在前面 2 问题描述 2.1 问题现场 2.2 快速排查 2.3 判断问题 3 知识点突破 3.1 场景复现 3.2 深入分析 3.3 涨点新知识 4 经验总结 5 参考链接 6 更多 ...

  3. Android编译优化系列-kapt篇

    一.背景 本文是编译优化系列文章之 kapt 优化篇,后续还会有 build cache, kotlin, dex 优化等文章,敬请期待.本文由Client Infra->Build Infra ...

  4. GCC编译优化指南【作者:金步国】

    GCC编译优化指南[作者:金步国] GCC编译优化指南 作者:金步国 版权声明 本文作者是一位自由软件爱好者,所以本文虽然不是软件,但是本着 GPL 的精神发布.任何人都可以自由使用.转载.复制和再分 ...

  5. GCC编译优化应用预编译头

    服务器编译优化记录 对项目编译优化过程中一些思路和脚本工具实现.对内存受限的编译环境有一些帮助. 工具: https://github.com/wangxiaobai-dd/GccPrecompile ...

  6. GCC 编译优化指南

    前言 网上关于编译优化的文章很多,但大多零零散散,不成体系,本文试图给出一个完整和清晰的优化思路,同时提供在实践中如何进行优化的详尽参考.但是,在介绍所有优化知识之前首先引用LFS-Book中的一句忠 ...

  7. GCC 编译优化等级

    参考资料:<Using the GNU Compiler Collection> 打开优化标志会使编译器尝试以牺牲编译时间和调试程序的能力为代价来提高性能和/或代码大小. 根据目标和 GC ...

  8. gcc编译c语言调用mysql存储过程代码出现的问题list

    1.问题:mysql.c:1:19: 错误:mysql/mysql.h:没有那个文件或目录 结局: 原因:没有装 mysql-devel-5.0.22-2.1.i386.rpm [root@local ...

  9. gcc编译流程及中间表示层RTL的探索

    gcc编译流程及中间表示层RTL的探索收藏 新一篇: 解读VC++编程中的文件操作API和CFile类 | 旧一篇: Effective Item21 尽可能使用const 内容摘要 本文将以 C 语 ...

最新文章

  1. python 爬虫入门
  2. [android] 手机卫士黑名单功能(列表展示)
  3. video标签 在微信浏览器打开,不弹出大的独立窗口 而是直接播放。
  4. requestbody前端怎么传_学习前端开发前的基础知识了解「V1001」
  5. web安全之XSS基础-常见编码科普
  6. 在源文件(.c)和头文件(.h)中声明和定义的区别——C语言
  7. NetApp收购Data Domain 当上冤大头?
  8. mongoose用模型更新不了,因为模型对象中默认带有_id会提示errmsg: “Performing an update on the path ‘_id‘ would modify the i
  9. 表格对决CSS--一场生死之战 (转自“清清月儿”)
  10. php路由中间件,lumen5.5学习路由和中间件(四)
  11. TensorFlow实现语音识别
  12. java学习之面向对象和封装
  13. iOS开发:国际化之app支持多种语言切换
  14. 剖析云计算技术及架构(2 云存储)
  15. 01-蘑菇街爬虫准备工作1
  16. 华为路由器默认用户名密码
  17. Element UI数字组件四舍五入问题及居右显示
  18. 参与一个Python的开源项目Python-QQ
  19. DNS服务器基本配置
  20. 基于S7–1500的单部六层电梯教程(三)

热门文章

  1. latex初学者入门(二)
  2. 亚马逊测评做单总是被砍单封号是什么原因?
  3. mysql log-slave-update_mysql数据库log-slave-updates 参数解释
  4. Java闲杂笔记摘抄
  5. 多线程同步机制的几种方法
  6. 支付宝SDK下载地址
  7. python 使用for循环,遍历列表里想要的值
  8. NMS非极大值抑制的原理
  9. MVC5 + EF6 + Bootstrap3 (9) HtmlHelper用法大全(下)
  10. 《23种设计模式之原型模式(2种实现)》