Pourrais-je jamais accéder à l’adresse zéro?

La constante 0 est utilisée comme pointeur nul en C et C ++. Mais comme dans la question “Pointeur vers une adresse fixe spécifique “, il semble possible d’atsortingbuer des adresses fixes. Y a-t-il un besoin imaginable, dans un système quelconque, pour une tâche de bas niveau, d’accéder à l’adresse 0?

Si c’est le cas, comment cela est-il résolu avec 0 comme pointeur nul et tout?

Si non, qu’est-ce qui assure qu’il n’y a pas un tel besoin?

Ni dans C, ni dans C ++, la valeur null-pointeur est liée de quelque façon à l’adresse physique 0 . Le fait que vous utilisez la constante 0 dans le code source pour définir un pointeur sur une valeur de pointeur nul n’est rien de plus qu’un morceau de sucre syntaxique . Le compilateur est nécessaire pour le traduire dans l’adresse physique réelle utilisée comme valeur null-pointeur sur la plate-forme spécifique.

En d’autres termes, 0 dans le code source n’a aucune importance physique. Cela aurait pu être 42 ou 13 , par exemple. C’est-à-dire que les auteurs de la langue, s’ils étaient si heureux, auraient pu faire en sorte que vous deviez faire p = 42 pour définir le pointeur p sur une valeur de pointeur nul. Encore une fois, cela ne signifie pas que l’adresse physique 42 devrait être réservée aux pointeurs nuls. Le compilateur serait obligé de traduire le code source p = 42 en code machine pour que la valeur de pointeur physique réelle ( 0x0000 ou 0xBAAD ) soit 0xBAAD dans le pointeur p . C’est exactement comme ça maintenant avec la constante 0 .

Notez également que ni C ni C ++ ne fournissent une fonctionnalité ssortingctement définie qui vous permettrait d’atsortingbuer une adresse physique spécifique à un pointeur. Donc, votre question sur “comment on atsortingbuer 0 adresse à un pointeur” n’a officiellement aucune réponse. Vous ne pouvez simplement pas atsortingbuer une adresse spécifique à un pointeur en C / C ++. Cependant, dans le domaine des fonctionnalités définies par l’implémentation, la conversion explicite de nombre entier à pointeur est censée avoir cet effet. Donc, vous le feriez comme suit

 uintptr_t address = 0; void *p = (void *) address; 

Notez que ce n’est pas la même chose que de faire

 void *p = 0; 

Ce dernier produit toujours la valeur null-pointer, tandis que le premier en général ne le fait pas. Le premier produira normalement un pointeur sur l’adresse physique 0 , qui pourrait ou non être la valeur null-pointeur sur la plate-forme donnée.

Sur une note tangentielle: vous pourriez être intéressé de savoir qu’avec le compilateur C ++ de Microsoft, un pointeur NULL vers membre sera représenté par le modèle de bits 0xFFFFFFFF sur une machine 32 bits. C’est:

 struct foo { int field; }; int foo::*pmember = 0; // 'null' member pointer 

pmember aura le modèle de bit «tous les». C’est parce que vous avez besoin de cette valeur pour la distinguer de

 int foo::*pmember = &foo::field; 

où le modèle de bits sera en effet par “tous les zéros” – puisque nous voulons décaler 0 dans la structure foo.

Les autres compilateurs C ++ peuvent choisir un modèle de bit différent pour un pointeur null sur member, mais la principale observation est que ce ne sera pas le modèle de bit à zéros que vous attendiez.

Vous partez d’une prémisse erronée. Lorsque vous affectez une constante entière avec la valeur 0 à un pointeur, cela devient une constante de pointeur nul. Cela ne signifie toutefois pas qu’un pointeur nul fait nécessairement référence à l’adresse 0. Bien au contraire, les normes C et C ++ sont toutes deux très claires: un pointeur nul peut désigner une adresse autre que zéro.

Voici ce que cela implique: vous devez mettre de côté une adresse à laquelle un pointeur nul se réfère – mais il peut s’agir essentiellement de n’importe quelle adresse que vous choisissez. Lorsque vous convertissez zéro en pointeur, il doit se référer à l’adresse choisie – mais c’est tout ce qui est vraiment nécessaire. Par exemple, si vous décidiez que convertir un entier en un point impliquerait d’append 0x8000 à l’entier, alors le pointeur nul à fait référence à l’adresse 0x8000 au lieu de l’adresse 0.

