繁体   English   中英

可以修改 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 编译的这段代码中,您将能够写入内存

正如已经指出的那样,尝试修改 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) 它不会与对该字符串的任何其他引用共享。 所以修改它是安全的,如果实际上这是你想要的。

是否有任何其他编译器写入指针并且在保护模式下使用处理器不会发生任何内存破坏故障?

一些 GCC 编译器如何修改常量字符指针?

根据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 手册 - C 方言选项

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

写入字符串常量是一个非常糟糕的主意; “常数”应该是常数。

GCC 4.0 删除了该选项(发行说明); 最后一个 GCC3 系列是 2006 年 3 月的 gcc3.4.6。虽然显然在那个版本中变得有问题

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

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


有关的:


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

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

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

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

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

您的文字“aaa”在匿名位置生成一个包含四个 const char 'a', 'a', 'a', '\\0' 的静态数组,并返回指向第一个 'a' 的指针,转换为 char*。

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

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

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

但是您也在询问平台是否定义了这种行为。 事实上并非如此。

在一些优化提示下,编译器将合并字符串常量,因此写入一个常量将写入该常量的其他用途。 这个编译器我用过一次,它合并字符串的能力很强。

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM