Detectando desbordamiento firmado en C / C ++


82

A primera vista, esta pregunta puede parecer un duplicado de ¿Cómo detectar el desbordamiento de enteros? , sin embargo, en realidad es significativamente diferente.

Descubrí que si bien detectar un desbordamiento de enteros sin firmar es bastante trivial, detectar un desbordamiento con signo en C / C ++ es en realidad más difícil de lo que la mayoría de la gente piensa.

La forma más obvia, pero ingenua, de hacerlo sería algo como:

int add(int lhs, int rhs)
{
 int sum = lhs + rhs;
 if ((lhs >= 0 && sum < rhs) || (lhs < 0 && sum > rhs)) {
  /* an overflow has occurred */
  abort();
 }
 return sum; 
}

El problema con esto es que de acuerdo con el estándar C, el desbordamiento de enteros con signo es un comportamiento indefinido. En otras palabras, de acuerdo con el estándar, tan pronto como provoque un desbordamiento firmado, su programa es tan inválido como si hubiera desreferenciado un puntero nulo. Por lo tanto, no puede causar un comportamiento indefinido y luego intentar detectar el desbordamiento después del hecho, como en el ejemplo de verificación posterior a la condición anterior.

Aunque es probable que la comprobación anterior funcione en muchos compiladores, no puede contar con ella. De hecho, debido a que el estándar C dice que el desbordamiento de enteros con signo no está definido, algunos compiladores (como GCC) optimizarán la verificación anterior cuando se establezcan los indicadores de optimización, porque el compilador asume que un desbordamiento firmado es imposible. Esto rompe totalmente el intento de verificar el desbordamiento.

Entonces, otra forma posible de verificar el desbordamiento sería:

int add(int lhs, int rhs)
{
 if (lhs >= 0 && rhs >= 0) {
  if (INT_MAX - lhs <= rhs) {
   /* overflow has occurred */
   abort();
  }
 }
 else if (lhs < 0 && rhs < 0) {
  if (lhs <= INT_MIN - rhs) {
   /* overflow has occurred */
   abort();
  }
 }

 return lhs + rhs;
}

Esto parece más prometedor, ya que en realidad no sumamos los dos enteros juntos hasta que nos aseguremos de antemano de que realizar dicha adición no resultará en un desbordamiento. Por lo tanto, no causamos ningún comportamiento indefinido.

Sin embargo, esta solución es, desafortunadamente, mucho menos eficiente que la solución inicial, ya que debe realizar una operación de resta solo para probar si su operación de suma funcionará. E incluso si no le importa este (pequeño) impacto en el rendimiento, todavía no estoy del todo convencido de que esta solución sea adecuada. La expresión lhs <= INT_MIN - rhsparece exactamente el tipo de expresión que el compilador podría optimizar, pensando que el desbordamiento firmado es imposible.

Entonces, ¿hay una mejor solución aquí? ¿Algo que está garantizado para 1) no causar un comportamiento indefinido y 2) no proporcionar al compilador la oportunidad de optimizar las comprobaciones de desbordamiento? Estaba pensando que podría haber alguna manera de hacerlo convirtiendo ambos operandos a unsigned y realizando comprobaciones rodando su propia aritmética en complemento a dos, pero no estoy realmente seguro de cómo hacerlo.


1
En lugar de intentar detectar, ¿no es mejor escribir código que no tenga posibilidad de desbordamiento?
Arun

9
@ArunSaha: Es realmente difícil hacer cálculos y asegurarse de que no se desborden, y es imposible de probar en el caso general. La práctica habitual es utilizar un tipo de número entero lo más amplio posible y esperanza.
David Thornley

6
@Amardeep: Desreferenciar un puntero nulo es igualmente indefinido como desbordamiento firmado. El comportamiento indefinido significa que, en lo que respecta al Estándar, cualquier cosa puede suceder. No se puede asumir que el sistema no estará en un estado inválido e inestable después del desbordamiento firmado. El OP señaló una consecuencia de esto: es perfectamente legal que el optimizador elimine el código que detecta el desbordamiento firmado una vez que sucede.
David Thornley

