C语言中的字符串字面量能被修改吗?

6

最近我有一个问题,我知道指向一个常量数组的指针在初始化为下面代码中所示时,位于.rodata区域,而该区域只能读取。 然而,在模式C11中,我看到在此内存地址写入会导致未定义的行为。 我知道Borland的Turbo-C编译器可以写入指针指向的位置,这是因为某些系统(如MS-DOS)上的处理器在实模式下运行吗?还是独立于处理器的操作模式?是否有其他编译器可以写入指针并且不会出现任何内存违规,即在保护模式下使用处理器?

#include <stdio.h>

int main(void) {
    char *st = "aaa";
    *st = 'b'; 
    return 0;
}

在使用Turbo-C在MS-DOS中进行编译时,您将能够向内存中写入数据。

4
永远不要这样做。在某些系统中,字符串“aaa”被放置在只读内存中,如果尝试修改它,则会导致运行时错误。此外,如果在同一编译单元中有另一个实例的字符串“aaa”,它们可能共享相同的存储空间,这样更改一个字符串将更改另一个字符串。 - Tom Karzes
2
这取决于您正在运行的系统功能。如果它是嵌入式系统,并且代码和字符串文字在ROM中,那么写入字符串文字甚至任何内存保护都将在物理上变得不可能。 - Weather Vane
2
这个问题的意义是什么?行为是未定义的。如果有编译器一致地表现出一种方式与其他编译器不同,那又有什么关系呢? - John Bollinger
3
虽然这种行为在C标准中没有定义,但某些编译器可能会对其进行定义,在这种情况下,如果你使用这样的编译器,就可以依赖它。其中一个例子是gcc 4.0之前带有 -fwritable-strings选项的版本。Turbo C编译器也可能会定义这种行为,但我不确定。请注意,仅仅因为它没有故障并不意味着该行为被定义了。您需要阅读编译器文档。 - prl
3
Turbo-C是ANSI-C正式发布前的开发工具,但它实现了大部分的ANSI-C功能。K&R C是ANSI-C之前的版本,它并没有规定不能向字符串常量写入数据(在K&R C中并没有“const”的概念)。默认情况下,Turbo-C不会合并重复的字符串常量,这样可以避免覆盖其他人使用的字符串常量(这使得针对K&R C编写的代码能够按预期工作)。如果使用-d选项打开了重复合并,则不能再向这些字符串常量写入数据,而应该将它们视为指向常量数据的指针。 - Michael Petch
显示剩余5条评论
5个回答

9

正如所指出的那样,尝试在C语言中修改常量字符串将导致未定义的行为。这有几个原因。

其中一个原因是字符串可能位于只读内存中。这允许它在同一程序的多个实例之间共享,并且如果所在页面被换出(因为该页面是只读的,因此可以从可执行文件重新加载),不需要将其保存到磁盘。它还通过在尝试修改时引发错误(例如分段故障)来帮助检测运行时错误。

另一个原因是字符串可能是共享的。许多编译器(例如gcc)会注意到在编译单元中出现相同的文字字符串时,它们将为其共享相同的存储空间。因此,如果程序修改一个实例,则可能也会影响其他实例。

也从未需要这样做,因为可以通过使用静态字符数组轻松实现相同的预期效果。例如:

#include <stdio.h>

int main(void) {
    static char st_arr[] = "aaa";
    char *st = st_arr;
    *st = 'b'; 
    return 0;
}

这段代码实现了与原始代码相同的功能,但没有产生任何未定义行为,且占用的内存也相同。在这个例子中,字符串 "aaa" 作为数组初始化程序使用,并没有自己的存储空间。数组 st_arr 替代了原来示例中的常量字符串,但是 (1) 它不会被放在只读内存中,(2) 也不会与任何对该字符串的其他引用共享。因此,如果需要修改它,则是安全的。


幸运的是,对于这个答案的制定,实际上这只是我感到好奇,如果我必须更改指针所指向的内容,我会像你那样使用它。 - Yuri Albuquerque
2
@YuriAlbuquerque 抱歉,我漏掉了 static 关键字 - 我刚刚添加了它。对此感到抱歉! - Tom Karzes
没有问题,:) - Yuri Albuquerque

7

有没有其他编译器可以写入指针并在保护模式下使用处理器而不会导致内存泄漏失败?

