繁体   English   中英

为什么写入用字符串文字初始化的“char *s”而不是“char s[]”时会出现分段错误?

[英]Why do I get a segmentation fault when writing to a "char *s" initialized with a string literal, but not "char s[]"?

以下代码在第 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 常见问题解答,问题 1.32

:这些初始化有什么区别?
char a[] = "string literal";
char *p = "string literal";
如果我尝试为p[i]分配一个新值,我的程序就会崩溃。

:字符串文字(C 源代码中双引号字符串的正式术语)可以以两种略有不同的方式使用:

  1. 作为 char 数组的初始值设定项,就像在char a[]的声明中一样,它指定该数组中字符的初始值(以及,如果需要,它的大小)。
  2. 在其他任何地方,它都会变成一个未命名的静态字符数组,并且这个未命名的数组可能存储在只读存储器中,因此不一定要修改。 在表达式上下文中,数组会像往常一样立即转换为指针(参见第 6 节),因此第二个声明将 p 初始化为指向未命名数组的第一个元素。

一些编译器有一个开关控制字符串文字是否可写(用于编译旧代码),有些编译器可能有选项使字符串文字被正式视为 const char 数组(以便更好地捕获错误)。

通常,字符串文字在程序运行时存储在只读内存中。 这是为了防止您意外更改字符串常量。 在您的第一个示例中, "string"存储在只读内存中, *str指向第一个字符。 当您尝试将第一个字符更改为'z'

在第二个示例中,字符串"string"由编译器从其只读主目录复制str[]数组。 然后允许更改第一个字符。 您可以通过打印每个地址来检查:

printf("%p", str);

此外,在第二个示例中打印str的大小将显示编译器已为其分配了 7 个字节:

printf("%d", sizeof(str));

大多数这些答案是正确的,但只是为了增加一点清晰度......

人们所指的“只读存储器”是 ASM 术语中的文本段。 它与加载指令的内存位置相同。 出于安全等显而易见的原因,这是只读的。 当您创建一个初始化为字符串的 char* 时,字符串数据被编译到文本段中,并且程序初始化指针以指向文本段。 所以如果你想改变它,kaboom。 段错误。

当编写为数组时,编译器将初始化的字符串数据放在数据段中,这与全局变量等所在的位置相同。 该内存是可变的,因为数据段中没有指令。 这一次,当编译器初始化字符数组(它仍然只是一个 char*)时,它指向的是数据段而不是文本段,您可以在运行时安全地更改它。

为什么在写入字符串时会出现分段错误?

C99 N1256 草案

字符串文字有两种不同的用途:

  1. 初始化char[]

     char c[] = "abc";

    这是“更神奇的”,并在 6.7.8/14“初始化”中描述:

    字符类型的数组可以由字符串文字初始化,可选地括在大括号中。 字符串文字的连续字符(如果有空间或数组大小未知,则包括终止空字符)初始化数组的元素。

    所以这只是一个快捷方式:

     char c[] = {'a', 'b', 'c', '\\0'};

    像任何其他常规数组一样,可以修改c

  2. 在其他任何地方:它生成一个:

    所以当你写:

     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";

定义“普通”字符数组对象st其元素用字符串文字进行初始化。

此声明等同于

char s[] = { 'a', 'b', 'c', '\\0' }, t[] = { 'a', 'b', 'c' };

数组的内容是可修改的。 另一方面,声明

char *p = "abc";

定义p类型为“指向 char 的指针”,并将其初始化为指向一个类型为“char 数组”的对象,长度为 4,其元素用字符串文字初始化。 如果尝试使用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

结论:GCC 将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”是一个字符串常量,字符串常量不应该被修改,因为它们经常被放置在只读存储器中。 “str”是用于修改常量的指针。

在第二个代码中,“string”是一个数组初始值设定项,是

char str[7] =  { 's', 't', 'r', 'i', 'n', 'g', '\0' };

“str”是分配在栈上的数组,可以自由修改。

因为在第一个示例的上下文中"whatever"的类型是const char * (即使您将其分配给非常量 char*),这意味着您不应该尝试写入它。

编译器通过将字符串放在内存的只读部分来强制执行此操作,因此写入它会生成段错误。

char *str = "string";  

上面设置str指向文字值"string" ,它在程序的二进制图像中被硬编码,它可能在内存中被标记为只读。

所以str[0]=试图写入应用程序的只读代码。 我猜这可能是依赖于编译器的。

要理解这个错误或问题,你应该首先知道指针和数组的区别,所以在这里我首先向你解释它们的区别

字符串数组

 char strarray[] = "hello";

在内存数组中存储的是连续的内存单元,存储为[h][e][l][l][o][\\0] =>[]是 1 个字符字节大小的内存单元,这个连续的内存单元可以是在此处通过名为 strarray 的名称访问。所以此处的字符串数组strarray本身包含初始化为它的字符串的所有字符。在这种情况下,此处为"hello"因此我们可以通过按每个字符的索引值访问每个字符来轻松更改其内存内容

`strarray[0]='m'` it access character at index 0 which is 'h'in strarray

并且它的值更改为'm'所以 strarray 值更改为"mello"

这里需要注意的一点是,我们可以通过逐个字符更改字符串数组的内容,但不能将其他字符串直接初始化为它,例如strarray="new string"是无效的

指针

众所周知,指针指向内存中的内存位置,未初始化的指针指向随机内存位置,初始化后指向特定的内存位置。

char *ptr = "hello";

此处指针 ptr 被初始化为字符串"hello" ,它是存储在只读存储器 (ROM) 中的常量字符串,因此"hello"无法更改,因为它存储在 ROM 中

并且 ptr 存储在堆栈部分并指向常量字符串"hello"

所以 ptr[0]='m' 无效,因为你不能访问只读内存

