【bug秘史】UINT8数据超出类型范围输出0x0102

2023-05-16

案发现场

开发一个项目,前期测试环境是:simulator + sparc(leon3) + sylxios。
里面通信协议用到了很多和校验,于是便有如下实现函数:

/*********************************************************************************************************
** 函数名称: getChecksum
** 功能描述: 获取一组数据校验和
** 输 入  : uiLength       数据字节数
**           pucData        数据指针
** 输 出  : 校验值
*********************************************************************************************************/
UINT8  getChecksum (UINT  uiLength, UINT8 *pucData)
{
    UINT    i;
    UINT8   ucVerify = 0;

    if ((uiLength == 0) || (pucData == NULL)) {
        return  (uiVerify);
    }
 
    for (i = 0; i < uiLength; i++) {
        ucVerify += pucData[i];
    }

    return  (ucVerify);
}

此函数运行测试一直正常。

后来业务上又用到了2字节和4字节的校验和,为了统一接口,将原校验和函数改为32位返回值,这样无论是需要UINT8、UINT16还是UINT32类型的校验和都可以通过这一个函数来获取。赋值时自动获取低8位,低16位或全部32位校验和。修改后校验函数如下:

/*********************************************************************************************************
** 函数名称: getChecksum
** 功能描述: 获取一组数据校验和
** 输 入  : uiLength       数据字节数
**           pucData        数据指针
** 输 出  : 校验值
*********************************************************************************************************/
UINT32  getChecksum (UINT  uiLength, UINT8 *pucData)
{
    UINT    i;
    UINT32  uiVerify = 0;

    if ((uiLength == 0) || (pucData == NULL)) {
        return  (uiVerify);
    }
 
    for (i = 0; i < uiLength; i++) {
        uiVerify += pucData[i];
    }

    return  (uiVerify);
}

然后测试运行,就报错了,提示接收帧校验错误。我开启了原始数据打印输出,并增加了打印内容,把输出的校验以及计算的校验都打印出来,运行截图如下:
在这里插入图片描述
相关实现代码如下:
在这里插入图片描述
我们看原始数据,0x03 + 0x55 + 0xaa 确实等于0x0102,但我只取UINT8类型校验和,高位1应该会丢掉,应该为0x02和末尾输出校验相同,此接收帧校验应该是通过才对。
但程序运行实际进入了不等分支,且打印计算校验值为0x102,和源码逻辑完全不符啊。
而且进一步发现,对校验值与0xff或类型强转UINT8也是不起作用的,ucChecksum[1]的值依旧是0x102。

UINT8类型值还能大于0xff?难道用一个UINT32的值给UINT8的变量赋值还会有问题?见鬼了。

简化测试

项目工程文件众多,相对复杂,为排除其他干扰,先写一个最简单的app进行验证。
代码如下使用相同的校验和函数,源数据和校验流程,运行结果是正常的,那说明程序这么写也没问题啊。
在这里插入图片描述

深入追踪

在原有工程基础上又进行了一些测试,发现3种方法可以让程序运行正常:

  1. 将getChecksum函数返回值改为UINT8类型;
  2. 使用debug模式调试;
  3. 在ucChecksum变量前加 volatile关键字;

这些方式虽然能解决问题,但并未找到出错根本原因,而且这些改法在实际项目中不好实现。
用方法1的话,需要将程序中需要16和32位校验和的地方单独换新的校验和函数;方法2的话,debug版镜像运行速度满,关键是体积大flash根本放不下;方法3的话,我需要在所有类似地方都加volatile?

还得继续找原因。

反汇编分析

我又对程序的反汇编进行了研究,发现了执行错误的根源。
对执行错误的源码,分别获取debug和release版的反汇编,并进行分析。其中debug是O0级优化(也就是不优化),release版是O2等级优化。

debug版编译选项

