2011-06-23 10 views
14

¿Por qué el código siguiente funciona sin ningún tipo de bloqueo @ runtime?¿Por qué mi programa no falla cuando escribo después del final de una matriz?

Y también el tamaño es completamente dependiente de la máquina/plataforma/compilador !!. Incluso puedo dar hasta 200 en una máquina de 64 bits. ¿cómo se detectaría una falla de segmentación en la función principal en el sistema operativo?

void main(int argc, char* argv[]) 
{ 
    int arr[3]; 
    arr[4] = 99; 
} 

¿De dónde viene este espacio de búfer? ¿Es esta la pila asignada a un proceso?

+7

El desbordamiento de pila ocurre cuando se asigna demasiada memoria de la pila. En este caso, suponiendo 'sizeof (int) == 4', ha asignado 12 bytes insignificantes de la pila. Tu código está escribiendo más allá del final de una matriz. Eso no es desbordamiento de pila. Es un comportamiento indefinido. –

+0

Viene del mismo lugar donde obtuvo el resto de su RAM, probablemente quien le vendió la computadora. 'arr [3]' significa "designar 3' int' de espacio disponible para mi uso ", no significa" crear 3 'int' de espacio fuera del éter", aunque eso sería una implementación legal si fuera físicamente posible. Estás garabateando sobre cualquier memoria/dirección que se encuentre adyacente a 'arr' (bueno, al lado, pero uno en realidad), que como dice David es UB. Sí, es parte de tu stack (los estándares C y C++ no hablan de stack, pero en la práctica es donde van las variables automáticas). –

+0

@vprajan - He actualizado su título para reflejar la pregunta, ya que aquí hay una buena respuesta para llamar la atención. –

Respuesta

67

algo que escribí hace algún tiempo para la educación con propósitos ...

Considérese el siguiente c-programa:

int q[200]; 

main(void) { 
    int i; 
    for(i=0;i<2000;i++) { 
     q[i]=i; 
    } 
} 

después de compilar y ejecutar ella, un volcado de memoria se produce:

$ gcc -ggdb3 segfault.c 
$ ulimit -c unlimited 
$ ./a.out 
Segmentation fault (core dumped) 

ahora usando gdb para llevar a cabo un análisis post mortem:

$ gdb -q ./a.out core 
Program terminated with signal 11, Segmentation fault. 
[New process 7221] 
#0 0x080483b4 in main() at s.c:8 
8  q[i]=i; 
(gdb) p i 
$1 = 1008 
(gdb) 

eh, el programa no segfault cuando uno escribió fuera de los 200 elementos asignados, en su lugar se estrelló cuando i = 1008, ¿por qué?

Ingresar páginas.

se puede determinar el tamaño de página de varias maneras en UNIX/Linux, una forma es utilizar el sysconf función del sistema() así:

#include <stdio.h> 
#include <unistd.h> // sysconf(3) 

int main(void) { 
    printf("The page size for this system is %ld bytes.\n", 
      sysconf(_SC_PAGESIZE)); 

    return 0; 
} 

que da la salida:

El tamaño de página de este sistema es 4096 bytes.

o se puede utilizar el getconf utilidad de comandos como esto:

$ getconf PAGESIZE 
4096 

post mortem

Resulta que la violación de segmento no se produce en i = 200 pero a i = 1008, veamos por qué. Comenzar GDB para hacer algunas ananlysis post mortem:

$gdb -q ./a.out core 

