【问题标题】:Weird results for conditional operator with GCC and bool pointers带有 GCC 和布尔指针的条件运算符的奇怪结果
【发布时间】:2015-02-24 00:57:05
【问题描述】:

在下面的代码中,我将memset() 一个stdbool.h bool 变量值123。 (也许这是未定义的行为?)然后我将指向该变量的指针传递给受害函数,该受害函数尝试使用条件操作来防止意外值。但是,出于某种原因,GCC 似乎完全删除了条件操作。

#include <stdio.h>
#include <stdbool.h>
#include <string.h>

void victim(bool* foo)
{
    int bar = *foo ? 1 : 0;
    printf("%d\n", bar);
}

int main()
{
    bool x;
    bool *foo = &x;
    memset(foo, 123, sizeof(bool));
    victim(foo);
    return 0;
}
user@host:~$ gcc -Wall -O0 test.c 用户@主机:~$ ./a.out 123

让这特别烦人的是victim()函数实际上是在一个库中,如果值大于1就会崩溃。

在 GCC 版本 4.8.2-19ubuntu1 和 4.7.2-5 上转载。未在 clang 上复制。

【问题讨论】:

  • 来自 C99 标准:6.5.2 2 声明为 _Bool 类型的对象大到足以存储值 0 和 1。
  • 通过将x 定义为bool,您已经向编译器承诺您只会在其中存储01。通过将123 存储在x1 中,您对编译器撒了谎。 “如果你对编译器撒谎,它就会报仇雪恨。” ——亨利·斯宾塞
  • @SeverinPappadeux:是的,但由于任何非位域对象必须至少为CHAR_BIT 位(和CHAR_BIT &gt;= 8),它也足够大以容纳123 的值。您不会因为 bool 的大小而将其存储在 bool 对象中,但这是未定义的行为。
  • 由于包含了&lt;stdbool.h&gt; 标头,您可以考虑仅使用truefalse 值(就像在Pascal 中使用Boolean 类型一样)。它会使您的代码更具可读性,并使您远离其他值。
  • @MSalters:一些位模式可以是陷阱表示;访问具有这种表示形式的对象(通过适当类型的左值)会导致未定义的行为。 _Bool 的规则以我目前懒得探索的方式使事情变得混乱。底线:在 _Bool 对象中存储除 01 之外的值是应避免的事情。

标签: c gcc undefined-behavior conditional-operator


【解决方案1】:

(也许这是未定义的行为?)

不是直接,而是从对象中读取。

引用 C99:

6.2.6 类型的表示

6.2.6.1 常规

5 某些对象表示不需要表示对象类型的值。如果存储 一个对象的值具有这样的表示,并由一个左值表达式读取 没有字符类型,行为未定义。 [...]

基本上,这意味着如果特定实现已确定 bool 的唯一两个有效字节是 01,那么您最好确保您不使用任何诡计尝试将其设置为任何其他值。

【讨论】:

  • Gcc doc 读取 GCC 仅支持二进制补码整数类型,并且所有位模式都是普通值。 这意味着,_Bool 也没有陷阱表示。不确定,这是否是文档中的草率,或者标准中是否有其他内容允许这种优化。
  • @mafso 我认为这是措辞草率。无符号整数类型(包括_Bool)永远不能使用二进制补码,因为它们根本没有任何符号位。
  • 是的,二进制补码不适用,但这不会改变“所有位模式都是普通值”并且_Bool 必须至少有 8 位。换一种说法:严格来说,Gcc 是否有必要将_Bool 记录为具有CHAR_BIT - 1 填充位以使问题中的优化成为可能?我不确定标准中是否有另一部分强制执行此操作。 [...]
  • [...] _Bool 必须能够表示 0 和 1 并不自动意味着它是 UB 在其中存储不同的东西。 char 必须能够保存基本执行字符集中的每个字符并不意味着您不能在其中存储任何其他内容(例如,127 是完全合法的,无论这是否在基本执行字符集中) .
  • @mafso 标准中没有要求实现记录任何类型是否具有填充位,除非通过将 sizeof(T) * CHAR_BITT_MAX 中定义的 T_MAX 的值进行比较间接地进行比较&lt;limits.h&gt; .如果一个类型有填充位,则标准中没有要求实现记录这些填充位是否有效(填充位的错误值是否会产生陷阱表示)。
【解决方案2】:

GCC编译这个程序时,汇编语言输出包括序列

movzbl (%rax), %eax
movzbl %al, %eax
movl %eax, -4(%rbp)

执行以下操作:

  1. *foo中的32位(在汇编中用(%rax)表示)复制到寄存器%eax,并用零填充%eax的高位(不是说有,因为%eax是一个 32 位寄存器)。
  2. %eax的低8位(用%al表示)复制到%eax,并用零填充%eax的高位。作为 C 程序员,您可以将其理解为 %eax &amp;= 0xff
  3. %eax的值复制到%rbp上方4个字节,也就是bar在栈上的位置。

所以这段代码是汇编语言的翻译

int bar = *foo & 0xff;

很明显,GCC 已经优化了这一行,因为bool 不应该包含除 0 或 1 以外的任何值。

如果你把C源代码中的相关行改成这个

int bar = *((int*)foo) ? 1 : 0;

然后程序集更改为

movl (%rax), %eax
testl %eax, %eax
setne %al
movzbl %al, %eax
movl %eax, -4(%rbp)

执行以下操作:

  1. *foo(在汇编中用(%rax)表示)复制32位到寄存器%eax
  2. 针对自身测试%eax 的 32 位,这意味着将其与自身进行“与”并根据结果在处理器中设置一些标志。 (这里不需要ANDing,但没有指令可以简单地检查寄存器并设置标志。)
  3. 如果 ANDing 的结果为 0,则将 %eax(用 %al 表示)的低 8 位设置为 1,否则设置为 0。
  4. %eax的低8位(用%al表示)复制到%eax,并用零填充%eax的高位,与第一个sn-p一样。李>
  5. %eax的值复制到%rbp上方4个字节,也就是bar在栈上的位置;也和第一个 sn-p 一样。

这实际上是对 C 代码的忠实翻译。事实上,如果你将演员表添加到(int*) 并编译并运行程序,你会看到它确实输出了1

【讨论】:

  • 这是迄今为止最好的答案。我猜你因为“迟到”而获得的选票较少。
  • 这告诉“GCC 实际做了什么”,这是对 hvd 的回答“为什么允许 GCC 这样做”的一个很好的补充。我碰巧对后者更感兴趣,这就是为什么我接受了那个。
  • @jpa 是的,我认为这将是对其他答案的一个很好的补充。虽然我会注意到你的问题实际上并没有任何东西(它只是说“看看这个奇怪的行为”),这让你很难说出你想要哪种答案。跨度>
【解决方案3】:

bool 中存储不同于01 的值是C 中未定义的行为。

其实是这样的:

int bar = *foo ? 1 : 0;

用接近这个的东西进行了优化:

int bar = *foo ? *foo : 0;

【讨论】:

  • 您可能会更进一步说,由于在这种情况下x ? x : 0 是一个身份,因此它会进一步优化为x,从而得到结果。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-11-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-01-29
  • 2014-02-16
  • 2023-01-16
相关资源
最近更新 更多