¿Cómo obtuve un valor mayor a 8 bits de tamaño de un entero de 8 bits?

118

Localicé un error extremadamente desagradable que se escondía detrás de esta pequeña joya. Soy consciente de que según la especificación de C ++, los desbordamientos firmados son un comportamiento indefinido, pero solo cuando el desbordamiento se produce cuando el valor se extiende al ancho de bits sizeof(int). Según tengo entendido, aumentar un charnunca debería ser un comportamiento indefinido siempre que sizeof(char) < sizeof(int). Pero eso no explica cómo cse obtiene un valor imposible . Como un entero de 8 bits, ¿cómo pueden ccontener valores mayores que su ancho de bits?

Código

// Compiled with gcc-4.7.2
#include <cstdio>
#include <stdint.h>
#include <climits>

int main()
{
   int8_t c = 0;
   printf("SCHAR_MIN: %i\n", SCHAR_MIN);
   printf("SCHAR_MAX: %i\n", SCHAR_MAX);

   for (int32_t i = 0; i <= 300; i++)
      printf("c: %i\n", c--);

   printf("c: %i\n", c);

   return 0;
}

Salida

SCHAR_MIN: -128
SCHAR_MAX: 127
c: 0
c: -1
c: -2
c: -3
...
c: -127
c: -128  // <= The next value should still be an 8-bit value.
c: -129  // <= What? That's more than 8 bits!
c: -130  // <= Uh...
c: -131
...
c: -297
c: -298  // <= Getting ridiculous now.
c: -299
c: -300
c: -45   // <= ..........

Compruébalo en ideone.

No firmado
fuente
61
"Soy consciente de que según la especificación de C ++, los desbordamientos firmados no están definidos". -- Correcto. Para ser precisos, no solo el valor no está definido, el comportamiento lo es. Parecer obtener resultados físicamente imposibles es una consecuencia válida.
@hvd Estoy seguro de que alguien tiene una explicación de cómo las implementaciones comunes de C ++ causan este comportamiento. Quizás tenga que ver con la alineación o ¿cómo printf()funciona la conversión?
rliu
Otros han abordado el problema principal. Mi comentario es más general y se relaciona con enfoques de diagnóstico. Creo que parte de la razón por la que encontraste este enigma es la creencia infalible de que era posible. Obviamente, no es imposible, así que acéptalo y mira de nuevo
Tim X
@TimX - Observé el comportamiento y obviamente llegué a la conclusión de que no era imposible en ese sentido. Mi uso de la palabra se refiere a un número entero de 8 bits que contiene un valor de 9 bits, lo cual es una imposibilidad por definición. El hecho de que esto haya ocurrido sugiere que no se trata como un valor de 8 bits. Como otros han abordado, esto se debe a un error del compilador. La única imposibilidad aparente aquí es un valor de 9 bits en un espacio de 8 bits, y esta aparente imposibilidad se explica porque el espacio es realmente "más grande" de lo que se informó.
firmar
Lo acabo de probar en mi máquina y el resultado es el que debería ser. c: -120 c: -121 c: -122 c: -123 c: -124 c: -125 c: -126 c: -127 c: -128 c: 127 c: 126 c: 125 c: 124 c: 123 c: 122 c: 121 c: 120 c: 119 c: 118 c: 117 Y mi entorno es: Ubuntu-12.10 gcc-4.7.2
VELVETDETH

Respuestas:

111

Este es un error del compilador.

Aunque obtener resultados imposibles para un comportamiento indefinido es una consecuencia válida, en realidad no hay un comportamiento indefinido en su código. Lo que sucede es que el compilador cree que el comportamiento no está definido y lo optimiza en consecuencia.

Si cse define como int8_t, y int8_tasciende a int, c--se supone que realiza la resta c - 1en intaritmética y convierte el resultado en int8_t. La resta en intno se desborda, y la conversión de valores integrales fuera de rango a otro tipo integral es válida. Si el tipo de destino está firmado, el resultado está definido por la implementación, pero debe ser un valor válido para el tipo de destino. (Y si el tipo de destino no está firmado, el resultado está bien definido, pero eso no se aplica aquí).