Il convient également de noter que le fait de déréférencer un pointeur nul entraîne un comportement indéfini. Cela signifie que vous ne pouvez pas le faire en code portable , mais cela ne signifie pas que vous ne pouvez pas le faire du tout. Lorsque vous écrivez du code pour de petits microcontrôleurs, il est assez courant d’inclure des morceaux de code qui ne sont pas portables du tout. La lecture à partir d’une adresse peut vous donner la valeur d’un capteur, tandis que l’écriture à la même adresse peut activer un moteur pas à pas (par exemple). L’appareil suivant (même en utilisant exactement le même processeur) pourrait être connecté, ces deux adresses étant alors référées à la mémoire RAM normale.

Même si un pointeur null fait référence à l’adresse 0, cela ne vous empêche pas de l’utiliser pour lire et / ou écrire tout ce qui se trouve à cette adresse – cela vous empêche simplement de le faire de manière portable – mais cela ne vous empêche pas importe vraiment beaucoup. La seule raison pour laquelle l’adresse zéro serait normalement importante serait si elle était décodée pour se connecter à autre chose qu’un stockage normal, de sorte que vous ne pourrez probablement pas l’utiliser de manière totalement portable.

Le compilateur s’en charge pour vous ( comp.lang.c FAQ ):

Si une machine utilise un modèle de bit non nul pour les pointeurs nuls, le compilateur doit le générer lorsque le programmeur le demande, en écrivant “0” ou “NULL”, un pointeur nul. Par conséquent, #définir NULL à 0 sur une machine pour laquelle les pointeurs NULL internes ne sont pas nuls est aussi valable que sur tous les autres, car le compilateur doit (et peut) générer les pointeurs NULL corrects en réponse à des 0 non apparents.

Vous pouvez arriver à adresser zéro en référençant zéro à partir d’un contexte non-pointeur.

En pratique, les compilateurs C laisseront volontiers votre programme tenter d’écrire à l’adresse 0. La vérification de chaque opération au moment de l’exécution pour un pointeur NULL serait un peu coûteuse. Sur les ordinateurs, le programme se bloquera car le système d’exploitation l’interdit. Sur les systèmes embarqués sans protection de la mémoire, le programme écrira en effet à l’adresse 0, ce qui provoquera souvent le blocage de l’ensemble du système.

L’adresse 0 peut être utile sur un système embarqué (un terme général pour un processeur qui n’est pas sur un ordinateur; ils exécutent tout de votre chaîne stéréo à votre appareil photo numérique). Habituellement, les systèmes sont conçus pour que vous n’ayez pas besoin d’écrire à l’adresse 0. Dans tous les cas que je connais, c’est une sorte d’adresse spéciale. Même si le programmeur doit lui écrire (par exemple, pour configurer une table d’interruption), il lui suffira de lui écrire pendant la séquence de démarrage initiale (généralement un langage d’assemblage court pour configurer l’environnement pour C).

L’adresse mémoire 0 est également appelée la page zéro . Ceci est rempli par le BIOS et contient des informations sur le matériel exécuté sur votre système. Tous les kernelx modernes protègent cette région de la mémoire. Vous ne devriez jamais avoir besoin d’accéder à cette mémoire, mais si vous voulez, vous devez le faire depuis le kernel, un module du kernel fera l’affaire.

Sur le x86, l’adresse 0 (ou plutôt 0000: 0000) et sa proximité en mode réel est l’emplacement du vecteur d’interruption. Dans le mauvais vieux temps, vous écrivez généralement des valeurs dans le vecteur d’interruption pour installer les gestionnaires d’interruption (ou si vous êtes plus discipliné, utilisez le service MS-DOS 0x25). Les compilateurs C pour MS-DOS définissaient un type de pointeur distant qui, lorsqu’il était assigné NULL ou 0, recevrait le modèle de bit 0000 dans sa partie de segment et 0000 dans sa partie de décalage.

Bien sûr, un programme incohérent qui a accidentellement écrit sur un pointeur distant dont la valeur était 0000: 0000 entraînerait de très graves problèmes sur la machine, en le verrouillant généralement et en forçant un redémarrage.

Dans la question du lien, les gens discutent de la mise aux adresses fixes dans un microcontrôleur . Lorsque vous programmez un microcontrôleur, tout est beaucoup plus bas.

Vous n’avez même pas de système d’exploitation en termes de PC de bureau / serveur, et vous n’avez pas de mémoire virtuelle et tout ça. Donc, il est bien et même nécessaire d’accéder à la mémoire à une adresse spécifique. Sur un ordinateur de bureau / serveur moderne, il est inutile et même dangereux.

J’ai compilé du code en utilisant gcc pour le Motorola HC11, qui n’a pas de MMU et 0 est une très bonne adresse, et j’ai été déçu de découvrir que pour écrire à l’adresse 0, vous écrivez simplement. Il n’y a pas de différence entre NULL et adresse 0.

