web-dev-qa-db-ja.com

「char s []」ではなく「char * s」で初期化された文字列に書き込むと、セグメンテーション違反が発生するのはなぜですか?

次のコードは、行2でセグフォールトを受け取ります。

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でテスト済み。

271
Markus

C FAQを参照してください 質問1.32

Q:これらの初期化の違いは何ですか?
char a[] = "string literal";
char *p = "string literal";
p[i]に新しい値を割り当てようとすると、プログラムがクラッシュします。

A:文字列リテラル(Cソースの二重引用符で囲まれた文字列の正式な用語)は、わずかに異なる2つの方法で使用できます。

  1. char a[]の宣言のように、charの配列の初期化子として、その配列内の文字の初期値(および必要に応じてそのサイズ)を指定します。
  2. それ以外の場所では、名前のない静的な文字の配列になり、この名前のない配列は読み取り専用メモリに格納される可能性があるため、必ずしも変更することはできません。式コンテキストでは、配列は通常どおり一度にポインターに変換されるため(セクション6を参照)、2番目の宣言は名前のない配列の最初の要素を指すようにpを初期化します。

一部のコンパイラには、文字列リテラルが書き込み可能かどうかを制御するスイッチがあり(古いコードをコンパイルするため)、一部のコンパイラには文字列リテラルをconst charの配列として正式に処理するオプションがあります(エラー検出を向上させるため)。

222
matli

通常、文字列リテラルは、プログラムの実行時に読み取り専用メモリに保存されます。これは、誤って文字列定数を変更しないようにするためです。最初の例では、"string"は読み取り専用メモリに格納され、*strは最初の文字を指します。セグメンテーション違反は、最初の文字を'z'に変更しようとすると発生します。

2番目の例では、文字列"string"は、コンパイラによって読み取り専用ホームからstr[]配列にコピーされています。その後、最初の文字の変更が許可されます。これを確認するには、それぞれのアドレスを印刷します。

printf("%p", str);

また、2番目の例でstrのサイズを印刷すると、コンパイラーが7バイトを割り当てていることがわかります。

printf("%d", sizeof(str));
94
Greg Hewgill

これらの答えのほとんどは正しいですが、もう少し明確にするために...

人々が参照している「読み取り専用メモリ」は、ASM用語のテキストセグメントです。これは、命令がロードされるメモリ内の同じ場所です。これは、セキュリティなどの明らかな理由で読み取り専用です。文字列に初期化されたchar *を作成すると、文字列データはテキストセグメントにコンパイルされ、プログラムはポインターを初期化してテキストセグメントを指します。それを変更しようとすると、kaboom。セグフォルト。

配列として書き込まれる場合、コンパイラーは初期化された文字列データを代わりにデータセグメントに配置します。これは、グローバル変数などが存在する場所と同じです。データセグメントに命令がないため、このメモリは可変です。今回は、コンパイラーが文字配列(まだchar *のまま)を初期化するときに、テキストセグメントではなくデータセグメントを指しているので、実行時に安全に変更できます。

31
Bob Somers

文字列に書き込むときにセグメンテーション違反が発生するのはなぜですか?

C99 N1256ドラフト

文字列リテラルには2つの異なる使用法があります。

  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では、1つまたは複数の文字列リテラルから生じる各マルチバイト文字シーケンスに、値ゼロのバイトまたはコードが追加されます。次に、マルチバイト文字シーケンスを使用して、シーケンスを格納するのに十分な静的ストレージの継続時間と長さの配列を初期化します。文字列リテラルの場合、配列要素はchar型であり、マルチバイト文字シーケンスの個々のバイトで初期化されます[...]

    6これらの配列が明確であるかどうかは、その要素に適切な値がある場合には指定されていません。プログラムがそのような配列を変更しようとする場合、動作は未定義です。

6.7.8/32「初期化」に直接的な例を示します。

例8:宣言

char s[] = "abc", t[3] = "abc";

要素が文字列リテラルで初期化される「プレーン」文字配列オブジェクトsおよびtを定義します。

この宣言は次と同じです

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

配列の内容は変更可能です。一方、宣言

char *p = "abc";

型「charへのポインター」でpを定義し、長さ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

結論: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

最初のコードでは、「文字列」は文字列定数であり、文字列定数は読み取り専用メモリに配置されることが多いため、変更しないでください。 「str」は、定数を変更するために使用されるポインターです。