16
@Amardeep: mencioné tal implementación. GCC eliminará el código de verificación de desbordamiento cuando se establezcan los indicadores de optimización. Así que básicamente romperá tu programa. Podría decirse que esto es peor que una desreferenciación de puntero nulo, ya que puede resultar en fallas de seguridad sutiles, mientras que la desreferenciación de un nulo probablemente golpeará sin rodeos su programa con una falla de segmentación.
Channel72

2
@Amardeep: Ciertamente he parecido implementaciones en las que, dependiendo de la configuración del compilador, el desbordamiento causaría una trampa. Sería bueno si los lenguajes permitieran especificar si determinadas variables o cantidades sin firmar deberían (1) ajustarse limpiamente, (2) fallar o (3) hacer lo que sea conveniente. Tenga en cuenta que si una variable es más pequeña que el tamaño del registro de una máquina, requerir que las cantidades sin firmar se envuelvan limpiamente puede evitar la generación de un código óptimo.
supercat

Respuestas:


26

Su enfoque con la resta es correcto y está bien definido. Un compilador no puede optimizarlo.

Otro enfoque correcto, si tiene un tipo de entero más grande disponible, es realizar la aritmética en el tipo más grande y luego verificar que el resultado se ajuste al tipo más pequeño al volver a convertirlo

int sum(int a, int b)
{
    long long c;
    assert(LLONG_MAX>INT_MAX);
    c = (long long)a + b;
    if (c < INT_MIN || c > INT_MAX) abort();
    return c;
}

Un buen compilador debería convertir toda la adición y la ifdeclaración en una intsuma de tamaño y un salto condicional único y nunca realizar la adición más grande.

Editar: como señaló Stephen, tengo problemas para obtener un compilador (no tan bueno), gcc, para generar el cmo. El código que genera no es terriblemente lento, pero ciertamente subóptimo. Si alguien conoce variantes de este código que harán que gcc haga lo correcto, me encantaría verlas.


1
Para cualquiera que quiera usar esto, asegúrese de estar viendo mi versión editada. En el original omití estúpidamente el elenco long longantes de la adición.
R .. GitHub DEJA DE AYUDAR A ICE

2
Por curiosidad, ¿ha conseguido un compilador para realizar esta optimización? Una prueba rápida contra algunos compiladores no encontró ninguno que pudiera hacerlo.
Stephen Canon

2
En x86_64, no hay nada ineficiente en el uso de enteros de 32 bits. El rendimiento es idéntico al de 64 bits. Una motivación para usar tipos de tamaño de palabra más pequeños que los nativos es que es extremadamente eficiente manejar condiciones de desbordamiento o acarreo (para aritmética de precisión arbitraria) ya que el desbordamiento / acarreo ocurre en una ubicación directamente accesible.
R .. GitHub DEJA DE AYUDAR A ICE

2
@R., @Steven: no, el código de resta que dio el OP no es correcto, vea mi respuesta. También doy un código allí que solo lo hace con dos comparaciones como máximo. Quizás a los compiladores les vaya mejor con eso.
Jens Gustedt

3
Este enfoque no funciona en la plataforma poco común donde sizeof(long long) == sizeof(int). C especifica solo eso sizeof(long long) >= sizeof(int).
chux - Reincorporar a Monica

36

No, su segundo código no es correcto, pero está cerca: si establece

int half = INT_MAX/2;
int half1 = half + 1;

el resultado de una adición es INT_MAX. ( INT_MAXes siempre un número impar). Entonces esta es una entrada válida. Pero en tu rutina tendrás INT_MAX - half == half1y abortarías. Un falso positivo.

Este error se puede reparar poniendo en <lugar de <=ambos cheques.

Pero también su código no es óptimo. Lo siguiente sería suficiente:

int add(int lhs, int rhs)
{
 if (lhs >= 0) {
  if (INT_MAX - lhs < rhs) {
   /* would overflow */
   abort();
  }
 }
 else {
  if (rhs < INT_MIN - lhs) {
   /* would overflow */
   abort();
  }
 }
 return lhs + rhs;
}

Para ver que esto es válido, debes sumar simbólicamente lhsen ambos lados de las desigualdades, y esto te da exactamente las condiciones aritméticas de que tu resultado está fuera de los límites.


+1 para obtener la mejor respuesta. Menor: sugiera /* overflow will occurred */enfatizar que el objetivo es detectar que se habría producido un desbordamiento si el código lo hubiera hecho lhs + rhssin hacer realmente la suma.
chux - Reincorporar a Monica

16

En mi humilde opinión, la forma más oriental de lidiar con el código C ++ sensible al desbordamiento es usar SafeInt<T>. Esta es una plantilla C ++ multiplataforma alojada en code plex que proporciona las garantías de seguridad que desea aquí.

Lo encuentro muy intuitivo de usar, ya que proporciona muchos de los mismos patrones de uso que las operaciones numéricas normales y expresa flujos por encima y por debajo a través de excepciones.


14

Para el caso de gcc, de las notas de la versión de gcc 5.0 podemos ver que ahora proporciona un __builtin_add_overflowdesbordamiento para verificar además:

Se ha agregado un nuevo conjunto de funciones integradas para aritmética con verificación de desbordamiento: __builtin_add_overflow, __builtin_sub_overflow y __builtin_mul_overflow y para compatibilidad con clang también otras variantes. Estas incorporaciones tienen dos argumentos integrales (que no necesitan tener el mismo tipo), los argumentos se extienden al tipo con signo de precisión infinita, +, - o * se realiza en ellos, y el resultado se almacena en una variable entera apuntada a por el último argumento. Si el valor almacenado es igual al resultado de precisión infinita, las funciones integradas devuelven falso, de lo contrario verdadero. El tipo de variable entera que contendrá el resultado puede ser diferente de los tipos de los dos primeros argumentos.

Por ejemplo:

__builtin_add_overflow( rhs, lhs, &result )

Podemos ver en el documento gcc Funciones incorporadas para realizar aritmética con desbordamiento Verificando que:

[...] estas funciones integradas tienen un comportamiento completamente definido para todos los valores de los argumentos.

clang también proporciona un conjunto de componentes aritméticos comprobados :

Clang proporciona un conjunto de incorporaciones que implementan aritmética comprobada para aplicaciones críticas de seguridad de una manera rápida y fácilmente expresable en C.

en este caso, el incorporado sería:

__builtin_sadd_overflow( rhs, lhs, &result )

Esta función parece ser muy útil excepto por una cosa: int result; __builtin_add_overflow(INT_MAX, 1, &result);no dice explícitamente lo que está almacenado en resultel desbordamiento y, desafortunadamente , no dice nada al especificar que no ocurre un comportamiento indefinido . Ciertamente esa era la intención, no UB. Mejor si especificaba eso.
chux - Reincorporar a Monica

1
@chux buen punto, dice aquí que el resultado siempre está definido, actualicé mi respuesta. Sería bastante irónico si ese no fuera el caso.
Shafik Yaghmour

Interesante su nueva referencia no tiene un (unsigned) long long *resultpara __builtin_(s/u)addll_overflow. Ciertamente, estos son un error. Hace que uno se pregunte sobre la veracidad de otros aspectos. IAC, es bueno ver estos __builtin_add/sub/mull_overflow(). Espero que lleguen a la especificación C algún día.
chux - Restablecer a Monica

1
+1 esto genera un ensamblaje mucho mejor que cualquier cosa que pueda obtener en C estándar, al menos no sin confiar en el optimizador de su compilador para descubrir lo que está haciendo. Uno debe detectar cuándo están disponibles tales incorporaciones y solo usar una solución estándar cuando el compilador no proporciona una.
Alex Reinking

11

Si usa un ensamblador en línea, puede verificar el indicador de desbordamiento . Otra posibilidad es que puede utilizar un tipo de datos seguro. . Recomiendo leer este documento sobre Integer Security .


6
+1 Esta es otra forma de decir "Si C no lo define, entonces se ve obligado a adoptar un comportamiento específico de la plataforma". Tantas cosas que se solucionan fácilmente en el ensamblaje no están definidas en C, lo que crea montañas a partir de un grano de arena en nombre de la portabilidad.
Mike DeSimone

5
Di un voto negativo por una respuesta ASM a una pregunta C. Como he dicho, hay formas portátiles correctas de escribir el cheque en C que generarán exactamente el mismo asm que escribiría a mano. Naturalmente, si los usa, el impacto en el rendimiento será el mismo, y tendrá un impacto mucho menor que el material de seguridad de C ++ que también recomendó.
R .. GitHub DEJA DE AYUDAR A ICE

