web-dev-qa-db-fra.com

Pourquoi les littéraux de chaîne C sont-ils en lecture seule?

Quel (s) avantage (s) des littéraux de chaîne étant en lecture seule justifient (-ies/-ied):

  1. Encore une autre façon de se tirer une balle dans le pied

    char *foo = "bar";
    foo[0] = 'd'; /* SEGFAULT */
    
  2. Incapacité à initialiser avec élégance un tableau de lecture-écriture de mots sur une seule ligne:

    char *foo[] = { "bar", "baz", "running out of traditional placeholder names" };
    foo[1][2] = 'n'; /* SEGFAULT */ 
    
  3. Compliquant la langue elle-même.

    char *foo = "bar";
    char var[] = "baz";
    some_func(foo); /* VERY DANGEROUS! */
    some_func(var); /* LESS DANGEROUS! */
    

Économie de mémoire? J'ai lu quelque part (je n'ai pas trouvé la source maintenant) il y a si longtemps, quand RAM était rare, les compilateurs ont essayé d'optimiser l'utilisation de la mémoire en fusion de chaînes similaires.

Par exemple, "more" et "regex" deviendraient "moregex". Est-ce encore vrai aujourd'hui, à l'ère des films numériques de qualité Blu-ray? Je comprends que les systèmes embarqués fonctionnent toujours dans un environnement de ressources limitées, mais la quantité de mémoire disponible a quand même considérablement augmenté.

Problèmes de compatibilité? Je suppose qu'un programme hérité qui tenterait d'accéder à la mémoire en lecture seule planterait ou continuerait avec un bogue non découvert. Par conséquent, aucun programme hérité ne devrait essayer d'accéder au littéral de chaîne et, par conséquent, permettre d'écrire dans le littéral de chaîne ne nuirait pas aux programmes hérités valides, non piratés et portables .

Y a-t-il d'autres raisons? Mon raisonnement est-il incorrect? Serait-il raisonnable d'envisager une modification des littéraux de chaîne en lecture-écriture dans les nouvelles normes C ou au moins d'ajouter une option au compilateur? Cela a-t-il été envisagé auparavant ou mes "problèmes" sont-ils trop mineurs et insignifiants pour déranger qui que ce soit?

28
Marius Macijauskas