GCC 3及更早版本曾支持gcc -fwriteable-strings,以允许您编译旧的K&R C代码,在其中这似乎是合法的,根据https://gcc.gnu.org/onlinedocs/gcc-3.3.6/gcc/Incompatibilities.html。(这是ISO C中未定义的行为,因此是ISO C程序中的错误)。该选项将定义ISO C未定义的赋值行为。

GCC 3.3.6手册 - C语言方言选项

-fwritable-strings
将字符串常量存储在可写数据段中,并且不使它们唯一化。这是为了与旧程序兼容,这些程序假定它们可以写入字符串常量。

向字符串常量写入内容是一个非常糟糕的想法;“常量”应该是恒定不变的。

GCC 4.0已删除该选项(发行说明);最后一个GCC3系列是2006年3月的gcc3.4.6。然而,显然在那个版本中 它已经变得有缺陷

gcc -fwritable-strings会将字符串字面量视为非const匿名字符数组(参见@gnasher的答案),因此它们会进入.data节,而不是.rodata,从而链接到可执行文件的一个段中,该段映射到读+写页面,而不是只读页面。(可执行段基本上与x86分段无关,它只是从可执行文件到内存的启动+范围内存映射。)

它还会禁用重复字符串合并,因此char *foo() { return "hello"; }char *bar() { return "hello"; }将返回不同的指针值,而不是合并相同的字符串字面量。


相关:


链接选项:仍然是未定义行为,因此可能不可行

在GNU/Linux上,使用ld -N--omagic)进行链接将使文本(以及数据)部分变为读+写。即使现代GNU Binutils ld.rodata放入自己的部分(通常具有读取但执行权限),而不是将其作为.text的一部分。让.text可写可能很容易成为安全问题:您永远不希望同时拥有写入和执行权限的页面,否则某些错误(如缓冲区溢出)可能会变成代码注入攻击。

要从gcc中执行此操作,请使用gcc -Wl,-N将该选项传递给ld进行链接。

这并不意味着写入const对象时就不再是未定义行为。例如,编译器仍将合并重复的字符串,因此写入一个char *foo =“hello”;将影响整个程序中所有其他使用"hello"的地方,甚至跨文件。


应该使用什么:

如果你想要可写的内容,请使用static char foo[] = "hello";,其中引用的字符串只是非const数组的一个初始化器。作为奖励,这比全局范围内的static char *foo = "hello";更有效率,因为获取数据的层级减少了一级:它只是一个数组而不是存储在内存中的指针。


4
你在询问平台是否会导致未定义的行为变得被定义。对于这个问题的答案是肯定的。
但你也在询问平台是否定义了此行为。事实上它并没有。
在某些优化提示下,编译器将合并字符串常量,因此写入一个常量将写入该常量的其他用途。我曾使用过这种编译器,它能够很好地合并字符串。
不要编写这样的代码。它不好。当你转向更现代化的平台时,你会后悔编写这种风格的代码。

3

你的字面值 "aaa" 会在匿名位置产生一个由四个 const char 'a','a','a','\0' 组成的静态数组,并返回指向第一个 'a' 的指针,转换为 char*。

试图修改这四个字符中的任何一个都是未定义的行为。未定义行为可以做任何事情,从按预期修改字符,假装修改字符,什么也不做或崩溃。

基本上与 static const char anonymous[4] = { 'a', 'a', 'a', '\0' }; char* st = (char*) &anonymous [0];相同。


3
为了补充以上正确的答案,DOS在实模式下运行,因此没有只读内存。所有内存都是平面的且可写的。因此,在当时,将文本写入字面值(就像在任何类型的const变量中一样)被定义为良好的操作。

是的,这也是我想知道的原因之一。例如,在Intel 8086处理器上,编译器无法保护内存区域,对吗? - Yuri Albuquerque
是的,实模式无法保护内存。 - Michael Chourdakis
2
在v8086模式下运行的实模式代码(这是EMM386和其他扩展内存管理器操作以模拟扩展内存的方式)理论上可以允许运行时环境(例如VCPI或DPMI)将内存页面标记为只读。例如,DOS多任务处理程序会将视频内存标记为只读,以便可以捕获写入,从而允许虚拟化屏幕访问(当然,这对DOS来说是透明的)。 - Michael Petch

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接