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


286

次のコードは、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でテスト済み。


1
その面白い-しかし、これは実際にコンパイルして、Visual Studio開発者のコ​​マンドプロンプトでWindowsコンパイラ(cl)を使用すると完全に実行されます。しばらく混乱しました...
David Refaeli

回答:


240

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

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

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

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

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


7
他のいくつかの点:(1)segfaultは前述のように発生しますが、その発生は実行環境の関数です。同じコードが組み込みシステムにあった場合、書き込みは効果がないか、実際にはsをzに変更する可能性があります。(2)文字列リテラルは書き込み不可であるため、コンパイラは「文字列」の2つのインスタンスを同じ場所に配置することでスペースを節約できます。または、コードのどこかに「別の文字列」がある場合、1つのメモリチャンクで両方のリテラルをサポートできます。明らかに、コードがそれらのバイトを変更することを許可された場合、奇妙で困難なバグが発生する可能性があります。
greggo 2011

1
@greggo:良い点。また、MMUが搭載されたシステムでmprotect読み取り専用保護を使用してこれを行う方法もあります(ここを参照)。

したがって、char * p = "blah"は実際には一時的な配列?weirdを作成します。
rahul tyagi 14

1
C ++で2年間執筆した後... TIL
zeboidlund

@rahultyagiどういう意味ですか?
Suraj Jain

105

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

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

printf("%p", str);

また、str2番目の例でのサイズを出力すると、コンパイラが7バイトを割り当てたことを示します。

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

13
printfで "%p"を使用する場合は、printf( "%p"、(void *)str);のように、ポインタをvoid *にキャストする必要があります。printfでsize_tを印刷するとき、最新のC標準(C99)を使用している場合は、「%zu」を使用する必要があります。
Chris Young

4
また、サイズの括弧は、型のサイズを取る場合にのみ必要です(引数はキャストのように見えます)。sizeofは関数ではなく演算子であることに注意してください。
くつろぐ


34

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

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

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


しかし、「読み取り専用メモリ」を変更できる実装が存在する可能性があるのは本当ではないでしょうか。
Pacerier 2013

配列として記述した場合、コンパイラーは、初期化された文字列データが静的またはグローバルの場合、データセグメントに配置します。それ以外の場合(たとえば、通常の自動配列の場合)、関数mainのスタックフレーム内のスタックに配置されます。正しい?
SE

26

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

C99 N1256ドラフト

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

  1. 初期化char[]

    char c[] = "abc";      

    これは「より魔法」であり、6.7.8 / 14「初期化」で説明されています。

    文字タイプの配列は、文字列リテラルで初期化できます。オプションで中括弧で囲むこともできます。文字列リテラルの連続する文字(スペースがある場合、または配列のサイズが不明な場合は、終了のnull文字を含む)は、配列の要素を初期化します。

    したがって、これは次のショートカットにすぎません。

    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つまたは複数の文字列リテラルから生成される各マルチバイト文字シーケンスに、値0のバイトまたはコードが追加されます。次に、マルチバイト文字シーケンスを使用して、シーケンスを含めるのに十分な静的ストレージ期間と長さの配列を初期化します。文字列リテラルの場合、配列要素は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タイプ "pointer to char"で定義し、要素が文字列リテラルで初期化される長さ4のタイプ "array of 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

17

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

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

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

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


1
スタック上、またはstrがグローバルまたはの場合はデータセグメントstatic
Gauthier 2016年

12

"whatever"最初の例のコンテキストでのタイプはconst char *(非const char *に割り当てた場合でも)であるため、書き込みを試みてはなりません。

コンパイラは、文字列をメモリの読み取り専用部分に配置することでこれを強制しているため、文字列に書き込むとsegfaultが生成されます。


8

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

文字列配列

 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

7
char *str = "string";  

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

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


6
char *str = "string";

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

char str[] = "string";

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


書くint *b = {1,2,3) ように書けchar *s = "HelloWorld"ますか?
Suraj Jain 2016

6

@matliがリンクしたCのFAQには言及されていますが、ここにはまだ誰もいません。そのため、明確にするために、文字配列(ソースで二重引用符で囲まれた文字列)が文字配列の初期化以外の場所で使用されている場合(つまり、@ Markの2番目の例は正しく機能します)。その文字列は、コンパイラーによって特別な静的文字列テーブルに格納されます。これは、本質的に匿名である(変数 "name"を持たない)グローバル静的変数(もちろん読み取り専用)を作成するのと同じです。 ")。読み取り専用の部分が重要な部分であり、そしてなぜ、@マークの最初のコード例のセグメンテーションフォルトです。


書くint *b = {1,2,3) ように書けchar *s = "HelloWorld"ますか?
Suraj Jain

4

 char *str = "string";

lineはポインタを定義し、それをリテラル文字列にポイントします。リテラル文字列は書き込み可能ではないので、次のようにすると:

  str[0] = 'z';

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

この線:

char str[] = "string";

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


書くint *b = {1,2,3) ように書けchar *s = "HelloWorld"ますか?
Suraj Jain 2016

3

"string"のような文字列リテラルは、おそらく実行可能ファイルのアドレス空間に読み取り専用データとして割り当てられます(コンパイラーに与えるか取るか)。触ってみると、水着エリアにいるとビックリし、ワンセグで知らせてくれます。

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


2
// 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. 

1

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

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


@Ferruccio 、? はい、constプレフィックスは変数を読み取り専用にします
EsmaeelE

Cでは、文字列リテラルはタイプchar [N]ではなくタイプなconst char [N]ので、警告はありません。(少なくともを渡すことで、gccで変更できます-Wwrite-strings。)
メルポメン

0

文字列は、

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

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

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


-1

最初は、変更できない1つの定数文字列です。2番目は初期化された値を持つ配列なので、変更できます。


-2

セグメンテーション違反は、アクセスできないメモリにアクセスしようとすると発生します。

char *str 変更不可能な文字列へのポインタです(segfaultが発生する理由)。

一方char str[]、配列であり、変更可能です。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.