Acceder a una matriz fuera de límites no da error, ¿por qué?


177

Estoy asignando valores en un programa C ++ fuera de los límites como este:

#include <iostream>
using namespace std;
int main()
{
    int array[2];
    array[0] = 1;
    array[1] = 2;
    array[3] = 3;
    array[4] = 4;
    cout << array[3] << endl;
    cout << array[4] << endl;
    return 0;
}

El programa imprime 3y 4. No debería ser posible. Estoy usando g ++ 4.3.3

Aquí está compilar y ejecutar el comando

$ g++ -W -Wall errorRange.cpp -o errorRange
$ ./errorRange
3
4

Solo cuando asigno array[3000]=3000me da un error de segmentación.

Si gcc no comprueba los límites de la matriz, ¿cómo puedo estar seguro de que mi programa es correcto, ya que puede provocar algunos problemas graves más adelante?

Reemplacé el código anterior con

vector<int> vint(2);
vint[0] = 0;
vint[1] = 1;
vint[2] = 2;
vint[5] = 5;
cout << vint[2] << endl;
cout << vint[5] << endl;

y este tampoco produce ningún error.



16
El código tiene errores, por supuesto, pero genera un comportamiento indefinido . Indefinido significa que puede o no ejecutarse hasta su finalización. No hay garantía de un choque.
dmckee --- ex-gatito moderador

44
Puede estar seguro de que su programa es correcto al no jugar con arreglos sin procesar. Los programadores de C ++ deberían usar clases de contenedor en su lugar, excepto en la programación integrada / OS. Lea esto por razones de contenedores de usuarios. parashift.com/c++-faq-lite/containers.html
jkeys

8
Tenga en cuenta que los vectores no necesariamente comprueban el rango usando []. El uso de .at () hace lo mismo que [] pero hace una comprobación de rango.
David Thornley

44
¡A vector no se redimensiona automáticamente al acceder a elementos fuera de los límites! ¡Es solo UB!
Pavel Minaev

Respuestas:


364

Bienvenido al mejor amigo de todos los programadores de C / C ++: Comportamiento indefinido .

Hay muchas cosas que no están especificadas por el estándar de idioma, por una variedad de razones. Este es uno de ellos.

En general, cada vez que te encuentras con un comportamiento indefinido, puede pasar cualquier cosa . La aplicación puede bloquearse, puede congelarse, puede expulsar su unidad de CD-ROM o hacer que los demonios salgan de su nariz. Puede formatear su disco duro o enviar por correo electrónico toda su pornografía a su abuela.

Incluso, si tiene mala suerte, parece funcionar correctamente.

El lenguaje simplemente dice lo que debería suceder si accede a los elementos dentro de los límites de una matriz. Se deja sin definir lo que sucede si se sale de los límites. Puede parecer que funciona hoy en su compilador, pero no es legal C o C ++, y no hay garantía de que seguirá funcionando la próxima vez que ejecute el programa. O que tiene los datos esenciales no sobrescritos incluso ahora, y simplemente no se han encontrado con los problemas, que se va a causar - todavía.

En cuanto a por qué no hay verificación de límites, hay un par de aspectos en la respuesta:

  • Una matriz es un remanente de C. Las matrices C son tan primitivas como puede ser. Solo una secuencia de elementos con direcciones contiguas. No hay comprobación de límites porque simplemente está exponiendo la memoria en bruto. Implementar un mecanismo robusto de verificación de límites habría sido casi imposible en C.
  • En C ++, la comprobación de límites es posible en los tipos de clase. Pero una matriz sigue siendo la antigua y simple compatible con C. No es una clase Además, C ++ también se basa en otra regla que hace que la comprobación de límites no sea ideal. El principio rector de C ++ es "no paga por lo que no usa". Si su código es correcto, no necesita verificación de límites, y no debería verse obligado a pagar los gastos generales de la verificación de límites de tiempo de ejecución.
  • Entonces C ++ ofrece la std::vectorplantilla de clase, que permite ambos. operator[]Está diseñado para ser eficiente. El estándar de idioma no requiere que realice la verificación de límites (aunque tampoco lo prohíbe). Un vector también tiene la at()función miembro que garantiza la verificación de límites. Entonces, en C ++, obtienes lo mejor de ambos mundos si usas un vector. Obtiene un rendimiento similar a una matriz sin verificación de límites, y tiene la capacidad de utilizar el acceso con verificación de límites cuando lo desee.

