我正在帮助一个学生完成作业,遇到了一个非常棘手的“错误”,我无法解释,并且想知道我是否可以得到帮助以了解到底发生了什么。

问题是要实施以下内容:

给定一个长度为 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

  • 您能提供示例输入及其预期输出吗?


    – 

  • 5
    ~(buf[0])提升buf[0]int,然后执行按位补码。如果校验和应该用八位补码来计算,则使用(uint8_t) ~ (unsigned) buf[0]。((uint8_t) ~buf[0]通常就足够了,但在使用补码或符号和幅度的 C 实现中,存在假设故障。)


    – 


  • 参见


    – 

  • 4
    @elialm,提示:调试与位相关的问题时,请以十六进制打印调试信息。 FFBF,而不是65471


    – 


最佳答案
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类型为、intsigned 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~intintchecksumuint16_t

例如,如果的值为msg_buf[i]0x33,则首先将其提升为值 0x00000033。然后应用运算int后,~结果将为 0xffffff77。此值将作为 添加到 的当前值,然后将结果截断为然后再赋值。checksumintuint16_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
    适合打高尔夫球,但却掩盖了正常发展的否定意图。


    –