Toggle navigation
首页
问答
文章
积分商城
专家
专区
更多专区...
文档中心
返回主站
搜索
提问
会员
中心
登录
注册
gcc
原创征文
【GCC编译优化系列】究竟什么样的代码会导致函数调用的栈溢出
发布于 2022-11-30 19:48:30 浏览:1175
订阅该版
**文章目录** [toc] # 1 问题现场 事情是这样的,最近我们在考虑招收一批新鲜血液,首次尝试大规模招收应届生和大三实习生。也是受领导所托,让我出几道笔试题,刚好我最近在项目中遇到一个栈溢出的问题,于是简单写了这么一段函数: ```c __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编译器最为讨论,准确地说,编译器版本是: ```c 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函数。 ```c .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 = 0 push {r7, lr} .cfi_def_cfa_offset 8 .cfi_offset 7, -8 .cfi_offset 14, -4 sub sp, sp, #16 //定义msg数组,长度为16个字节 .cfi_def_cfa_offset 24 add r7, sp, #0 .cfi_def_cfa_register 7 .loc 1 300 13 movs r3, r7 movs r2, #0 str r2, [r3] adds r3, r3, #4 movs r2, #12 movs r1, #0 movs r0, r3 bl memset //调用memset函数对msg数组进行清0操作,因为我定义msg数组时,加了 {0} 做初始化 .loc 1 302 5 ldr r1, .L77 movs r3, r7 movs r2, #100 movs r0, r3 bl memcpy //调用memcpy函数对msg数组进行拷贝赋值 .loc 1 304 15 movs r3, r7 ldrb r3, [r3] .loc 1 305 1 movs r0, r3 //把msg[0]放到r0寄存器,准备退出函数 mov sp, r7 add sp, sp, #16 @ sp needed pop {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** 后,我们来看下得到的汇编代码是怎么样的: ```c .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 .LVU197 movs r0, #104 //整数104赋值给r0寄存器,准备函数退出 @ sp needed bx lr //函数退出 .cfi_endproc ``` 简单一看,哗,这个汇编代码精简了许多,至少从代码行数上就下降了不少。 细细一看,多了些门道。 1)首先,"hello6666666666666666666666666666\000" 字符串没有再被定义了; 2)其次,msg数组也没有被定义了; 3)这段汇编代码,有效的代码其实就只有2句:` movs r0, #104` 和 `bx lr` 啥意思?就这么简单? 原来的在编译优化级别Os的帮助下,这个函数直接就知道了最后返回的ret,其实就是字符串"hello6666666666666666666666666666\000"的第一个字符,即'h',而这个'h'字符的ASCII值,正好就是 104(10进制数)。 所以这个函数简单地不能再简单,就变成了类似这样的伪代码: ```c int test_handler(void) { return 104; } ``` 所以,你跟我说,这样的函数代码会发生 **栈溢出** 吗?这显然是不会的啊! 另外一点,也值得注意的是,细心的朋友可能看到我在 test_handler函数前加了一个 **__attribute__ ((noinline))**;这个是GCC的扩展功能,目的就是告诉编译器,这个函数需要帮忙保留,不要变成 **内联函数**。 倘若,我把这个修饰去掉后,会是怎么样呢?显然会把这个函数编译成 **内联函数**,具体的汇编代码是怎么样的,感兴趣的朋友可以自己去实践一下。 # 4 经验总结 这个小小的代码片段,告诉我们的道理就是: - 别总是盯着眼前的 **”苟且“(代码)**,必要的时候,还需要看得更深,看得更远,别被眼前的一切所蒙蔽了; - **纸上得来终觉浅,绝知此事要躬行**;教科书上面的讲解并没有错,但是放到实际的应用场景下,不见得一定会100%争取; - 努力提升自己 **汇编、反汇编** 的技术能力,关键时候也许你助你一臂之力。 # 5 更多分享 > **[架构师李肯](https://recan.blog.csdn.net/?type=blog)** > > **架构师李肯**(**全网同名**),一个专注于嵌入式IoT领域的架构师。有着近10年的嵌入式一线开发经验,深耕IoT领域多年,熟知IoT领域的业务发展,深度掌握IoT领域的相关技术栈,包括但不限于主流RTOS内核的实现及其移植、硬件驱动移植开发、网络通讯协议开发、编译构建原理及其实现、底层汇编及编译原理、编译优化及代码重构、主流IoT云平台的对接、嵌入式IoT系统的架构设计等等。拥有多项IoT领域的发明专利,热衷于技术分享,有多年撰写技术博客的经验积累,连续多月获得RT-Thread官方技术社区原创技术博文优秀奖,荣获[CSDN博客专家](https://recan.blog.csdn.net/?type=blog)、[CSDN物联网领域优质创作者](http://yyds.recan-li.cn)、[2021年度CSDN&RT-Thread技术社区之星](https://blog.csdn.net/szullc/article/details/123860472)、[2022年RT-Thread全球技术大会讲师](https://club.rt-thread.org/ask/article/afa56894c113369a.html)、[RT-Thread官方嵌入式开源社区认证专家](https://club.rt-thread.org/ask/experts.html)、[RT-Thread 2021年度论坛之星TOP4](https://club.rt-thread.org/ask/article/3317.html)、[华为云云享专家(嵌入式物联网架构设计师)](https://bbs.huaweicloud.com/community/usersnew/id_1573655458316259)等荣誉。坚信【知识改变命运,技术改变世界】! 最后,非常欢迎大家关注我的同名公众号:[**《架构师李肯》**](https://mp.weixin.qq.com/s?__biz=MzA4MTk2MjYzMQ==&mid=2247484136&idx=1&sn=aa06784a05471ddbf7cedfdc26c3e3c1&chksm=9f8db100a8fa3816501a7a27e7b3e5eb5df39c74d34b2c9837f052054412676b6adb9e96fbc0#rd),不定期更新硬核技术文章,大家互相学习,共同成长。 ![screenshot_公众号+微信号+二维码.png](https://oss-club.rt-thread.org/uploads/20230116/35452f2193c31b08b514ab1be8959858.png.webp)
8
条评论
默认排序
按发布时间排序
登录
注册新账号
关于作者
李肯陪你玩赚嵌入式
2022年度和2023年度RT-Thread社区优秀开源布道师,COC深圳城市开发者社区主理人,专注于嵌入式物联网的架构设计
文章
47
回答
504
被采纳
82
关注TA
发私信
相关文章
1
gcc编译不能链接超过 2MB
2
Studio默认的工具链为什么那么老,支持用新的吗?
3
studio 字节对齐问题求解
4
BSP(zynq7000)例程是否没有做过gcc 9的编译测试?
5
环境搭建问题和AT软件包的问题
6
arm-none-eabi-gcc下编译utest变量未定义
7
rtconfig.py 下编译标志解释
8
关于GCC和ARMCC编译后生成BIN,文件的大小
9
gcc环境 libc 使用, code size 优化请教
10
rt-thread使用win32API
推荐文章
1
RT-Thread应用项目汇总
2
玩转RT-Thread系列教程
3
国产MCU移植系列教程汇总,欢迎查看!
4
机器人操作系统 (ROS2) 和 RT-Thread 通信
5
五分钟玩转RT-Thread新社区
6
【技术三千问】之《玩转ART-Pi》,看这篇就够了!干货汇总
7
关于STM32H7开发板上使用SDIO接口驱动SD卡挂载文件系统的问题总结
8
STM32的“GPU”——DMA2D实例详解
9
RT-Thread隐藏的宝藏之completion
10
【ART-PI】RT-Thread 开启RTC 与 Alarm组件
热门标签
RT-Thread Studio
串口
Env
LWIP
SPI
AT
Bootloader
Hardfault
CAN总线
FinSH
ART-Pi
USB
DMA
文件系统
RT-Thread
SCons
RT-Thread Nano
线程
MQTT
STM32
RTC
FAL
rt-smart
ESP8266
I2C_IIC
UART
WIZnet_W5500
ota在线升级
PWM
cubemx
freemodbus
flash
packages_软件包
BSP
潘多拉开发板_Pandora
定时器
ADC
GD32
flashDB
socket
中断
编译报错
Debug
SFUD
msh
rt_mq_消息队列_msg_queue
keil_MDK
ulog
MicroPython
C++_cpp
本月问答贡献
出出啊
1517
个答案
342
次被采纳
小小李sunny
1443
个答案
289
次被采纳
张世争
805
个答案
174
次被采纳
crystal266
547
个答案
161
次被采纳
whj467467222
1222
个答案
148
次被采纳
本月文章贡献
出出啊
1
篇文章
4
次点赞
小小李sunny
1
篇文章
1
次点赞
张世争
1
篇文章
1
次点赞
crystal266
2
篇文章
2
次点赞
whj467467222
2
篇文章
1
次点赞
回到
顶部
发布
问题
投诉
建议
回到
底部