55
@Jaif: hemos estado usando esta cosa de matriz durante tanto tiempo, pero ¿por qué no hay pruebas para verificar un error tan simple?
seg.server.fault

77
El principio de diseño de C ++ era que no debería ser más lento que el código C equivalente, y C no realiza la comprobación de la matriz. El principio de diseño de C era básicamente velocidad, ya que estaba destinado a la programación del sistema. La verificación enlazada de la matriz lleva tiempo, por lo que no se realiza. Para la mayoría de los usos en C ++, debe usar un contenedor en lugar de una matriz de todos modos, y puede elegir entre una verificación enlazada o ninguna verificación enlazada accediendo a un elemento a través de .at () o [] respectivamente.
KTC

44
@seg Tal cheque cuesta algo. Si escribe el código correcto, no desea pagar ese precio. Dicho esto, me he convertido en un método completo de conversión a std :: vector's at (), que se comprueba. Su uso ha expuesto bastantes errores en lo que pensé que era el código "correcto".

10
Creo que las versiones antiguas de GCC en realidad lanzaron Emacs y una simulación de Towers of Hanoi en él, cuando encontró ciertos tipos de comportamiento indefinido. Como dije, puede pasar cualquier cosa . ;)
jalf

44
Todo ya se ha dicho, por lo que esto solo garantiza una pequeña adición. Las compilaciones de depuración pueden ser muy indulgentes en estas circunstancias en comparación con las compilaciones de lanzamiento. Debido a que la información de depuración se incluye en los binarios de depuración, hay menos posibilidades de que se sobrescriba algo vital. Es por eso que a veces las compilaciones de depuración parecen funcionar bien mientras que la compilación de lanzamiento se bloquea.
Rico

31

Con g ++, se puede añadir la opción de línea de comandos: -fstack-protector-all.

En su ejemplo, resultó en lo siguiente:

> g++ -o t -fstack-protector-all t.cc
> ./t
3
4
/bin/bash: line 1: 15450 Segmentation fault      ./t

Realmente no te ayuda a encontrar o resolver el problema, pero al menos el segfault te permitirá saber que algo está mal.


10
Acabo de encontrar una opción aún mejor: -fmudflap
Hi-Angel

1
@ Hi-Angel: el equivalente moderno es el -fsanitize=addressque detecta este error tanto en tiempo de compilación (si está optimizando) como en tiempo de ejecución.
Nate Eldredge hace

@NateEldredge +1, hoy en día incluso lo uso -fsanitize=undefined,address. Pero vale la pena señalar que hay casos raros de esquina con la biblioteca estándar, cuando el desinfectante no detecta el acceso fuera de los límites . Por esta razón, recomendaría usar adicionalmente la -D_GLIBCXX_DEBUGopción, que agrega aún más comprobaciones.
Hola Ángel hace

12

g ++ no verifica los límites de la matriz, y puede sobrescribir algo con 3,4 pero nada realmente importante, si intenta con números más altos, se bloqueará.

Solo está sobrescribiendo partes de la pila que no se usan, puede continuar hasta llegar al final del espacio asignado para la pila y eventualmente se bloqueará

EDITAR: no tiene forma de lidiar con eso, tal vez un analizador de código estático podría revelar esas fallas, pero eso es demasiado simple, puede tener fallas similares (pero más complejas) sin ser detectadas incluso para analizadores estáticos


66
¿De dónde sacas si en la dirección de la matriz [3] y la matriz [4], no hay "nada realmente importante"?
namezero

7

Es un comportamiento indefinido hasta donde yo sé. Ejecute un programa más grande con eso y se bloqueará en algún lugar del camino. La comprobación de límites no forma parte de las matrices sin formato (o incluso std :: vector).

