Que se passe-t-il dans le système d’exploitation lorsque nous déréférencons un pointeur NULL dans C?

Disons qu’il y a un pointeur et que nous l’initialisons avec NULL.

int* ptr = NULL; *ptr = 10; 

Maintenant, le programme se ptr car ptr ne pointe vers aucune adresse et nous lui atsortingbuons une valeur, qui est un access non valide. Donc, la question est: qu’est-ce qui se passe en interne dans le système d’exploitation? Un défaut de page / segmentation se produit-il? Le kernel va-t-il même chercher dans la table de pages? Ou le crash se produit avant cela?

Je sais que je ne ferais pas une telle chose dans aucun programme, mais juste pour savoir ce qui se passe en interne dans le système d’exploitation ou le compilateur dans un tel cas. Et ce n’est PAS une question en double.

Réponse courte : cela dépend de nombreux facteurs, notamment le compilateur, l’architecture du processeur, le modèle de processeur spécifique et le système d’exploitation.

Réponse longue (x86 et x86-64) : Passons au niveau le plus bas: le processeur. Sur x86 et x86-64, ce code sera généralement compilé en une séquence d’instructions ou d’instructions comme celle-ci:

 movl $10, 0x00000000 

Qui dit “stocke le nombre entier constant 10 à l’adresse de mémoire virtuelle 0”. Les manuels des développeurs de logiciels Intel® 64 et IA-32 Architectures décrivent en détail ce qui se passe lorsque cette instruction est exécutée. Je vais donc la résumer pour vous.

Le processeur peut fonctionner dans plusieurs modes différents, dont plusieurs sont compatibles avec des processeurs beaucoup plus anciens. Les systèmes d’exploitation modernes exécutent du code de niveau utilisateur dans un mode appelé mode protégé , qui utilise la pagination pour convertir les adresses virtuelles en adresses physiques.

Pour chaque processus, le système d’exploitation conserve une table de pages qui dicte comment les adresses sont mappées. La table de pages est stockée en mémoire dans un format spécifique (et protégé de manière à ce qu’elle ne puisse pas être modifiée par le code utilisateur) que le processeur comprend. Pour chaque access mémoire qui se produit, le processeur le traduit en fonction de la table de pages. Si la traduction réussit, elle exécute la lecture / écriture correspondante à l’emplacement de la mémoire physique.

Les choses intéressantes se produisent lorsque la traduction d’adresse échoue. Toutes les adresses ne sont pas valides et si un access mémoire génère une adresse non valide, le processeur déclenche une exception de défaillance de page . Cela déclenche une transition du mode utilisateur (aka niveau de privilège actuel (CPL) 3 sur x86 / x86-64) en mode kernel (aka CPL 0) à un emplacement spécifique dans le code du kernel, tel que défini par la table de descripteurs d’interruption (IDT) .

Le kernel reprend le contrôle et, en fonction des informations provenant de l’exception et de la table de pages du processus, détermine ce qui s’est passé. Dans ce cas, il se rend compte que le processus de niveau utilisateur a accédé à un emplacement de mémoire non valide, puis il réagit en conséquence. Sous Windows, il appelle la gestion des exceptions structurée pour permettre au code utilisateur de gérer l’exception. Sur les systèmes POSIX, le système d’exploitation fournira un signal SIGSEGV au processus.

Dans d’autres cas, le système d’exploitation traitera la défaillance de la page en interne et redémarrera le processus à partir de son emplacement actuel, comme si rien ne s’était passé. Par exemple, des pages de garde sont placées au bas de la stack pour permettre à la stack de croître à la demande jusqu’à une limite, au lieu de préallouer une grande quantité de mémoire à la stack. Des mécanismes similaires sont utilisés pour obtenir une mémoire de copie sur écriture .

Dans les systèmes d’exploitation modernes, les tables de pages sont généralement configurées pour que l’adresse 0 soit une adresse virtuelle non valide. Mais il est parfois possible de changer cela, par exemple sous Linux en écrivant 0 dans le pseudo /proc/sys/vm/mmap_min_addr , après quoi il est possible d’utiliser mmap(2) pour mapper l’adresse virtuelle 0. le pointeur ne provoquerait pas de faute de page.

La discussion ci-dessus concerne tout ce qui se passe lorsque le code d’origine est exécuté dans l’espace utilisateur. Mais cela pourrait aussi se produire dans le kernel. Le kernel peut (et est certainement beaucoup plus susceptible que le code utilisateur) de mapper l’adresse virtuelle 0, donc un tel access mémoire serait normal. Mais si ce n’est pas mappé, alors ce qui se passe alors est en grande partie similaire: le CPU déclenche une erreur de défaut de page qui intercepte un point prédéfini sur le kernel, le kernel examine ce qui s’est passé et réagit en conséquence. Si le kernel ne peut pas récupérer de l’exception, il panique généralement d’une certaine manière ( panique du kernel, oops du kernel ou BSOD sous Windows, par exemple) en imprimant des informations de débogage sur la console ou le port série, puis en les arrêtant.

