Redéfinir NULL

118

J'écris du code C pour un système où l'adresse 0x0000 est valide et contient des E / S de port. Par conséquent, tous les bogues possibles qui accèdent à un pointeur NULL resteront non détectés et en même temps provoqueront un comportement dangereux.

Pour cette raison, je souhaite redéfinir NULL pour être une autre adresse, par exemple une adresse qui n'est pas valide. Si j'accède accidentellement à une telle adresse, j'obtiendrai une interruption matérielle où je pourrai gérer l'erreur. Il se trouve que j'ai accès à stddef.h pour ce compilateur, donc je peux en fait modifier l'en-tête standard et redéfinir NULL.

Ma question est la suivante: cela sera-t-il en conflit avec la norme C? Pour autant que je sache à partir de 7.17 dans la norme, la macro est définie par l'implémentation. Y a-t-il quelque chose ailleurs dans la norme indiquant que NULL doit être 0?

Un autre problème est que de nombreux compilateurs effectuent une initialisation statique en définissant tout sur zéro, quel que soit le type de données. Même si la norme stipule que le compilateur doit définir les entiers sur zéro et les pointeurs sur NULL. Si je redéfini NULL pour mon compilateur, je sais qu'une telle initialisation statique échouera. Pourrais-je considérer cela comme un comportement incorrect du compilateur même si j'ai audacieusement modifié manuellement les en-têtes du compilateur? Parce que je sais avec certitude que ce compilateur particulier n'accède pas à la macro NULL lors de l'initialisation statique.

Lundin
la source
3
C'est une très bonne question. Je n'ai pas de réponse pour vous, mais je dois vous demander: êtes-vous sûr qu'il n'est pas possible de déplacer vos données valides à 0x00 et de laisser NULL être une adresse invalide comme dans les systèmes "normaux"? Si vous ne pouvez pas, alors les seules adresses non valides en toute sécurité à utiliser sont celles que vous pouvez être sûr de pouvoir allouer, puis mprotectsécuriser. Ou, si la plate-forme n'a pas d'ASLR ou autre, des adresses au-delà de la mémoire physique de la plate-forme. Bonne chance.
Borealid
8
Comment cela fonctionnera-t-il si votre code utilise if(ptr) { /* do something on ptr*/ }? Cela fonctionnera-t-il si NULL est défini différemment de 0x0?
Xavier T.28
3
Le pointeur C n'a pas de relation forcée avec les adresses mémoire. Tant que les règles de l'arithmétique des pointeurs sont respectées, une valeur de pointeurs peut être n'importe quoi. La plupart des implémentations choisissent d'utiliser les adresses mémoire comme valeurs de pointeur, mais elles pourraient utiliser n'importe quoi tant qu'il s'agit d'un isomorphisme.
datenwolf
2
@bdonlan Cela enfreindrait également les règles (consultatives) de MISRA-C.
Lundin le
2
@Andreas Oui, c'est aussi ce que je pense. Les spécialistes du matériel ne devraient pas être autorisés à concevoir du matériel dans lequel les logiciels devraient fonctionner! :)
Lundin

Réponses:

84

La norme C n'exige pas que les pointeurs nuls soient à l'adresse zéro de la machine. CEPENDANT, convertir une 0constante en valeur de pointeur doit donner un NULLpointeur (§6.3.2.3 / 3), et évaluer le pointeur nul comme un booléen doit être faux. Cela peut être un peu gênant si vous vraiment ne voulez une adresse zéro, et NULLn'est pas l'adresse zéro.

Néanmoins, avec des modifications (lourdes) du compilateur et de la bibliothèque standard, il n'est pas impossible de se NULLfaire représenter avec un motif de bits alternatif tout en restant strictement conforme à la bibliothèque standard. Cependant, il ne suffit pas de simplement changer la définition de NULLlui-même, comme cela NULLserait alors vrai.