Use std :: vector with std::vector::iterator's en su lugar para que no tenga que preocuparse por eso.

Editar:

Solo por diversión, ejecute esto y vea cuánto tiempo hasta que se cuelgue:

int main()
{
   int array[1];

   for (int i = 0; i != 100000; i++)
   {
       array[i] = i;
   }

   return 0; //will be lucky to ever reach this
}

Edit2:

No corras eso.

Edit3:

OK, aquí hay una lección rápida sobre matrices y sus relaciones con punteros:

Cuando usa la indexación de matriz, realmente está usando un puntero disfrazado (llamado "referencia"), que se desreferencia automáticamente. Es por eso que en lugar de * (matriz [1]), la matriz [1] devuelve automáticamente el valor en ese valor.

Cuando tiene un puntero a una matriz, así:

int array[5];
int *ptr = array;

Entonces, la "matriz" en la segunda declaración está realmente decayendo a un puntero a la primera matriz. Este es un comportamiento equivalente a esto:

int *ptr = &array[0];

Cuando intenta acceder más allá de lo que asignó, en realidad solo está usando un puntero a otra memoria (de la que C ++ no se quejará). Tomando mi programa de ejemplo anterior, eso es equivalente a esto:

int main()
{
   int array[1];
   int *ptr = array;

   for (int i = 0; i != 100000; i++, ptr++)
   {
       *ptr++ = i;
   }

   return 0; //will be lucky to ever reach this
}

El compilador no se quejará porque en la programación, a menudo tiene que comunicarse con otros programas, especialmente con el sistema operativo. Esto se hace con punteros bastante.


3
Creo que olvidó incrementar "ptr" en su último ejemplo allí. Has producido accidentalmente un código bien definido.
Jeff Lake

1
Jaja, ¿ves por qué no deberías usar matrices sin procesar?
jkeys

"Es por eso que en lugar de * (matriz [1]), la matriz [1] devuelve automáticamente el valor en ese valor". ¿Estás seguro de que * (array [1]) funcionará correctamente? Creo que debería ser * (matriz + 1). ps: Lol, es como enviar un mensaje al pasado. Pero, de todos modos:
muyustan

5

Insinuación

Si desea tener matrices de tamaño de restricción rápidas con verificación de error de rango, intente usar boost :: array , (también std :: tr1 :: array de <tr1/array>él será el contenedor estándar en la próxima especificación de C ++). Es mucho más rápido que std :: vector. Reserva memoria en el montón o dentro de la instancia de clase, al igual que int array [].
Este es un código de muestra simple:

#include <iostream>
#include <boost/array.hpp>
int main()
{
    boost::array<int,2> array;
    array.at(0) = 1; // checking index is inside range
    array[1] = 2;    // no error check, as fast as int array[2];
    try
    {
       // index is inside range
       std::cout << "array.at(0) = " << array.at(0) << std::endl;

       // index is outside range, throwing exception
       std::cout << "array.at(2) = " << array.at(2) << std::endl; 

       // never comes here
       std::cout << "array.at(1) = " << array.at(1) << std::endl;  
    }
    catch(const std::out_of_range& r)
    {
        std::cout << "Something goes wrong: " << r.what() << std::endl;
    }
    return 0;
}

Este programa imprimirá:

array.at(0) = 1
Something goes wrong: array<>: index out of range

4

C o C ++ no comprobarán los límites de un acceso a la matriz.

Está asignando la matriz en la pila. La indexación de la matriz vía array[3]es equivalente a * (array + 3), donde matriz es un puntero a & matriz [0]. Esto dará como resultado un comportamiento indefinido.

Una forma de detectar esto a veces en C es usar un verificador estático, como una férula . Si tu corres:

splint +bounds array.c

en,

int main(void)
{
    int array[1];

    array[1] = 1;

    return 0;
}

entonces recibirás la advertencia:

