¿Por qué un programa que accede ilegalmente a un puntero a otro no falla?

9 minutos de lectura

¿Por que un programa que accede ilegalmente a un puntero
pavan.mankala

Un programa que accede ilegalmente de puntero a puntero no falla con SIGSEGV. Esto no es algo bueno, pero me pregunto cómo podría ser esto y cómo sobrevivió el proceso durante muchos días en producción. Es desconcertante para mí.

He probado este programa en Windows, Linux, OpenVMS y Mac OS y nunca se han quejado.

#include <stdio.h>
#include <string.h>

void printx(void *rec) { // I know this should have been a **
    char str[1000];
    memcpy(str, rec, 1000);
    printf("%*.s\n", 1000, str);
    printf("Whoa..!! I have not crashed yet :-P");
}

int main(int argc, char **argv) {
    void *x = 0; // you could also say void *x = (void *)10;
    printx(&x);
}

  • Este es un comportamiento indefinido, por lo que no fallar es un resultado perfecto. Use una herramienta de verificación de memoria adecuada si desea depurar este tipo de cosas.

    – Dave

    25 de julio de 2013 a las 7:59

  • Es solo que pasé un puntero a otro y cuando memcpy intenta eliminar la referencia del puntero en la función printx () e intenta copiar algunos 1000 bytes de basura, debería haberse bloqueado

    – pavan.mankala

    25 de julio de 2013 a las 8:00

  • El verificador de memoria como valgrind intenta informar ese tipo de cosas. De lo contrario no hay garantía.

    – hivert

    25 de julio de 2013 a las 8:11

  • Ver: Hoteles.

    – TRIG

    25 de julio de 2013 a las 12:27

1647722416 430 ¿Por que un programa que accede ilegalmente a un puntero
Wallyk

No me sorprende la falta de un fallo de memoria. el programa es no desreferenciando un puntero no inicializado. En cambio, está copiando e imprimiendo el contenido de la memoria comenzando en una variable de puntero y los 996 (o 992) bytes más allá.

Dado que el puntero es una variable de pila, está imprimiendo memoria cerca de la parte superior de la pila hacia abajo. Esa memoria contiene el marco de pila de main(): posiblemente algunos valores de registro guardados, un recuento de argumentos del programa, un puntero a los argumentos del programa, un puntero a una lista de variables de entorno y un registro de instrucciones guardado para main() para regresar, generalmente en el código de inicio de la biblioteca de tiempo de ejecución de C. En todas las implementaciones que he investigado, los marcos de pila a continuación tienen copias de las propias variables de entorno, una matriz de punteros a ellas y una matriz de punteros a los argumentos del programa. En entornos Unix (que insinúa que está utilizando), las cadenas de argumentos del programa estarán debajo de eso.

Toda esta memoria es “segura” para imprimir, excepto que aparecerán algunos caracteres no imprimibles que podrían estropear una terminal de visualización.

El principal problema potencial es si hay suficiente memoria de pila asignada y asignada para evitar un SIGSEGV durante el acceso. A falla de segmento podría suceder si hay muy pocos datos ambientales. O si la implementación coloca esos datos en otro lugar para que solo haya unas pocas palabras de pila aquí. Sugiero confirmar eso limpiando las variables de entorno y volviendo a ejecutar el programa.

Este código no sería tan inofensivo si alguna de las convenciones de tiempo de ejecución de C no fuera cierta:

  • La arquitectura utiliza una pila.
  • Una variable local (void *x) está asignado en la pila
  • La pila crece hacia la memoria numerada más baja
  • Los parámetros se pasan en la pila.
  • Si main() se llama con argumentos. (Algunos entornos de servicio ligero, como los procesadores integrados, invocan main() sin parámetros).

En todas las implementaciones modernas convencionales, todo esto es generalmente cierto.

  • @pavan.mankala: De nada. De hecho, he incursionado en escribir y mantener varios compiladores, y pasé bastante tiempo lidiando con la interfaz para llamar main()principalmente para optimizar un entorno de memoria limitada.

    – Wallyk

    25 de julio de 2013 a las 8:27

  • +1: se garantiza que la desreferenciación de un puntero nulo generará un error de segmento en la mayoría de los sistemas operativos (. Hay software que depende de esto.

    – Joni

    25 de julio de 2013 a las 8:29


  • @DevSolar, lo que es puramente académico es afirmar que lo que sucede es un comportamiento indefinido, sin explicación del comportamiento observado real. Diría que es buena ingeniería y buena informática poder explicar lo que hace una máquina en cualquier situación dada, incluso cuando, o especialmente cuando, la especificación del lenguaje deja la responsabilidad de alguna decisión a los implementadores del compilador y el sistema operativo. Nuestros programas no se ejecutan en un vacío aislado de problemas reales.

    – Joni

    25 de julio de 2013 a las 9:33

  • @Joni: Entonces crees que es una buena explicación decir que los punteros son de 32 o 64 bits, los parámetros se pasan en la pila, las pilas se extienden hacia abajo, que hay un puntero a las variables de entorno al lado argc / argvdirección de retorno guardada en la pila, yadda yadda, sin ni siquiera calificar esa declaración como “… en Linux y Windows”? Suponiendo dichosamente que hay es tal cosa como SIGSEGV en la máquina de destino, y que un acceso ilegal no colapsará todo el sistema operativo? Por cada una de esas declaraciones, conozco un sistema en el que esa suposición no se cumple.

    – DevSolar

    25 de julio de 2013 a las 9:44


  • @DevSolar, muchas de esas cosas se pueden deducir de la pregunta. Por ejemplo, el OP ya menciona SIGSEGV, lo que implica familiaridad con un sistema Unix de algún tipo, y luego menciona dos ejemplos: Linux y OS X. En cuanto a los punteros de 32/64 bits, no parece una suposición necesaria, pero tiende ser el caso de las máquinas en las que puede ejecutar Windows y OS X. En cuanto a que en realidad hay una pila y pilas que se extienden hacia abajo, así es como los compiladores en estas plataformas generalmente organizar la memoria, de nuevo algo bastante seguro de asumir. Pero sí, la respuesta podría ser mejor si matizara los supuestos.

    – Joni

    25 de julio de 2013 a las 10:09

1647722416 845 ¿Por que un programa que accede ilegalmente a un puntero
DevSolar

El acceso ilegal a la memoria es comportamiento indefinido. Esto significa que su programa puede que accidente, pero no está garantizado, porque el comportamiento exacto es indefinido.

(Una broma entre los desarrolladores, especialmente cuando se enfrentan a compañeros de trabajo que no se preocupan por tales cosas, es que “invocar un comportamiento indefinido podría formatear su disco duro, simplemente no está garantizado”. ;-))