make -k all 
sparc-sylixos-elf-gcc -mcpu=v8 -msoft-float  -O0 -g3 -gdwarf-2 -Wall -fmessage-length=0 -fsigned-char -fno-short-enums -fno-strict-aliasing   -fPIC -DSYLIXOS   -I"D:/work/lc3233/leon3/base1129/libsylixos/SylixOS" -I"D:/work/lc3233/leon3/base1129/libsylixos/SylixOS/include" -I"D:/work/lc3233/leon3/base1129/libsylixos/SylixOS/include/network"  -MMD -MP -MF ./Debug/dep/test/src/getChecksum.d -c src/getChecksum.c -o Debug/obj/test/src/getChecksum.o
sparc-sylixos-elf-gcc -mcpu=v8 -msoft-float  -O0 -g3 -gdwarf-2 -Wall -fmessage-length=0 -fsigned-char -fno-short-enums -fno-strict-aliasing   -fPIC -DSYLIXOS   -I"D:/work/lc3233/leon3/base1129/libsylixos/SylixOS" -I"D:/work/lc3233/leon3/base1129/libsylixos/SylixOS/include" -I"D:/work/lc3233/leon3/base1129/libsylixos/SylixOS/include/network"  -MMD -MP -MF ./Debug/dep/test/src/test.d -c src/test.c -o Debug/obj/test/src/test.o
sparc-sylixos-elf-gcc -mcpu=v8 -msoft-float -Wl,-shared -fPIC -shared  ./Debug/obj/test/src/getChecksum.o ./Debug/obj/test/src/test.o -L"D:/work/lc3233/leon3/base1129/libsylixos/Debug" -L"D:/work/lc3233/leon3/base1129/libsylixos/Release"   -lvpmpdm -lm -lgcc -o Debug/test
sparc-sylixos-elf-strip  Debug/test -o Debug/strip/test
create  ./Debug/test ./Debug/strip/test success.

release版编译选项

make -k all 
sparc-sylixos-elf-gcc -mcpu=v8 -msoft-float  -O2 -g1 -gdwarf-2 -Wall -fmessage-length=0 -fsigned-char -fno-short-enums -fno-strict-aliasing   -fPIC -DSYLIXOS   -I"D:/work/lc3233/leon3/base1129/libsylixos/SylixOS" -I"D:/work/lc3233/leon3/base1129/libsylixos/SylixOS/include" -I"D:/work/lc3233/leon3/base1129/libsylixos/SylixOS/include/network"  -MMD -MP -MF ./Release/dep/test/src/getChecksum.d -c src/getChecksum.c -o Release/obj/test/src/getChecksum.o
sparc-sylixos-elf-gcc -mcpu=v8 -msoft-float  -O2 -g1 -gdwarf-2 -Wall -fmessage-length=0 -fsigned-char -fno-short-enums -fno-strict-aliasing   -fPIC -DSYLIXOS   -I"D:/work/lc3233/leon3/base1129/libsylixos/SylixOS" -I"D:/work/lc3233/leon3/base1129/libsylixos/SylixOS/include" -I"D:/work/lc3233/leon3/base1129/libsylixos/SylixOS/include/network"  -MMD -MP -MF ./Release/dep/test/src/test.d -c src/test.c -o Release/obj/test/src/test.o
sparc-sylixos-elf-gcc -mcpu=v8 -msoft-float -Wl,-shared -fPIC -shared  ./Release/obj/test/src/getChecksum.o ./Release/obj/test/src/test.o -L"D:/work/lc3233/leon3/base1129/libsylixos/Release" -L"D:/work/lc3233/leon3/base1129/libsylixos/Debug"   -lvpmpdm -lm -lgcc -o Release/test
sparc-sylixos-elf-strip  Release/test -o Release/strip/test
create  ./Release/test ./Release/strip/test success.