array.c: (en la función main) array.c: 5: 9: Probable tienda fuera de los límites: array [1] No se puede resolver la restricción: requiere 0> = 1 necesario para satisfacer la condición previa: requiere maxSet (array @ array .c: 5: 9)> = 1 Una escritura de memoria puede escribir en una dirección más allá del búfer asignado.


Corrección: ya ha sido asignado por el sistema operativo u otro programa. Está sobrescribiendo otro recuerdo.
jkeys el

1
Decir que "C / C ++ no verificará los límites" no es del todo correcto: no hay nada que impida que una implementación en particular lo haga, ya sea de forma predeterminada o con algunos indicadores de compilación. Es solo que ninguno de ellos se molesta.
Pavel Minaev

3

Ciertamente está sobrescribiendo su pila, pero el programa es lo suficientemente simple como para que los efectos de esto pasen desapercibidos.


2
Si la pila se sobrescribe o no depende de la plataforma.
Chris Cleeland

3

Ejecute esto a través de Valgrind y es posible que vea un error.

Como señaló Falaina, valgrind no detecta muchas instancias de corrupción de la pila. Acabo de probar la muestra bajo valgrind, y de hecho informa cero errores. Sin embargo, Valgrind puede ser instrumental para encontrar muchos otros tipos de problemas de memoria, simplemente no es particularmente útil en este caso a menos que modifique su bulid para incluir la opción --stack-check. Si crea y ejecuta la muestra como

g++ --stack-check -W -Wall errorRange.cpp -o errorRange
valgrind ./errorRange

valgrind será informar de un error.


2
En realidad, Valgrind es bastante pobre para determinar los accesos incorrectos a la matriz en la pila. (y con razón, lo mejor que puede hacer es marcar toda la pila como una ubicación de escritura válida)
Falaina

@Falaina: buen punto, pero Valgrind puede detectar al menos algunos errores de pila.
Todd Stout

Y valgrind no verá nada malo en el código porque el compilador es lo suficientemente inteligente como para optimizar la matriz y simplemente genera un literal 3 y 4. Esa optimización ocurre antes de que gcc verifique los límites de la matriz, por lo que el gcc de advertencia fuera de límites tener no se muestra.
Goswin von Brederlow

2

Comportamiento indefinido trabajando a su favor. Cualquier recuerdo que estés golpeando aparentemente no tiene nada importante. Tenga en cuenta que C y C ++ no verifican los límites en las matrices, por lo que cosas como esa no se detectarán en el momento de la compilación o la ejecución.


55
No, el comportamiento indefinido "funciona a tu favor" cuando falla de manera limpia. Cuando parece funcionar, ese es el peor escenario posible.
jalf

@JohnBode: Entonces sería mejor si corrige la redacción según el comentario de jalf
Destructor

1

Cuando inicializa la matriz con int array[2], se asigna espacio para 2 enteros; pero el identificador arraysimplemente apunta al comienzo de ese espacio. Cuando luego accede array[3]y array[4], el compilador simplemente incrementa esa dirección para señalar dónde estarían esos valores, si la matriz fuera lo suficientemente larga; intente acceder a algo como array[42]sin inicializarlo primero, terminará obteniendo el valor que ya estaba en la memoria en esa ubicación.

Editar:

Más información sobre punteros / matrices: http://home.netcom.com/~tjensen/ptr/pointers.htm


0

cuando declaras int array [2]; Usted reserva 2 espacios de memoria de 4 bytes cada uno (programa de 32 bits). si escribe array [4] en su código, todavía corresponde a una llamada válida, pero solo en tiempo de ejecución arrojará una excepción no controlada. C ++ utiliza la gestión manual de la memoria. Esto es en realidad una falla de seguridad que se usó para piratear programas

Esto puede ayudar a comprender:

int * somepointer;

somepointer [0] = somepointer [5];


0

Según tengo entendido, las variables locales se asignan en la pila, por lo que salir de los límites en su propia pila solo puede sobrescribir alguna otra variable local, a menos que vaya demasiado y supere el tamaño de su pila. Como no tiene otras variables declaradas en su función, no causa ningún efecto secundario. Intente declarar otra variable / matriz justo después de la primera y vea qué sucederá con ella.


