Redefiniendo NULL


118

Estoy escribiendo código C para un sistema donde la dirección 0x0000 es válida y contiene E / S de puerto. Por lo tanto, cualquier posible error que acceda a un puntero NULL permanecerá sin ser detectado y, al mismo tiempo, provocará un comportamiento peligroso.

Por esta razón, deseo redefinir NULL para que sea otra dirección, por ejemplo, una dirección que no es válida. Si accedo accidentalmente a dicha dirección, obtendré una interrupción de hardware en la que puedo manejar el error. Resulta que tengo acceso a stddef.h para este compilador, por lo que puedo modificar el encabezado estándar y redefinir NULL.

Mi pregunta es: ¿entrará en conflicto con el estándar C? Por lo que puedo decir de 7.17 en el estándar, la macro está definida por la implementación. ¿Hay algo en otra parte del estándar que indique que NULL debe ser 0?

Otro problema es que muchos compiladores realizan una inicialización estática poniendo todo en cero, sin importar el tipo de datos. Aunque el estándar dice que el compilador debe establecer los enteros en cero y los punteros en NULL. Si redefiniera NULL para mi compilador, entonces sé que dicha inicialización estática fallará. ¿Podría considerar eso como un comportamiento incorrecto del compilador a pesar de que modifiqué audazmente los encabezados del compilador manualmente? Porque sé con certeza que este compilador en particular no accede a la macro NULL cuando realiza una inicialización estática.


3
Esta es una muy buena pregunta. No tengo una respuesta para ti, pero tengo que preguntarte: ¿estás seguro de que no es posible mover tus cosas válidas a 0x00 y dejar que NULL sea una dirección inválida como en los sistemas "normales"? Si no puede, entonces las únicas direcciones no válidas de forma segura para usar serían aquellas que pueda estar seguro de que puede asignar y luego mprotectproteger. O, si la plataforma no tiene ASLR o similar, direcciones más allá de la memoria física de la plataforma. Buena suerte.
Borealid

8
¿Cómo funcionará si su código está usando if(ptr) { /* do something on ptr*/ }? ¿Funcionará si NULL se define de manera diferente a 0x0?
Xavier T.28 de

3
El puntero C no tiene relación forzada con las direcciones de memoria. Siempre que se cumplan las reglas de la aritmética de punteros, un valor de punteros puede ser cualquier cosa. La mayoría de las implementaciones eligen usar las direcciones de memoria como valores de puntero, pero podrían usar cualquier cosa siempre que sea un isomorfismo.
datenwolf

2
@bdonlan Eso también violaría las reglas (de advertencia) en MISRA-C.
Lundin

2
@Andreas Sí, esos también son mis pensamientos. ¡No se debe permitir que la gente de hardware diseñe hardware en el que deba ejecutarse el software! :)
Lundin

Respuestas:


84

El estándar C no requiere que los punteros nulos estén en la dirección cero de la máquina. SIN EMBARGO, convertir una 0constante a un valor de puntero debe resultar en un NULLpuntero (§6.3.2.3 / 3), y evaluar el puntero nulo como un booleano debe ser falso. Esto puede ser un poco incómodo si realmente no quiere una dirección cero, y NULLno es la dirección cero.

Sin embargo, con modificaciones (importantes) en el compilador y la biblioteca estándar, no es imposible NULLser representado con un patrón de bits alternativo sin dejar de ser estrictamente conforme con la biblioteca estándar. Sin embargo, no es suficiente simplemente cambiar la definición de NULLsí mismo, ya que entonces NULLse evaluaría como verdadero.

Específicamente, necesitaría:

  • Haga arreglos para que los ceros literales en las asignaciones a punteros (o lanzamientos a punteros) se conviertan en algún otro valor mágico como -1.
  • Organice pruebas de igualdad entre punteros y un entero constante 0para verificar el valor mágico en su lugar (§6.5.9 / 6)
  • Organice todos los contextos en los que un tipo de puntero se evalúa como booleano para verificar la igualdad con el valor mágico en lugar de verificar cero. Esto se deriva de la semántica de las pruebas de igualdad, pero el compilador puede implementarlo internamente de manera diferente. Consulte §6.5.13 / 3, §6.5.14 / 3, §6.5.15 / 4, §6.5.3.3 / 5, §6.8.4.1 / 2, §6.8.5 / 4
  • Como señaló caf, actualice la semántica para la inicialización de objetos estáticos (§6.7.8 / 10) e inicializadores compuestos parciales (§6.7.8 / 21) para reflejar la nueva representación de puntero nulo.
  • Cree una forma alternativa de acceder a la verdadera dirección cero.