但是 ptr 可以直接初始化为其他字符串值,因为它只是指针,因此它可以指向其数据类型变量的任何内存地址

ptr="new string"; is valid
char *str = "string";

分配一个指向字符串文字的指针,编译器将其放入可执行文件的不可修改部分;

char str[] = "string";

分配并初始化一个可修改的本地数组

@matli 链接的 C FAQ 提到了它,但这里还没有其他人提到它,所以为了澄清:如果字符串文字(源代码中的双引号字符串)用于初始化字符数组以外的任何地方(即:@ Mark 的第二个例子,它工作正常),该字符串由编译器存储在一个特殊的静态字符串表中,这类似于创建一个本质上是匿名的全局静态变量(当然是只读的)(没有变量“name” ”)。 只读部分是重要的部分,这也是@Mark 的第一个代码示例出现段错误的原因。

 char *str = "string";

line 定义了一个指针并将其指向一个文字字符串。 文字字符串不可写,所以当你这样做时:

  str[0] = 'z';

你得到一个段错误。 在某些平台上,文字可能在可写内存中,因此您不会看到段错误,但无论如何它都是无效代码(导致未定义的行为)。

线路:

char str[] = "string";

分配一个字符数组,将字面量字符串拷贝到该数组中,是完全可写的,所以后续更新没有问题。

像“string”这样的字符串文字可能作为只读数据分配在您的可执行文件的地址空间中(提供或接受您的编译器)。 当你去触摸它时,它吓坏了你在它的泳衣区,并通过段错误让你知道。

在您的第一个示例中,您将获得指向该常量数据的指针。 在您的第二个示例中,您正在使用 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是一个指向"string"的指针。 允许编译器将字符串文字放在内存中您无法写入但只能读取的位置。 (这真的应该触发警告,因为您将const char *分配给char * 。您是否禁用了警告,或者您只是忽略了它们?)

其次,您正在创建一个数组,这是您可以完全访问的内存,并使用"string"对其进行初始化。 您正在创建一个char[7] (六个用于字母,一个用于终止 '\\0'),并且您可以随意使用它。

假设字符串是,

char a[] = "string literal copied to stack";
char *p  = "string literal referenced by p";

在第一种情况下,当 'a' 进入范围时将复制文字。 这里 'a' 是一个定义在栈上的数组。 这意味着字符串将在堆栈上创建,其数据从代码(文本)内存中复制,代码(文本)内存通常是只读的(这是特定于实现的,编译器也可以将此只读程序数据放在可读写内存中)。

在第二种情况下,p 是在堆栈(本地范围)上定义的指针,并引用存储在其他位置的字符串文字(程序数据或文本)。 通常修改这样的内存不是好的做法,也不鼓励。

恒定记忆

由于字符串文字在设计上是只读的,因此它们存储在内存的常量部分 存储在那里的数据是不可变的,即无法更改。 因此,在 C 代码中定义的所有字符串文字都在此处获得只读内存地址。

堆栈内存

内存的堆栈部分是局部变量地址所在的地方,例如函数中定义的变量。


正如@matli 的回答所暗示的那样,有两种处理字符串这些常量字符串的方法。

1. 指向字符串字面量的指针

当我们定义一个指向字符串文字的指针时,我们正在创建一个位于堆栈内存中的指针变量。 它指向底层字符串文字所在的只读地址。

#include <stdio.h>

int main(void) {
  char *s = "hello";
  printf("%p\n", &s);  // Prints a read-only address, e.g. 0x7ffc8e224620
  return 0;
}

如果我们尝试通过插入来修改s

s[0] = 'H';

我们得到一个Segmentation fault (core dumped) 我们试图访问我们不应该访问的内存。 我们正在尝试修改只读地址0x7ffc8e224620

2. 字符数组

就示例而言,假设存储在常量内存中的字符串文字"Hello"具有与上述相同的只读内存地址0x7ffc8e224620

#include <stdio.h>

int main(void) {
  // We create an array from a string literal with address 0x7ffc8e224620.
  // C initializes an array variable in the stack, let's give it address
  // 0x7ffc7a9a9db2.
  // C then copies the read-only value from 0x7ffc8e224620 into 
  // 0x7ffc7a9a9db2 to give us a local copy we can mutate.
  char a[] = "hello";

  // We can now mutate the local copy
  a[0] = 'H';

  printf("%p\n", &a);  // Prints the Stack address, e.g. 0x7ffc7a9a9db2
  printf("%s\n", a);   // Prints "Hello"

  return 0;
}

注意:在 1. 中使用指向字符串文字的指针时,最佳实践是使用const关键字,例如const *s = "hello" 这更具可读性,编译器会在违反时提供更好的帮助。 然后它会抛出一个类似error: assignment of read-only location '*s'而不是 seg fault 的错误。 在您手动编译代码之前,编辑器中的 Linters 也可能会发现错误。

第一个是不能修改的常量字符串。 第二个是带有初始化值的数组,因此可以对其进行修改。

Section 5.5 Character Pointers and Functions K&R Section 5.5 Character Pointers and Functions也讨论了这个话题:

这些定义之间有一个重要的区别:

char amessage[] = "now is the time"; /* an array */
char *pmessage = "now is the time"; /* a pointer */

amessage是一个数组,刚好足以容纳初始化它的字符序列和'\\0' 数组中的单个字符可能会更改,但amessage将始终引用相同的存储。 另一方面, pmessage是一个指针,初始化为指向一个字符串常量; 该指针随后可能会被修改为指向其他地方,但如果您尝试修改字符串内容,则结果未定义。

当您尝试访问无法访问的内存时会导致分段错误。

char *str是一个指向不可修改字符串的指针(获取段错误的原因)。

char str[]是一个数组,可以修改..

暂无
暂无

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

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