1
@Matthieu: Si está escribiendo código que solo se usará en una implementación, y esa implementación garantiza que algo funcionará, y necesita un buen rendimiento de enteros, ciertamente puede usar trucos específicos de implementación. Sin embargo, eso no es lo que pedía el OP.
David Thornley

3
C distingue el comportamiento definido por la implementación y el comportamiento no definido por buenas razones, e incluso si algo con UB "funciona" en la versión actual de su implementación, eso no significa que continuará funcionando en versiones futuras. Considere gcc y el comportamiento de desbordamiento firmado ...
R .. GitHub DETENGA AYUDAR A ICE

2
Dado que basé mi -1 en una afirmación de que podríamos obtener código C para generar el conjunto idéntico, supongo que es justo retractarlo cuando todos los compiladores principales resultan ser basura en este sentido ..
R .. GitHub STOP HELPING ICE

6

La forma más rápida posible es utilizar el GCC incorporado:

int add(int lhs, int rhs) {
    int sum;
    if (__builtin_add_overflow(lhs, rhs, &sum))
        abort();
    return sum;
}

En x86, GCC compila esto en:

    mov %edi, %eax
    add %esi, %eax
    jo call_abort 
    ret
call_abort:
    call abort

que utiliza la detección de desbordamiento incorporada del procesador.

Si no está de acuerdo con el uso de las funciones integradas de GCC, la siguiente forma más rápida es usar operaciones de bits en los bits de signo. Además, el desbordamiento firmado se produce cuando:

  • los dos operandos tienen el mismo signo, y
  • el resultado tiene un signo diferente al de los operandos.

El bit de signo de ~(lhs ^ rhs)está activado si los operandos tienen el mismo signo y el bit de signo de lhs ^ sumestá activado si el resultado tiene un signo diferente al de los operandos. Entonces puede hacer la adición en forma sin firmar para evitar un comportamiento indefinido, y luego usar el bit de signo de ~(lhs ^ rhs) & (lhs ^ sum):

int add(int lhs, int rhs) {
    unsigned sum = (unsigned) lhs + (unsigned) rhs;
    if ((~(lhs ^ rhs) & (lhs ^ sum)) & 0x80000000)
        abort();
    return (int) sum;
}

Esto se compila en:

    lea (%rsi,%rdi), %eax
    xor %edi, %esi
    not %esi
    xor %eax, %edi
    test %edi, %esi
    js call_abort
    ret
call_abort:
    call abort

que es bastante más rápido que la conversión a un tipo de 64 bits en una máquina de 32 bits (con gcc):

    push %ebx
    mov 12(%esp), %ecx
    mov 8(%esp), %eax
    mov %ecx, %ebx
    sar $31, %ebx
    clt
    add %ecx, %eax
    adc %ebx, %edx
    mov %eax, %ecx
    add $-2147483648, %ecx
    mov %edx, %ebx
    adc $0, %ebx
    cmp $0, %ebx
    ja call_abort
    pop %ebx
    ret
call_abort:
    call abort

1

Es posible que tenga más suerte al convertir a enteros de 64 bits y probar condiciones similares como esa. Por ejemplo:

#include <stdint.h>

...

int64_t sum = (int64_t)lhs + (int64_t)rhs;
if (sum < INT_MIN || sum > INT_MAX) {
    // Overflow occurred!
}
else {
    return sum;
}

Es posible que desee ver más de cerca cómo funcionará la extensión de letreros aquí, pero creo que es correcto.


Elimina el bit a bit-and y la conversión de la declaración de retorno. Son incorrectos como están escritos. La conversión de tipos enteros con signo más grandes a tipos más pequeños está perfectamente bien definida siempre que el valor se ajuste al tipo más pequeño y no necesite una conversión explícita. Cualquier compilador que dé una advertencia y sugiera que agregue una conversión cuando se acaba de verificar que el valor no se desborde es un compilador roto.
R .. GitHub DEJA DE AYUDAR A ICE

@R Tienes razón, solo me gusta ser explícito sobre mis elencos. Sin embargo, lo cambiaré por corrección. Para futuros lectores, la línea de retorno decía return (int32_t)(sum & 0xffffffff);.
Jonathan