Hay algunas cosas que no tiene que manejar. Por ejemplo:

int x = 0;
void *p = (void*)x;

Después de esto, pNO se garantiza que sea un puntero nulo. Solo es necesario manejar asignaciones constantes (este es un buen enfoque para acceder a la dirección cero verdadera). Igualmente:

int x = 0;
assert(x == (void*)0); // CAN BE FALSE

También:

void *p = NULL;
int x = (int)p;

xno se garantiza que lo sea 0.

En resumen, esta misma condición aparentemente fue considerada por el comité de lenguaje C, y se hicieron consideraciones para aquellos que elegirían una representación alternativa para NULL. Todo lo que tienes que hacer ahora es realizar cambios importantes en tu compilador, y listo :)

Como nota al margen, puede ser posible implementar estos cambios con una etapa de transformación del código fuente antes del compilador propiamente dicho. Es decir, en lugar del flujo normal de preprocesador -> compilador -> ensamblador -> enlazador, agregaría un preprocesador -> transformación NULL -> compilador -> ensamblador -> enlazador. Entonces podrías hacer transformaciones como:

p = 0;
if (p) { ... }
/* becomes */
p = (void*)-1;
if ((void*)(p) != (void*)(-1)) { ... }

Esto requeriría un analizador de C completo, así como un analizador de tipos y análisis de definiciones de tipos y declaraciones de variables para determinar qué identificadores corresponden a punteros. Sin embargo, al hacer esto, podría evitar tener que realizar cambios en las partes de generación de código del compilador propiamente dicho. clang puede ser útil para implementar esto; entiendo que fue diseñado con transformaciones como esta en mente. Por supuesto, es probable que también deba realizar cambios en la biblioteca estándar.


2
Ok, no había encontrado el texto en §6.3.2.3, pero sospechaba que habría tal declaración en alguna parte :). Supongo que esto responde a mi pregunta, según el estándar no se me permite redefinir NULL a menos que me apetezca escribir un nuevo compilador de C para respaldarme :)
Lundin

2
Un buen truco es piratear el compilador para que el puntero <-> conversiones enteras XOR un valor específico que sea un puntero inválido y aún lo suficientemente trivial como para que la arquitectura de destino pueda hacerlo de forma económica (por lo general, ese sería un valor con un solo conjunto de bits , como 0x20000000).
Simon Richter

2
Otra cosa que necesitaría cambiar en el compilador es la inicialización de objetos con tipo compuesto: si un objeto se inicializa parcialmente, entonces cualquier puntero para el que no esté presente un iniciador explícito debe inicializarse NULL.
caf

20

El estándar establece que una expresión de constante entera con valor 0, o una expresión de este void *tipo convertida al tipo, es una constante de puntero nulo. Esto significa que (void *)0siempre es un puntero nulo, pero dado int i = 0;, (void *)ino tiene por qué serlo.

La implementación de C consta del compilador junto con sus encabezados. Si modifica los encabezados para redefinir NULL, pero no modifica el compilador para corregir inicializaciones estáticas, entonces ha creado una implementación no conforme. Es toda la implementación en conjunto la que tiene un comportamiento incorrecto, y si la rompiste, realmente no tienes a nadie más a quien culpar;)

Debe corregir algo más que inicializaciones estáticas, por supuesto: dado un puntero p, if (p)es equivalente a if (p != NULL), debido a la regla anterior.


8

Si usa la biblioteca C std, tendrá problemas con las funciones que pueden devolver NULL. Por ejemplo, la documentación de malloc dice:

Si la función no pudo asignar el bloque de memoria solicitado, se devuelve un puntero nulo.

