c++ - new - pointers meaning
¿Podría alguna vez querer acceder a la dirección cero? (17)
A veces utilicé cargas de la dirección cero (en una plataforma conocida donde eso garantizaría segfault) para bloquear deliberadamente un símbolo con nombre informativo en el código de la biblioteca si el usuario viola alguna condición necesaria y no hay una buena manera de lanzar una excepción disponible para mí " Segfault at someFunction$xWasnt16ByteAligned
" es un mensaje de error bastante efectivo para alertar a alguien sobre lo que hizo mal y cómo solucionarlo. Dicho esto, no recomendaría hacer un hábito de ese tipo de cosas.
La constante 0 se usa como el puntero nulo en C y C ++. Pero como en la pregunta "Puntero a una dirección fija específica " , parece haber un posible uso de la asignación de direcciones fijas. ¿Existe alguna necesidad concebible, en cualquier sistema, para cualquier tarea de bajo nivel, para acceder a la dirección 0?
Si existe, ¿cómo se resuelve con 0 siendo el puntero nulo y todo?
Si no, ¿qué hace que sea seguro que no hay tal necesidad?
C / C ++ no te permite escribir en ninguna dirección. Es el sistema operativo el que puede generar una señal cuando un usuario accede a una dirección prohibida. C y C ++ le aseguran que cualquier memoria obtenida del montón, será diferente de 0.
El compilador se encarga de esto por ti ( comp.lang.c FAQ ):
Si una máquina utiliza un patrón de bits distinto de cero para punteros nulos, es responsabilidad del compilador generarlo cuando el programador solicite, escribiendo "0" o "NULO", un puntero nulo. Por lo tanto, #definir NULL como 0 en una máquina para la que los punteros nulos internos son distintos de cero es tan válido como en cualquier otro, porque el compilador debe (y puede) generar los punteros nulos correctos de la máquina en respuesta a los 0 sin adornos que se ven en contextos de puntero.
Puede obtener la dirección cero haciendo referencia a cero desde un contexto sin puntero.
En el x86, la dirección 0 (o más bien, 0000: 0000) y su proximidad en modo real es la ubicación del vector de interrupción. En los viejos tiempos malos, normalmente escribía valores en el vector de interrupción para instalar interruptores de interrupción (o si era más disciplinado, usaba el servicio de MS-DOS 0x25). Los compiladores de C para MS-DOS definían un tipo de puntero lejano que cuando se le asignaba NULL o 0 recibía el patrón de bits 0000 en su parte de segmento y 0000 en su parte de desplazamiento.
Por supuesto, un programa que se porta mal y que accidentalmente escribió a un puntero lejano cuyo valor era 0000: 0000 podría causar que cosas muy malas sucedan en la máquina, generalmente encerrándola y forzando un reinicio.
En la práctica, los compiladores de C dejarán que su programa intente escribir en la dirección 0. Comprobar que cada puntero funcione en tiempo de ejecución para un puntero NULL sería un poco caro. En las computadoras, el programa se bloqueará porque el sistema operativo lo prohíbe. En los sistemas integrados sin protección de memoria, el programa escribirá en la dirección 0, que a menudo bloqueará todo el sistema.
La dirección 0 puede ser útil en un sistema integrado (un término general para una CPU que no está en una computadora, sino que ejecuta todo desde su estéreo a su cámara digital). Por lo general, los sistemas están diseñados para que no tenga que escribir en la dirección 0. En todos los casos que conozco, es algún tipo de dirección especial. Incluso si el programador necesita escribir en él (por ejemplo, para configurar una tabla de interrupción), solo necesitarían escribir en él durante la secuencia de inicio inicial (generalmente un poco de lenguaje ensamblador para configurar el entorno para C).
En la pregunta del enlace, las personas están discutiendo la configuración de direcciones fijas en un microcontrolador . Cuando se programa un microcontrolador, todo está en un nivel mucho más bajo allí.
Incluso no tiene un sistema operativo en términos de PC de escritorio / servidor, y no tiene memoria virtual y esas cosas. Entonces, está bien e incluso es necesario acceder a la memoria en una dirección específica. En una computadora de escritorio / servidor moderna es inútil e incluso peligroso.
En una nota tangencial: le puede interesar saber que con el compilador C ++ de Microsoft, un puntero NULL al miembro se representará como el patrón de bits 0xFFFFFFFF en una máquina de 32 bits. Es decir:
struct foo
{
int field;
};
int foo::*pmember = 0; // ''null'' member pointer
pmember tendrá el patrón de bits ''todos''. Esto se debe a que necesita este valor para distinguirlo de
int foo::*pmember = &foo::field;
donde el patrón de bits será de hecho por "todos los ceros", ya que queremos el desplazamiento 0 en la estructura foo.
Otros compiladores de C ++ pueden elegir un patrón de bits diferente para un puntero nulo al miembro, pero la observación clave es que no será el patrón de bits de ceros completos que podría haber estado esperando.
Estás empezando desde una premisa equivocada. Cuando asigna una constante entera con el valor 0 a un puntero, se convierte en una constante de puntero nulo. Sin embargo, esto no significa que un puntero nulo se refiera necesariamente a la dirección 0. Por el contrario, los estándares C y C ++ son muy claros en cuanto a que un puntero nulo puede referirse a una dirección que no sea cero.
Lo que se reduce a esto es esto: tienes que separar una dirección a la que se referiría un puntero nulo, pero puede ser básicamente cualquier dirección que elijas. Cuando convierte cero en un puntero, tiene que referirse a la dirección elegida, pero eso es todo lo que realmente se necesita. Solo por ejemplo, si decidió que convertir un entero a un punto significaría agregar 0x8000 al entero, entonces el puntero nulo en realidad se referiría a la dirección 0x8000 en lugar de a la dirección 0.
También vale la pena señalar que desreferenciar un puntero nulo da como resultado un comportamiento indefinido. Eso significa que no puedes hacerlo en código portátil , pero eso no significa que no puedas hacerlo en absoluto. Cuando está escribiendo código para microcontroladores pequeños y tal, es bastante común incluir algunas partes del código que no son portátiles en absoluto. Leer desde una dirección puede darle el valor de un sensor, mientras que escribir en la misma dirección podría activar un motor paso a paso (por ejemplo). El siguiente dispositivo (incluso utilizando exactamente el mismo procesador) podría estar conectado, por lo que ambas direcciones se referían a la RAM normal.
Incluso si un puntero nulo se refiere a la dirección 0, eso no le impide usarlo para leer y / o escribir lo que sea que esté en esa dirección, simplemente le impide hacerlo de manera portátil , pero eso no Realmente importa mucho. La única razón por la que normalmente sería importante la dirección cero sería si se descodifica para conectarse a algo que no sea el almacenamiento normal, por lo que probablemente no pueda usarlo de manera totalmente portátil de todos modos.
La dirección de memoria 0 también se llama la Página cero . Esto está poblado por el BIOS, y contiene información sobre el hardware que se ejecuta en su sistema. Todos los núcleos modernos protegen esta región de la memoria. Nunca deberías necesitar acceder a esta memoria, pero si quieres hacerlo desde dentro del kernel, un módulo kernel hará el truco.
Puede sorprender a muchas personas, pero en el lenguaje C central no existe un puntero nulo especial. Eres totalmente libre de leer y escribir en la dirección 0 si es físicamente posible.
El código siguiente ni siquiera se compila, ya que NULL no está definido:
int main(int argc, char *argv[])
{
void *p = NULL;
return 0;
}
OTOH, el siguiente código compila, y usted puede leer y escribir la dirección 0, si el hardware / sistema operativo lo permite:
int main(int argc, char *argv[])
{
int *p = 0;
*p = 42;
int x = *p; /* let''s assume C99 */
}
Tenga en cuenta que no incluí nada en los ejemplos anteriores. Si comenzamos a incluir cosas de la biblioteca C estándar, NULL se define mágicamente. Por lo que recuerdo, proviene de string.h
.
NULL aún no es una característica del núcleo C, es una CONVENCIÓN de muchas funciones de la biblioteca C para indicar la invalidez de los punteros. La biblioteca C en la plataforma dada definirá NULL a una ubicación de memoria que de todos modos no es accesible. Probémoslo en una PC con Linux:
#include <stdio.h>
int main(int argc, char *argv[])
{
int *p = NULL;
printf("NULL is address %p/n", p);
printf("Contents of address NULL is %d/n", *p);
return 0;
}
El resultado es:
NULL is address 0x0
Segmentation fault (core dumped)
Entonces, nuestra biblioteca C define NULL para hacer frente a cero, que resulta ser inaccesible. Pero no era el compilador de C, ni siquiera la función de biblioteca C printf()
que manejaba la dirección cero especialmente. Todos felizmente intentaron trabajar con eso normalmente. Fue el SO el que detectó una falla de segmentación cuando printf
intentó leer desde la dirección cero.
Recopilé un código usando gcc para el Motorola HC11, que no tiene MMU y 0 es una dirección perfectamente buena, y me decepcionó saber que para escribir en la dirección 0, simplemente escribe. No hay diferencia entre NULL y la dirección 0.
Y puedo ver por qué. Quiero decir, no es realmente posible definir un NULL único en una arquitectura donde cada ubicación de memoria es potencialmente válida, así que supongo que los autores de gcc dijeron que 0 era lo suficientemente bueno para NULL, ya sea que sea una dirección válida o no.
char *null = 0;
; Clears 8-bit AR and BR and stores it as a 16-bit pointer on the stack.
; The stack pointer, ironically, is stored at address 0.
1b: 4f clra
1c: 5f clrb
1d: de 00 ldx *0 <main>
1f: ed 05 std 5,x
Cuando lo comparo con otro puntero, el compilador genera una comparación regular. Lo que significa que de ninguna manera considera que char *null = 0
sea un puntero NULL especial, y de hecho un puntero a la dirección 0 y un puntero "NULL" serán iguales.
; addr is a pointer stored at 7,x (offset of 7 from the address in XR) and
; the "NULL" pointer is at 5,y (offset of 5 from the address in YR). It doesn''t
; treat the so-called NULL pointer as a special pointer, which is not standards
; compliant as far as I know.
37: de 00 ldx *0 <main>
39: ec 07 ldd 7,x
3b: 18 de 00 ldy *0 <main>
3e: cd a3 05 cpd 5,y
41: 26 10 bne 53 <.LM7>
Por lo tanto, para responder a la pregunta original, supongo que mi respuesta es verificar la implementación de su compilador y averiguar si se molestaron en implementar un valor NULL único. Si no, no tienes que preocuparte por eso. ;)
(Por supuesto, esta respuesta no cumple con los estándares).
Recuerde que en todos los casos normales, realmente no ve direcciones específicas. Cuando asigna memoria, el sistema operativo le proporciona la dirección de esa porción de memoria.
Cuando toma la referencia de una variable, la variable ya ha sido asignada en una dirección determinada por el sistema.
Así que acceder a la dirección cero no es realmente un problema, porque cuando sigues un puntero, no importa qué dirección apunta, solo que es válido:
int* i = new int(); // suppose this returns a pointer to address zero
*i = 42; // now we''re accessing address zero, writing the value 42 to it
Entonces, si necesita acceder a la dirección cero, generalmente funcionará bien.
Lo 0 == nulo solo se convierte realmente en un problema si por alguna razón estás accediendo directamente a la memoria física. Tal vez está escribiendo un kernel de sistema operativo o algo así. En ese caso, va a escribir en direcciones de memoria específicas (especialmente las asignadas a registros de hardware), por lo que es posible que necesite escribir en la dirección cero. Pero entonces realmente está eludiendo C ++ y confiando en los detalles de su compilador y plataforma de hardware.
Por supuesto, si necesita escribir en la dirección cero, eso es posible. Solo la constante 0
representa un puntero nulo. El valor entero no constante cero no dará, si está asignado a un puntero, un puntero nulo.
Entonces, simplemente podría hacer algo como esto:
int i = 0;
int* zeroaddr = (int*)i;
ahora zeroaddr apuntará a dirección cero (*), pero no será, en sentido estricto, un puntero nulo, porque el valor cero no fue constante.
(*): eso no es del todo cierto. El estándar C ++ solo garantiza un "mapeo definido por la implementación" entre enteros y direcciones. Podría convertir el 0 a la dirección 0x1633de20` o cualquier otra dirección que quiera. Pero el mapeo es generalmente el intuitivo y obvio, donde el entero 0 se asigna a la dirección cero)
Sí, es posible que desee acceder a la dirección de memoria 0x0h. Por qué querrías hacer esto depende de la plataforma. Un procesador puede usar esto para un vector de reinicio, de modo que escribir en él haga que la CPU se reinicie. También podría usarse para un vector de interrupción, como una interfaz mapeada en memoria de algún recurso de hardware (contador de programa, reloj del sistema, etc.), o incluso podría ser válida como una simple dirección de memoria antigua. No hay nada necesariamente mágico sobre la dirección de memoria cero, es solo una que históricamente se usó para fines especiales (reiniciar vectores y similares). Los lenguajes tipo C siguen esta tradición al usar cero como la dirección para un puntero NULL, pero en realidad el hardware subyacente puede o no ver la dirección cero como especial.
La necesidad de acceder a la dirección cero generalmente surge solo en detalles de bajo nivel como gestores de arranque o controladores. En estos casos, el compilador puede proporcionar opciones / pragmas para compilar una sección de código sin optimizaciones (para evitar que el puntero cero se extraiga como un puntero NULL) o el ensamblaje en línea se puede usar para acceder a la dirección verdadera cero.
Se puede escribir para hacer frente a cero, pero depende de varios factores, como su sistema operativo, la arquitectura de destino y la configuración de MMU. De hecho, puede ser una herramienta de depuración útil (pero no siempre).
Por ejemplo, hace unos años, mientras trabajaba en un sistema integrado (con pocas herramientas de depuración disponibles), tuvimos un problema que resultó en un reinicio cálido. Para ayudar a localizar el problema, estábamos depurando usando sprintf (NULL, ...); y un cable serie de 9600 baudios. Como dije, pocas herramientas de depuración están disponibles. Con nuestra configuración, sabíamos que un reinicio cálido no dañaría los primeros 256 bytes de memoria. Por lo tanto, después del reinicio en caliente, podemos pausar el cargador y volcar el contenido de la memoria para averiguar qué sucedió antes del reinicio.
Si recuerdo correctamente, en un microcontrolador AVR, el archivo de registro se mapea en un espacio de direcciones de la RAM y el registro R0 está en la dirección 0x00. Fue claramente hecho a propósito y aparentemente Atmel cree que hay situaciones, cuando es conveniente acceder a la dirección 0x00 en lugar de escribir R0 explícitamente.
En la memoria del programa, en la dirección 0x0000 hay un vector de interrupción de reinicio y, nuevamente, esta dirección está claramente destinada a ser accedida cuando se programa el chip.
Todo depende de si la máquina tiene memoria virtual. Normalmente, los sistemas que lo tienen ponen una página no escribible allí, que probablemente sea el comportamiento al que estás acostumbrado. Sin embargo, en sistemas sin él (generalmente microcontroladores en estos días, pero solían ser mucho más comunes), a menudo hay cosas muy interesantes en esa área, como una tabla de interrupción. Recuerdo hackear esas cosas en los tiempos de los sistemas de 8 bits; diversión, y no demasiado dolor cuando tuvo que restablecer el sistema y comenzar de nuevo. :-)
Ni en C ni en C ++ el valor del puntero nulo está de ninguna manera ligado a la dirección física 0
. El hecho de que use constante 0
en el código fuente para establecer un puntero a valor de puntero nulo no es más que una pieza de azúcar sintáctica . El compilador debe traducirlo a la dirección física real utilizada como valor de puntero nulo en la plataforma específica.
En otras palabras, 0
en el código fuente no tiene importancia física alguna. Podría haber sido 42
o 13
, por ejemplo. Es decir, los autores del lenguaje, si así lo desean, podrían haberlo hecho de modo que tuvieras que hacer p = 42
para establecer el puntero p
en el valor del puntero nulo. De nuevo, esto no significa que la dirección física 42
deba reservarse para punteros nulos. Se requeriría que el compilador traduzca el código fuente p = 42
al código de máquina que rellenaría el valor real del puntero nulo ( 0x0000
o 0xBAAD
) en el puntero p
. Así es exactamente como es ahora con constante 0
.
También tenga en cuenta que ni C ni C ++ proporcionan una característica estrictamente definida que le permita asignar una dirección física específica a un puntero. Así que su pregunta sobre "cómo uno asignaría 0 dirección a un puntero" formalmente no tiene respuesta. Simplemente no puede asignar una dirección específica a un puntero en C / C ++. Sin embargo, en el ámbito de las características definidas por la implementación, la conversión explícita de entero a puntero pretende tener ese efecto. Entonces, lo harías de la siguiente manera
uintptr_t address = 0;
void *p = (void *) address;
Tenga en cuenta que esto no es lo mismo que hacer
void *p = 0;
Este último siempre produce el valor del puntero nulo, mientras que el primero, en general, no. El primero normalmente producirá un puntero a la dirección física 0
, que podría o no ser el valor del puntero nulo en la plataforma dada.