c gcc undefined-behavior conditional-operator

Resultados extraños para el operador condicional con GCC y punteros bool.



undefined-behavior conditional-operator (3)

(Quizás este es un comportamiento indefinido?)

No directamente, pero la lectura del objeto después es.

Cotizando C99:

6.2.6 Representaciones de tipos.

6.2.6.1 General

5 Ciertas representaciones de objetos no necesitan representar un valor del tipo de objeto. Si el valor almacenado de un objeto tiene una representación de este tipo y es leído por una expresión lvalue que no tiene tipo de carácter, el comportamiento no está definido. [...]

Básicamente, lo que esto significa es que si una implementación en particular ha decidido que los dos únicos bytes válidos para un bool son 0 y 1 , entonces es mejor que se asegure de no usar ningún truco para intentar establecerlo en otro valor. .

En el siguiente código, memset() una variable stdbool.h bool para valorar 123 . (¿Quizás este es un comportamiento indefinido?) Luego paso un puntero a esta variable a una función víctima, que intenta protegerse contra valores inesperados mediante una operación condicional. Sin embargo, GCC por alguna razón parece eliminar la operación condicional por completo.

#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 user@host:~$ ./a.out 123

Lo que hace que esto sea particularmente molesto es que la función victim() está realmente dentro de una biblioteca y se bloqueará si el valor es más de 1.

Reproducido en GCC versiones 4.8.2-19ubuntu1 y 4.7.2-5. No reproducido en clang.


Almacenar un valor diferente de 0 o 1 en un bool es un comportamiento indefinido en C.

Así que en realidad esto:

int bar = *foo ? 1 : 0;

Se optimiza con algo parecido a esto:

int bar = *foo ? *foo : 0;


Cuando GCC compila este programa, la salida del lenguaje ensamblador incluye la secuencia

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

que hace lo siguiente:

  1. Copie 32 bits de *foo (denotado por (%rax) en ensamblaje) al registro %eax y complete los bits de orden superior de %eax con ceros (no hay ninguno, porque %eax es un registro de 32 bits ).
  2. Copie los 8 bits de orden inferior de %eax (denotado por %al ) en %eax y complete los bits de orden superior de %eax con ceros. Como programador de C, entenderías esto como %eax &= 0xff .
  3. Copie el valor de %eax a 4 bytes por encima de %rbp , que es la ubicación de la bar en la pila.

Así que este código es una traducción en lenguaje ensamblador de

int bar = *foo & 0xff;

Claramente, GCC ha optimizado la línea basándose en el hecho de que un bool nunca debe tener un valor que no sea 0 o 1.

Si cambia la línea correspondiente en la fuente C a este

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

entonces el montaje cambia a

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

que hace lo siguiente:

  1. Copie 32 bits de *foo (denotado por (%rax) en ensamblaje) en el registro %eax .
  2. Pruebe 32 bits de %eax contra sí mismo, lo que significa llevarlo consigo mismo y establecer algunas marcas en el procesador según el resultado. (El ANDing no es necesario aquí, pero no hay instrucciones para simplemente verificar un registro y establecer indicadores).
  3. Establezca los 8 bits de orden inferior de %eax (denotado por %al ) en 1 si el resultado de la AND es 0, o en 0 en caso contrario.
  4. Copie los 8 bits de orden inferior de %eax (denotado por %al ) en %eax y complete los bits de orden superior de %eax con ceros, como en el primer fragmento.
  5. Copie el valor de %eax a 4 bytes por encima de %rbp , que es la ubicación de la bar en la pila; También como en el primer fragmento.

Esta es en realidad una traducción fiel del código C Y, de hecho, si agrega la conversión a (int*) y compila y ejecuta el programa, verá que hace el resultado 1 .