Et je peux voir pourquoi. Je veux dire, il n’est pas vraiment possible de définir un NULL unique sur une architecture où chaque emplacement de mémoire est potentiellement valide, donc je suppose que les auteurs de gcc ont dit que 0 était suffisant pour NULL, que ce soit une adresse valide ou non.

  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 
1f: ed 05 std 5,x

Lorsque je le compare avec un autre pointeur, le compilateur génère une comparaison régulière. Ce qui signifie que char *null = 0 n’est en aucun cas un pointeur NULL spécial et qu’un pointeur sur l’adresse 0 et un pointeur “NULL” seront égaux.

 ; 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 
39: ec 07 ldd 7,x 3b: 18 de 00 ldy *0
3e: cd a3 05 cpd 5,y 41: 26 10 bne 53 <.LM7>

Donc, pour répondre à la question initiale, je pense que ma réponse est de vérifier l’implémentation de votre compilateur et de savoir s’ils ont même pris la peine d’implémenter une valeur NULL unique. Sinon, vous n’avez pas à vous en soucier. 😉

(Bien sûr, cette réponse n’est pas conforme aux normes.)

Tout dépend si la machine dispose d’une mémoire virtuelle. Les systèmes sur lesquels il est installé vont généralement y placer une page insaisissable, ce qui est probablement le comportement auquel vous êtes habitué. Cependant, dans les systèmes qui en sont dépourvus (en général, les microcontrôleurs actuels, mais ils étaient beaucoup plus courants), il y a souvent des choses très intéressantes dans ce domaine, comme la table des interruptions. Je me souviens avoir piraté ces choses à l’époque des systèmes 8 bits; amusant et pas trop difficile quand il fallait réinitialiser le système et recommencer. 🙂

Oui, vous pouvez vouloir accéder à l’adresse mémoire 0x0h. Pourquoi vous voulez le faire dépend de la plate-forme. Un processeur peut l’utiliser pour un vecteur de réinitialisation, de sorte que l’écriture entraîne la réinitialisation du processeur. Il pourrait également être utilisé pour un vecteur d’interruption, en tant qu’interface mappée en mémoire vers une ressource matérielle (compteur de programme, horloge système, etc.), ou il pourrait même être valide en tant qu’ancienne adresse mémoire. Il n’y a rien de magique à propos de l’adresse mémoire zéro, c’est juste une adresse qui a été historiquement utilisée à des fins spéciales (réinitialiser les vecteurs et autres). Les langages de type C suivent cette tradition en utilisant zéro comme adresse pour un pointeur NULL, mais en réalité, le matériel sous-jacent peut ou non voir l’adresse zéro comme étant spéciale.

La nécessité d’accéder à l’adresse zéro ne survient généralement que dans les détails de bas niveau tels que les chargeurs de démarrage ou les pilotes. Dans ces cas, le compilateur peut fournir des options / pragmas pour comstackr une section de code sans optimisations (pour éviter que le pointeur zéro ne soit extrait en tant que pointeur NULL) ou l’assemblage en ligne peut être utilisé pour accéder à la véritable adresse zéro.

C / C ++ ne vous permet pas d’écrire à une adresse. C’est le système d’exploitation qui peut générer un signal lorsqu’un utilisateur accède à une adresse interdite. C et C ++ vous assurent que toute la mémoire obtenue à partir du tas sera différente de 0.

J’ai parfois utilisé des charges de l’adresse zéro (sur une plate-forme connue où il serait garanti que le défaut segfault) de se bloquer délibérément sur un symbole nommé de manière informative dans le code de la bibliothèque si l’utilisateur ne respecte pas certaines conditions une exception à ma disposition. ” Segfault at someFunction$xWasnt16ByteAligned ” est un message d’erreur assez efficace pour alerter quelqu’un de ce qu’il a fait de mal et comment le réparer. Cela dit, je ne recommanderais pas de prendre l’habitude de ce genre de chose.

L’écriture à l’adresse zéro peut être effectuée, mais cela dépend de plusieurs facteurs tels que votre système d’exploitation, l’architecture cible et la configuration MMU. En fait, cela peut être un outil de débogage utile (mais pas toujours).

Par exemple, il y a quelques années, lorsque nous travaillions sur un système embarqué (avec peu d’outils de débogage disponibles), nous avions un problème qui entraînait un redémarrage à chaud. Pour aider à localiser le problème, nous avons débogué en utilisant sprintf (NULL, …); et un câble série à 9600 bauds. Comme je l’ai dit, peu d’outils de débogage sont disponibles. Avec notre configuration, nous savions qu’un redémarrage à chaud ne corromprait pas les 256 premiers octets de mémoire. Ainsi, après le redémarrage à chaud, nous avons pu mettre en pause le chargeur et vider le contenu de la mémoire pour savoir ce qui s’est passé avant le redémarrage.