Historiquement (peut-être en réécrivant certaines parties), c'était le contraire. Sur les tout premiers ordinateurs du début des années 1970 (peut-être PDP-11 ) exécutant un C embryonnaire prototypique (peut-être [ ~ # ~] bcpl [~ # ~] ) il n'y avait pas [~ # ~] mmu [~ # ~] et pas de protection de la mémoire (qui existait sur les plus anciennes IBM/360 mainframes). Ainsi, chaque octet de mémoire (y compris ceux gérant les chaînes littérales ou le code machine) pourrait être écrasé par un programme erroné (imaginez un programme changeant certains % En / Dans un printf (3) chaîne de format). Par conséquent, les chaînes et constantes littérales étaient accessibles en écriture.

Adolescent en 1975, j'ai codé au musée du Palais de la Découverte à Paris sur des ordinateurs anciens des années 1960 sans protection mémoire: IBM/1620 n'avait qu'un mémoire de base - que vous pouvez initialiser via le clavier, vous avez donc dû taper plusieurs dizaines de chiffres pour lire le programme initial sur des bandes perforées; CAB/500 avait une mémoire à tambour magnétique; vous pouvez désactiver l'écriture de certaines pistes via des commutateurs mécaniques près du tambour.

Plus tard, les ordinateurs ont obtenu une forme d'unité de gestion de la mémoire (MMU) avec une certaine protection de la mémoire. Il y avait un périphérique interdisant au CPU d'écraser une sorte de mémoire. Ainsi, certains segments de mémoire, notamment le segment de code (aka .text Segment) sont devenus en lecture seule (sauf par le système d'exploitation qui les a chargés du disque). Il était naturel pour le compilateur et l'éditeur de liens de placer les chaînes littérales dans ce segment de code, et les chaînes littérales sont devenues en lecture seule. Lorsque votre programme a essayé de les écraser, c'était mauvais, un comportement indéfini . Et avoir un segment de code en lecture seule dans la mémoire virtuelle donne un avantage significatif: plusieurs processus exécutant le même programme partagent le même [~ # ~] ram [~ # ~] ( mémoire physique pages) pour ce segment de code ( voir l'indicateur MAP_SHARED pour mmap (2) sous Linux).

Aujourd'hui, les microcontrôleurs bon marché ont une mémoire morte (par exemple leur Flash ou ROM), et y conserver leur code (et les chaînes littérales et autres constantes). Et les vrais microprocesseurs (comme celui de votre tablette, ordinateur portable ou de bureau) ont une unité de gestion de mémoire sophistiquée et cache des machines utilisées pour mémoire virtuelle & pagination . Ainsi, le segment de code du programme exécutable (par exemple dans [~ # ~] elf [~ # ~] ) est la mémoire mappée en tant que segment en lecture seule, partageable et exécutable (par mmap (2) ou execve (2) sous Linux; BTW vous pouvez donner des directives à ld pour obtenir un segment de code accessible en écriture si vous le vouliez vraiment). L'écrire ou en abuser est généralement un défaut de segmentation .

Le standard C est donc baroque: légalement (uniquement pour des raisons historiques), les chaînes littérales ne sont pas des tableaux const char[], Mais uniquement des tableaux char[] Dont l'écrasement est interdit.

BTW, peu de langues actuelles autorisent l'écrasement des littéraux de chaîne (même Ocaml qui, historiquement et mal) avait des chaînes littérales inscriptibles, a récemment changé ce comportement en 4.02, et a maintenant des chaînes en lecture seule).

Les compilateurs C actuels peuvent optimiser et avoir "ions" Et "expressions" Partagent leurs 5 derniers octets (y compris l'octet nul final).

Essayez de compiler votre code C dans le fichier foo.c Avec gcc -O -fverbose-asm -S foo.c Et regardez à l'intérieur du fichier assembleur généré foo.s Par [~ # ~] gcc [~ # ~]

Enfin, la sémantique de C est suffisamment complexe (en savoir plus sur CompCert & Frama-C qui essaient de le capturer) et l'ajout de chaînes littérales constantes inscriptibles le rendrait encore plus obscur tout en rendant les programmes plus faibles et encore moins sécurisés (et avec un comportement moins défini), il est donc très peu probable que les futures normes C acceptent des chaînes littérales inscriptibles. Peut-être qu'au contraire, ils feraient d'eux const char[] Des tableaux comme ils devraient être moralement.

Notez également que pour de nombreuses raisons, les données mutables sont plus difficiles à gérer par l'ordinateur (cohérence du cache), à ​​coder, à comprendre par le développeur, que les données constantes. Il est donc préférable que la plupart de vos données (et notamment les chaînes littérales) restent immuables . En savoir plus sur la programmation fonctionnellele paradigme .

Dans les anciens jours Fortran77 sur IBM/7094, un bogue pouvait même changer une constante: si vous CALL FOO(1) et si FOO arrivaient à modifier son argument passé par référence à 2, l'implémentation pourrait avoir changé d'autres occurrences de 1 en 2, et c'était un bug vraiment méchant, assez difficile à trouver.

40

Les compilateurs ne pouvaient pas combiner "more" et "regex", car le premier a un octet nul après le e tandis que le second a un x, mais de nombreux compilateurs combineraient des littéraux de chaîne qui correspondaient parfaitement, et certains correspondraient également à des littéraux de chaîne qui partageaient une queue commune. Le code qui change un littéral de chaîne peut donc changer un littéral de chaîne différent qui est utilisé à des fins entièrement différentes mais qui contient les mêmes caractères.

Un problème similaire se poserait dans FORTRAN avant l'invention de C. Les arguments étaient toujours transmis par adresse plutôt que par valeur. Une routine pour ajouter deux nombres serait donc équivalente à:

float sum(float *f1, float *f2) { return *f1 + *f2; }

Dans le cas où l'on voudrait passer une valeur constante (par exemple 4.0) à sum, le compilateur créerait une variable anonyme et l'initierait à 4.0. Si la même valeur était transmise à plusieurs fonctions, le compilateur transmettrait la même adresse à toutes. Par conséquent, si une fonction qui modifiait l'un de ses paramètres passait une constante à virgule flottante, la valeur de cette constante ailleurs dans le programme pourrait être modifiée en conséquence, conduisant ainsi au dicton "Les variables ne le seront pas; les constantes ne sont pas 't ".

2
supercat

Considérez un programme comme grep. De nombreuses constantes littérales sont des messages d'erreur. En les considérant comme immuables et faisant partie du segment de code, une machine multi-utilisateur peut utiliser le segment de code identique dans RAM pour des dizaines de les utilisateurs exécutant tous grep simultanément, même de manière asynchrone.

0
Ross Presser