¿Qué sucede en el sistema operativo cuando eliminamos la referencia a un puntero NULL en C?

12 minutos de lectura

avatar de usuario
h4ck3d

Digamos que hay un puntero y lo inicializamos con NULL.

int* ptr = NULL;
*ptr = 10;

Ahora, el programa se bloqueará ya que ptr no apunta a ninguna dirección y le estamos asignando un valor, que es un acceso no válido. Entonces, la pregunta es, ¿qué sucede internamente en el sistema operativo? ¿Se produce un error de página o de segmentación? ¿El núcleo incluso buscará en la tabla de páginas? ¿O el accidente ocurrió antes de eso?

Sé que no haría tal cosa en ningún programa, pero esto es solo para saber qué sucede internamente en el sistema operativo o el compilador en tal caso. Y NO es una pregunta duplicada.

  • Una respuesta real aquí requeriría que especifique de qué sistema operativo y qué CPU está hablando.

    – TJD

    28 de septiembre de 2012 a las 18:49

  • @TJDLinux. Toma cualquier CPU. Estoy hablando en GENERAL.

    – h4ck3d

    28/09/2012 a las 18:50

  • @sTEAK.: No se puede hablar de esto “en general”. El lenguaje dice que quitar la referencia a un puntero nulo da como resultado un comportamiento indefinido. Lo que su procesador y sistema operativo dados harán al respecto es específico de la implementación. No existe un “en general”

    – Ed S.

    28 de septiembre de 2012 a las 19:06


  • @sTEAK: el comportamiento absolutamente depende de qué sistema operativo y CPU estemos hablando.

    – Juan Bode

    28 de septiembre de 2012 a las 19:09

  • “Toma cualquier CPU. Estoy hablando en GENERAL”. No. El punto es que las CPU tienen un mecanismo diferente para lidiar con esto. En algunos casos eso es sin mecanismo.

    – dmckee — gatito ex-moderador

    28/09/2012 a las 21:33


avatar de usuario
adam rosenfield

Respuesta corta: depende de muchos factores, incluidos el compilador, la arquitectura del procesador, el modelo de procesador específico y el sistema operativo, entre otros.

Respuesta larga (x86 y x86-64): Bajemos al nivel más bajo: la CPU. En x86 y x86-64, ese código generalmente se compilará en una instrucción o secuencia de instrucciones como esta:

movl $10, 0x00000000

Que dice “almacenar el entero constante 10 en la dirección de memoria virtual 0”. los Manuales para desarrolladores de software de las arquitecturas Intel® 64 e IA-32 describa en detalle lo que sucede cuando se ejecuta esta instrucción, así que lo resumiré para usted.

La CPU puede funcionar en varios modos diferentes, varios de los cuales son para compatibilidad con versiones anteriores de CPU mucho más antiguas. Los sistemas operativos modernos ejecutan código a nivel de usuario en un modo llamado modo protegidoque utiliza paginación para convertir direcciones virtuales en direcciones físicas.

Para cada proceso, el sistema operativo mantiene un tabla de paginas que dicta cómo se asignan las direcciones. La tabla de páginas se almacena en la memoria en un formato específico (y se protege para que no puedan ser modificadas por el código de usuario) que la CPU entiende. Por cada acceso a la memoria que ocurre, la CPU lo traduce de acuerdo con la tabla de páginas. Si la traducción tiene éxito, realiza la lectura/escritura correspondiente en la ubicación de la memoria física.

Las cosas interesantes suceden cuando falla la traducción de direcciones. No todas las direcciones son válidas, y si algún acceso a la memoria genera una dirección no válida, el procesador genera un excepción de fallo de página. Esto desencadena una transición de modo de usuario (también conocido como nivel de privilegio actual (CPL) 3 en x86/x86-64) en modo núcleo (también conocido como CPL 0) a una ubicación específica en el código del kernel, según lo define el tabla de descriptores de interrupciones (IDT).

El kernel recupera el control y, basándose en la información de la excepción y la tabla de páginas del proceso, descubre lo que sucedió. En este caso, se da cuenta de que el proceso a nivel de usuario accedió a una ubicación de memoria no válida y luego reacciona en consecuencia. En Windows, invocará manejo estructurado de excepciones para permitir que el código de usuario maneje la excepción. En los sistemas POSIX, el sistema operativo entregará un SIGSEGV señal al proceso.

En otros casos, el sistema operativo manejará la falla de la página internamente y reiniciará el proceso desde su ubicación actual como si nada hubiera pasado. Por ejemplo, páginas de guardia se colocan en la parte inferior de la pila para permitir que la pila crezca bajo demanda hasta un límite, en lugar de preasignar una gran cantidad de memoria para la pila. Se utilizan mecanismos similares para lograr Copiar en escrito memoria.