Voir aussi Beaucoup de bruit pour NULL: Exploiter un déréférencement NULL du kernel pour un exemple de la manière dont un attaquant pourrait exploiter un bogue de déréférencement du pointeur null depuis le kernel afin de gagner des privilèges root sur une machine Linux.

Pour souligner les différences d’architecture, un système d’exploitation développé et mis à jour par une société connue sous le nom d’acronyme de trois lettres et souvent appelé «grande couleur primaire» a une détermination NULL extrêmement complexe.

Ils utilisent un espace d’adressage linéaire de 128 bits pour TOUTES les données (mémoire ET disque) en une seule “chose” géante. Conformément à leur système d’exploitation, un pointeur “valide” doit être placé sur une limite de 128 bits dans cet espace d’adressage. Ceci, bien sûr, provoque des effets secondaires fascinants pour les structures, emballées ou non, qui contiennent des pointeurs. De toute façon, caché dans une page dédiée par processus est un bitmap qui assigne un bit pour chaque emplacement valide dans un espace d’adressage de processus où un pointeur valide peut être placé. TOUS les opcodes de leur matériel et de leur système d’exploitation qui peuvent générer et renvoyer une adresse mémoire valide et l’assigner à un pointeur définiront le bit qui représente l’adresse mémoire où se trouve ce pointeur (le pointeur cible).

Alors, pourquoi devrait-on s’en soucier? Pour cette simple raison:

 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. } 

Ce qui est vraiment intéressant, c’est ceci.

 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. } 

C’est quelque chose que vous devez voir pour y croire. Je ne peux même pas imaginer que le ménage soit fait pour maintenir ce bitmap, en particulier lors de la copie de valeurs de pointeur ou de la libération de mémoire dynamic.

Sur les CPU prenant en charge la mémoire virtuelle, une exception de défaillance de page sera généralement émise si vous essayez de lire à l’adresse de mémoire 0x0 . Le gestionnaire de pannes de la page du système d’exploitation sera appelé, le système d’exploitation décidera alors que la page n’est pas valide et abandonne votre programme.

Notez que sur certains CPU, vous pouvez également accéder en toute sécurité à l’adresse mémoire 0x0 .

Comme le standard C dit que le déréférencement d’un pointeur NULL n’est pas défini, si le compilateur est capable de détecter à la compilation (ou à l’exécution) que vous supprimez un pointeur NULL, il peut abandonner le programme avec un message d’erreur détaillé .

(C99, 6.5.3.2.p4) “Si une valeur invalide a été affectée au pointeur, le comportement de l’opérateur unaire * est indéfini.87)”

87): “Parmi les valeurs non valides de déréférencement d’un pointeur par l’opérateur unaire * figurent un pointeur nul, une adresse mal alignée pour le type d’object pointé et l’adresse d’un object après la fin de sa durée de vie.”

Dans un cas typique , int *ptr = NULL; définira ptr sur l’adresse 0. Le standard C (et le standard C ++) fait très attention à ne pas en avoir besoin, mais il est néanmoins extrêmement fréquent.

Quand vous faites *ptr = 10; , le processeur génèrerait normalement 0 sur les lignes d’adresse et 10 sur les lignes de données, tout en définissant une ligne R / W pour indiquer une écriture (et, si le bus a une telle chose, affirmer la ligne mémoire vs E / S) pour indiquer une écriture dans la mémoire, pas I / O).

En supposant que le processeur supporte la protection de la mémoire (et que vous utilisez un système d’exploitation qui le permet), le processeur vérifie cet access (tenté) avant que cela ne se produise. Par exemple, un processeur Intel / AMD moderne utilisera des tables de pagination qui mappent des adresses virtuelles avec des adresses physiques. Dans un cas typique, l’adresse 0 ne sera associée à aucune adresse physique. Dans ce cas, le processeur générera une exception de violation d’access. Par exemple, Microsoft Windows laisse les 4 premiers mégaoctets non mappés, de sorte que toute adresse dans cette plage entraînera normalement une violation d’access.

Sur un ancien CPU (ou un ancien système d’exploitation qui n’active pas les fonctionnalités de protection du processeur), la tentative d’écriture réussira souvent. Par exemple, sous MS-DOS, l’écriture via un pointeur NULL consiste simplement à écrire l’adresse zéro. Dans les petits et moyens modèles (avec des adresses de 16 bits pour les données), la plupart des compilateurs écrivent un motif connu sur les premiers octets du segment de données et, à la fin du programme, vérifient si ce modèle rest intact (et faire quelque chose pour indiquer que vous avez écrit via un pointeur NULL en cas d’échec). En modèle compact ou grand format (adresses de données 20 bits), ils écrivent généralement à l’adresse zéro sans avertissement.

J’imagine que cela dépend de la plateforme et du compilateur. Le pointeur NULL pourrait être implémenté en utilisant une page NULL, auquel cas vous auriez un défaut de page, ou il pourrait être inférieur à la limite de segment pour un segment d’expansion, auquel cas vous auriez une erreur de segmentation.

Ce n’est pas une réponse définitive, juste ma conjecture.