2
Tenga en cuenta que si escribe sum & 0xffffffff, sumse convierte implícitamente a tipo unsigned int(asumiendo 32 bits int) porque 0xfffffffftiene tipo unsigned int. Entonces el resultado de bit a bit y es un unsigned int, y si sumfue negativo, estará fuera del rango de valores admitidos por int32_t. La conversión a int32_ttiene un comportamiento definido por la implementación.
R .. GitHub DEJA DE AYUDAR A ICE

Tenga en cuenta que esto no funcionará en entornos ILP64 donde intlos correos electrónicos son de 64 bits.
rtx13

1

Qué tal si:

int sum(int n1, int n2)
{
  int result;
  if (n1 >= 0)
  {
    result = (n1 - INT_MAX)+n2; /* Can't overflow */
    if (result > 0) return INT_MAX; else return (result + INT_MAX);
  }
  else
  {
    result = (n1 - INT_MIN)+n2; /* Can't overflow */
    if (0 > result) return INT_MIN; else return (result + INT_MIN);
  }
}

Creo que debería funcionar para cualquier legítimo INT_MINy INT_MAX(simétrico o no); la función como se muestra en los clips, pero debería ser obvio cómo obtener otros comportamientos).


+1 para un buen enfoque alternativo que quizás sea más intuitivo.
R .. GitHub DEJA DE AYUDAR A ICE

1
Creo que esto - result = (n1 - INT_MAX)+n2;- podría desbordarse, si n1 fuera pequeño (digamos 0) y n2 fuera negativo.
davmac

@davmac: Hmm ... tal vez sea necesario dividir tres casos: comenzar con uno para (n1 ^ n2) < 0, que en una máquina en complemento a dos implicaría que los valores tienen el signo opuesto y pueden agregarse directamente. Si los valores tienen el mismo signo, entonces el enfoque dado arriba sería seguro. Por otro lado, tengo curiosidad por saber si los autores del Estándar esperaban que las implementaciones para el hardware de desbordamiento silencioso en complemento de dos saltaran los rieles en caso de desbordamiento de una manera que no forzara una terminación anormal inmediata del programa, pero causara interrupción impredecible de otros cálculos.
supercat

0

La solución obvia es convertir a unsigned, para obtener el comportamiento de desbordamiento sin firmar bien definido:

int add(int lhs, int rhs) 
{ 
   int sum = (unsigned)lhs + (unsigned)rhs; 
   if ((lhs >= 0 && sum < rhs) || (lhs < 0 && sum > rhs)) { 
      /* an overflow has occurred */ 
      abort(); 
   } 
   return sum;  
} 

Esto reemplaza el comportamiento de desbordamiento firmado indefinido con la conversión definida por la implementación de valores fuera de rango entre firmados y no firmados, por lo que debe verificar la documentación de su compilador para saber exactamente qué sucederá, pero al menos debería estar bien definido, y debería hacer lo correcto en cualquier máquina de complemento a dos que no genere señales sobre conversiones, que es prácticamente todas las máquinas y compiladores de C construidos en los últimos 20 años.


Todavía estás almacenando el resultado en sum, que es un int. Eso da como resultado un resultado definido por la implementación o una señal definida por la implementación que se genera si el valor de (unsigned)lhs + (unsigned)rhses mayor que INT_MAX.
R .. GitHub DEJA DE AYUDAR A ICE

2
@R: ese es el punto: el comportamiento está definido por la implementación, en lugar de indefinido, por lo que la implementación debe documentar lo que hace y hacerlo de manera coherente. Una señal solo se puede generar si la implementación la documenta, en cuyo caso siempre se debe generar y puede usar ese comportamiento.
Chris Dodd

0

En caso de agregar dos longvalores, el código portátil puede dividir el longvalor en intpartes bajas y altas (o en shortpartes en caso de que longtenga el mismo tamaño que int):

static_assert(sizeof(long) == 2*sizeof(int), "");
long a, b;
int ai[2] = {int(a), int(a >> (8*sizeof(int)))};
int bi[2] = {int(b), int(b >> (8*sizeof(int))});
... use the 'long' type to add the elements of 'ai' and 'bi'

El uso del ensamblaje en línea es la forma más rápida si se dirige a una CPU en particular:

