【汇编实战开发笔记】从汇编代码中找出一段普通的for循环变成“死循环”的根本原因

作者:recan

原文链接:

https://club.rt-thread.org/ask/article/4e41fb1de844e925.html


1 前言



在我的上一篇文章中,有讲到掌握汇编知识的重要性,关键时刻可能还会拯救你于泥潭之中。


那么,本篇文章,我将再介绍一个使用汇编知识排查疑难问题的方法,希望对大家有所帮助。


2 问题描述



问题是这样的,前一段时间我们项目组在进行一项自测试中,偶然发现我们的代码好像挂了一样:现象就是命令行输入不了,但是没有看到复位信息输出。


当时,我们一个小伙伴说:“好像我们的系统挂了?”当我了解到这个现象之后,根据我之前的排查经验,我当即得出了一个结论:“可能是我们的代码跑进死循环了,好好检查下”!


于是,我们开始debug代码,加了一些必要的调试信息,最终发现有一个计算校验的函数,调进去了但是没有退出来,而这个校验的函数非常之简单,它就长这样:


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大法再来一遍:结果出乎我们的意料,请看:



log输出:


[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的调用之处:

uint16_t res = checksum(&data[0], len - 1);



看似真相了,当len为0的时候,传入的值不就是-1吗?


好像是这么回事,但是-1进去,它是uint8_t的呀,顶多就是255啊?怎么变成了4294967295? 到底是谁干的啊?


同时也发现关键问题了,这里并不是真正意义的“死循环”,而是for循环执行太久了,导致长时间无法结束,因为我们的主频才160MHZ,CPU就是猛跑,从1加到0xFFFFFFFF,也需要好长一段时间呢!


4 场景再现



为了充分说明这个问题,我尽可能地还原下当时我们的代码场景:

/*一个结构体定义数据不要急于吐槽它的定义,这代码是开源的,冤有头。。。还有不要怀疑是字节对齐不对齐的问题,曾经我也怀疑过,最后知道真相的时候,我被打脸了!*/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;


/* 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();}
/* 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 柳暗花明



偶然有一天,我正浏览到一篇关于编译器做代码优化的文章,它是在知乎上发出来的,我看到其中一个重要线索:

突然我脑子里,闪过一个疑问:“会不会那段for循环的checksum函数,正是因为调用方没有申明checksum函数,也就是说没有include对应的头文件导致编译器做了默认处理呢?”?


我们都知道,在使用gcc编译器编译C代码时,如果一个函数未申明就调用,是会报一个警告的:“warning: implicit declaration of function ‘checksum’ [-Wimplicit-function-declaration]”!


同时,尤其编译器不知道被调用函数的原型,那么它只能依靠你的调用代码结合一些默认值做假设:


比如我们的调用代码是:

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]”!


6.2 盘根问底

看编译器的行为,我们肯定是要看其对应的汇编文件,这里有两个地方需要看,一个是checksum函数的汇编,还有一个调用checksum函数附近的汇编。


我们一起看看:


/* checksum 函数的汇编代码 */    .section    .text.checksum,"ax",%progbits    .align    1    .global    checksum    .code    16    .thumb_func    .type    checksum, %functionchecksum:.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位的寄存器。


    .section    .text.verify_checksum,"ax",%progbits    .align    1    .global    verify_checksum    .code    16    .thumb_func    .type    verify_checksum, %functionverify_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的汇编就发生了些许的变化,同时编译输出的地方,那个编译警告也都消失了。


* 添加头文件之后的汇编代码 */        .section    .text.verify_checksum,"ax",%progbits    .align    1    .global    verify_checksum    .code    16    .thumb_func    .type    verify_checksum, %functionverify_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


为了好对比,我直接使用对比工具贴图上来看下:



查了下多出来的这两条指令:lsls和lsrs,参考这里。


一个是算术左移24位,一个是算术右移24位,倒来倒去,无非就是把高24位给情况,这样-1的值传入checksum的时候,就只有0x000000FF了,而不是0xFFFFFFFF。


这样就把uint8_t len拉回正常的逻辑了,自然也就不会出现之前的for循环一直退不出来了。


7 扩展延伸



上面我提及的场景对应的是ARM平台的,由于我们的代码是跨平台的,支持RISC-V架构,X86架构等等。


7.1 RISC-V架构

所以我们来对比看下RISC-V架构下的情况:


这么看,RISC-V的处理也是够粗暴的,一个addi指令,把高24位去掉就完事了!!!


7.2 80x86架构

我push了一个简易的工程代码到github,以便于重现此问题,感兴趣的可以看这里。


很遗憾的是,在80x86上竟然没有复现此问题。


代码的核心差别就是是否include 2.h:


汇编代码确实有差异:


但是跑出来的效果确实一样的:


总结下没有复现问题的原因,可能是:


编译选项没有使用正确?

80x86编译器更懂事?更能知道如何合理编译代码?

还有未知的编译特性未了解到?

7.3 其他架构

感兴趣的可以在其他平台上验证下,是否有类似的问题,欢迎讨论。


8 经验总结



请提升你的代码编译严谨性,如果是gcc编译器,-Wall -Werror -Os是最低要求;

谈优化代码前,请close掉你的代码编译异常,先达到 0 error 0 warning 再说;

请重视warning: implicit declaration of function这个编译警告;

如果使用gcc编译器,不提示任何编译警告和错误,并不代表编译器没有告诉你,也许是你使用-w选项编译了输出,你仅仅是在自欺欺人而已;

老老实实在调用函数前申明你的函数,或者包含其对应的头文件,有时候编译器的默认行文不见得就可靠;

代码细节很重要,真的是细节决定成败;

不放过一丝可能性,作为一个攻城狮,这点专研精神需要时刻挂在心里;

大胆假设,小心求证,亘古不变的方法论。


内容仅供各位学习参考,文章仅代表作者个人看法,不代表本平台观点,版权归原作者所有,转载请联系作者,如有版权疑问,请联系本微信后台,我们会第一时间处理。




END






爱我就给我点在看


点击 阅读原文


RTThread物联网操作系统 帮助您了解RT-Thread相关的资讯.
评论
  • 最近几年,新能源汽车愈发受到消费者的青睐,其销量也是一路走高。据中汽协公布的数据显示,2024年10月,新能源汽车产销分别完成146.3万辆和143万辆,同比分别增长48%和49.6%。而结合各家新能源车企所公布的销量数据来看,比亚迪再度夺得了销冠宝座,其10月新能源汽车销量达到了502657辆,同比增长66.53%。众所周知,比亚迪是新能源汽车领域的重要参与者,其一举一动向来为外界所关注。日前,比亚迪汽车旗下品牌方程豹汽车推出了新车方程豹豹8,该款车型一上市就迅速吸引了消费者的目光,成为SUV
    刘旷 2024-12-02 09:32 59浏览
  • 光耦合器作为关键技术组件,在确保安全性、可靠性和效率方面发挥着不可或缺的作用。无论是混合动力和电动汽车(HEV),还是军事和航空航天系统,它们都以卓越的性能支持高要求的应用环境,成为现代复杂系统中的隐形功臣。在迈向更环保技术和先进系统的过程中,光耦合器的重要性愈加凸显。1.混合动力和电动汽车中的光耦合器电池管理:保护动力源在电动汽车中,电池管理系统(BMS)是最佳充电、放电和性能监控背后的大脑。光耦合器在这里充当守门人,将高压电池组与敏感的低压电路隔离开来。这不仅可以防止潜在的损坏,还可以提高乘
    腾恩科技-彭工 2024-11-29 16:12 117浏览
  • 在电子技术快速发展的今天,KLV15002光耦固态继电器以高性能和强可靠性完美解决行业需求。该光继电器旨在提供无与伦比的电气隔离和无缝切换,是现代系统的终极选择。无论是在电信、工业自动化还是测试环境中,KLV15002光耦合器固态继电器都完美融合了效率和耐用性,可满足当今苛刻的应用需求。为什么选择KLV15002光耦合器固态继电器?不妥协的电压隔离从本质上讲,KLV15002优先考虑安全性。输入到输出隔离达到3750Vrms(后缀为V的型号为5000Vrms),确保即使在高压情况下,敏感的低功耗
    克里雅半导体科技 2024-11-29 16:15 119浏览
  • 戴上XR眼镜去“追龙”是种什么体验?2024年11月30日,由上海自然博物馆(上海科技馆分馆)与三湘印象联合出品、三湘印象旗下观印象艺术发展有限公司(下简称“观印象”)承制的《又见恐龙》XR嘉年华在上海自然博物馆重磅开幕。该体验项目将于12月1日正式对公众开放,持续至2025年3月30日。双向奔赴,恐龙IP撞上元宇宙不久前,上海市经济和信息化委员会等部门联合印发了《上海市超高清视听产业发展行动方案》,特别提到“支持博物馆、主题乐园等场所推动超高清视听技术应用,丰富线下文旅消费体验”。作为上海自然
    电子与消费 2024-11-30 22:03 70浏览
  • 国产光耦合器因其在电子系统中的重要作用而受到认可,可提供可靠的电气隔离并保护敏感电路免受高压干扰。然而,随着行业向5G和高频数据传输等高速应用迈进,对其性能和寿命的担忧已成为焦点。本文深入探讨了国产光耦合器在高频环境中面临的挑战,并探索了克服这些限制的创新方法。高频性能:一个持续关注的问题信号传输中的挑战国产光耦合器传统上利用LED和光电晶体管进行信号隔离。虽然这些组件对于标准应用有效,但在高频下面临挑战。随着工作频率的增加,信号延迟和数据保真度降低很常见,限制了它们在电信和高速计算等领域的有效
    腾恩科技-彭工 2024-11-29 16:11 106浏览
  • RDDI-DAP错误通常与调试接口相关,特别是在使用CMSIS-DAP协议进行嵌入式系统开发时。以下是一些可能的原因和解决方法: 1. 硬件连接问题:     检查调试器(如ST-Link)与目标板之间的连接是否牢固。     确保所有必要的引脚都已正确连接,没有松动或短路。 2. 电源问题:     确保目标板和调试器都有足够的电源供应。     检查电源电压是否符合目标板的规格要求。 3. 固件问题: &n
    丙丁先生 2024-12-01 17:37 57浏览
  • 光伏逆变器是一种高效的能量转换设备,它能够将光伏太阳能板(PV)产生的不稳定的直流电压转换成与市电频率同步的交流电。这种转换后的电能不仅可以回馈至商用输电网络,还能供独立电网系统使用。光伏逆变器在商业光伏储能电站和家庭独立储能系统等应用领域中得到了广泛的应用。光耦合器,以其高速信号传输、出色的共模抑制比以及单向信号传输和光电隔离的特性,在光伏逆变器中扮演着至关重要的角色。它确保了系统的安全隔离、干扰的有效隔离以及通信信号的精准传输。光耦合器的使用不仅提高了系统的稳定性和安全性,而且由于其低功耗的
    晶台光耦 2024-12-02 10:40 54浏览
  • 艾迈斯欧司朗全新“样片申请”小程序,逾160种LED、传感器、多芯片组合等产品样片一触即达。轻松3步完成申请,境内免费包邮到家!本期热荐性能显著提升的OSLON® Optimal,GF CSSRML.24ams OSRAM 基于最新芯片技术推出全新LED产品OSLON® Optimal系列,实现了显著的性能升级。该系列提供五种不同颜色的光源选项,包括Hyper Red(660 nm,PDN)、Red(640 nm)、Deep Blue(450 nm,PDN)、Far Red(730 nm)及Ho
    艾迈斯欧司朗 2024-11-29 16:55 155浏览
  • 国产光耦合器正以其创新性和多样性引领行业发展。凭借强大的研发能力,国内制造商推出了适应汽车、电信等领域独特需求的专业化光耦合器,为各行业的技术进步提供了重要支持。本文将重点探讨国产光耦合器的技术创新与产品多样性,以及它们在推动产业升级中的重要作用。国产光耦合器创新的作用满足现代需求的创新模式新设计正在满足不断变化的市场需求。例如,高速光耦合器满足了电信和数据处理系统中快速信号传输的需求。同时,栅极驱动光耦合器支持电动汽车(EV)和工业电机驱动器等大功率应用中的精确高效控制。先进材料和设计将碳化硅
    克里雅半导体科技 2024-11-29 16:18 157浏览
  • 学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习笔记&记录学习习笔记&记学习学习笔记&记录学习学习笔记&记录学习习笔记&记录学习学习笔记&记录学习学习笔记记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&
    youyeye 2024-11-30 14:30 62浏览
  • 《高速PCB设计经验规则应用实践》+PCB绘制学习与验证读书首先看目录,我感兴趣的是这一节;作者在书中列举了一条经典规则,然后进行详细分析,通过公式推导图表列举说明了传统的这一规则是受到电容加工特点影响的,在使用了MLCC陶瓷电容后这一条规则已经不再实用了。图书还列举了高速PCB设计需要的专业工具和仿真软件,当然由于篇幅所限,只是介绍了一点点设计步骤;我最感兴趣的部分还是元件布局的经验规则,在这里列举如下:在这里,演示一下,我根据书本知识进行电机驱动的布局:这也算知行合一吧。对于布局书中有一句:
    wuyu2009 2024-11-30 20:30 86浏览
我要评论
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