分析
先看debug版汇编操作,它是以g1和g2作为两个校验值进行比较和打印的,在比较这两个校验前,不但进行了单字节从内存中的加载操作,还进行了0xff与操作,这就保证了两个校验值都是按UINT8类型操作的。执行结果也是正确的。
在这里插入图片描述
再看release版汇编操作,它是用i3和o0寄存器作为两个校验值比较和打印的。i3对应ucChecksum[0]它是以单字节方式从内存加载的,且与过了0xff,保证了按UINT8类型处理。o0寄存器对应ucChecksum[1],按照SPARC的寄存器传参规则,它直接就是getChecksum函数的返回值,没有进行任何额外操作,而函数实现也是按32位返回的,那o0的值为0x102也就说的通了。
而且无论对校验值与0xff或类型强转UINT8对这部分汇编都是不影响的,都是直接用函数返回值做校验和,这也和前面的测试得以印证。
在这里插入图片描述
那就是说,release版优化编译下,会直接用函数返回值寄存器来充当结果,而不管赋值目标是什么类型,编译器真要是按这样的规则优化那程序都没法写了,而且前面的简化测试结果是正常的呀。

简化测试程序(release版)编译选项及过程如下:

make -k all 
sparc-sylixos-elf-gcc -mcpu=v8 -msoft-float  -O2 -g1 -gdwarf-2 -Wall -fmessage-length=0 -fsigned-char -fno-short-enums -fno-strict-aliasing   -fPIC -DSYLIXOS   -I"D:/work/lc3233/leon3/base1129/libsylixos/SylixOS" -I"D:/work/lc3233/leon3/base1129/libsylixos/SylixOS/include" -I"D:/work/lc3233/leon3/base1129/libsylixos/SylixOS/include/network"  -MMD -MP -MF ./Release/dep/test/src/getChecksum.d -c src/getChecksum.c -o Release/obj/test/src/getChecksum.o
sparc-sylixos-elf-gcc -mcpu=v8 -msoft-float  -O2 -g1 -gdwarf-2 -Wall -fmessage-length=0 -fsigned-char -fno-short-enums -fno-strict-aliasing   -fPIC -DSYLIXOS   -I"D:/work/lc3233/leon3/base1129/libsylixos/SylixOS" -I"D:/work/lc3233/leon3/base1129/libsylixos/SylixOS/include" -I"D:/work/lc3233/leon3/base1129/libsylixos/SylixOS/include/network"  -MMD -MP -MF ./Release/dep/test/src/test.d -c src/test.c -o Release/obj/test/src/test.o
sparc-sylixos-elf-gcc -mcpu=v8 -msoft-float -Wl,-shared -fPIC -shared  ./Release/obj/test/src/getChecksum.o ./Release/obj/test/src/test.o -L"D:/work/lc3233/leon3/base1129/libsylixos/Release" -L"D:/work/lc3233/leon3/base1129/libsylixos/Debug"   -lvpmpdm -lm -lgcc -o Release/test
sparc-sylixos-elf-strip  Release/test -o Release/strip/test
create  ./Release/test ./Release/strip/test success.

获取简化测试程序(release版)的关键反汇编源码如下:

在这里插入图片描述
它是用i3和g1寄存器作为两个校验值比较和打印的。i3对应ucChecksum[0]它是以单字节方式从内存加载的,且与过了0xff,保证了按UINT8类型处理。g1寄存器对应ucChecksum[1],它是由o0与0xff得到的,而o0就是getChecksum函数的返回值。也就是说,简化测试程序(release版)的结果是有取低8位操作,而原始程序没有。他俩是相同的编译器,相同的编译选项(都是o2),相同的运行平台,执行源码也是一样的,但编译器优化的效果却不同,为啥不同呢,总不能是编译器按心情随机选择吧?

# 真相只有一个

后面又进行了一些测试和探寻,发现了问题的症结:因为头文件包含操作错误,使得getChecksum函数实现是已改为返回UINT32类型,而声明的返回还是原来的UINT8类型,此时用release版编译就会出现错误。

即release模式编译下如果函数定义和声明不同时,就可能造成预期外的结果。这样,我们可以构造一个最小规模的,可复现相同问题的程序,源码及运行结果如下图:
在这里插入图片描述
这部分源码其实就是在上面简化测试程序的基础上,把getChecksum函数放单独一个文件,且函数声明是返回UINT8,这是就会复现之前的问题。如果保证函数定义和声明一致,同为UINT8或UINT32,运行结果就都是正确的。那开发中的问题,改正头文件包含错误,就可以解决了。

追问

