可以修改 C 中的字符串文字吗?

Can a string literal in C be modified?

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

#include <stdio.h>

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

在 MS-DOS 下使用 Turbo-C 编译的代码中,您将能够写入内存

您的文字 "aaa" 在匿名位置生成一个由四个 const char 'a'、'a'、'a'、'\0' 和 returns 指向第一个 'a' 的指针,转换为 char*。

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

基本和static const char anonymous[4] = { 'a', 'a', 'a', '\0' }; char* st = (char*) &anonymous [0];

你问的是平台是否会导致未定义的行为被定义。这个问题的答案是肯定的。

但是你也在问平台是否定义了这种行为。其实不然。

在一些优化提示下,编译器会合并字符串常量,这样写入一个常量就会写入该常量的其他用途。这个编译器我用过一次,合并字符串的能力还不错

不要写这段代码。这不好。当您转向更现代的平台时,您会后悔以这种风格编写代码。

如前所述,尝试修改 C 中的常量字符串会导致未定义的行为。这有几个原因。

一个原因是字符串可能放在read-only内存中。这允许它在同一程序的多个实例之间共享,并且如果它所在的页面被调出,则不需要将内存保存到磁盘(因为该页面是 read-only,因此可以稍后重新加载来自可执行文件)。它还通过在尝试修改它时给出错误(例如分段错误)来帮助检测 run-time 错误。

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

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

#include <stdio.h>

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

这完全符合发布的代码试图执行的操作,但没有任何未定义的行为。它还占用相同数量的内存。在此示例中,字符串 "aaa" 用作数组初始值设定项,并且没有自己的任何存储空间。数组 st_arr 取代了原始示例中的常量字符串,但 (1) 它不会放在 read-only 内存中,并且 (2) 它不会与任何其他引用共享字符串。所以修改它是安全的,如果这正是你想要的。

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

Is there any other compiler that writes to the pointer and does not take any memory breach failure using the processor in protected mode?

根据 https://gcc.gnu.org/onlinedocs/gcc-3.3.6/gcc/Incompatibilities.html

GCC 3 和更早版本曾经支持 gcc -fwriteable-strings 让您编译旧的 K&R C,这显然是合法的。 (这是 ISO C 中的未定义行为,因此是 ISO C 程序中的错误)。该选项将定义 ISO C 未定义的赋值行为。

GCC 3.3.6 manual - C Dialect options

-fwritable-strings
Store string constants in the writable data segment and don't uniquize them. This is for compatibility with old programs which assume they can write into string constants.

Writing into string constants is a very bad idea; “constants” should be constant.

GCC 4.0 删除了该选项(release notes); the last GCC3 series was gcc3.4.6 in March 2006. Although apparently it had become buggy 在该版本中。

gcc -fwritable-strings 会将字符串文字视为 non-const 匿名字符数组(请参阅@gnasher 的回答),因此它们进入 .data 部分而不是 .rodata,并且因此链接到映射到读+写页面的可执行文件段,而不是read-only。 (可执行段与x86分段基本没有关系,只是从可执行文件到内存的一个start+rangememory-mapping。)

并且它会禁用 duplicate-string 合并,因此 char *foo() { return "hello"; }char *bar() { return "hello"; } 会 return 不同的指针值,而不是合并相同的字符串文字。


相关:


链接器选项:仍然是未定义的行为,所以可能不可行

在 GNU/Linux 上,与 ld -N (--omagic) 的链接将使文本(以及数据)部分可读写。这可能适用于 .rodata,即使现代 GNU Binutils ld.rodata 放在它自己的部分(通常具有读取权限但 没有 执行权限)而不是使其成为 .text 的一部分。让 .text 可写很容易成为一个安全问题:你永远不希望页面同时具有 write+exec,否则像缓冲区溢出这样的错误可能会变成 code-injection 攻击。

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

这对写入 const 对象的未定义行为没有任何作用。 例如编译器仍将合并重复的字符串,因此写入一个 char *foo = "hello"; 将影响整个程序中 "hello" 的所有其他使用,甚至跨文件。


改用什么:

如果您想要可写的内容,请使用 static char foo[] = "hello";,其中引用的字符串只是 non-const 数组的数组初始值设定项。 作为奖励,这个在全局范围内比 static char *foo = "hello"; 更有效,因为获取数据的间接级别更少:它只是一个数组而不是存储在内存中的指针。