按位运算导致意外的变量大小

Bitwise operation results in unexpected variable size

上下文

我们正在移植最初使用 8 位 C 编译器为 PIC 微控制器编译的 C 代码。为了防止无符号全局变量(例如,错误计数器)滚回零而使用的一个常见习惯用法如下:

if(~counter) counter++;

此处的按位运算符会反转所有位,并且仅当 counter 小于最大值时该语句才为真。重要的是,无论变量大小如何,这都有效。

问题

我们现在的目标是使用 GCC 的 32 位 ARM 处理器。我们注意到相同的代码会产生不同的结果。据我们所知,按位补码运算 return 的值看起来与我们预期的大小不同。为了重现这一点,我们在 GCC 中编译:

uint8_t i = 0;
int sz;

sz = sizeof(i);
printf("Size of variable: %d\n", sz); // Size of variable: 1

sz = sizeof(~i);
printf("Size of result: %d\n", sz); // Size of result: 4

在第一行输出中,我们得到了预期的结果:i 是 1 个字节。但是,i 的按位补码实际上是 四个字节 这会导致问题,因为现在与此进行比较不会给出预期的结果.例如,如果执行(其中 i 是正确初始化的 uint8_t):

if(~i) i++;

我们将看到 i "wrap around" 从 0xFF 回到 0x00。这种行为在 GCC 中与它在以前的编译器和 8 位 PIC 微控制器中按我们预期的方式工作时相比是不同的。

我们知道我们可以通过像这样转换来解决这个问题:

if((uint8_t)~i) i++;

或者,通过

if(i < 0xFF) i++;

然而,在这两种解决方法中,变量的大小必须是已知的,并且对于软件开发人员来说很容易出错。这些类型的上限检查发生在整个代码库中。变量有多种大小(例如,uint16_tunsigned char 等),我们不希望在其他工作代码库中更改这些变量。

问题

我们对问题的理解是否正确,是否有解决此问题的选项而不需要重新访问我们使用过该习语的每个案例?我们的假设是否正确,像按位补码这样的操作应该 return 与操作数大小相同的结果?这似乎会中断,具体取决于处理器架构。我觉得我正在服用疯狂的药丸,而 C 应该比这更便携。同样,我们对此的理解可能是错误的。

从表面上看,这似乎不是一个大问题,但这个以前有效的习语已在数百个地方使用,我们渴望在进行昂贵的更改之前了解这一点。


注意:这里有一个看似相似但不完全重复的问题:Bitwise operation on char gives 32 bit result

我没有看到那里讨论的问题的实际症结所在,即按位补码的结果大小与传递给运算符的结果大小不同。

sizeof(i);你请求变量的大小i,所以1

in sizeof(~i); 你请求表达式类型的大小,它是一个 int,在你的案例 4


使用

if(~i)

要知道 i 是否不值 255(在你的情况下 uint8_t)不是很可读,就做

if (i != 255)

您将拥有一个可移植且可读的代码


There are multiple sizes of variables (eg., uint16_t and unsigned char etc.)

管理任意大小的unsigned :

if (i != (((uintmax_t) 2 << (sizeof(i)*CHAR_BIT-1)) - 1))

表达式是常量,因此在编译时计算。

#include 对于 CHAR_BIT#include 对于 uintmax_t

您看到的是整数提升的结果。在表达式中使用整数值的大多数情况下,如果值的类型小于 int,则该值将提升为 int。这记录在 C standard:

的第 6.3.1.1p2 节中