项目问题是解决了,但是,我声明了UINT8类型返回值,那ucChecksum[1]不更应该是个8位数据,只应该是0x02而不能是0x102吗?而且同样的程序,为啥改成debug编译就没事了呢?加volatile或者保证定义和声明一致又为啥能消除问题?

先看下getChecksum函数输出UINT8和UINT32是汇编有啥差别,比较如下:
在这里插入图片描述
可见,返回值为UINT8时,在函数返回前就会进行与0xff操作,保证输出是单字节。也就是说,函数返回值都是由寄存器(32位/64位)来传递的,但会根据定义处类型在返回前进行相应处理,保证寄存器的实际值与定义类型相匹配。

而在release优化编译时,函数调用方会认为返回值寄存器已按声明处类型处理过,无需再对返回值寄存器寄存器就那些处理,直接使用返回值寄存器作为返回值变量。所以对一个UINT8类型的返回值进行与0xff或强转UINT8都是没有必要的,都会被优化掉。

当函数定义和声明类型相同时,函数中返回值输出处理和函数调研方的调用操作是匹配的,程序不会有问题,而当定义和声明类型不相同时,则可能造成类型不匹配,从而出现不符合C语言规则的现象。

volatile的作用

那为啥变量加volatile关键字也能规避问题呢?因为volatile关键字会仿真编译器优化后直接用寄存器做变量,而是要在每次访问变量时都要进行内存读写操作,最终比较时用的寄存器值是来至内存而不是函数返回,所以不会出现类型错误问题。

下图是有误volatile关键字的反汇编对比,明显能看到,getChecksum函数返回值寄存器(o0)是先按单字节存储到内存,又按单字节从内存中读取后才进行比较的。
在这里插入图片描述

举一反三

  1. 上面的结论在ARM平台上也测试了下,效果一样,说明这是编译器普遍存在的特性,而不是SPARC体系特有。
  2. 上面讨论的是函数返回值的定义和声明不相同可能会造成错误,举一反三,那函数输入参数和全局变量的定义类型和声明类型不同时也可能造成类似问题。
  3. 一定要保证函数/变量的声明和定义相同,否则在优化编译下会出问题。
  4. 万事皆有因,一定要追查到底。尤其是计算机软件,一个小问题背后可能涉及很多很深入的原理。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【bug秘史】UINT8数据超出类型范围输出0x0102 的相关文章

