【问题标题】:Conditional bit setting条件位设置
【发布时间】:2017-09-18 05:32:27
【问题描述】:

根据我的小型研究,以下两个函数是等效的(从性能的角度来看),因为即使是非常轻微的优化 (-O1) 也会产生相同的汇编代码。

代码 1:

#define BIT_N (10)

extern unsigned int isBitSet;

unsigned int Foo() {
    unsigned int res1 = 0;

    if (isBitSet)
    {
        res1 |= ( 1u << BIT_N );
    }

    return res1;
}

代码 2:

#define BIT_N (10)

extern unsigned int isBitSet;

unsigned int Foo() {
    unsigned int res1 = 0;

    res1 |= ( (!!isBitSet) << BIT_N );

    return res1;
}

Code 1Code 2 的反汇编是相同的(使用 gcc 6.3 for x86_64 + 优化 -O2 编译后):

Foo():
    mov     edx, DWORD PTR isBitSet[rip]
    xor     eax, eax
    test    edx, edx
    setne   al
    sal     eax, 10
    ret

我个人更喜欢 Code 2 的 C 版本,它看起来更干净。但它和代码 1 一样安全吗?因为我看到了一些潜在的陷阱,比如如果isBitSet 的类型为intBIT_N 31,那么代码res1 = ( (!!isBitSet) &lt;&lt; BIT_N); 将导致未定义的行为

问题:

还有其他陷阱吗? Code 1 真的比 Code 2 更安全吗? 如果是,是否有任何已知方法可以使 Code 2 更安全而无需太多开销?

【问题讨论】:

  • 如果res1 = 5 一开始会怎样?
  • “代码 1 和代码 2 的反汇编是相同的”。如果是,你的编译器坏了。代码 1 使用 |=,代码 2 使用 =
  • @JeremyP 但它不会改变结果。 +查看我对 tilz0R 的评论
  • @JeremyP 我修改了代码以具有相同的|=。但这真的不是重点。
  • 我发现if (isBitSet)(!!isBitSet) 清晰得多。你应该考虑让人们更容易阅读代码而不是编译器(无论如何它似乎都明白了)。

标签: c security optimization conditional


【解决方案1】:

因为我看到了一些潜在的陷阱,例如 isBitSet 的类型是否为 int 和 BIT_N 31

您已经经历了另一个陷阱,即您认为isBitSet 的类型很重要。 C 中的所有“逻辑”运算符,例如 !,都返回值为 1 或 0 的类型 int

因此,(!!isBitSet) &lt;&lt; BIT_N 表达式在设计上是危险的,不应使用,因为提到了未定义的行为。

不应该使用它的另一个原因是程序员可能错误地认为表达式的结果类型是isBitSet 的类型。因此,如果程序员会编写一些依赖于无符号环绕的东西,例如:

((!!isBitSet) << BIT_N) + UINT16_C(something)

那么这也会导致未定义的行为 = 32 位系统上的整数溢出,因为 + 的左操作数是有符号的,这不是预期的。

问题的根源是在一行中使用多个运算符。这几乎总是不好的做法,并且可能导致许多错误。我的经验也是,!! 技巧的存在通常是可疑代码的标志。

理想的、完全可移植的代码版本是:

uint32_t Foo (void) 
{ 
  uint32_t result;

  if(isBitSet)
  {
    result = 1u << BIT_N;
  }
  else
  {
    result = 0;
  }

  return result;
}

或者如果你愿意,完全等效:

uint32_t Foo (void) 
{ 
  return isBitSet ? 1u<<BIT_N : 0;
}

【讨论】:

    【解决方案2】:

    运算符!返回的类型是int,所以可以得到UB。 (isBitSet 类型的签名无关紧要)。

    从 N1570,6.5.7 移位运算符:

    1. E1 &lt;&lt; E2 的结果是 E1 左移 E2 位位置;空出的位被填充 零。如果 E1 有无符号类型,则结果的值为 E1 × 2E2,减少模 比结果类型中可表示的最大值大一。 如果 E1 有一个签名 类型和非负值,并且 E1 × 2E2 在结果类型中是可表示的,那么就是 结果值;否则,行为未定义。

    但是,修复是直截了当的;转换前转换为无符号类型:

    res1 |= (unsigned)(!!isBitSet) << BIT_N;
    

    2 的整机不应该有任何开销,所以演员阵容基本上是免费的。

    【讨论】:

    • isBitSet 的类型无关紧要。
    【解决方案3】:

    type of !!isBitSet is int(值为01)。随后的left-shift results in undefined behavior if the resulting value cannot be represented as an int。例如,假设 32 位 int 和二进制补码,如果 !!isBitSet == 1BIT_N &gt;= 31 会发生这种情况(因为 1 位移入或移出符号位)。所以,你提到的潜在陷阱确实是一个。

    除此之外,代码定义良好,并且两个函数具有相同的行为。因此,只要避免上述未定义行为的条件,第二个功能就等同于第一个。

    也就是说,第一个应该是首选,因为它的意图更明确(因此更易于阅读),并且没有未定义行为的可能性。

    【讨论】:

      猜你喜欢
      • 2020-12-13
      • 2011-04-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-09-22
      • 2014-12-07
      相关资源
      最近更新 更多