The following may be used in an expression wherever an intor unsigned int may be used

  • An object or expression with an integer type (other than intor unsigned int) whose integer conversion rank is less than or equal to the rank of int and unsigned int.
  • A bit-field of type _Bool, int ,signed int, orunsigned int`.

If an int can represent all values of the original type (as restricted by the width, for a bit-field), the value is converted to an int; otherwise, it is converted to an unsigned int. These are called the integer promotions. All other types are unchanged by the integer promotions.

因此,如果变量的类型为 uint8_t 且值为 255,则在其上使用除强制转换或赋值以外的任何运算符将首先将其转换为值为 255 的 int 类型,然后再执行手术。这就是为什么 sizeof(~i) 给你 4 而不是 1。

第 6.5.3.3 节描述了整数提升适用于 ~ 运算符:

The result of the ~ operator is the bitwise complement of its (promoted) operand (that is, each bit in the result is set if and only if the corresponding bit in the converted operand is not set). The integer promotions are performed on the operand, and the result has the promoted type. If the promoted type is an unsigned type, the expression ~E is equivalent to the maximum value representable in that type minus E.

所以假设一个 32 位 int,如果 counter 有 8 位值 0xff 它被转换为 32 位值 0x000000ff,并应用 ~ 给你 0xffffff00.

可能最简单的处理方法是在不知道类型的情况下检查值在递增后是否为 0,如果是则递减。

if (!++counter) counter--;

无符号整数的环绕在两个方向上起作用,因此递减值 0 会得到最大的正值。

6.5.3.3 Unary arithmetic operators
...
4 The result of the ~ operator is the bitwise complement of its (promoted) operand (that is, each bit in the result is set if and only if the corresponding bit in the converted operand is not set). The integer promotions are performed on the operand, and the result has the promoted type. If the promoted type is an unsigned type, the expression ~E is equivalent to the maximum value representable in that type minus E.

C 2011 Online Draft

问题是 ~ 的操作数在应用运算符之前被提升为 int

不幸的是,我认为没有简单的解决方法。写作

if ( counter + 1 ) counter++;

不会有帮助,因为那里也有促销活动。我唯一可以建议的是为您希望该对象表示的最大 创建一些符号常量并对其进行测试:

#define MAX_COUNTER 255
...
if ( counter < MAX_COUNTER-1 ) counter++;

在 stdint.h 之前,变量大小可能因编译器而异,C 中的实际变量类型仍然是 int、long 等,并且仍然由编译器作者定义它们的大小。不是一些标准或目标特定的假设。然后作者需要创建 stdint.h 来映射两个世界,这就是 stdint.h 将 uint_this 映射到 int、long、short 的目的。

如果您从另一个编译器移植代码并且它使用 char、short、int、long,那么您必须检查每种类型并自己进行移植,这是没有办法解决的。或者你最终得到了正确的变量大小,声明发生了变化,但编写的代码有效....

if(~counter) counter++;

或...直接提供掩码或类型转换

if((~counter)&0xFF) counter++;
if((uint_8)(~counter)) counter++;

归根结底,如果您希望此代码正常工作,则必须将其移植到新平台。您选择如何。是的,您必须花时间处理每个案例并正确处理,否则您将不断回到这段更昂贵的代码。

如果您在移植之前隔离代码中的变量类型以及变量类型的大小,那么隔离执行此操作的变量(应该很容易 grep)并使用 stdint.h 定义更改它们的声明希望将来不会改变,你会感到惊讶,但有时会使用错误的 headers 所以甚至检查一下,这样你晚上可以睡得更好

if(sizeof(uint_8)!=1) return(FAIL);

虽然这种编码风格有效 (if(~counter) counter++;),但为了现在和将来的可移植性需求,最好使用掩码来专门限制大小(而不是依赖于声明) ,在代码首先编写时执行此操作,或者只是完成端口,然后您就不必再 re-port 改天再做一次。或者为了使代码更具可读性,然后执行 if x<0xFF then or x!=0xFF 或类似的东西,然后编译器可以将其优化为与任何这些解决方案相同的代码,只是使其更具可读性和风险更低...

取决于产品的重要性或您想发送多少次 patches/updates 或开卡车或步行到实验室来解决您是否试图找到快速解决方案或只是触摸受影响的代码行。如果只有一百或几个,那就不是那么大的端口了。

如果 x 是一些无符号整数类型,这里有几个实现“向 x 添加 1 但限制在最大可表示值”的选项:

  1. 当且仅当 x 小于其类型中可表示的最大值时加一个:

    x += x < Maximum(x);
    

    Maximum的定义见下条。这个方法 很有可能被编译器优化为高效 指令,例如比较,某种形式的条件设置或移动, 和一个添加。

  2. 比较类型的最大值:

    if (x < ((uintmax_t) 2u << sizeof x * CHAR_BIT - 1) - 1) ++x
    

    (计算 2N,其中 Nx,通过将 2 移动 N−1 位。我们这样做而不是移动 1 N 位,因为移动了C标准没有定义bits in a type,CHAR_BIT宏可能有些人不熟悉,它是一个字节的位数,所以sizeof x * CHAR_BIT是type的位数x.)

    为了美观和清晰起见,可以根据需要将其包装在宏中:

    #define Maximum(x) (((uintmax_t) 2u << sizeof (x) * CHAR_BIT - 1) - 1)
    if (x < Maximum(x)) ++x;
    
  3. 递增 x 并在回零时更正,使用 if:

    if (!++x) --x; // !++x is true if ++x wraps to zero.
    
  4. 递增 x 并在回零时更正,使用表达式:

    ++x; x -= !x;
    

    这名义上是无分支的(有时对性能有益),但编译器可以像上面一样实现它,如果需要使用分支但如果目标体系结构有合适的指令则可能使用无条件指令。

  5. 使用上述宏的无分支选项是:

    x += 1 - x/Maximum(x);
    

    如果 x 是其类型的最大值,则计算结果为 x += 1-1。否则就是x += 1-0。然而,除法在许多架构上有些慢。编译器可以将其优化为不带除法的指令,具体取决于编译器和目标体系结构。