Toggle navigation
首页
问答
文章
积分商城
专家
专区
更多专区...
文档中心
返回主站
搜索
提问
会员
中心
登录
注册
C语言
死循环
汇编
【汇编实战开发笔记】从汇编代码中找出一段普通的for循环变成“死循环”的根本原因
5.00
发布于 2022-01-31 00:14:06 浏览:1465
订阅该版
[tocm] # 1 前言 在我的[上一篇文章](https://blog.csdn.net/szullc/article/details/122757860)中,有讲到掌握汇编知识的重要性,关键时刻可能还会拯救你于泥潭之中。 那么,本篇文章,我将再介绍一个使用汇编知识排查疑难问题的方法,希望对大家有所帮助。 # 2 问题描述 问题是这样的,前一段时间我们项目组在进行一项自测试中,偶然发现我们的代码好像挂了一样:**现象就是命令行输入不了,但是没有看到复位信息输出**。 当时,我们一个小伙伴说:“好像我们的系统挂了?”当我了解到这个现象之后,根据我之前的排查经验,我当即得出了一个结论:“可能是我们的代码跑进**死循环**了,好好检查下”! 于是,我们开始debug代码,加了一些必要的调试信息,最终发现有一个计算校验的函数,调进去了但是没有退出来,而这个校验的函数非常之简单,它就长这样: ```c uint16_t checksum(uint8_t *data, uint8_t len) { uint8_t i; uint16_t sum = 0, res; for (i = 0; i < len; i++) { sum += data[i]; } res = sum ; return res; } ``` 我想当你看到这段函数时,肯定也是:“**卧槽,这TM不就是算累加校验和吗?怎么可能会死循环?**” 没错,当时我们的争论的场景也的确如此! # 3 简单分析 这个checksum函数真的是非常简单,入参简单、实现也简单、返回值也简单,根本不存在难点。 一步步来分析: 既然代码没有崩溃,证明data指针肯定非NULL的,不会有问题; 倒是这个len有些可疑,len的类型是uint8_t无符号的,它的范围是0-255;但是如果外面传入的是-1呢? 如果传入-1,强制转换为uint8_t,其值也是255,那么下面的for循环,依然只会跑256次,它必须得退出呀? 有没有可能for循环的过程中,栈的值被修改了,然后i的值和len的值都变了,进而for的次数改变了? 于是我们开始打印i和len的值,发现他们两个的值,都是正常变化的,并不是刚刚想的那样。 这就很奇怪了!!! 如果说这个for循环要“无限”循环下去,造成“死循环”,必须满足的条件是len很大很大,但是len不是uint8_t类型嘛?最大也就255呀? printf大法再来一遍:结果出乎我们的意料,请看: ![image-20220131021624188](https://oss-club.rt-thread.org/uploads/20220714/0c43271e85d0d57040131ba83ae80f0bb9ac86da.png) log输出: ```c [12-21 19:45:38]checksum 128 len: 4294967295 [12-21 19:45:38]0 4294967295 [12-21 19:45:38]1 4294967295 [12-21 19:45:38]2 4294967295 [12-21 19:45:38]3 4294967295 [12-21 19:45:38]4 4294967295 [12-21 19:45:38]5 4294967295 [12-21 19:45:38]6 4294967295 [12-21 19:45:38]7 4294967295 [12-21 19:45:38]8 4294967295 [12-21 19:45:38]9 4294967295 [12-21 19:45:38]10 4294967295 。。。省略很多 [12-21 19:45:38]250 4294967295 [12-21 19:45:38]251 4294967295 [12-21 19:45:38]252 4294967295 [12-21 19:45:38]253 4294967295 [12-21 19:45:38]254 4294967295 [12-21 19:45:38]255 4294967295 [12-21 19:45:38]256 4294967295 [12-21 19:45:38]257 4294967295 [12-21 19:45:38]258 4294967295 [12-21 19:45:38]259 4294967295 [12-21 19:45:38]260 4294967295 。。。还在不停的打印 ``` 看到这里似乎有点眉目了?len的值为**4294967295**? 这个值不是**0xFFFFFFFF**吗? 我们再使用**%d**打印了一下len,发现值为-1。 回过头来看下checksum的调用之处: ```c uint16_t res = checksum(&data[0], len - 1); ``` 看似真相了,当len为0的时候,传入的值不就是-1吗? 好像是这么回事,但是-1进去,它是uint8_t的呀,顶多就是255啊?怎么变成了**4294967295**? 到底是谁干的啊? 同时也发现关键问题了,这里并不是真正意义的**“死循环”**,而是for循环执行太久了,导致长时间无法结束,因为我们的主频才160MHZ,CPU就是猛跑,从1加到0xFFFFFFFF,也需要好长一段时间呢! # 4 场景再现 为了充分说明这个问题,我尽可能地还原下当时我们的代码场景: ```c /* 一个结构体定义数据 不要急于吐槽它的定义,这代码是开源的,冤有头。。。 还有不要怀疑是字节对齐不对齐的问题,曾经我也怀疑过,最后知道真相的时候,我被打脸了! */ typedef struct _data_t { /* result, final result */ uint8_t len; uint8_t flag; uint8_t passwd_len; uint8_t *passwd; uint8_t ssid_len; uint8_t *ssid; uint8_t token_len; uint8_t *token; uint8_t bssid_type_len; uint8_t *bssid; uint8_t ssid_is_gbk; uint8_t ssid_auto_complete_disable; uint8_t data[127]; uint8_t checksum; } data_t; ``` ```c /* 1.c 调用checksum的C文件 */ /* 定义全局的数据 */ static data_t g_data; /* 设置全局的数据 */ void set_global_data(void) { g_data.len = 0; } void handle_global_data(void) { uint16_t res = checksum(&g_data.data[0], g_data.len - 0); //sometimes no return form checksum } void test_func_entry(void) { set_global_data(); handle_global_data(); } ``` ```c /* 2.c 定义checksum函数的工具类 */ uint16_t checksum(uint8_t *data, uint8_t len) { uint8_t i; uint16_t sum = 0, res; for (i = 0; i < len; i++) { sum += data[i]; } res = sum ; return res; } ``` 在我的第一次认知里,还是len=-1=255的情况,由于g_data.data只有127字节,但它最后是可以访问到255下标的,其实这本身就有数据非法访问的问题;但是经过仔细论证,得出的结论是,这并不会导致死循环,或者说并不会改变len的值;因为checksum里面知识读取data指针的值,并没改变它的值,即便越界了,顶多访问了别人,并不会出啥异常(至少在我们的处理器平台是这样)。 这个问题对我们来说,真的是百思不得其解,为了规避掉这个问题,我们在调用checksum的时候做了判断,但len为0的时候直接不调用,也就绕过了这个问题。 但是作为一个深挖底层逻辑的攻城狮来说,我们不应该放过这样的细节,或许还有什么我们未发现的潜在风险呢? 这个问题一直困扰着我,时不时有空的时候,我就会想想,到底还有什么情况还会导致这个现象? # 5 柳暗花明 偶然有一天,我正浏览到一篇关于编译器做代码优化的[文章](https://zhuanlan.zhihu.com/p/458079943?utm_source=wechat_session&utm_medium=social&utm_oi=52858527416320&s_r=0),它是在知乎上发出来的,我看到其中一个重要线索: ![image-20220131145534882](https://oss-club.rt-thread.org/uploads/20220714/51b40cb489038fa76d6c1f3e30b392e48683e9f1.png) 突然我脑子里,闪过一个疑问:“会不会那段for循环的checksum函数,正是因为调用方没有申明checksum函数,也就是说没有include对应的头文件导致编译器做了默认处理呢?”? 我们都知道,在使用gcc编译器编译C代码时,如果一个函数未申明就调用,是会报一个**警告**的:**“warning: implicit declaration of function 'checksum' [-Wimplicit-function-declaration]”**! 同时,尤其编译器不知道被调用函数的原型,那么它只能依靠你的调用代码结合一些默认值做假设: 比如我们的调用代码是: ```c uint16_t res = checksum(&g_data.data[0], g_data.len - 0); ``` 这里,我猜测编译器的行为就是,你有一个叫checksum的函数,但我找不到它的原型,那么我就按**“返回值是uint16_t类型,第一个参数是int型,第二个参数也是int型”**来吧! 为何,gcc默认参数列表都是int类型?这是我的假想猜测,下面我们再论证,究竟是不是这样? 有了这个假设之后,我们回到ARM汇编在函数调用时的参数,这时R0应该等于&g_data.data[0],R1应该等于-1。 由于R0/R1都是32位的寄存器,在存储数据的时候,无所谓**有符号和无符号一说**,且本问题R0没有出现问题,我们仅讨论R1。 这个时候R1的寄存器值,应该是**“-1 = 0xFFFFFFFF”**,这个假设很关键,如果分析地很顺利,那么这个for循环不停地循环下去,才可以有理论进行下去。 # 6 找到证据 既然上面我们发现了端倪,那么我们应该进一步找到相关的证据,证明我们的想法;同时,如果这个问题根源在于include头文件,那么当我们添加了头文件之后,这个问题应该不会再复现。我们来看下,究竟是不是这样? ## 6.1 究竟是不是警告 由于我们的代码实在太多警告了,就属于那种 **0 error N warnings** 那种,属于你需要找一个警告往往好费好大劲! 经过好一番检索,果不其然,还真的报警告了,的确是**“warning: implicit declaration of function 'checksum' [-Wimplicit-function-declaration]”**! ![image-20220131153421647](https://oss-club.rt-thread.org/uploads/20220714/716fc15cfbc245b80a724fa98d6b2781522ede69.png) ## 6.2 盘根问底 看编译器的行为,我们肯定是要看其对应的汇编文件,这里有两个地方需要看,一个是checksum函数的汇编,还有一个调用checksum函数附近的汇编。 我们一起看看: ```c /* checksum 函数的汇编代码 */ .section .text.checksum,"ax",%progbits .align 1 .global checksum .code 16 .thumb_func .type checksum, %function checksum: .LFB4: .loc 1 125 0 .cfi_startproc @ args = 0, pretend = 0, frame = 0 @ frame_needed = 0, uses_anonymous_args = 0 .LVL27: push {r4, r5, r6, lr} .cfi_def_cfa_offset 16 .cfi_offset 4, -16 .cfi_offset 5, -12 .cfi_offset 6, -8 .cfi_offset 14, -4 .loc 1 125 0 movs r4, r0 movs r5, r1 // r1 -> r5 ,即 len的值存在r5中 .loc 1 129 0 movs r2, r1 ldr r0, .L29 .LVL28: bl printf //打印len的值 .LVL29: movs r3, r4 .loc 1 127 0 movs r0, #0 adds r5, r4, r5 .LVL30: .L26: .loc 1 130 0 discriminator 1 cmp r3, r5 //for循环里面的关键判断,即 i < len beq .L28 // 退出for循环 .loc 1 131 0 discriminator 3 //下面就是for循环的循环执行体 ldrb r2, [r3] adds r3, r3, #1 .LVL31: adds r0, r0, r2 .LVL32: lsls r0, r0, #16 lsrs r0, r0, #16 .LVL33: b .L26 .LVL34: .L28: .loc 1 136 0 @ sp needed .LVL35: pop {r4, r5, r6, pc} .L30: .align 2 .L29: .word .LC12 .cfi_endproc .LFE4: .size checksum, .-checksum ``` 由它的汇编代码可知,for循环执行多少次,关键在于**r5寄存器的值,也就是len的值**。 注意在汇编代码这里,是看不到r5是uint8_t还是uint32_t的,它仅仅是一个32位的寄存器。 ```c .section .text.verify_checksum,"ax",%progbits .align 1 .global verify_checksum .code 16 .thumb_func .type verify_checksum, %function verify_checksum: .LFB5: .loc 1 81 0 .cfi_startproc @ args = 0, pretend = 0, frame = 0 @ frame_needed = 0, uses_anonymous_args = 0 .LVL17: push {r4, lr} .cfi_def_cfa_offset 8 .cfi_offset 4, -8 .cfi_offset 14, -4 .loc 1 83 0 ldr r4, .L20 .loc 1 91 0 @ sp needed .loc 1 83 0 movs r0, r4 //r0存储结构体g_data的地址 ldrb r1, [r4] //将g_data的第一个字节,即g_data.len赋值为r1 adds r0, r0, #34 //r0的地址偏移34个字节,即偏移到g_data.data的位置; subs r1, r1, #1 //关键的一步:r1 = r1 - 1 由于我们复现问题的时候,g_data.len是为0的,所以此时r1的值就是0xFFFFFFFF bl checksum //调用checksum函数,第1-2个入参,分别是r0和r1 .LVL18: .loc 1 84 0 adds r4, r4, #160 .loc 1 89 0 ldrb r3, [r4] lsls r0, r0, #24 .LVL19: lsrs r0, r0, #24 subs r0, r0, r3 .loc 1 91 0 pop {r4, pc} .L21: .align 2 .L20: .word .LANCHOR4 .cfi_endproc .LFE5: .size verify_checksum, .-verify_checksum ``` 了解汇编知识的,看到上面的汇编代码,结合checksum函数的汇编代码,就应该明白,我前面的假设成立了,但len传入到checksum函数时,它的值真的是0xFFFFFFFF,而使用%u打印出来,就是4294967295。 到此,罪魁祸首其实已经找到了,与其说是编译器的**无故优化**,倒不如说是程序猿写代码不严谨,没有正确处理掉这个编译警告。 ## 6.3 解除风险 既然找到了问题根源,那么我们尝试下解除这个风险。 方法其实也很简单,直接需要在调用checksum函数的1.c中,include一下checksum函数所在的头文件即可。 添加之后,我们看下发生的变化,很显然,checksum函数的汇编代码肯定是没有任何不变的,应该它压根没有改; 而调用checksum的汇编就发生了些许的变化,同时编译输出的地方,那个编译警告也都消失了。 ```c /* 添加头文件之后的汇编代码 */ .section .text.verify_checksum,"ax",%progbits .align 1 .global verify_checksum .code 16 .thumb_func .type verify_checksum, %function verify_checksum: .LFB5: .loc 1 81 0 .cfi_startproc @ args = 0, pretend = 0, frame = 0 @ frame_needed = 0, uses_anonymous_args = 0 .LVL17: push {r4, lr} .cfi_def_cfa_offset 8 .cfi_offset 4, -8 .cfi_offset 14, -4 .loc 1 83 0 ldr r4, .L20 .loc 1 91 0 @ sp needed .loc 1 83 0 movs r0, r4 ldrb r1, [r4] adds r0, r0, #34 subs r1, r1, #1 //r1寄存器的一样的操作 r1 = r1 - 1 lsls r1, r1, #24 //关键改变!!! r1 = r1 * (2的24次幂),也就是算术左移24位 lsrs r1, r1, #24 //关键改变!!! r1 = r1 / (2的24次幂),也就是算术右移24位 bl checksum .LVL18: .loc 1 84 0 adds r4, r4, #160 .loc 1 89 0 ldrb r3, [r4] lsls r0, r0, #24 .LVL19: lsrs r0, r0, #24 subs r0, r0, r3 .loc 1 91 0 pop {r4, pc} .L21: .align 2 .L20: .word .LANCHOR4 .cfi_endproc .LFE5: .size verify_checksum, .-verify_checksum ``` 为了好对比,我直接使用对比工具贴图上来看下: ![image-20220131155818422](https://oss-club.rt-thread.org/uploads/20220714/ffdba8a5e0853e353d966103964af44d9d2af667.png) 查了下多出来的这两条指令:lsls和lsrs,[参考这里](http://blog.sina.com.cn/s/blog_a6559d920102wwvp.html)。 一个是算术左移24位,一个是算术右移24位,倒来倒去,无非就是把高24位给情况,这样-1的值传入checksum的时候,就只有0x000000FF了,而不是0xFFFFFFFF。 这样就把uint8_t len拉回正常的逻辑了,自然也就不会出现之前的for循环一直退不出来了。 # 7 扩展延伸 上面我提及的场景对应的是ARM平台的,由于我们的代码是跨平台的,支持RISC-V架构,X86架构等等。 ## 7.1 RISC-V架构 所以我们来对比看下RISC-V架构下的情况: ![image-20220131161246073](https://oss-club.rt-thread.org/uploads/20220714/4dfee9ead263e68d7853452e3340aa4b1cd00b01.png)这么看,RISC-V的处理也是够粗暴的,一个addi指令,把高24位去掉就完事了!!! ## 7.2 80x86架构 我push了一个简易的工程代码到[github](https://github.com/recan-li/coding-01workstation/tree/master/workspace/gcc/for_loop),以便于重现此问题,感兴趣的可以看[这里](https://github.com/recan-li/coding-01workstation/tree/master/workspace/gcc/for_loop)。 很遗憾的是,在80x86上竟然**没有复现此问题**。 代码的核心差别就是是否include 2.h: ![image-20220131170413498](https://oss-club.rt-thread.org/uploads/20220714/33c163e9059d3a097a1af8b7fb338318d8523bdd.png) 汇编代码确实有差异: ![image-20220131170211876](https://oss-club.rt-thread.org/uploads/20220714/a6ff02c7ee765de575921ab3dbee4762ce905514.png) 但是跑出来的效果确实一样的: ![image-20220131170252989](https://oss-club.rt-thread.org/uploads/20220714/f6040a57b5617583e920a7e87d3e598f2e48ba4b.png) 总结下没有复现问题的原因,可能是: - 编译选项没有使用正确? - 80x86编译器更懂事?更能知道如何合理编译代码? - 还有未知的编译特性未了解到? ## 7.3 其他架构 感兴趣的可以在其他平台上验证下,是否有类似的问题,欢迎讨论。 # 8 经验总结 - 请提升你的代码编译严谨性,如果是gcc编译器,-Wall -Werror -Os是最低要求; - 谈优化代码前,请close掉你的代码编译**异常**,先达到 **0 error 0 warning** 再说; - 请重视**warning: implicit declaration of function**这个编译警告; - 如果使用gcc编译器,不提示任何编译警告和错误,并不代表编译器没有告诉你,也许是你使用-w选项编译了输出,你仅仅是在自欺欺人而已; - 老老实实在调用函数前申明你的函数,或者包含其对应的头文件,有时候编译器的默认行文不见得就可靠; - 代码细节很重要,真的是细节决定成败; - 不放过一丝可能性,作为一个攻城狮,这点专研精神需要时刻挂在心里; - 大胆假设,小心求证,亘古不变的方法论。 # 9 更多分享 欢迎关注我的[github仓库01workstation](https://github.com/recan-li/01workstation),日常分享一些开发笔记和项目实战,欢迎指正问题。 同时也非常欢迎关注我的CSDN主页和专栏: [【CSDN主页:架构师李肯】](http://yyds.recan-li.cn) [【RT-Thread主页:架构师李肯】](https://club.rt-thread.org/u/18001) [【C/C++语言编程专栏】](https://blog.csdn.net/szullc/category_8450784.html) [【GCC专栏】](https://blog.csdn.net/szullc/category_8626555.html) [【信息安全专栏】](https://blog.csdn.net/szullc/category_8452787.html) [【RT-Thread开发笔记】](https://blog.csdn.net/szullc/category_11461616.html) [【freeRTOS开发笔记】](https://blog.csdn.net/szullc/category_11467856.html) [【BLE蓝牙开发笔记】](https://blog.csdn.net/szullc/category_11615545.html) [【ARM开发笔记】](https://blog.csdn.net/szullc/category_11575847.html) [【RISC-V开发笔记】](https://blog.csdn.net/szullc/category_11494874.html) 有问题的话,可以跟我讨论,知无不答,谢谢大家。
9
条评论
默认排序
按发布时间排序
登录
注册新账号
关于作者
李肯陪你玩赚嵌入式
2022年度和2023年度RT-Thread社区优秀开源布道师,COC深圳城市开发者社区主理人,专注于嵌入式物联网的架构设计
文章
47
回答
504
被采纳
82
关注TA
发私信
相关文章
1
RT-Thread内存和字符串相关函数与C语言自带的内存和字符串相关函数冲突问题
2
嵌入式RT-thread中初始化线程函数中(void *)entry的意义何在
3
cJSON parse 失败,请问怎么解决?
4
请教一个C语言顺序表的问题,不知道有没有大佬帮吗解答
5
小白请教,关于头文件引用,、、哪些场景需要引用它们?
6
使用中断有warn。。。。。。。。。。。
7
局部变量位置被编译器改写
8
RTthread 两个例程结合在一起会出现incompatible type for argument 2 of 'led_matrix_set_color'
9
有没有在单片机编程使用goto语句的?
10
请问如何读取另一个c文件中的温湿度数据啊
推荐文章
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在线升级
freemodbus
PWM
flash
cubemx
packages_软件包
BSP
潘多拉开发板_Pandora
定时器
ADC
flashDB
GD32
socket
中断
编译报错
Debug
SFUD
rt_mq_消息队列_msg_queue
msh
keil_MDK
ulog
C++_cpp
MicroPython
本月问答贡献
a1012112796
10
个答案
1
次被采纳
踩姑娘的小蘑菇
4
个答案
1
次被采纳
红枫
4
个答案
1
次被采纳
张世争
4
个答案
1
次被采纳
Ryan_CW
4
个答案
1
次被采纳
本月文章贡献
catcatbing
3
篇文章
5
次点赞
YZRD
2
篇文章
5
次点赞
qq1078249029
2
篇文章
2
次点赞
xnosky
2
篇文章
1
次点赞
Woshizhapuren
1
篇文章
5
次点赞
回到
顶部
发布
问题
投诉
建议
回到
底部