簡體   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