Plus précisément, vous devrez:

  • Faites en sorte que les zéros littéraux dans les affectations aux pointeurs (ou les casts aux pointeurs) soient convertis en une autre valeur magique telle que -1.
  • Organiser des tests d'égalité entre les pointeurs et un entier constant 0pour vérifier la valeur magique à la place (§6.5.9 / 6)
  • Organisez tous les contextes dans lesquels un type de pointeur est évalué comme un booléen pour vérifier l'égalité avec la valeur magique au lieu de vérifier zéro. Cela découle de la sémantique du test d'égalité, mais le compilateur peut l'implémenter différemment en interne. Voir §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
  • Comme caf l'a souligné, mettez à jour la sémantique pour l'initialisation des objets statiques (§6.7.8 / 10) et des initialiseurs composés partiels (§6.7.8 / 21) pour refléter la nouvelle représentation de pointeur nul.
  • Créez un autre moyen d'accéder à la véritable adresse zéro.

Il y a certaines choses que vous n'avez pas à gérer. Par exemple:

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

Après cela, il pn'est PAS garanti d'être un pointeur nul. Seules les affectations constantes doivent être traitées (c'est une bonne approche pour accéder à la véritable adresse zéro). Également:

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

Aussi:

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

xn'est pas garanti 0.

En bref, cette condition même a apparemment été examinée par le comité de langue C, et des considérations ont été faites pour ceux qui choisiraient une représentation alternative pour NULL. Tout ce que vous avez à faire maintenant est d'apporter des modifications majeures à votre compilateur, et hop, vous avez terminé :)

En remarque, il peut être possible d'implémenter ces modifications avec une étape de transformation du code source avant le compilateur proprement dit. Autrement dit, au lieu du flux normal du préprocesseur -> compilateur -> assembleur -> éditeur de liens, vous ajouteriez un préprocesseur -> transformation NULL -> compilateur -> assembleur -> éditeur de liens. Ensuite, vous pouvez faire des transformations comme:

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

Cela nécessiterait un analyseur C complet, ainsi qu'un analyseur de type et une analyse des typedefs et des déclarations de variables pour déterminer quels identificateurs correspondent aux pointeurs. Cependant, en faisant cela, vous pourriez éviter d'avoir à apporter des modifications aux parties de génération de code du compilateur proprement dit. clang peut être utile pour l'implémentation - je comprends qu'il a été conçu avec des transformations comme celle-ci à l'esprit. Vous devrez probablement également apporter des modifications à la bibliothèque standard.

bdonlan
la source
2
Ok je n'avais pas trouvé le texte au §6.3.2.3, mais je soupçonnais qu'il y aurait une telle déclaration quelque part :). Je suppose que cela répond à ma question, selon la norme, je ne suis pas autorisé à redéfinir NULL sauf si j'ai envie d'écrire un nouveau compilateur C pour me sauvegarder :)
Lundin
2
Une bonne astuce consiste à pirater le compilateur afin que le pointeur <-> conversions d'entiers XOR une valeur spécifique qui est un pointeur invalide et encore assez trivial pour que l'architecture cible puisse le faire à moindre coût (généralement, ce serait une valeur avec un seul bit défini , comme 0x20000000).
Simon Richter
2
Une autre chose que vous auriez besoin de changer dans le compilateur est l'initialisation des objets avec un type composé - si un objet est partiellement initialisé, alors tous les pointeurs pour lesquels un initaliseur explicite n'est pas présent doivent être initialisés NULL.
caf
20

La norme stipule qu'une expression constante entière avec la valeur 0, ou une telle expression convertie en void *type, est une constante de pointeur nul. Cela signifie qu'il (void *)0s'agit toujours d'un pointeur nul, mais donné int i = 0;, (void *)in'a pas besoin de l'être.

L'implémentation C se compose du compilateur avec ses en-têtes. Si vous modifiez les en-têtes pour redéfinir NULL, mais ne modifiez pas le compilateur pour corriger les initialisations statiques, vous avez créé une implémentation non conforme. C'est toute l'implémentation prise ensemble qui a un comportement incorrect, et si vous l'avez cassée, vous n'avez vraiment personne d'autre à blâmer;)

Vous devez corriger plus que de simples initialisations statiques, bien sûr - étant donné un pointeur p, if (p)équivaut à if (p != NULL), en raison de la règle ci-dessus.

caf
la source
8

Si vous utilisez la bibliothèque C std, vous rencontrerez des problèmes avec les fonctions qui peuvent renvoyer NULL. Par exemple, la documentation malloc indique:

Si la fonction n'a pas réussi à allouer le bloc de mémoire demandé, un pointeur nul est renvoyé.