0

Cuando escribe 'array [index]' en C, lo traduce a las instrucciones de la máquina.

La traducción es algo así como:

  1. 'obtener la dirección de la matriz'
  2. 'obtener el tamaño del tipo de matriz de objetos está compuesto por'
  3. 'multiplica el tamaño del tipo por el índice'
  4. 'agregar el resultado a la dirección de la matriz'
  5. 'lee lo que hay en la dirección resultante'

El resultado aborda algo que puede o no ser parte de la matriz. A cambio de la velocidad vertiginosa de las instrucciones de la máquina, pierde la red de seguridad de la computadora que revisa las cosas por usted. Si eres meticuloso y cuidadoso, no es un problema. Si eres descuidado o cometes un error, te quemas. A veces puede generar una instrucción no válida que causa una excepción, a veces no.


0

Un buen enfoque que he visto a menudo y que me han utilizado en realidad es inyectar algún elemento de tipo NULL (o uno creado, como uint THIS_IS_INFINITY = 82862863263;) al final de la matriz.

Luego, en la comprobación de la condición del bucle, TYPE *pagesWordshay algún tipo de matriz de puntero:

int pagesWordsLength = sizeof(pagesWords) / sizeof(pagesWords[0]);

realloc (pagesWords, sizeof(pagesWords[0]) * (pagesWordsLength + 1);

pagesWords[pagesWordsLength] = MY_NULL;

for (uint i = 0; i < 1000; i++)
{
  if (pagesWords[i] == MY_NULL)
  {
    break;
  }
}

Esta solución no se redactará si la matriz está llena de structtipos.


0

Como se mencionó ahora en la pregunta, usar std :: vector :: at resolverá el problema y realizará una comprobación encuadernada antes de acceder.

Si necesita una matriz de tamaño constante que se encuentra en la pila como su primer código, use el nuevo contenedor C ++ 11 std :: array; como vector hay std :: array :: en la función. De hecho, la función existe en todos los contenedores estándar en los que tiene un significado, es decir, donde se define el operador [] :( deque, map, unordered_map) con la excepción de std :: bitset en el que se llama std :: bitset: :prueba.


0

libstdc ++, que forma parte de gcc, tiene un modo de depuración especial para la verificación de errores. Está habilitado por la bandera del compilador -D_GLIBCXX_DEBUG. Entre otras cosas, limita los controles std::vectora costa del rendimiento. Aquí hay una demostración en línea con la versión reciente de gcc.

Entonces, en realidad, puede hacer una verificación de límites con el modo de depuración de libstdc ++, pero debe hacerlo solo cuando realice pruebas, ya que cuesta un rendimiento notable en comparación con el modo normal de libstdc ++.


0

Si cambia su programa ligeramente:

#include <iostream>
using namespace std;
int main()
{
    int array[2];
    INT NOTHING;
    CHAR FOO[4];
    STRCPY(FOO, "BAR");
    array[0] = 1;
    array[1] = 2;
    array[3] = 3;
    array[4] = 4;
    cout << array[3] << endl;
    cout << array[4] << endl;
    COUT << FOO << ENDL;
    return 0;
}

(Cambios en las mayúsculas: escríbalas en minúsculas si va a intentar esto).

Verás que la variable foo ha sido destruida. Su código se almacenan los valores en la matriz inexistente [3] y la matriz [4], y ser capaz de recuperar correctamente, pero el almacenamiento real utilizado será de foo .

Por lo tanto, puede "escapar" al exceder los límites de la matriz en su ejemplo original, pero a costa de causar daños en otros lugares, daños que pueden resultar muy difíciles de diagnosticar.

En cuanto a por qué no hay comprobación automática de límites, un programa escrito correctamente no lo necesita. Una vez que se haya hecho esto, no hay razón para verificar los límites de tiempo de ejecución y hacerlo simplemente ralentizaría el programa. Lo mejor es que todo se resuelva durante el diseño y la codificación.

C ++ se basa en C, que fue diseñado para estar lo más cerca posible del lenguaje ensamblador.

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.