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

29

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! */

Économiser de la mémoire? J'ai lu quelque part (je n'ai pas pu trouver la source maintenant) il y a longtemps, lorsque la RAM était rare, les compilateurs ont essayé d'optimiser l'utilisation de la mémoire en fusionnant des 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 morte 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, autoriser l'écriture 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é avant ou mes "problèmes" sont-ils trop mineurs et insignifiants pour déranger qui que ce soit?

Marius Macijauskas
la source
12
Je suppose que vous avez regardé à quoi ressemblent les littéraux de chaîne dans le code compilé ?
2
Regardez l'assemblage que contient le lien que j'ai fourni. C'est juste là.
8
Votre exemple «moregex» ne fonctionnerait pas en raison d'une terminaison nulle.
dan04
4
Vous ne voulez pas écraser les constantes car cela changera leur valeur. La prochaine fois que vous voudrez utiliser la même constante, ce sera différent. Le compilateur / runtime doit trouver les constantes quelque part, et où que ce soit, vous ne devriez pas être autorisé à modifier.
Erik Eidt
1
"Les littéraux de chaîne sont donc stockés dans la mémoire du programme, pas dans la RAM, et un débordement de tampon entraînerait la corruption du programme lui-même?" L'image du programme est également en RAM. Pour être précis, les littéraux de chaîne sont stockés dans le même segment de RAM utilisé pour stocker l'image du programme. Et oui, écraser la chaîne pourrait corrompre le programme. À l'époque de MS-DOS et CP / M, il n'y avait pas de protection de mémoire, vous pouviez faire des choses comme ça, et cela causait généralement de terribles problèmes. Les premiers virus PC utiliseraient de telles astuces pour modifier votre programme afin de formater votre disque dur lorsque vous tentiez de l'exécuter.
Charles E. Grant

Réponses:

40

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 de MMU ni de protection de mémoire (qui existaient sur la plupart des ordinateurs centraux IBM / 360 plus anciens ). Ainsi , chaque octet de mémoire (y compris ceux qui manipulent des chaînes littérales ou code machine) pourrait être remplacé par un programme erroné (imaginez un programme changeant certains %à /une 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'une mémoire centrale - que vous pouviez initialiser via le clavier, il fallait donc taper plusieurs dizaines de chiffres pour lire le programme initial sur des bandes perforées; Le 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 .textsegment) sont devenus en lecture seule (sauf par le système d'exploitation qui les a chargés à partir 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 non défini . Et avoir un segment de code en lecture seule dans la mémoire virtuelle offre un avantage significatif: plusieurs processus exécutant le même programme partagent la même RAM ( mémoire physiquepages) pour ce segment de code (voir l' MAP_SHAREDindicateur pour mmap (2) sous Linux).

Aujourd'hui, les microcontrôleurs bon marché ont une mémoire en lecture seule (par exemple leur Flash ou ROM) et y conservent 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 un mécanisme de cache utilisé pour la mémoire virtuelle et la pagination . Ainsi, le segment de code du programme exécutable (par exemple dans ELF ) est mappé en mémoire 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 à ldpour 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 .

La norme C est donc baroque: légalement (uniquement pour des raisons historiques), les chaînes littérales ne sont pas des const char[]tableaux, mais uniquement des char[]tableaux 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 sont capables d'optimiser et d'avoir "ions"et de "expressions"partager leurs 5 derniers octets (y compris l'octet nul final).

Essayez de compiler votre code C dans un fichier foo.cavec gcc -O -fverbose-asm -S foo.cet regardez à l'intérieur du fichier assembleur généré foo.spar GCC

Enfin, la sémantique de C est suffisamment complexe (en savoir plus sur CompCert et 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 moins comportement défini), il est donc très peu probable que les futures normes C acceptent des chaînes littérales inscriptibles. Peut-être au contraire en feraient-ils des const char[]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 le paradigme de programmation fonctionnelle .

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

Basile Starynkevitch
la source
Est-ce pour protéger les chaînes comme constantes? Même s'ils ne sont pas définis comme constdans la norme ( stackoverflow.com/questions/2245664/… )?
Marius Macijauskas
Êtes-vous sûr que les premiers ordinateurs n'avaient pas de mémoire en lecture seule? N'était-ce pas beaucoup moins cher que le bélier? De plus, les mettre dans la mémoire RO ne provoque pas UB à essayer de les modifier par erreur, mais à compter sur l'OP qui ne le fait pas et sur le fait qu'il viole cette confiance. Voir par exemple les programmes Fortran où tous les littéraux 1se comportent soudainement comme des 2s et un tel plaisir ...
Deduplicator
1
Adolescent dans un musée, j'ai codé en 1975 sur de vieux ordinateurs IBM / 1620 et CAB500. Aucun n'avait de ROM: IBM / 1620 avait une mémoire de base et CAB500 avait un tambour magnétique (certaines pistes pouvaient être désactivées pour être inscriptibles par un interrupteur mécanique)
Basile Starynkevitch
2
Il convient également de le souligner: le fait de mettre des littéraux dans le segment de code signifie qu'ils peuvent être partagés entre plusieurs copies du programme car l'initialisation se produit au moment de la compilation plutôt qu'au moment de l'exécution.
Blrfl
@Deduplicator Eh bien, j'ai vu une machine exécutant une variante BASIC qui vous permettait de modifier les constantes entières (je ne sais pas si vous aviez besoin de le tromper en le faisant, par exemple en passant des arguments "byref" ou si un simple let 2 = 3fonctionnait). Cela a eu pour résultat beaucoup de FUN (dans la définition du mot Dwarf Fortress), bien sûr. Je n'ai aucune idée de la façon dont l'interprète a été conçu pour permettre cela, mais c'était le cas.
Luaan
2

Les compilateurs ne pouvaient pas se combiner "more"et "regex", parce que le premier a un octet nul après le emoment où le second en 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 partageant 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'initialiserait 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 ".

supercat
la source