我正在帮助一个学生完成作业,遇到了一个非常棘手的“错误”,我无法解释,并且想知道我是否可以得到帮助以了解到底发生了什么。
问题是要实施以下内容:
给定一个长度为 N 的字节数组 (
buf
),计算以下公式给出的 16 位校验和:
checksum = ~(buf[0]) + ~(buf[1]) + ... + ~(buf[N-2]) + ~(buf[N-1])
学生做的实现非常简单:
uint16_t calculate_checksum(uint8_t *msg_buf, size_t msg_length)
{
uint16_t checksum = 0;
for (size_t i = 0; i < msg_length; i++)
{
checksum += ~(msg_buf[i]);
}
return checksum;
}
然而令我惊讶的是,这次实施并没有取得预期的结果。
我尝试checksum
通过在循环中打印变量的值来查看变量,结果产生了意想不到的模式:
65471
65459
65449
65287
65276
65253
65166
65113
65094
...
校验和从 开始0
,但在第一次加法之后,该值位于 的上限某处,uint16_t
并且向下移动。我怀疑该值下溢是因为在对其进行补码之前msg_buf[i]
被隐式转换为uint16_t
。但我不明白为什么。我期望首先msg_buf
使用 进行索引i
,然后计算字节补码(这将产生 0-255 内的值),然后才将其(隐式)提升为uint16_t
。
我尝试查看语句的汇编输出checksum += ~(msg_buf[i])
,它似乎支持该理论(使用 ARM gcc 14.1.0)。请注意,[r7, #4]
是msg_buf
指针,[r7, #8]
是i
和[r7, #14]
是。汇编对减法做了一些奇怪的事情,如果在补码之前将字节转换为 a,checksum
则会产生相同的结果。msg_buf[i]
uint16_t
ldr r2, [r7, #4]
ldr r3, [r7, #8]
add r3, r3, r2
ldrb r3, [r3] @ zero_extendqisi2
mov r2, r3
ldrh r3, [r7, #14] @ movhi
subs r3, r3, r2
uxth r3, r3
subs r3, r3, #1
strh r3, [r7, #14] @ movhi
知道了这一点,我们想出的解决方案就很简单了。我们选择只使用AND
右侧的结果来0xFF
去除高位,这样就得到了正确的校验和。
所以,问题基本上已经解决了,但我仍然不明白为什么会出现这个问题。也许这种行为是预料之中的,但我不知道正确的操作顺序,或者发生了其他事情。我真的不知道。
有人能解释一下为什么会发生这种情况吗?
4
最佳答案
3
…我期望首先
msg_buf
用 进行索引i
,然后计算字节uint16_t
补数(这将产生一个 0-255 之间的值),只有这样它才会(隐式地)提升为。
C 2018 6.5.3.3 4 对于运算符指定~
“对操作数执行整数提升…”对 a 的整数提升uint8_t
将其提升为int
。
因此,~(msg_buf[i])
对 进行按位补码运算int
。这会将高位设置为 1。要计算 8 位补码,可以使用(uint8_t) ~msg_buf[i]
或~msg_buf[i] & 0xFF
。
对于一般情况,代码checksum += (uint8_t) ~msg_buf[i];
就足够了。C 标准允许的理论情况主要是算术溢出或转换为uint8_t
可能产生与期望值不同的值,但它们不会发生在普通的 C 实现中。
|
这里发生的事情是整数提升的结果。
int
在大多数情况下,当表达式中使用小于的类型时,它将被提升为类型。这在int
的第 6.3.1.1p2 节中有详细说明:
下列内容可以在表达式中任何可以使用
int
或的
地方使用:unsigned int
- 具有整数类型(除
int
或 之外unsigned int
)的对象或表达式,其整数转换等级小于或等于int
和的等级unsigned int
。_Bool
类型为、int
、signed int
或 的位域unsigned int
。如果
int
可以表示原始类型的所有值(对于位字段,受宽度限制),则该值将转换为int; otherwise, it is converted to an
无符号整数。这些称为整数提升。58 )所有其他类型均不受整数提升的影响
并且有关一元算术运算符的第 6.5.3.3p4 节对运算符作出了如下规定~
:
运算符的结果
~
是其(提升的)操作数的按位补码(即,当且仅当转换后的操作数中的相应位未设置时,结果中的每一位才会设置)。对操作数执行整数提升,结果具有提升的类型。如果提升的类型是无符号类型,则表达式 ~E 相当于该类型中可表示的最大值减去 E。
因此,对于这句话:
checksum += ~(msg_buf[i]);
在应用运算符之前,的值msg_buf[i]
会提升为类型。假设 是 32 位的,则该值的高 3 个字节将全部为零。因此,当应用运算符时,这 3 个字节的所有位都将设置为 1。然后,当将该值添加到类型时,将保留低 16 位,其中高 8 位全部设置为 1。int
~
int
int
checksum
uint16_t
例如,如果的值为msg_buf[i]
0x33,则首先将其提升为值 0x00000033。然后应用运算符int
后,~
结果将为 0xffffff77。此值将作为 添加到 的当前值,然后将结果截断为然后再赋值。checksum
int
uint16_t
应用运算符后~
,需要先使用以下任一方法将结果还原回 8 位值:
checksum += (uint8_t)(~msg_buf[i]);
或者位掩码:
checksum += ~msg_buf[i] & 0xff;
|
作为其他答案的补充,可以提到的替代解决方案如下:
checksum += msg_buf[i] ^ 0xFF;
原因是与 0xFF 进行异或只会翻转低 8 位,无论是否msg_buf[i]
被提升。
1
-
1适合打高尔夫球,但却掩盖了正常发展的否定意图。
–
|
–
~(buf[0])
提升buf[0]
为int
,然后执行按位补码。如果校验和应该用八位补码来计算,则使用(uint8_t) ~ (unsigned) buf[0]
。((uint8_t) ~buf[0]
通常就足够了,但在使用补码或符号和幅度的 C 实现中,存在假设故障。)–
–
FFBF
,而不是65471
。–
|