fuente
No lo describiría como un "error". Dado que el desbordamiento firmado provoca un comportamiento indefinido, el compilador está perfectamente autorizado para asumir que no sucederá y optimizar el ciclo para mantener los valores intermedios de cen un tipo más amplio. Presumiblemente, eso es lo que está sucediendo aquí.
Mike Seymour
4
@MikeSeymour: El único desbordamiento aquí está en la conversión (implícita). El desbordamiento en la conversión firmada no tiene un comportamiento indefinido; simplemente produce un resultado definido por la implementación (o genera una señal definida por la implementación, pero eso no parece estar sucediendo aquí). La diferencia de definición entre las operaciones aritméticas y las conversiones es extraña, pero así es como la define el estándar del lenguaje.
Keith Thompson
2
@KeithThompson Eso es algo que difiere entre C y C ++: C permite una señal definida por la implementación, C ++ no. C ++ simplemente dice "Si el tipo de destino está firmado, el valor no cambia si se puede representar en el tipo de destino (y el ancho del campo de bits); de lo contrario, el valor está definido por la implementación".
Da la casualidad de que no puedo reproducir el comportamiento extraño en g ++ 4.8.0.
Daniel Landau
2
@DanielLandau Ver comentario 38 en ese error: "Corregido para 4.8.0". :)
15

Un compilador puede tener errores distintos a las no conformidades con el estándar, porque existen otros requisitos. Un compilador debe ser compatible con otras versiones de sí mismo. También se puede esperar que sea compatible de alguna manera con otros compiladores, y también que se ajuste a algunas creencias sobre el comportamiento sostenidas por la mayoría de su base de usuarios.

En este caso, parece ser un error de conformidad. La expresión c--debe manipularse cde forma similar a c = c - 1. Aquí, el valor de ca la derecha se promueve a tipo int, y luego se lleva a cabo la resta. Como cestá en el rango de int8_t, esta resta no se desbordará, pero puede producir un valor que esté fuera del rango de int8_t. Cuando se asigna este valor, se vuelve a realizar una conversión al tipo int8_tpara que el resultado vuelva a encajar c. En el caso de fuera de rango, la conversión tiene un valor definido por la implementación. Pero un valor fuera del rango de int8_tno es un valor definido por la implementación válido. Una implementación no puede "definir" que un tipo de 8 bits contenga repentinamente 9 o más bits. Que el valor esté definido por la implementación significa que int8_tse produce algo en el rango de y el programa continúa. Por lo tanto, el estándar C permite comportamientos como la aritmética de saturación (común en los DSP) o el envolvente (arquitecturas convencionales).

El compilador está utilizando un tipo de máquina subyacente más amplio al manipular valores de tipos enteros pequeños como int8_to char. Cuando se realiza aritmética, los resultados que están fuera del rango del tipo de entero pequeño se pueden capturar de manera confiable en este tipo más amplio. Para preservar el comportamiento visible externamente de que la variable es un tipo de 8 bits, el resultado más amplio tiene que truncarse en el rango de 8 bits. Se requiere código explícito para hacer eso, ya que las ubicaciones de almacenamiento de la máquina (registros) son más anchas que 8 bits y están contentas con los valores más grandes. Aquí, el compilador se olvidó de normalizar el valor y simplemente se lo pasó tal cual printf. El especificador de conversión %ien printfno tiene idea de que el argumento proviene originalmente de int8_tcálculos; solo está trabajando con unint argumento.

Kaz
fuente
Esta es una explicación lúcida.
David Healy
El compilador produce un buen código con el optimizador desactivado. Por lo tanto, las explicaciones que utilizan "reglas" y "definiciones" no son aplicables. Es un error en el optimizador.
14

No puedo incluir esto en un comentario, así que lo publico como respuesta.

Por alguna extraña razón, --resulta que el operador es el culpable.

Probé el código publicado en Ideone y lo reemplacé c--con c = c - 1y los valores permanecieron dentro del rango [-128 ... 127]:

c: -123
c: -124
c: -125
c: -126
c: -127
c: -128 // about to overflow
c: 127  // woop
c: 126
c: 125
c: 124
c: 123
c: 122

Freaky ey? No sé mucho sobre lo que hace el compilador con expresiones como i++o i--. Es probable que promueva el valor de retorno de an inty lo pase. Esa es la única conclusión lógica a la que puedo llegar porque, de hecho, ESTÁS obteniendo valores que no pueden caber en 8 bits.

