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:
- 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 ). - 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
. - Copie el valor de
%eax
a 4 bytes por encima de%rbp
, que es la ubicación de labar
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:
- Copie 32 bits de
*foo
(denotado por(%rax)
en ensamblaje) en el registro%eax
. - 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). - 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. - 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. - Copie el valor de
%eax
a 4 bytes por encima de%rbp
, que es la ubicación de labar
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
.