En los sistemas operativos modernos, las tablas de páginas generalmente se configuran para hacer que la dirección 0 sea una dirección virtual no válida. Pero a veces es posible cambiar eso, por ejemplo, en Linux escribiendo 0 en el pseudoarchivo /proc/sys/vm/mmap_min_addrdespués de lo cual es posible usar mmap(2) para asignar la dirección virtual 0. En ese caso, eliminar la referencia de un puntero nulo no provocaría un error de página.

La discusión anterior trata sobre lo que sucede cuando el código original se ejecuta en el espacio del usuario. Pero esto también podría ocurrir dentro del kernel. El kernel puede (y ciertamente es mucho más probable que lo haga el código de usuario) mapear la dirección virtual 0, por lo que dicho acceso a la memoria sería normal. Pero si no está mapeado, lo que sucede entonces es muy similar: la CPU genera un error de falla de página que se atrapa en un punto predefinido en el kernel, el kernel examina lo que sucedió y reacciona en consecuencia. Si el kernel no puede recuperarse de la excepción, normalmente entrará en pánico de alguna manera (panico kernel, kernel upso un BSOD en Windows, por ejemplo) imprimiendo información de depuración en la consola o el puerto serie y luego deteniéndolo.

Ver también Mucho ruido y pocas nueces sobre NULL: Explotación de una desreferencia NULL del kernel para obtener un ejemplo de cómo un atacante podría explotar un error de desreferencia de puntero nulo desde el interior del kernel para obtener privilegios de root en una máquina Linux.

  • dirección virtual 0 ciertamente no siempre es inválido; AIX asigna una página de solo lectura en 0. Ver software-ingeniería.web.cern.ch/software-ingeniería/Productos/…

    – ecatmur

    28/09/2012 a las 19:10

  • Además, debe considerar que su código podría estar ejecutándose en el interior el núcleo, donde la página en la dirección virtual 0 generalmente se mapea.

    – ecatmur

    28 de septiembre de 2012 a las 19:12

  • Más información sobre la paginación x86: stackoverflow.com/questions/18431261/how-does-x86-paging-work

    – Ciro Santilli Путлер Капут 六四事

    20 de julio de 2015 a las 12:32


  • En este caso, se da cuenta de que el proceso de nivel de usuario accedió a una ubicación de memoria no válida. ¿Cómo se da cuenta de que la dirección no es válida y no es un error de página?

    – tabs_over_spaces

    20/04/2017 a las 17:00

  • Se agregó P: ¿Dónde se almacena la información sobre el espacio de direcciones del proceso? Dado que esta es la información que debe verificarse si una traducción perdida es una falla de página (traer código/datos del disco) o una falla de segmentación (finalizar proceso).

    – tabs_over_spaces

    20 de abril de 2017 a las 17:08

Como nota al margen, solo para obligar a las diferencias en las arquitecturas, un determinado sistema operativo desarrollado y mantenido por una empresa conocida por su acrónimo de tres letras y a menudo denominado como un gran color primario tiene una determinación NULL muy fascinante.

Utilizan un espacio de direcciones lineales de 128 bits para TODOS los datos (memoria Y disco) en una “cosa” gigante. De acuerdo con su sistema operativo, un puntero “válido” deber colocarse en un límite de 128 bits dentro de ese espacio de direcciones. Esto, por cierto, causa efectos secundarios fascinantes para las estructuras, empaquetadas o no, que albergan punteros. De todos modos, escondido en una página dedicada por proceso hay un mapa de bits que asigna uno poco para cada ubicación válida en un espacio de direcciones de proceso donde puede colocarse un puntero válido. TODOS los códigos de operación en su hardware y sistema operativo que pueden generar y devolver una dirección de memoria válida y asignarla a un puntero establecerán el bit que representa la dirección de memoria donde se encuentra ese puntero (el puntero de destino).

Entonces, ¿por qué debería importarle a alguien? Por esta sencilla razón:

int a = 0;
int *p = &a;
int *q = p-1;

if (p)
{
// p is valid, p's bit is lit, this code will run.
}

if (q)
{
   // the address stored in q is not valid. q's bit is not lit. this will NOT run.
}

Lo verdaderamente interesante es esto.

if (p == NULL)
{
   // p is valid. this will NOT run.
}

if (q == NULL)
{
   // q is not valid, and therefore treated as NULL, this WILL run.
}