Debido a que malloc y las funciones relacionadas ya están compiladas en binarios con un valor NULL específico, si redefine NULL, no podrá usar directamente la biblioteca C std a menos que pueda reconstruir toda su cadena de herramientas, incluidas las bibliotecas C std.

También debido al uso de NULL por parte de la biblioteca estándar, si redefine NULL antes de incluir los encabezados estándar, puede sobrescribir una definición NULL listada en los encabezados. Cualquier cosa en línea sería incompatible con los objetos compilados.

En su lugar, definiría su propio NULL, "MYPRODUCT_NULL", para sus propios usos y evitaría o traduciría desde / hacia la biblioteca C std.


6

Deje NULL solo y trate IO al puerto 0x0000 como un caso especial, tal vez usando una rutina escrita en ensamblador y, por lo tanto, no esté sujeta a la semántica C estándar. IOW, no redefina NULL, redefina el puerto 0x00000.

Tenga en cuenta que si está escribiendo o modificando un compilador de C, el trabajo requerido para evitar desreferenciar NULL (asumiendo que en su caso la CPU no está ayudando) es el mismo sin importar cómo se defina NULL, por lo que es más fácil dejar NULL definido como cero, y asegúrese de que cero nunca pueda ser desreferenciado de C.


El problema solo surgirá cuando se acceda accidentalmente a NULL, no cuando se acceda intencionalmente al puerto. ¿Por qué redefiniría la E / S del puerto para entonces? Ya está funcionando como debería.
Lundin

2
@Lundin Accidentalmente o no, NULL solo se puede desreferenciar en un programa C usando *p, p[]o p(), por lo que el compilador solo necesita preocuparse por aquellos para proteger el puerto IO 0x0000.
Apalala

@Lundin La segunda parte de su pregunta: una vez que restringe el acceso a la dirección cero desde C, necesita otra forma de llegar al puerto 0x0000. Una función escrita en ensamblador puede hacer eso. Desde dentro de C, el puerto podría asignarse a 0xFFFF o lo que sea, pero es mejor usar una función y olvidarse del número de puerto.
Apalala

3

Teniendo en cuenta la extrema dificultad de redefinir NULL como lo mencionaron otros, tal vez sea más fácil redefinir la desreferenciación para direcciones de hardware conocidas. Al crear una dirección, agregue 1 a cada dirección conocida, de modo que su puerto IO conocido sea:

  #define CREATE_HW_ADDR(x)(x+1)
  #define DEREFERENCE_HW_ADDR(x)(*(x-1))

  int* wellKnownIoPort = CREATE_HW_ADDR(0x00000000);

  printf("IoPortIs" DEREFERENCE_HW_ADDR(wellKnownIoPort));

Si las direcciones que le preocupan están agrupadas y puede estar seguro de que agregar 1 a la dirección no entrará en conflicto con nada (lo que no debería en la mayoría de los casos), es posible que pueda hacerlo de manera segura. Y luego no necesita preocuparse por reconstruir su cadena de herramientas / std lib y expresiones en el formulario:

  if (pointer)
  {
     ...
  }

seguirá funcionando

Loco, lo sé, pero pensé en tirar la idea por ahí :).


El problema solo surgirá cuando se acceda accidentalmente a NULL, no cuando se acceda intencionalmente al puerto. ¿Por qué redefiniría la E / S del puerto para entonces? Ya está funcionando como debería.
Lundin

@LundIn Supongo que tienes que elegir cuál es más doloroso, modificar la reconstrucción de toda la cadena de herramientas o cambiar esta parte de tu código.
Doug T.

2

El patrón de bits para el puntero nulo puede no ser el mismo que el patrón de bits para el entero 0. Pero la expansión de la macro NULL debe ser una constante de puntero nulo, es decir, un entero constante de valor 0 que se puede convertir a (void *).

Para lograr el resultado que desea sin dejar de cumplir, tendrá que modificar (o quizás configurar) su cadena de herramientas, pero es posible.


1

Estás buscando problemas. Redefinir NULLa un valor no nulo romperá este código:

   si (myPointer)
   {
      // myPointer no es nulo
      ...
   }
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.