Rappelez-vous que dans tous les cas normaux, vous ne voyez pas d’adresses spécifiques. Lorsque vous allouez de la mémoire, le système d’exploitation vous fournit l’adresse de ce bloc de mémoire.

Lorsque vous prenez la référence d’une variable, la variable a déjà été allouée à une adresse déterminée par le système.

Donc, accéder à l’adresse zéro n’est pas vraiment un problème, car lorsque vous suivez un pointeur, vous ne vous souciez pas de l’adresse vers laquelle il pointe, mais seulement de sa validité:

 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 

Donc, si vous avez besoin d’accéder à l’adresse zéro, cela fonctionnera généralement très bien.

La chose nulle 0 == ne devient vraiment un problème que si, pour une raison quelconque, vous accédez directement à la mémoire physique. Peut-être écrivez-vous un kernel de système d’exploitation ou quelque chose comme ça vous-même. Dans ce cas, vous allez écrire sur des adresses mémoire spécifiques (en particulier celles mappées sur des registres matériels), et vous devrez peut-être écrire sur l’adresse zéro. Mais alors, vous ignorez vraiment C ++ et vous vous fiez aux spécificités de votre compilateur et de votre plate-forme matérielle.

Bien sûr, si vous devez écrire sur l’adresse zéro, c’est possible. Seule la constante 0 représente un pointeur nul. La valeur entière non constante zéro ne sera pas, si elle est affectée à un pointeur, donner un pointeur nul.

Donc, vous pourriez simplement faire quelque chose comme ceci:

 int i = 0; int* zeroaddr = (int*)i; 

maintenant zeroaddr pointera sur l’adresse zéro (*), mais ce ne sera pas, à proprement parler, un pointeur nul, car la valeur zéro n’était pas constante.

(*): ce n’est pas tout à fait vrai. Le standard C ++ garantit uniquement un “mappage défini par l’implémentation” entre les entiers et les adresses. Il pourrait convertir le 0 à l’adresse 0x1633de20` ou toute autre adresse qu’il aime. Mais le mapping est généralement intuitif et évident, où l’entier 0 est mappé à l’adresse zéro)

Si je me souviens bien, dans un microcontrôleur AVR, le fichier de registre est mappé dans un espace adresse de la RAM et le registre R0 est à l’adresse 0x00. C’était clairement fait dans un but et apparemment Atmel pense qu’il y a des situations, quand il est commode d’accéder à l’adresse 0x00 au lieu d’écrire explicitement R0.

Dans la mémoire du programme, à l’adresse 0x0000, il y a un vecteur d’interruption de réinitialisation et cette adresse est clairement destinée à être accédée lors de la programmation de la puce.

Cela peut surprendre beaucoup de gens, mais dans le langage C, il n’existe pas de pointeur null spécial. Vous êtes totalement libre de lire et d’écrire à l’adresse 0 si c’est physiquement possible.

Le code ci-dessous ne comstack même pas, car NULL n’est pas défini:

 int main(int argc, char *argv[]) { void *p = NULL; return 0; } 

OTOH, le code ci-dessous comstack, et vous pouvez lire et écrire l’adresse 0, si le matériel / le système d’exploitation le permet:

 int main(int argc, char *argv[]) { int *p = 0; *p = 42; int x = *p; /* let's assume C99 */ } 

Veuillez noter que je n’ai rien inclus dans les exemples ci-dessus. Si nous commençons à inclure des éléments de la bibliothèque C standard, NULL devient magiquement défini. Pour autant que je me souvienne, il vient de ssortingng.h .

NULL n’est toujours pas une fonctionnalité du kernel C, c’est une convention de nombreuses fonctions de la bibliothèque C pour indiquer l’invalidité des pointeurs. La bibliothèque C sur la plate-forme donnée définira NULL sur un emplacement mémoire qui n’est pas accessible de toute façon. Essayons sur un PC Linux:

 #include  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; } 

Le résultat est:

 NULL is address 0x0 Segmentation fault (core dumped) 

Donc, notre bibliothèque C définit NULL pour adresser zéro, ce qui s’avère inaccessible. Mais ce n’était pas le compilateur C, pas même la fonction printf() bibliothèque C qui traitait spécialement l’adresse zéro. Ils ont tous essayé de travailler avec elle normalement. C’est le système d’exploitation qui a détecté une erreur de segmentation lorsque printf tenté de lire l’adresse zéro.