2番目のコードでは、「string」は配列初期化子であり、

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

「str」はスタックに割り当てられた配列で、自由に変更できます。

17
Andru Luvisi

最初の例のコンテキストの"whatever"の型はconst char *であるため(非const char *に割り当てた場合でも)、これを試してはいけません。

コンパイラは、文字列をメモリの読み取り専用部分に配置することでこれを実施しているため、それに書き込むとセグメンテーション違反が発生します。

12
Mike F

このエラーまたは問題を理解するには、まずポインターと配列の違いの違いを知っておく必要がありますので、ここではまず違いの違いを説明します

文字列配列

 char strarray[] = "hello";

メモリ配列は連続メモリセルに格納され、[h][e][l][l][o][\0] =>[]は1文字バイトサイズのメモリセルとして格納され、この連続メモリセルはstrarray hereという名前でアクセスできます。ここでは、文字列配列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
8
user2426842
char *str = "string";  

上記は、strが、プログラムのバイナリイメージにハードコーディングされているリテラル値"string"を指すように設定します。

そのため、str[0]=はアプリケーションの読み取り専用コードに書き込もうとしています。これはおそらくコンパイラに依存していると思います。

7
DougN
char *str = "string";

コンパイラが実行可能ファイルの変更不可能な部分に入れている文字列リテラルへのポインタを割り当てます。

char str[] = "string";

変更可能なローカル配列を割り当てて初期化します

6
Rob Walker

@matliがリンクしているC FAQが言及していますが、ここには他の誰もまだ言及していないため、明確にするために:文字列リテラル(ソース内の二重引用符付き文字列)がどこでも使用されている場合以外の文字配列を初期化する(すなわち:正常に動作する@Markの2番目の例)、その文字列はコンパイラによって特別な static string table 、これは本質的に匿名の(変数「名前」を持たない)グローバルな静的変数(もちろん読み取り専用)を作成することに似ています。 read-only の部分は重要な部分であり、@ Markの最初のコード例がセグメンテーション違反を起こす理由です。

6
rpj

 char *str = "string";

lineはポインタを定義し、リテラル文字列を指します。リテラル文字列は書き込み可能ではないため、次の操作を行います。

  str[0] = 'z';

セグエラーが発生します。一部のプラットフォームでは、リテラルは書き込み可能なメモリ内にあるため、セグメンテーション違反は表示されませんが、それでも無効なコード(未定義の動作になります)です。

この線:

char str[] = "string";

文字の配列を割り当て、copiesリテラル文字列をその配列に挿入します。これは完全に書き込み可能であるため、その後の更新は問題ありません。

4
Michael Burr

「string」などの文字列リテラルは、おそらく実行可能ファイルのアドレス空間に読み取り専用データとして割り当てられます(コンパイラを使用するか、使用します)。あなたがそれに触れると、それはあなたがその水着エリアにいることを驚かせ、セグフォールトであなたに知らせます。

最初の例では、そのconstデータへのポインターを取得しています。 2番目の例では、constデータのコピーで7文字の配列を初期化しています。

3
Jurney
// 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. 
2
jokeysmurf

そもそも、str"string"を指すポインターです。コンパイラーは、書き込みできないが読み取りしかできないメモリー内の場所に文字列リテラルを置くことができます。 (const char *char *に割り当てているため、これは本当に警告をトリガーするはずでした。警告を無効にしましたか、それとも単に無視しましたか?)

2番目に、配列を作成します。これは、完全にアクセスできるメモリであり、"string"で初期化します。 char[7](文字用に6つ、終端の '\ 0'用に1つ)を作成し、それで好きなことをします。

1
David Thornley

文字列が

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

最初の場合、リテラルは「a」がスコープに入ったときにコピーされます。ここで、「a」はスタックで定義された配列です。文字列がスタック上に作成され、そのデータがコード(テキスト)メモリからコピーされることを意味します。これは通常読み取り専用です(これは実装固有です。コンパイラはこの読み取り専用プログラムデータを読み取り/書き込み可能なメモリに配置することもできます) )。

2番目の場合、pはスタック(ローカルスコープ)で定義され、どこに保存されている文字列リテラル(プログラムデータまたはテキスト)を参照するポインターです。通常、このようなメモリを変更することは良い習慣ではなく、推奨されません。

0
Venki