Actualizar: Aquí hay una discusión candente. Sí, los desarrolladores de sistemas deben saber qué Realmente sucede en un sistema dado. Pero tal conocimiento está ligado a la CPU, el sistema operativo, el compilador, etc., y generalmente tiene una utilidad limitada, porque incluso si hace que el código funcione, todavía ser de muy mala calidad. Es por eso que limité mi respuesta al punto más importante, y la pregunta real (“por qué no falla esto”):

El código publicado en la pregunta no tiene un comportamiento bien definido, pero eso solo significa que realmente no puede confiar en lo que hace, no que deberían choque.

  • Aquí no se produce ningún acceso ilegal a la memoria. Aunque el programa accede descuidadamente a la memoria de la pila en masa, no hay nada particularmente malo en esto siempre que haya suficientes datos en la pila. Su respuesta es correcta hasta donde llega, pero no aborda el código anterior.

    – Wallyk

    25 de julio de 2013 a las 8:23


  • @wallyk: no sé sobre C11, pero el estándar C99 no menciona “apilar” en ninguna parte. Dependiendo de cómo sea su implementación maneja la pila, leer 1000 bytes hará que te vayas más allá x, argv y argc a la nada (ilegal), o túre trespassing from x` en stry usando memcpy() en áreas de memoria superpuestas es por definición comportamiento indefinido. De cualquier manera, código ilegal. Y usted, señor, sin intención de ofender, es solo el tipo de codificador al que se hace referencia en la línea “formatear discos duros”: el tipo que no encuentra fallas inmediatas con un código como este porque puede que trabajo.

    – DevSolar

    25 de julio de 2013 a las 9:04


  • Estoy bastante seguro de que los estándares C11 y C99 abordan las características del idioma, no los detalles de implementación. Sin embargo, las pilas son bastante probadas y verdaderas. Los mainframes que usé hace mucho tiempo no los tenían, pero los lenguajes de estructura implementaron pilas para su conveniencia. No hay superposición de memoria para memcpy(): se asigna el destino de 1000 bytes y su fuente está en otra parte. (De hecho, he escrito código de formato de disco, pero siempre fue predecible e intencional).

    – Wallyk

    25 de julio de 2013 a las 9:09

  • @wallyk: Nuevamente, este código podría funcionar en la máquina A y la máquina B, y juntas A y B podrían representar el 99% de todos los sistemas en todo el mundo, pero el código publicado por el OP invoca indefinido comportamiento, que es definido como “comportamiento no definido por el lenguaje estándar”. Debido a que no está definido, un desarrollador cauteloso nunca debe confiar en lo que realmente sucede en el sistema Xporque voluntad romper algún día. Respeto su experiencia práctica, pero creo firmemente que a muchos aspirantes a programadores se les muestran demasiadas cosas “bajo el capó”, con muy pocas advertencias de “no toques esto”.

    – DevSolar

    25 de julio de 2013 a las 9:20

Si elimina la referencia a un puntero no válido, está invocando un comportamiento indefinido. Lo que significa que el programa puede fallar, puede funcionar, podría preparar un poco de café, lo que sea.

Cuando tengas

int main(int argc, char **argv) {
    void *x = 0; // you could also say void *x = (void *)10;
    printx(&x);
}

tu estas declarando x como un puntero con valor 0, y ese puntero vive en la pila ya que es una variable local. Ahora, estás pasando a printx los habla a de xlo que significa que con

memcpy(str, rec, 1000);

está copiando datos desde arriba de la pila (o, de hecho, desde la pila misma), a la pila (porque la dirección del puntero de la pila disminuye con cada pulsación). Es probable que los datos de origen estén cubiertos por la misma entrada de la tabla de páginas, ya que está copiando solo 1000 bytes, por lo que no tiene fallas de segmentación. Sin embargo, en última instancia, como ya se escribió, estamos hablando de un comportamiento indefinido.

Se estrellaría con gran probabilidad si usted escribe al área no accedida. Pero tu eres leyendo, puede estar bien. Pero el comportamiento seguirá siendo indefinido.

  • @pavan.mankala: Eso no es lo que está pasando. Por favor, vea mi respuesta.

    – Wallyk

    25 de julio de 2013 a las 8:17

  • int main(void) { return *(int *)rand(); } – Fallo de segmentación.

    – Ben Millwood

    25 de julio de 2013 a las 12:28

  • @pavan.mankala: Eso no es lo que está pasando. Por favor, vea mi respuesta.

    – Wallyk

    25 de julio de 2013 a las 8:17

  • int main(void) { return *(int *)rand(); } – Fallo de segmentación.

    – Ben Millwood

    25 de julio de 2013 a las 12:28

¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad