为什么在写入用“char *s”而不是“char s[]”初始化的字符串时会出现分段错误?
-
03-07-2019 - |
题
以下代码在第 2 行接收 seg 错误:
char *str = "string";
str[0] = 'z'; // could be also written as *str = 'z'
printf("%s\n", str);
虽然这工作得很好:
char str[] = "string";
str[0] = 'z';
printf("%s\n", str);
使用 MSVC 和 GCC 进行测试。
解决方案
请参阅C FAQ,问题1.32
问:这些初始化之间有什么区别?
char a [] =" string literal";
char * p =" string literal&quot ;;
如果我尝试为p [i]
分配一个新值,我的程序会崩溃。A :字符串文字(正式用语 对于C中的双引号字符串 来源)可以稍微使用两个 不同的方式:
- 作为char数组的初始值设定项,如
char a []
的声明,它指定初始值 该数组中的字符(和, 如果有必要,它的大小)。- 在其他地方,它变成一个未命名的静态字符数组, 并且可以存储该未命名的数组 在只读内存中,以及哪些内存 因此不一定如此 改性。在表达式上下文中, 数组一次转换为a 指针,像往常一样(见第6节),所以 第二个声明初始化p 指向未命名的数组的第一个 元件。
醇>有些编译器有一个开关 控制是否为字符串文字 是否可写(用于编译旧的 代码),有些可能有选项 导致字符串文字正式 被视为const char数组(for 更好的错误捕获)。
其他提示
通常,在运行程序时,字符串文字存储在只读存储器中。这是为了防止您意外更改字符串常量。在第一个示例中," string"
存储在只读存储器中, * str
指向第一个字符。当您尝试将第一个字符更改为'z'
时会发生段错误。
在第二个示例中,字符串" string"
由> 复制由编译器从其只读主页复制到 str []
数组。然后允许更改第一个字符。您可以通过打印每个地址来检查:
printf("%p", str);
此外,在第二个示例中打印 str
的大小将显示编译器为其分配了7个字节:
printf("%d", sizeof(str));
这些答案中的大多数都是正确的,但只是为了增加一点清晰度......
“只读存储器”。人们指的是ASM术语中的文本段。它是加载指令的内存中的相同位置。由于安全性等显而易见的原因,这是只读的。当您创建初始化为字符串的char *时,字符串数据将被编译到文本段中,程序会将指针初始化为指向文本段。因此,如果您尝试更改它,kaboom。段错误。
当编写为数组时,编译器会将初始化的字符串数据放在数据段中,这与全局变量等存在的位置相同。该内存是可变的,因为数据段中没有指令。这次编译器初始化字符数组(它仍然只是一个char *)时,它指向数据段而不是文本段,您可以在运行时安全地更改它。
为什么写入字符串时会出现分段错误?
C99 N1256草案
字符串文字有两种不同的用途:
初始化
char[]
:char c[] = "abc";
这是“更神奇的”,在 6.7.8/14“初始化”中进行了描述:
字符类型的数组可以由字符串文本初始化(可选) 用大括号封闭。字符串文本的连续字符(包括 如果有空间或数组大小未知,则终止 null 字符) 初始化 数组的元素。
所以这只是一个快捷方式:
char c[] = {'a', 'b', 'c', '\0'};
与任何其他常规数组一样,
c
可以修改。其他地方:它生成一个:
- 未命名的
- 字符数组 C 和 C++ 中字符串文字的类型是什么?
- 带静态存储
- 如果修改则给出 UB
所以当你写:
char *c = "abc";
这类似于:
/* __unnamed is magic because modifying it gives UB. */ static char __unnamed[] = "abc"; char *c = __unnamed;
请注意隐式转换
char[]
到char *
, ,这始终是合法的。那么如果你修改
c[0]
, ,你还修改__unnamed
, ,即UB。这在 6.4.5“字符串文字”中有记录:
5 在转换阶段 7 中,每个多字节都会附加一个值为零的字节或代码 由一个或多个字符串文本生成的字符序列。多字节字符 然后,序列用于初始化静态存储持续时间和长度的数组 足以包含序列。对于字符串文字,数组元素具有 键入 char,并使用多字节字符的单个字节进行初始化 序列 [...]
6 没有具体说明这些数组是否是不同的,前提是它们的元素具有 适当的值。如果程序尝试修改此类数组,则行为为 定义。
6.7.8/32“初始化”给出了一个直接的例子:
实施例8:宣言
char s[] = "abc", t[3] = "abc";
定义“普通”char 数组对象
s
和t
其元素用字符串文字初始化。该声明等同于
char s[] = { 'a', 'b', 'c', '\0' }, t[] = { 'a', 'b', 'c' };
数组的内容是可以修改的。另一方面,声明
char *p = "abc";
定义
p
类型为“指向 char 的指针”,并将其初始化为指向长度为 4 的“char 数组”类型的对象,该对象的元素使用字符串文字进行初始化。如果尝试使用p
修改数组的内容,行为未定义。
GCC 4.8 x86-64 ELF 实现
程序:
#include <stdio.h>
int main(void) {
char *s = "abc";
printf("%s\n", s);
return 0;
}
编译和反编译:
gcc -ggdb -std=c99 -c main.c
objdump -Sr main.o
输出包含:
char *s = "abc";
8: 48 c7 45 f8 00 00 00 movq $0x0,-0x8(%rbp)
f: 00
c: R_X86_64_32S .rodata
结论:海湾合作委员会商店 char*
它在 .rodata
部分,不在 .text
.
如果我们做同样的事 char[]
:
char s[] = "abc";
我们获得:
17: c7 45 f0 61 62 63 00 movl $0x636261,-0x10(%rbp)
所以它被存储在堆栈中(相对于 %rbp
).
但请注意,默认链接器脚本将 .rodata
和 .text
在同一个段中,具有执行权限但无写入权限。这可以通过以下方式观察到:
readelf -l a.out
其中包含:
Section to Segment mapping:
Segment Sections...
02 .text .rodata
在第一个代码中,“string”是字符串常量,永远不应修改字符串常量,因为它们通常放在只读内存中。 &QUOT; STR&QUOT;是一个用于修改常量的指针。
在第二个代码中,“string”是一个数组初始化器,是
的简写char str[7] = { 's', 't', 'r', 'i', 'n', 'g', '\0' };
&QUOT; STR&QUOT;是在堆栈上分配的数组,可以自由修改。
因为第一个例子的上下文中的&quot; whatever&quot;
的类型是 const char *
(即使你将它分配给非const char *) ,这意味着你不应该试着写信。
编译器通过将字符串放在内存的只读部分来强制执行此操作,因此写入它会生成段错误。
要了解此错误或问题,您应首先了解指针和数组的区别 所以这里首先我已经解释了你们之间的差异
字符串数组
char strarray[] = "hello";
存储器阵列存储在连续存储器单元中,存储为 [h] [e] [l] [l] [o] [\ 0] =&gt; []
为1个字节字节size memory memory,这个连续的内存单元可以通过名字命名为strarray来访问。这里的字符串数组 strarray
本身包含初始化为它的字符串的所有字符。在这种情况下&quot; hello&quot; ; 代码>
所以我们可以通过索引值
`strarray[0]='m'` it access character at index 0 which is 'h'in strarray
并将其值更改为'm'
,因此strarray值更改为&quot; mello&quot;
;
这里需要注意的一点是,我们可以通过逐字符更改字符串来更改字符串数组的内容,但不能像 strarray =&quot; new string&quot;
一样无法直接初始化其他字符串
指针
我们都知道指针指向内存中的内存位置, 未初始化的指针指向随机内存位置,因此在初始化后指向特定的内存位置
char *ptr = "hello";
此处指针ptr初始化为字符串&quot; hello&quot;
,这是存储在只读存储器(ROM)中的常量字符串,因此&quot; hello&quot;
不能更改为它存储在ROM
和ptr存储在堆栈部分并指向常量字符串&quot; hello&quot;
所以ptr [0] ='m'无效,因为你无法访问只读存储器
但是ptr可以直接初始化为其他字符串值,因为它只是指针,所以它可以指向其数据类型变量的任何内存地址
ptr="new string"; is valid
char *str = "string";
上面设置 str
指向文字值&quot; string&quot;
,它在程序的二进制图像中被硬编码,可能被标记为只读在记忆中。
所以 str [0] =
正试图写入应用程序的只读代码。我猜这可能是编译器依赖的。
char *str = "string";
分配一个指向字符串文字的指针,编译器将其放入可执行文件的不可修改部分;
char str[] = "string";
分配并初始化一个可修改的本地数组
@matli链接的C FAQ提及它,但此处还没有其他人,所以为了澄清:如果在以外的任何地方使用字符串文字(源中的双引号字符串)初始化一个字符数组(即:@ Mark的第二个例子,它正常工作),该字符串由编译器存储在一个特殊的静态字符串表中,类似于创建一个全局静态变量(当然是只读的,基本上是匿名的(没有变量“name”)。 只读部分是重要的部分,也是@Mark的第一个代码示例段错误的原因。
char *str = "string";
行定义指针并将其指向文字字符串。当您执行以下操作时,文字字符串是不可写的:
str[0] = 'z';
你得到一个段错误。在某些平台上,文字可能在可写内存中,因此您不会看到段错误,但它是无效的代码(导致未定义的行为),无论如何。
该行:
char str[] = "string";
将字符数组和拷贝文字字符串分配到该数组中,该数组是完全可写的,因此后续更新没有问题。
字符串文字,例如&quot; string&quot;可能在您的可执行文件的地址空间中被分配为只读数据(给出或带走您的编译器)。当你去触摸它时,它会吓到你在游泳衣区域,并让你知道一个段错误。
在第一个示例中,您将获得指向该const数据的指针。在第二个示例中,您将使用const数据的副本初始化包含7个字符的数组。
// create a string constant like this - will be read only
char *str_p;
str_p = "String constant";
// create an array of characters like this
char *arr_p;
char arr[] = "String in an array";
arr_p = &arr[0];
// now we try to change a character in the array first, this will work
*arr_p = 'E';
// lets try to change the first character of the string contant
*str_p = 'G'; // this will result in a segmentation fault. Comment it out to work.
/*-----------------------------------------------------------------------------
* String constants can't be modified. A segmentation fault is the result,
* because most operating systems will not allow a write
* operation on read only memory.
*-----------------------------------------------------------------------------*/
//print both strings to see if they have changed
printf("%s\n", str_p); //print the string without a variable
printf("%s\n", arr_p); //print the string, which is in an array.
首先, str
是一个指向&quot; string&quot;
的指针。允许编译器将字符串文字放在内存中无法写入的位置,但只能读取。 (这确实应该触发警告,因为你将 const char *
分配给 char *
。你是否禁用了警告,或者你只是忽略了它们? )
第二,你要创建一个数组,这是你可以完全访问的内存,并用&quot; string&quot;
初始化它。你正在创建一个 char [7]
(六个用于字母,一个用于终止'\ 0'),你用它做任何你想做的事。
假设字符串是,
char a[] = "string literal copied to stack";
char *p = "string literal referenced by p";
在第一种情况下,当'a'进入范围时,将复制文字。这里'a'是在堆栈上定义的数组。这意味着字符串将在堆栈上创建,其数据从代码(文本)内存中复制,代码(文本)内存通常是只读的(这是特定于实现的,编译器也可以将此只读程序数据放在可读写内存中) )。
在第二种情况下,p是在堆栈(本地范围)上定义的指针,并引用存储在其中的字符串文字(程序数据或文本)。通常修改这种记忆并不是好习惯,也不鼓励。
首先是一个不能修改的常量字符串。第二个是具有初始化值的数组,因此可以进行修改。
分段错误。
char * str
是指向不可修改的字符串的指针(获取seg错误的原因)..
而 char str []
是一个数组,可以修改..