随机推荐

  • 基于A*和势场寻路的快速小队伍动态势场避障寻路

    前言 先把本算法的适用场景和优缺点写在前面 xff0c 需要的可以继续看 xff0c 不适用的就可以直接略过了 然后在循序渐进介绍本算法 演示效果在最后 本算法适用场景 每次寻路以小队伍为单位 xff08 几个至几十个队员 xff09 队员
  • STM32 keil5 报错:flash download failed-cortex M3解决方法

    起因 因为之前自己使用的STM32都是使用的F4 xff0c 自己打了一块STM32F1C8T6板子 xff0c 焊好之后 xff0c 打开以前正点原子的例程的时候 xff0c 出现无法烧录的情况 xff0c 总是报这样的错误 xff0c
  • HTTP中的GET和POST方法详解

    一般来说GET是获取数据 xff0c POST是提交数据的 但是因为GET和POST都是HTTP的方法 xff0c HTTP又是基于TCP IP的关于数据在万维网中如何让通讯的协议 从本质上讲 xff0c GET和POST都是HTTP请求
  • 使用SocketTask,弃用wx

    经验总结 微信小程序websocket的注意事项 xff1a 使用SocketTask xff0c 弃用wx 示例代码 socketTask的示例代码如下 引用在微信小程序的一个页面中开启多个websocket连接的代码 var ws1 6
  • SimpleNES的编译与运行

    前言 如题 xff0c 最近搜索一些C 43 43 项目无意中看到它 xff0c 然后就动手构建了一把 xff0c 当然过程肯定会遇到一些问题的了 不然也不会写这篇文章跟大家分享了呀 xff0c 是吧 xff01 来来来 xff0c 先给大
  • 【注意/切记】 Android JIN开发过程中不要把long当jlong使用

    遇到问题 C C 43 43 调Java方法时直接把long类型的变量 xff0c 传给CallStaticVoidMethod导致对应的Java静态方法接收到了错误的long值 PS 实际的业务场比较复杂 浪费了好多时间去分析才定位到是这
  • Man手册常用快捷键之万事找h键

    最强最核心最根本的快捷键是 gt gt gt gt gt h 最核心 look everything you want to know is here 基本与常用 p 上翻一屏 control 43 p 上滚一行 n下播一屏 control
  • 2023年clang12编译问题与解决的记录

    最近编译clang12以及尝试基于clang开发一个C 43 43 的静态代码分析工具 xff0c 如下是遇到的环境相关的编译问题与解决方案 在此做个记录 xff0c 同时供可能会遇到同样问题的同学参考 环境说明 注 xff1a 如下是最终
  • Clang dump AST需要注意的事项

    Clang dump AST本质上是对一个OC C C 43 43 源文件编译单元执行编译 xff0c 即跟编译相关的配置都是要注意相关的参数 xff0c 比如依赖的头文件的路径 xff0c 特别编译的源文件依赖的其它路径下的头文件 xff
  • URI与URL的区别

    xff08 原网址 xff1a http zhidao baidu com question 38764759 html xff09 Web上可用的每种资源 HTML文档 图像 视频片段 程序等 由一个通过通用资源标志符 xff08 Uni
  • Python datetime  和 str 相互转化

    datetime 转化为 str now 61 datetime datetime now now strftime 39 Y m d H M S 39 print now 输出2012 03 05 16 26 23 870105 strf
  • BASE64编码规则

    Base64编码要求把3个8位字节 xff08 3 8 61 24 xff09 转化为4个6位的字节 xff08 4 6 61 24 xff09 xff0c 之后在6位的前面补两个0 xff0c 形成8位一个字节的形式 例如字符串 张3 x
  • Cygwin基本命令的使用方法

    原文链接 xff1a http blog chinaunix net space php uid 61 25580079 amp do 61 blog amp id 61 190413 基本操作命令 ls 以默认方式显示当前目录文件列表 l
  • 正确设置了charles,使用charles抓包某些app,无法抓到的原因(Android手机)

    Charles配置绝对正确 xff0c 还是使终抓不到包的可能的一种情况是 xff01 xff01 xff01 应用使用的网络库不支持代理 xff0c 所有的网络请求没有走代理 xff0c 而是走的正常wifi直连 当时的情况如下 xff1
  • 拉起抖音APP视频页的scheme

    拉起scheme地协议 snssdk1128 span class hljs regexp aweme span span class hljs regexp detail span span class hljs number 65344
  • 1. Windows下使用Mingw-w64 GCC编译安装Lua5.1.5

    除了一些游戏开发的脚本领域 xff0c 在以OpenResty作为Web框架的后端开发中 xff0c Lua也会大量的使用 xff0c 这就难免要对在Shell中测试 验证Lua语法 为此 xff0c 小编写了一篇在Windows环境下编译
  • VSCode Python3 三方库

    Mac VSCode python3 配置 setting 默认已经安装VSCode背景操作 默认已经安装VSCode 网上已经有好多教程安装使用Python3 这里只记录python3 的配置问题 背景 从想学Python 到实际操刀来
  • SylixOS命令行下内存操作/测试工具

    源头 memAccessCmd是用于SylixOS下对内存 寄存器进行访问和测试的工具 SylixOS下调测程序 xff0c 无论是应用 驱动还是内核 xff0c 如果能查看并操作指定内存 寄存器 xff0c 将是一个非常有效的手段 作者在
  • 博客里程碑

    2022 07 20 周三 排名突破5000好难啊 xff01 2022 07 26 周二 又前进了几名 xff0c 进入4000内了
  • 【bug秘史】UINT8数据超出类型范围输出0x0102

    案发现场 开发一个项目 xff0c 前期测试环境是 xff1a simulator 43 sparc leon3 43 sylxios 里面通信协议用到了很多和校验 xff0c 于是便有如下实现函数 xff1a span class tok