user123
fuente
4
Por las promociones integrales, c = c - 1medios c = (int8_t) ((int)c - 1. La conversión de un fuera de rango inten int8_ttiene un comportamiento definido pero un resultado definido por la implementación. En realidad, ¿no c--se supone que también debe realizar esas mismas conversiones?
12

Supongo que el hardware subyacente todavía usa un registro de 32 bits para mantener ese int8_t. Dado que la especificación no impone un comportamiento de desbordamiento, la implementación no comprueba el desbordamiento y también permite almacenar valores más grandes.


Si marca la variable local ya volatileque está forzando a usar memoria para ella y consecuentemente obtiene los valores esperados dentro del rango.

Zoltán
fuente
1
Oh wow. Olvidé que el ensamblado compilado almacenará las variables locales en los registros si puede. Esta parece ser la respuesta más probable junto con printfno preocuparse por los sizeofvalores de formato.
rliu
3
@roliu Ejecute g ++ -O2 -S code.cpp y verá el ensamblado. Además, printf () es una función de argumento variable, por lo que los argumentos cuyo rango sea menor que un int serán promovidos a un int.
nos
@nos me gustaría. No he podido instalar un cargador de arranque UEFI (rEFInd en particular) para que archlinux se ejecute en mi máquina, así que no he codificado con herramientas GNU en mucho tiempo. Lo haré ... eventualmente. Por ahora es solo C # en VS y tratando de recordar C / aprender algo de C ++ :)
rliu
@rollu Ejecutarlo en una máquina virtual, por ejemplo, VirtualBox
nos
@nos No quiero descarrilar el tema, pero sí, podría. También podría instalar linux con un cargador de arranque BIOS. Soy terco y si no puedo hacerlo funcionar con un cargador de arranque UEFI, probablemente no lo haré funcionar en absoluto: P.
rliu
11

El código ensamblador revela el problema:

:loop
mov esi, ebx
xor eax, eax
mov edi, OFFSET FLAT:.LC2   ;"c: %i\n"
sub ebx, 1
call    printf
cmp ebx, -301
jne loop

mov esi, -45
mov edi, OFFSET FLAT:.LC2   ;"c: %i\n"
xor eax, eax
call    printf

EBX debe agregarse con la disminución posterior de FF, o solo debe usarse BL con el resto de EBX limpio. Curioso que use sub en lugar de dec. El -45 es absolutamente misterioso. Es la inversión bit a bit de 300 y 255 = 44. -45 = ~ 44. Hay una conexión en alguna parte.

Pasa por mucho más trabajo usando c = c - 1:

mov eax, ebx
mov edi, OFFSET FLAT:.LC2   ;"c: %i\n"
add ebx, 1
not eax
movsx   ebp, al                 ;uses only the lower 8 bits
xor eax, eax
mov esi, ebp

Luego usa solo la porción baja de RAX, por lo que está restringido a -128 a 127. Opciones del compilador "-g -O2".

Sin optimización, produce el código correcto:

movzx   eax, BYTE PTR [rbp-1]
sub eax, 1
mov BYTE PTR [rbp-1], al
movsx   edx, BYTE PTR [rbp-1]
mov eax, OFFSET FLAT:.LC2   ;"c: %i\n"
mov esi, edx

Entonces es un error en el optimizador.


fuente
4

Use en %hhdlugar de %i! Debería resolver tu problema.

Lo que ve allí es el resultado de las optimizaciones del compilador combinadas con usted diciéndole a printf que imprima un número de 32 bits y luego presionando un número (supuestamente de 8 bits) en la pila, que en realidad tiene el tamaño de un puntero, porque así es como funciona el código de operación push en x86.

Zotta
fuente
1
Puedo reproducir el comportamiento original en mi sistema usando g++ -O3. Cambiar %ia %hhdno cambia nada.
Keith Thompson
3

Creo que esto se hace mediante la optimización del código:

for (int32_t i = 0; i <= 300; i++)
      printf("c: %i\n", c--);

El compilador usa la int32_t ivariable para iy c. Desactive la optimización o realice un envío directo printf("c: %i\n", (int8_t)c--);

Vsevolod
fuente
Luego, desactive la optimización. o haz algo como esto:(int8_t)(c & 0x0000ffff)--
Vsevolod
1

cse define en sí mismo como int8_t, pero cuando opera ++o --sobre int8_t, se convierte implícitamente primero en inty el resultado de la operación en su lugar, el valor interno de c se imprime con printf que resulta ser int.

Ver el valor real de cdespués del ciclo completo, especialmente después del último decremento

-301 + 256 = -45 (since it revolved entire 8 bit range once)

es el valor correcto que se asemeja al comportamiento -128 + 1 = 127

ccomienza a usar la intmemoria de tamaño, pero se imprime como int8_tcuando se imprime como él mismo usando solamente 8 bits. Utiliza todo 32 bitscuando se usa comoint

[Error del compilador]

Izhar Aazmi
fuente
0

Creo que sucedió porque su ciclo continuará hasta que el int i se convierta en 300 yc en -300. Y el último valor es porque

printf("c: %i\n", c);
r.mirzojonov
fuente
'c' es un valor de 8 bits, por lo que es imposible que tenga un número tan grande como -300.