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

发布于:2023-02-19 ⋅ 阅读:(469) ⋅ 点赞:(0)

案发现场

开发一个项目,前期测试环境是: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. 万事皆有因,一定要追查到底。尤其是计算机软件,一个小问题背后可能涉及很多很深入的原理。