Pourquoi un programme qui accède à un pointeur illégal vers un pointeur ne plante-t-il pas?

Un programme accédant de pointeur illégal à pointeur ne plante pas avec SIGSEGV. Ce n’est pas une bonne chose, mais je me demande comment cela pourrait être et comment le processus a survécu plusieurs jours en production. C’est ahurissant pour moi.

J’ai essayé ce programme sous Windows, Linux, OpenVMS et Mac OS et ils ne se sont jamais plaints.

#include  #include  void printx(void *rec) { // I know this should have been a ** char str[1000]; memcpy(str, rec, 1000); printf("%*.s\n", 1000, str); printf("Whoa..!! I have not crashed yet :-P"); } int main(int argc, char **argv) { void *x = 0; // you could also say void *x = (void *)10; printx(&x); } 

Je ne suis pas surpris par l’absence d’un défaut de mémoire. Le programme ne déréférence pas un pointeur non initialisé. Au lieu de cela, il copie et imprime le contenu de la mémoire en commençant par une variable de pointeur, et les 996 (ou 992) octets au-delà.

Comme le pointeur est une variable de stack, il imprime la mémoire près du haut de la stack pendant un certain temps. Cette mémoire contient le cadre de stack de main() : éventuellement des valeurs de registre sauvegardées, un nombre d’arguments de programme, un pointeur sur les arguments de programme, un pointeur sur une liste de variables d’environnement et un registre d’instructions sauvegardé que main() doit renvoyer , généralement dans le code de démarrage de la bibliothèque d’exécution C. Dans toutes les mises en œuvre que j’ai examinées, les frameworks de stack ci-dessous contiennent des copies des variables d’environnement elles-mêmes, un tableau de pointeurs sur celles-ci et un tableau de pointeurs sur les arguments du programme. Dans les environnements Unix (que vous indiquez que vous utilisez), les chaînes d’arguments du programme seront inférieures à celle-ci.

Toute cette mémoire est “sécurisée” pour l’impression, à l’exception de quelques caractères non imprimables qui risquent de gâcher un terminal d’affichage.

Le principal problème potentiel est de savoir s’il y a suffisamment de mémoire de stack allouée et mappée pour empêcher un SIGSEGV lors d’un access. Une défaillance de segment peut se produire si les données d’environnement sont trop peu nombreuses. Ou si l’implémentation place ces données ailleurs afin qu’il ne rest que quelques mots de stack. Je suggère de confirmer cela en nettoyant les variables d’environnement et en réexécutant le programme.

Ce code ne serait pas si inoffensif si l’une des conventions d’exécution C n’était pas vraie:

  • L’architecture utilise une stack
  • Une variable locale ( void *x ) est allouée sur la stack
  • La stack se développe vers la mémoire numérotée inférieure
  • Les parameters sont passés sur la stack
  • Si main() est appelé avec des arguments. (Certains environnements légers, tels que les processeurs intégrés, invoquent main() sans parameters.)

Dans toutes les mises en œuvre modernes, toutes ces choses sont généralement vraies.

L’access illégal à la mémoire est un comportement indéfini . Cela signifie que votre programme peut se bloquer, mais que cela n’est pas garanti, car le comportement exact n’est pas défini .

(Une blague parmi les développeurs, en particulier face à des collègues insouciants, est que “appeler un comportement indéfini peut formater votre disque dur, ce n’est tout simplement pas garanti”. ;-))

Mise à jour: Il y a une discussion chaude en cours ici. Oui, les développeurs de systèmes devraient savoir ce qui se passe réellement sur un système donné . Mais ces connaissances sont liées au processeur, au système d’exploitation, au compilateur, etc., et ont en général une utilité limitée, car même si vous faites fonctionner le code, sa qualité rest médiocre. C’est pourquoi j’ai limité ma réponse au point le plus important et à la question posée (“pourquoi ce crash ne survient-il pas”):

Le code affiché dans la question n’a pas de comportement bien défini, mais cela signifie simplement que vous ne pouvez pas vraiment compter sur ce qu’il fait, pas que cela devrait planter.

Si vous déréférencez un pointeur non valide, vous invoquez un comportement indéfini. Ce qui signifie que le programme peut tomber en panne, qu’il peut fonctionner, qu’il peut préparer du café, peu importe.

Lorsque vous avez

 int main(int argc, char **argv) { void *x = 0; // you could also say void *x = (void *)10; printx(&x); } 

Vous déclarez x tant que pointeur avec la valeur 0 et ce pointeur réside dans la stack puisqu’il s’agit d’une variable locale. Maintenant, vous printx à printx l’ adresse de x , ce qui signifie qu’avec

 memcpy(str, rec, 1000); 

vous copiez des données depuis le dessus de la stack (ou en fait depuis la stack elle-même) vers la stack (car l’adresse du pointeur de la stack diminue à chaque envoi). Les données source sont susceptibles d’être couvertes par la même entrée de table de pages, car vous ne copiez que 1 000 octets. Par conséquent, vous ne recevez aucune erreur de segmentation. Cependant, en fin de compte, comme cela a déjà été écrit, nous parlons de comportement non défini.

Si vous écrivez dans une zone inachevée, il risque fort de s’écraser. Mais vous lisez , ça peut aller. Mais le comportement ne sera toujours pas défini.