为什么C字符串文字是只读的?


29

字符串文字为只读的优点是(-ies / -ied):

  1. 另一种射击自己的方式

    char *foo = "bar";
    foo[0] = 'd'; /* SEGFAULT */
    
  2. 无法优雅地初始化一行中的可读写单词数组:

    char *foo[] = { "bar", "baz", "running out of traditional placeholder names" };
    foo[1][2] = 'n'; /* SEGFAULT */ 
    
  3. 使语言本身复杂化。

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

保存内存? 我很早以前就读过某处(现在找不到源),当时RAM不足,编译器试图通过合并相似的字符串来优化内存使用。

例如,“更多”和“正则表达式”将变为“更多gex”。在数字蓝光质量电影时代今天仍然如此吗?我知道嵌入式系统仍然可以在资源受限的环境中运行,但是仍然可以使用的内存量已大大增加。

兼容性问题? 我认为尝试访问只读内存的旧版程序将崩溃或继续出现未发现的错误。因此,任何遗留程序都不应尝试访问字符串文字,因此允许写入字符串文字不会损害有效的,非骇客的,可移植的遗留程序。

还有其他原因吗?我的推理不正确吗?考虑在新的C标准中更改读写字符串文字是否合理,或者至少在编译器中添加了一个选项?是之前考虑过这个问题,还是我的“问题”太小而无关紧要,以至于无人问津?



2
查看我提供的链接包含的程序集。它在那里。

8
您的“ moregex”示例由于无效终止而无法工作。
dan04 2015年

4
您不想覆盖常量,因为那样会改变它们的值。下次您要使用相同的常量时,它将有所不同。编译器/运行时必须从某个地方获取常量,并且无论何处都不得修改。
埃里克·艾德

1
“那么字符串文字存储在程序存储器中,而不是RAM中,而缓冲区溢出会导致程序本身损坏吗?” 程序映像也位于RAM中。确切地说,字符串文字存储在用于存储程序映像的RAM的同一段中。是的,覆盖字符串可能会破坏程序。早在MS-DOS和CP / M时代,就没有内存保护功能,您可以做这样的事情,并且通常会引起严重的问题。最初的PC病毒会使用类似的技巧来修改程序,以便在您尝试运行它时格式化硬盘。
查尔斯·格兰特

Answers:


40

从历史上看(也许通过重写部分内容),情况恰恰相反。在1970年代初期运行原型原型C(也许是BCPL)的第一台计算机(也许是PDP-11)上,没有MMU内存保护(在大多数较旧的IBM / 360大型机上都存在)。这样的存储器的每个字节(包括处理的文字串或机器代码)可能是由错误的程序被覆盖(想象一个程序改变某些到在的printf(3)格式的字符串)。因此,文字字符串和常量是可写的。%/

1975年少年时,我在1960年代的旧计算机上在巴黎的Palacous de laDécouverte博物馆编码,但没有内存保护:IBM / 1620只有一个核心内存-您可以通过键盘进行初始化,因此您必须键入几十个读取穿孔纸带上的初始程序的位数;CAB / 500具有磁鼓存储器;您可以通过鼓附近的机械开关禁用写一些音轨。

后来,计算机获得了某种形式的具有某些内存保护功能的内存管理单元(MMU)。有一个设备禁止CPU覆盖某种内存。因此,某些内存段,特别是代码段(又名代码段.text)变为只读的(操作系统从磁盘加载它们的除外)。编译器和链接器很自然地将文字字符串放入该代码段中,并且文字字符串变为只读。当您的程序尝试覆盖它们时,这很糟糕,这是未定义的行为。而且,在虚拟内存中具有只读代码段具有显着的优势:运行同一程序的多个进程共享同一RAM物理内存)。页)的代码段(在Linux上,请参见mmap(2)的MAP_SHARED标志)。

如今,廉价的微控制器具有一些只读存储器(例如其Flash或ROM),并将其代码(以及文字字符串和其他常量)保留在那里。真正的微处理器(如平板电脑,笔记本电脑或台式机中的微处理器)具有复杂的内存管理单元和用于虚拟内存页面调度的缓存机制。因此,可执行程序的代码段(例如,在ELF中)被映射为只读,可共享和可执行的段(在Linux上是通过mmap(2)execve(2)进行存储的;顺便说一句,您可以将指令提供给ld)如果您确实想获得一个可写的代码段)。写或滥用它通常是分段错误

因此,C标准是巴洛克式的:从法律上讲(仅出于历史原因),文字字符串不是const char[]数组,而只是char[]禁止覆盖的数组。

顺便说一句,当前很少有语言允许覆盖字符串文字(即使是Ocaml,从历史上也很糟糕的是,可写文字字符串最近在4.02中就改变了这种行为,现在具有只读字符串)。

当前的C编译器能够优化并拥有"ions""expressions"共享其最后5个字节(包括终止的空字节)。

尝试编译文件C代码foo.cgcc -O -fverbose-asm -S foo.c生成的汇编文件中,并期待foo.s通过GCC

最后,C 的语义足够复杂(了解有关试图捕获它的CompCertFrama-C的更多信息),并且添加可写的常量文字字符串将使其变得更加神秘,同时使程序更弱,甚至更不安全(并且花费更少的时间)定义的行为),因此将来的C标准不太可能接受可写的文字字符串。也许恰恰相反,它们会使它们const char[]像道德上那样排列整齐。

还请注意,由于多种原因,可变数据比常量数据更难由计算机处理(缓存一致性),难以编码(供开发人员理解)。因此,最好让您的大多数数据(尤其是文字字符串)保持不变。阅读有关函数式编程 范例的更多信息。

在IBM / 7094上的Fortran77旧版本中,错误甚至可以更改常量:如果您CALL FOO(1)并且如果FOO碰巧修改了通过引用2传递的参数,则实现可能会将其他情况从1更改为2,这确实是一个错误。顽皮的bug,很难找到。


这是为了保护字符串作为常量吗?即使它们没有按const标准定义(stackoverflow.com/questions/2245664/…)?
Marius Macijauskas,2015年

您确定第一台计算机没有只读存储器吗?那不是比ram便宜很多吗?同样,将它们放入RO内存中并不会导致UB尝试错误地修改它们,而是依靠OP不这样做,并且他违反了这种信任。例如,请参阅Fortran程序,其中所有文字1s突然表现像2s并如此有趣……
Deduplicator 2015年

1
少年时在博物馆里,我于1975年在旧的IBM / 1620和CAB500计算机上编码。两者都没有任何ROM:IBM / 1620具有核心内存,CAB500具有磁鼓(可以通过机械开关禁用某些轨道以使其可写)
Basile Starynkevitch 2015年

2
还值得指出的是:将文字放在代码段中意味着它们可以在程序的多个副本之间共享,因为初始化发生在编译时而不是运行时。
Blrfl 2015年

@Deduplicator好吧,我已经看过一台运行BASIC变量的机器,该机器允许您更改整数常量(我不确定是否需要欺骗它,例如,传递“ byref”参数,或者是否可行let 2 = 3)。当然,这导致了很多的乐趣(在“矮人要塞”这个词的定义中)。我不知道解释器的设计是允许它的,但事实是这样。
a安2015年

2

编译器无法合并"more""regex",因为前者e在后者之后有一个空字节,而后者则具有x,但是许多编译器会结合完全匹配的字符串文字,而有些也将匹配共享尾部的字符串文字。因此,更改字符串文字的代码可能会更改不同的字符串文字,该字符串文字用于某些完全不同的目的,但碰巧包含相同的字符。

在发明C之前,在FORTRAN中会出现类似的问题。参数总是按地址而不是按值传递。因此,将两个数字相加的例程将等效于:

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

如果要传递一个常量值(例如4.0)sum,则编译器将创建一个匿名变量并将其初始化为4.0。如果将相同的值传递给多个函数,则编译器会将相同的地址传递给所有这些函数。结果,如果将修改其参数之一的函数传递给浮点常量,结果该变量的值可能会在程序中的其他地方更改,从而导致这样的说法:“变量不会;常量不会't'。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.