Comme malloc et les fonctions associées sont déjà compilés en binaires avec une valeur NULL spécifique, si vous redéfinissez NULL, vous ne pourrez pas utiliser directement la bibliothèque C std à moins que vous ne puissiez reconstruire toute votre chaîne d'outils, y compris les bibliothèques C std.

De plus, en raison de l'utilisation de NULL par la bibliothèque std, si vous redéfinissez NULL avant d'inclure les en-têtes std, vous pouvez remplacer une définition NULL répertoriée dans les en-têtes. Tout élément inséré serait incompatible avec les objets compilés.

Je définirais plutôt votre propre NULL, "MYPRODUCT_NULL", pour vos propres utilisations et éviterais ou traduirais de / vers la bibliothèque C std.

Doug T.
la source
6

Laissez NULL seul et traitez IO sur le port 0x0000 comme un cas spécial, peut-être en utilisant une routine écrite en assembleur, et donc non soumis à la sémantique C standard. IOW, ne redéfinissez pas NULL, redéfinissez le port 0x00000.

Notez que si vous écrivez ou modifiez un compilateur C, le travail requis pour éviter de déréférencer NULL (en supposant que dans votre cas le CPU n'aide pas) est le même quelle que soit la définition de NULL, il est donc plus facile de laisser NULL défini comme zéro, et assurez-vous que zéro ne peut jamais être déréférencé de C.

Apalala
la source
Le problème ne se posera que lorsque NULL est accédé accidentellement, pas lorsque le port est intentionnellement accédé. Pourquoi devrais-je redéfinir les E / S de port pour ce moment? Cela fonctionne déjà comme il se doit.
Lundin
2
@Lundin Par accident ou non, NULL ne peut être déréférencé que dans un programme C en utilisant *p, p[]ou p(), donc le compilateur n'a besoin de se soucier que de ceux qui protègent le port IO 0x0000.
Apalala
@Lundin La deuxième partie de votre question: une fois que vous restreignez l'accès à l'adresse zéro depuis C, vous avez besoin d'un autre moyen d'atteindre le port 0x0000. Une fonction écrite en assembleur peut le faire. À partir de C, le port peut être mappé à 0xFFFF ou autre, mais il est préférable d'utiliser une fonction et d'oublier le numéro de port.
Apalala
3

Compte tenu de l'extrême difficulté de redéfinir NULL comme mentionné par d'autres, il est peut-être plus facile de redéfinir le déréférencement pour les adresses matérielles bien connues. Lors de la création d'une adresse, ajoutez 1 à chaque adresse connue, de sorte que votre port IO connu soit:

  #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 les adresses qui vous concernent sont regroupées et que vous pouvez être sûr que l'ajout de 1 à l'adresse n'entrera en conflit avec rien (ce qui ne devrait pas dans la plupart des cas), vous pourrez peut-être le faire en toute sécurité. Et puis vous n'avez pas à vous soucier de la reconstruction de votre chaîne d'outils / bibliothèque std et des expressions sous la forme:

  if (pointer)
  {
     ...
  }

travaille toujours

Fou je sais, mais je pensais juste que je lancerais l'idée là-bas :).

Doug T.
la source
Le problème ne se posera que lorsque NULL est accédé accidentellement, pas lorsque le port est intentionnellement accédé. Pourquoi devrais-je redéfinir les E / S de port pour ce moment? Cela fonctionne déjà comme il se doit.
Lundin le
@LundIn Je suppose que vous devez choisir ce qui est le plus douloureux, en modifiant la reconstruction de l'ensemble de la chaîne d'outils ou en modifiant cette partie de votre code.
Doug T.28
2

Le modèle de bits pour le pointeur nul peut ne pas être le même que le modèle de bits pour l'entier 0. Mais le développement de la macro NULL doit être une constante de pointeur nul, c'est-à-dire un entier constant de valeur 0 qui peut être converti en (void *).

Pour obtenir le résultat souhaité tout en restant conforme, vous devrez modifier (ou peut-être configurer) votre chaîne d'outils, mais c'est réalisable.

AProgrammeur
la source
1

Vous demandez des ennuis. La redéfinition NULLà une valeur non nulle cassera ce code:

   if (myPointer)
   {
      // myPointer n'est pas nul
      ...
   }
Tony le poney
la source