Core was generated by `./a.out'. 
Program terminated with signal 11, Segmentation fault. 
[New process 4605] 
#0 0x080483b4 in main() at seg.c:6 
6   q[i]=i; 
(gdb) p i 
$1 = 1008 
(gdb) p &q 
$2 = (int (*)[200]) 0x804a040 
(gdb) p &q[199] 
$3 = (int *) 0x804a35c 

q terminó a las 0x804a35c en la dirección, o más bien, el último byte de q [199] estaba en ese lugar. El tamaño de página es como vimos anteriormente 4096 bytes y el tamaño de palabra de 32 bits de la máquina da como resultado que una dirección virtual se descompone en un número de página de 20 bits y un desplazamiento de 12 bits.

q [] terminó en el número de página virtual:

0x804a = 32842 offset:

0x35c = 860 así que había todavía:

4096 - 864 = 3232 bytes que quedan en ese página de memoria en la que se asignó q []. Ese espacio puede contener:

3232/4 = 808 enteros, y el código tratado como si contuviera elementos de Q en la posición 200 a 1008.

Todos sabemos que esos elementos No existe y el compilador no se quejó, ni tampoco el hw, ya que tenemos permisos de escritura para esa página. Solo cuando i = 1008 hizo q [] se refiere a una dirección en una página diferente para la que no tenía permiso de escritura, la memoria virtual hw detectó esto y activó una segfault.

Un entero se almacena en 4 bytes, lo que significa que esta página contiene 808 (3236/4) elementos falsos adicionales lo que significa que todavía es perfectamente legal acceder a estos elementos desde q [200], q [201] hasta el final hasta el elemento 199 + 808 = 1007 (q [1007]) sin activar un fallo seg. Al acceder a q [1008] ingresa una nueva página para la cual los permisos son diferentes.

+4

+1 y deseando poder haber votado dos veces – SJuan76

+0

+1, ¡una excelente respuesta! – Nim

+0

+! .. excelente .......... –

5

Como está escribiendo fuera de los límites de su matriz, el comportamiento de su código no está definido.

Es la naturaleza del comportamiento indefinido que puede pasar cualquier cosa, incluida la falta de segfaults (el compilador no tiene obligación de realizar comprobaciones de límites).

Está escribiendo en la memoria que no ha asignado pero que está allí y que, probablemente, no se está utilizando para nada más. Su código puede comportarse de manera diferente si realiza cambios en partes del código aparentemente no relacionadas, a su OS, compilador, banderas de optimización, etc.

En otras palabras, una vez que se encuentre en ese territorio, todas las apuestas estarán apagadas.

2

Comportamiento indefinido: simplemente no observa ningún problema. La razón más probable es que sobrescriba un área de la memoria de la que el comportamiento del programa no depende antes: esa memoria es técnicamente modificable (el tamaño de la pila es de aproximadamente 1 megabyte en la mayoría de los casos) y no aparece indicación de error. No deberías confiar en esto.

0

Su código tiene Undefined Behavior. Eso significa que puede hacer cualquier cosa o nada. Dependiendo de su compilador y sistema operativo, etc., podría fallar.

Dicho esto, con muchos compiladores si no la mayoría, su código ni siquiera compilará.

Eso es porque tiene void main, mientras que tanto el estándar C como el estándar C++ requieren int main.

Sobre el único compilador que está contento con void main es Microsoft ’ s, Visual C++.

eso es un defecto compilador, pero desde que Microsoft tiene un montón de ejemplo, la documentación e incluso herramientas de generación de código que generan void main, es probable que nunca lo repare. Sin embargo, considere que escribir void main específico de Microsoft es un carácter más para escribir que el estándar int main. Entonces, ¿por qué no ir con los estándares?

Saludos & HTH.,

0

Se produce un error de segmentación cuando un proceso intenta sobreescribir una página en memoria que no posee; A menos que corras un largo trecho sobre el final de tu buffer, no vas a disparar un fallo seg.

La pila se encuentra en algún lugar de uno de los bloques de memoria propiedad de su aplicación. En este caso, has tenido suerte si no has sobrescrito algo importante. Ha sobrescrito quizás alguna memoria no utilizada. Si fueras un poco más desafortunado, podrías haber sobrescrito el marco de pila de otra función en la pila.

3

Mediante el uso de un tipo de matriz, que C++ ha heredado de C, que están implícitamente pidió no tener una prueba de alcance.

Si intenta esto en vez

void main(int argc, char* argv[]) 
{  
    std::vector<int> arr(3); 

    arr.at(4) = 99; 
} 

que se obtener una excepción lanzada.

Así C++ ofrece tanto una marcada y una interfaz sin marcar. Depende de usted seleccionar el que desea usar.

1

Para responder a su pregunta, ¿por qué no se detecta? La mayoría de los compiladores de C no analizan en tiempo de compilación lo que está haciendo con los punteros y la memoria, y nadie advierte en tiempo de compilación que ha escrito algo peligroso. En el tiempo de ejecución, tampoco hay un entorno administrado y controlado que cuida las referencias de tu memoria, por lo que nadie te impide leer memoria a la que no tienes derecho. La memoria se asigna a usted en ese momento (porque es solo parte de la pila no muy lejos de su función), por lo que el sistema operativo tampoco tiene un problema con eso.

Si desea mantener la mano mientras accede a la memoria, necesita un entorno administrado como Java o CLI, donde todo el programa está a cargo de otro, administrando el programa que busca esas transgresiones.

4

En cuanto a exactamente cuando/donde un local de accidentes de desbordamiento de búfer variable depende de algunos factores:

  1. La cantidad de datos en la pila ya en el momento de la función se llama que contiene el desbordamiento acceso a variables
  2. la cantidad de datos escritos en el desborde la variable/array en total

Recuerde que las pilas crecen hacia abajo. Es decir. la ejecución del proceso comienza con un stackpointer cerca del final de la memoria que se utilizará como pila. Sin embargo, no comienza en la última palabra asignada, y eso se debe a que el código de inicialización del sistema puede decidir pasar algún tipo de "información de inicio" al proceso en el momento de la creación, y con frecuencia lo hace en la pila.

Ese es el modo de falla habitual - un bloqueo al regresar de la función que contenía el código de desbordamiento.

Si el cantidad total de datos escritos en un búfer en la pila es mayor que la cantidad total de stackspace utilizado anteriormente (los llamantes/código de inicialización/otras variables) entonces obtendrá un accidente en cualquier acceso a memoria primero se ejecuta más allá de la parte superior (principio) de la pila. La dirección de bloqueo será justo después de un límite de página - SIGSEGV debido al acceso a la memoria más allá de la parte superior de la pila, donde no se asigna nada.

Si ese total es menor que el tamaño de la parte usada de la pila en este momento, entonces se va a trabajar sólo aceptable y Crash más adelante - de hecho, en las plataformas que almacenan direcciones de retorno en la pila (que es cierto para x86/x64), al regresar de su función. Esto se debe a que la instrucción de la CPU ret en realidad toma una palabra de la pila (la dirección de retorno) y redirige la ejecución allí. Si en lugar de la ubicación del código esperado, esta dirección contiene cualquier basura, se produce una excepción y su programa fallece.

Para ilustrar esto: Cuando main() se llama, la pila es el siguiente (en un programa de UNIX x86 de 32 bits):

[ esp   ] <return addr to caller> (which exits/terminates process) 
[ esp + 4  ] argc 
[ esp + 8  ] argv 
[ esp + 12  ] envp <third arg to main() on UNIX - environment variables> 
[ ...   ] 
[ ...   ] <other things - like actual strings in argv[], envp[] 
[ END   ] PAGE_SIZE-aligned stack top - unmapped beyond 

Cuando main() arranca, se asignará espacio en la pila para diversos fines, entre otros para alojar su matriz a ser desbordada. Esto hará que se vea como:

[ esp   ] <current bottom end of stack> 
[ ...   ] <possibly local vars of main()> 
[ esp + X  ] arr[0] 
[ esp + X + 4 ] arr[1] 
[ esp + X + 8 ] arr[2] 
[ esp + X + 12 ] <possibly other local vars of main()> 
[ ...   ] <possibly other things (saved regs)> 

[ old esp  ] <return addr to caller> (which exits/terminates process) 
[ old esp + 4 ] argc 
[ old esp + 8 ] argv 
[ old esp + 12 ] envp <third arg to main() on UNIX - environment variables> 
[ ...   ] 
[ ...   ] <other things - like actual strings in argv[], envp[] 
[ END   ] PAGE_SIZE-aligned stack top - unmapped beyond 

Esto significa que pueda camino felizmente el acceso más allá arr[2].

Para una muestra de diferentes accidentes que resulten de desbordamientos de búfer, el intento de éste:

#include <stdlib.h> 
#include <stdio.h> 

int main(int argc, char **argv) 
{ 
    int i, arr[3]; 

    for (i = 0; i < atoi(argv[1]); i++) 
     arr[i] = i; 

    do { 
     printf("argv[%d] = %s\n", argc, argv[argc]); 
    } while (--argc); 

    return 0; 
} 

y vea cómo diferente el accidente será cuando se desbordan el búfer por un poco (por ejemplo, 10) bits , en comparación con cuando se desborda más allá del final de la pila. Pruébalo con diferentes niveles de optimización y diferentes compiladores. Muy ilustrativo, ya que muestra tanto la mala conducta (no siempre imprimirá todos los argv[] correctamente) como los bloqueos en varios lugares, incluso bucles interminables (si, por ejemplo, el compilador coloca i o argc en la pila y el código lo sobrescribe durante el lazo).

Cuestiones relacionadas