【问题标题】:Overflow in bit fields位域溢出
【发布时间】:2011-06-21 22:52:18
【问题描述】:

我可以相信 C 编译器每次访问位字段时都会对 2^n 进行模运算吗? 或者是否有任何编译器/优化使得像下面这样的代码不会打印出溢出?

struct {
  uint8_t foo:2;
} G;

G.foo = 3;
G.foo++;

if(G.foo == 0) {
  printf("Overflow\n");
}

在此先感谢,弗洛里安

【问题讨论】:

  • 什么意思?您明确要求它将数据存储在两位中。此外,3 + 1 == 0 mod 4
  • 如果您在该结构中声明了其他位域,我会更关心该位会发生什么。例如uint8_t 酒吧:2; uint8_t foo:2; uint8_t 便便:2;它会影响酒吧或便便吗?
  • Charles,它只使用2位存储,所以只能存储0、1、2或3。

标签: c overflow bit-fields


【解决方案1】:

是的。我们可以从大会得到答案。 这是我在 Ubuntu 16.04、64bit、gcc 中编写的示例。

#include <stdio.h>

typedef unsigned int uint32_t;

struct {
  uint32_t foo1:8;
  uint32_t foo2:24;
} G;

int main() {
    G.foo1 = 0x12;
    G.foo2 = 0xffffff; // G is 0xfffff12
    printf("G.foo1=0x%02x, G.foo2=0x%06x, G=0x%08x\n", G.foo1, G.foo2, *(uint32_t *)&G);
    G.foo2++; // G.foo2 overflow
    printf("G.foo1=0x%02x, G.foo2=0x%06x, G=0x%08x\n", G.foo1, G.foo2, *(uint32_t *)&G);
    G.foo1 += (0xff-0x12+1); // // G.foo1 overflow
    printf("G.foo1=0x%02x, G.foo2=0x%06x, G=0x%08x\n", G.foo1, G.foo2, *(uint32_t *)&G);
    return 0;
}

gcc -S &lt;.c file&gt; 编译它。你可以得到汇编文件.s。这里我展示了G.foo2++;的汇编,我写了一些cmets。

movl    G(%rip), %eax
shrl    $8, %eax    #  0xfffff12-->0x00ffffff
addl    $1, %eax    # 0x00ffffff+1=0x01000000
andl    $16777215, %eax # 16777215=0xffffff, so eax still 0x01000000
sall    $8, %eax    # 0x01000000-->0x00000000
movl    %eax, %edx  # edx high-24bit is fool2
movl    G(%rip), %eax   # G.foo2, tmp123
movzbl  %al, %eax   # so eax=0x00000012
orl     %edx, %eax  # eax=0x00000012 | 0x00000000 = 0x00000012
movl    %eax, G(%rip)   # write to G

我们可以看到编译器会使用移位指令来保证你说的话。(注意:这里的G的内存布局是:

----------------------------------
|     foo2-24bit     | foo1-8bit |
----------------------------------

当然,前面的结果是:

G.foo1=0x12, G.foo2=0xffffff, G=0xffffff12
G.foo1=0x12, G.foo2=0x000000, G=0x00000012
G.foo1=0x00, G.foo2=0x000000, G=0x00000000

【讨论】:

    【解决方案2】:

    是的,你可以相信 C 编译器在这里做正确的事情,只要位字段声明为无符号类型,就像 uint8_t 一样。来自 C99 标准 §6.2.6.1/3:

    存储在无符号位域和无符号字符类型对象中的值应使用纯二进制表示法表示。40)

    来自 §6.7.2.1/9:

    位域被解释为由指定位数组成的有符号或无符号整数类型。104) 如果值 0 或 1 存储到非零宽度位域中键入_Bool,位域的值应与存储的值比较。

    并且来自 §6.2.5/9(强调我的):

    有符号整数类型的非负值范围是对应的无符号整数类型的子范围,同一值在每种类型中的表示是相同的。31)A涉及无符号操作数的计算永远不会溢出,因为无法由结果无符号整数类型表示的结果会以比结果类型可以表示的最大值大一的数字为模减少。

    所以是的,您可以确定任何符合标准的编译器都会将G.foo 溢出到 0,而不会产生任何其他不需要的副作用。

    【讨论】:

    • 正如 R.. 在下面的评论中向我指出的那样,计算实际上是有符号计算。这不会改变这里的答案,但它会在 int 为 32 位的架构上使用 31 位无符号位域(其中可能存在未定义的有符号溢出)。
    【解决方案3】:

    简短回答:是的,您可以相信模 2^n 会发生。

    在您的程序中, G.foo++; 实际上等价于G.foo = (unsigned int)G.foo + 1

    Unsigned int 算术总是产生 2^(size of unsigned int in bits) 结果。然后将最小权重的两位存储在G.foo 中,产生零。

    【讨论】:

    • 你的等价是错误的。 uint8_t 将被提升为 int,而不是 unsigned intG.foo++; 等价于 G.foo = ((int)G.foo + 1) % 4;
    • @R.. 你是对的(我总是把这些促销弄错了),但是上面亚当的回答中缺少一些东西,那就是 +1 实际上发生在签名的整数之间。
    【解决方案4】:

    没有。编译器为该字段分配 2 位,递增 3 得到 100b,放在两位中得到 0。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2012-02-21
      • 1970-01-01
      • 1970-01-01
      • 2014-07-19
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多