long a, b;
bool overflow;
#ifdef __amd64__
    asm (
        "addq %2, %0; seto %1"
        : "+r" (a), "=ro" (overflow)
        : "ro" (b)
    );
#else
    #error "unsupported CPU"
#endif
if(overflow) ...
// The result is stored in variable 'a'

-1

Creo que esto funciona:

int add(int lhs, int rhs) {
   volatile int sum = lhs + rhs;
   if (lhs != (sum - rhs) ) {
       /* overflow */
       //errno = ERANGE;
       abort();
   }
   return sum;
}

El uso de volatile evita que el compilador optimice la prueba porque cree que sumpuede haber cambiado entre la suma y la resta.

Usando gcc 4.4.3 para x86_64, el ensamblaje de este código hace la suma, la resta y la prueba, aunque almacena todo en la pila y las operaciones de pila innecesarias. Incluso lo intentéregister volatile int sum = pero el montaje fue el mismo.

Para una versión con solo int sum =(no volátil o registro) la función no hizo la prueba e hizo la suma usando solo una leainstrucción ( leaes Load Effective Address y se usa a menudo para hacer sumas sin tocar el registro de banderas).

Tu versión es un código más grande y tiene muchos más saltos, pero no sé cuál sería mejor .


4
-1 por mal uso de volatilepara enmascarar un comportamiento indefinido. Si "funciona", todavía está "teniendo suerte".
R .. GitHub DEJA DE AYUDAR A ICE

@R: Si no funciona, el compilador no se está implementando volatilecorrectamente. Todo lo que estaba intentando era una solución más simple a un problema muy común en una pregunta ya respondida.
nategoose

Sin embargo, donde podría fallar sería un sistema cuya representación numérica se ajustara a valores más bajos al desbordar los números enteros.
nategoose

Ese último comentario debe tener un "no" o "no".
nategoose

@nategoose, su afirmación de que "si no funciona, el compilador no está implementando volatile correctamente" es incorrecta. Por un lado, en la aritmética en complemento a dos, siempre será cierto que lhs = sum - rhs incluso si se produce un desbordamiento. Incluso si ese no fuera el caso, y aunque este ejemplo en particular es un poco artificial, el compilador podría, por ejemplo, generar código que realiza la suma, almacena el valor del resultado, lee el valor de nuevo en otro registro, compara el valor almacenado con la lectura valor y se da cuenta de que son los mismos y, por lo tanto, asume que no se ha producido un desbordamiento.
davmac

-1

Para mí, la verificación más simple sería verificar los signos de los operandos y de los resultados.

Examinemos la suma: el desbordamiento podría ocurrir en ambas direcciones, + o -, solo cuando ambos operandos tienen el mismo signo. Y, obviamente, el desbordamiento será cuando el signo del resultado no sea el mismo que el signo de los operandos.

Entonces, una verificación como esta será suficiente:

int a, b, sum;
sum = a + b;
if  (((a ^ ~b) & (a ^ sum)) & 0x80000000)
    detect_oveflow();

Editar: como sugirió Nils, esta es la ifcondición correcta :

((((unsigned int)a ^ ~(unsigned int)b) & ((unsigned int)a ^ (unsigned int)sum)) & 0x80000000)

Y desde cuando la instrucción

add eax, ebx 

conduce a un comportamiento indefinido? No existe tal cosa en la referencia del conjunto de instrucciones Intel x86.


2
Estás perdiendo el punto aquí. Su segunda línea de código sum = a + bpodría producir un comportamiento indefinido.
Channel72

si lanza sum, ayb a unsigned durante su adición de prueba, su código funcionará por cierto.
Nils Pipenbrinck

No está definido porque el programa se bloqueará o se comportará de manera diferente. Es exactamente lo que está haciendo el procesador para calcular la bandera OF. El estándar solo intenta protegerse de casos no estándar, pero no significa que no esté autorizado a hacer esto.
ruslik

@Nils sí, quería hacer eso, pero pensé que cuatro (usngined int)s lo harían mucho más ilegible. (ya sabes, primero lo lees y lo pruebas solo si te gustó).
ruslik

1
el comportamiento indefinido está en C, no después de compilar al ensamblaje
phuclv
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.