if (!p)
{
   // same as before. p is valid, therefore this won't run
}

if (!q)
{
   // same as before, q is NOT valid, therefore this WILL run.
}

Es algo que tienes que ver para creer. Ni siquiera puedo imaginar el mantenimiento realizado para mantener ese mapa de bits, especialmente al copiar valores de puntero o liberar memoria dinámica.

  • Lo siento, tengo problemas para analizar lo que dijiste aquí. Límite de 128 bits en un espacio de direcciones de 128 bits: ¿se refiere a un espacio de direcciones de 128 bits orientado a bytes? ¿128 bits de datos en el área límite? Si todo son bytes, entonces solo hay una región de 128 bits en un espacio de direcciones de 128 bits y, por lo tanto, no hay límites. ¿Es eso un error tipográfico? ¿Puedes elaborar o arreglar?

    –Ted Middleton

    23 de julio de 2015 a las 19:14

  • @TedMiddleton, no hay ningún error tipográfico. 128 bits son 16 bytes. Límite de 128 bits significa alineado a un múltiplo de 16 bytes. Espacio de direcciones de 128 bits significa un espacio de direcciones en el que un puntero tiene una longitud de 128 bits (16 bytes).

    – denis bider

    25 de julio de 2015 a las 14:56

avatar de usuario
ouah

En la CPU que admite memoria virtual, generalmente se emitirá una excepción de falla de página si intenta leer en la dirección de la memoria 0x0. Se invocará el controlador de fallas de la página del sistema operativo, el sistema operativo decidirá que la página no es válida y cancelará su programa.

Tenga en cuenta que en algunas CPU también puede acceder de forma segura a la dirección de la memoria 0x0.

Como dice el estándar C, la desreferenciación de un puntero nulo no está definida, si el compilador puede detectar en tiempo de compilación (o incluso en tiempo de ejecución) que está desreferenciando un puntero nulo, puede hacer lo que quiera, como abortar el programa con un mensaje de error detallado .

(C99, 6.5.3.2.p4) “Si se ha asignado un valor no válido al puntero, el comportamiento del operador unario * no está definido.87)”

87): “Entre los valores no válidos para desreferenciar un puntero mediante el operador unario * se encuentran un puntero nulo, una dirección alineada inapropiadamente para el tipo de objeto al que apunta y la dirección de un objeto después del final de su vida útil”.

en un típico caso, int *ptr = NULL; establecerá ptr para apuntar a la dirección 0. El estándar C (y el estándar C++) tiene mucho cuidado en no requiere eso, pero es extremadamente común sin embargo.

Cuando tu lo hagas *ptr = 10;la CPU normalmente generaría 0 en las líneas de dirección, y 10 en las líneas de datos, mientras configura una línea R/W para indicar una escritura (y, si el bus tiene tal cosa, afirmar la línea de memoria frente a E/S para indicar una escritura en la memoria, no E/S).

Suponiendo que la CPU admite la protección de la memoria (y está utilizando un sistema operativo que lo habilita), la CPU verificará ese (intento) de acceso antes de que suceda. Por ejemplo, una CPU Intel/AMD moderna utilizará tablas de paginación que asignan direcciones virtuales a direcciones físicas. En un caso típico, la dirección 0 no se asignará a ninguna dirección física. En este caso, la CPU generará una excepción de violación de acceso. Para un ejemplo bastante típico, Microsoft Windows deja los primeros 4 megabytes sin asignar, por lo que ninguna dirección en ese rango normalmente resultará en una violación de acceso.

En una CPU más antigua (o un sistema operativo más antiguo que no habilita las funciones de protección de la CPU), el intento de escritura a menudo tendrá éxito. Por ejemplo, bajo MS-DOS, escribir a través de un puntero NULL simplemente escribiría en la dirección cero. En el modelo pequeño o mediano (con direcciones de 16 bits para datos), la mayoría de los compiladores escribirían algún patrón conocido en los primeros bytes del segmento de datos, y cuando el programa terminara, verificarían si ese patrón permaneció intacto (y hacer algo para indicar que había escrito a través de un puntero NULL si fallaba). En el modelo compacto o grande (direcciones de datos de 20 bits), generalmente solo escriben en la dirección cero sin previo aviso.

Me imagino que esto depende de la plataforma y el compilador. El puntero NULL podría implementarse mediante el uso de una página NULL, en cuyo caso tendría un error de página, o podría estar por debajo del límite del segmento para un segmento desplegable, en cuyo caso tendría un error de segmentación.

Esta no es una respuesta definitiva, solo mi conjetura.

¿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