C语言实现CAN字节解码时带符号值解析异常问题排查
问题背景
使用C语言开发CAN字节解码函数时,已完成输入值字节序与运行系统自身字节序的适配逻辑,当前无符号值解码结果完全正常,但带符号值解码存在错误。
开发阶段默认带符号数据的最高位(MSB)为符号标志位:小端(LITTLE_ENDIAN_VAL)格式下符号位位于最后一个字节,大端(BIG_ENDIAN_VAL)格式下符号位位于第一个字节,怀疑对C语言带符号数的表示机制存在理解偏差,附现有实现代码如下:
/** * @brief can_interact_decode - converts array of length x containing hex bytes into a uint64 * @param[in] const uint8_t* - const array of hex bytes * @param[in] const size_t - length of hex bytes array * @param[in] const enum can_interact_signedness - whether the bytes are storing a signed value or not. SIGNED_VAL indicates signed, UNSIGNED_VAL indicates unsigned * @param[in] const enum can_interact_endianness - endianess. LITTLE_ENDIAN_VAL is little, BIG_ENDIAN_VAL is big * @return[out] uint64_t - interpreted value as unsigned int from hex bytes, taking other params into account */ uint64_t can_interact_decode(const uint8_t *payload, const size_t data_len, const enum can_interact_signedness is_signed, const enum can_interact_endianness byte_order) { uint64_t result; /* [0,0,0,0,0,0,0,0] */ uint8_t* blocks; /* array of 8 */ result = 0; blocks = (uint8_t*)(&result); if(byte_order == LITTLE_ENDIAN_VAL) { memcpy(blocks, payload, (is_signed ? data_len - 1 : data_len)); blocks[7] = is_signed ? payload[data_len - 1] : blocks[7]; result = le64toh(result); /* little endian->host byte order */ } else if(byte_order == BIG_ENDIAN_VAL) { memcpy(blocks + (8 - data_len) + (is_signed ? 1 : 0), (is_signed ? payload + 1 : payload), (is_signed ? data_len - 1 : data_len)); blocks[0] = is_signed ? payload[0] : blocks[0]; result = be64toh(result); /* big endian->host byte order */ } return result; }
代码问题排查
- 缺失补码符号扩展逻辑
CAN总线传输的带符号整型统一采用二进制补码表示,当数据长度小于8字节时,若值为负,所有超出原数据位宽的高位必须全部填充为1才能得到正确的64位值。现有代码仅将符号位所在字节挪到uint64_t的最高字节位置,中间空出的高位全部保持初始值0,完全不符合补码符号扩展规则。例如2字节小端有符号值0xFF 0xFF(对应十进制-1),现有逻辑处理后得到的值为0xFF000000000000FF,和正确值0xFFFFFFFFFFFFFFFF存在本质差异。 - 有符号值的字节拷贝位置完全错位
- 小端场景:代码直接拷贝
data_len-1个字节到result的起始内存位置,再单独把符号位所在的最后一个字节放到result的第7个字节位置,相当于把原数据的低data_len-1字节放在64位值的最低位,符号位直接扔到最高位,中间空出的6个字节既无值填充也无顺序对齐,字节位置完全错误。例如1字节小端有符号值0xFF(对应-1),现有逻辑拷贝0字节到result起始位置,仅将第7字节设为0xFF,转换后得到0xFF00000000000000,和正确值偏差极大。 - 大端场景:拷贝偏移计算
blocks + (8 - data_len) + (is_signed ? 1 : 0)逻辑错误。大端格式下输入的第一个字节是含符号位的最高有效字节,后续字节按位权从高到低排列,现有代码把符号位单独放到result的第0字节,剩余data_len-1字节从8-data_len+1偏移位置开始拷贝,直接导致次高字节位置比符号位还高,字节顺序完全混乱。
- 小端场景:代码直接拷贝
- 字节序转换函数使用场景错误
le64toh/be64toh的作用是转换长度为8字节、按对应字节序排列的完整整数到主机字节序,现有代码在调用转换函数前,result内存中的字节根本不是按目标字节序排列的完整64位值,转换结果自然不符合预期。
修复方案
- 先按无符号值的处理逻辑,根据输入字节序把所有payload字节正确拼装到64位result的对应位权位置,保证原数据的每一位在64位值中的位权和实际定义一致
- 如果是有符号值,判断原数据最高位(符号位)是否为1:若为1,将所有高于原数据位宽的高位全部置1,完成补码符号扩展
- 不要提前拆分符号位单独拷贝,避免字节错位问题。
参考修复代码:
uint64_t can_interact_decode(const uint8_t *payload, const size_t data_len, const enum can_interact_signedness is_signed, const enum can_interact_endianness byte_order) { uint64_t result = 0; // 先按无符号规则拼装字节 if (byte_order == LITTLE_ENDIAN_VAL) { for (size_t i = 0; i < data_len; i++) { result |= (uint64_t)payload[i] << (i * 8); } } else { // BIG_ENDIAN_VAL for (size_t i = 0; i < data_len; i++) { result |= (uint64_t)payload[i] << ((data_len - 1 - i) * 8); } } // 有符号值做符号扩展 if (is_signed == SIGNED_VAL) { uint64_t sign_bit = 1ULL << (data_len * 8 - 1); if (result & sign_bit) { // 所有高位补1 result |= ~((1ULL << (data_len * 8)) - 1); } } return result; }
内容的提问来源于stack exchange,提问作者Salih MSA
相关产品推荐
相关产品推荐

