"char s []"가 아닌 문자열 리터럴로 초기화 된 "char * s"에 쓸 때 왜 세그먼테이션 오류가 발생합니까?


288

다음 코드는 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로 테스트되었습니다.


1
재밌지 만 비주얼 스튜디오 개발자 명령 프롬프트에서 Windows 컴파일러 (cl)를 사용할 때 실제로 컴파일되고 완벽하게 실행됩니다. 잠깐 혼란스러워 했어요.
David Refaeli

답변:


242

C FAQ, 질문 1.32 참조

Q : 이러한 초기화의 차이점은 무엇입니까?
char a[] = "string literal";
char *p = "string literal";
에 새 값을 할당하려고하면 프로그램이 중단됩니다 p[i].

A : 문자열 리터럴 (C 소스에서 큰 따옴표로 묶인 문자열의 공식 용어)은 두 가지 약간 다른 방식으로 사용될 수 있습니다.

  1. 의 선언에서와 같이 char 배열의 초기화 char a[]자로 해당 배열에있는 문자의 초기 값 (필요한 경우 크기)을 지정합니다.
  2. 다른 곳에서는 명명되지 않은 정적 문자 배열로 바뀌며이 명명되지 않은 배열은 읽기 전용 메모리에 저장 될 수 있으므로 반드시 수정할 수는 없습니다. 표현식 컨텍스트에서, 배열은 평소와 같이 한 번에 포인터로 변환되므로 (섹션 6 참조) 두 번째 선언은 p를 초기화하여 명명되지 않은 배열의 첫 번째 요소를 가리 키도록합니다.

일부 컴파일러에는 문자열 리터럴을 쓸 수 있는지 여부를 제어하는 ​​스위치가 있으며 (오래된 코드를 컴파일하기 위해) 문자열 리터럴을 형식적으로 const char 배열로 처리하도록하는 옵션이있을 수 있습니다 (더 나은 오류 포착).


7
다른 몇 가지 요점 : (1) segfault는 설명 된대로 발생하지만 실행 환경의 기능입니다. 동일한 코드가 임베디드 시스템에있는 경우 쓰기가 효과가 없거나 실제로 s를 z로 변경할 수 있습니다. (2) 문자열 리터럴은 쓸 수 없으므로 컴파일러는 "string"의 두 인스턴스를 같은 위치에 두어 공간을 절약 할 수 있습니다. 또는 코드의 다른 곳에 "다른 문자열"이있는 경우 하나의 메모리 덩어리가 두 리터럴을 모두 지원할 수 있습니다. 분명히 코드가 바이트를 변경하도록 허용되면 이상하고 어려운 버그가 발생할 수 있습니다.
greggo

1
@ greggo : 좋은 지적입니다. mprotect읽기 전용 보호 를 사용하여 MMU가있는 시스템에서도이 작업을 수행 할 수 있습니다 ( 여기 참조 ).

따라서 char * p = "blah"는 실제로 임시 배열? weird를 만듭니다.
rahul tyagi

1
그리고 C ++로 작성 ... TIL 이년 후
zeboidlund

@rahultyagi 무슨 뜻인가요?
Suraj Jain

105

일반적으로 문자열 리터럴은 프로그램이 실행될 때 읽기 전용 메모리에 저장됩니다. 이것은 실수로 문자열 상수를 변경하지 못하게하기위한 것입니다. 첫 번째 예에서는 "string"읽기 전용 메모리에 저장되며 *str첫 번째 문자를 가리 킵니다. segfault는 첫 번째 문자를 다음과 같이 변경하려고 할 때 발생합니다'z' .

두 번째 예에서 문자열 "string"은 컴파일러가 읽기 전용 홈에서 배열 로 복사 합니다 str[]. 그런 다음 첫 번째 문자를 변경할 수 있습니다. 각 주소를 인쇄하여이를 확인할 수 있습니다.

printf("%p", str);

또한 str두 번째 예제에서 크기를 인쇄하면 컴파일러가 7 바이트를 할당했음을 알 수 있습니다.

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

13
printf에서 "% p"를 사용할 때마다 printf ( "% p", (void *) str)에서와 같이 포인터를 void *로 캐스팅해야합니다. printf로 size_t를 인쇄 할 때 최신 C 표준 (C99)을 사용하는 경우 "% zu"를 사용해야합니다.
Chris Young

4
또한 sizeof의 괄호는 유형의 크기를 취할 때만 필요합니다 (그런 다음 인수는 캐스트처럼 보입니다). sizeof는 함수가 아니라 연산자입니다.
8:25에


34

이 답변의 대부분은 정확하지만 조금 더 명확하게 추가하기 위해 ...

사람들이 말하는 "읽기 전용 메모리"는 ASM 용어의 텍스트 세그먼트입니다. 명령어가로드되는 메모리의 동일한 위치입니다. 보안과 같은 명백한 이유로 읽기 전용입니다. 문자열로 초기화 된 char *를 만들면 문자열 데이터가 텍스트 세그먼트로 컴파일되고 프로그램이 포인터를 초기화하여 텍스트 세그먼트를 가리 킵니다. 따라서 변경하려고하면 kaboom. Segfault.

배열로 쓰여질 때 컴파일러는 초기화 된 문자열 데이터를 데이터 세그먼트에 대신 배치합니다. 이는 전역 변수와 같은 곳에 있습니다. 데이터 세그먼트에 명령어가 없기 때문에이 메모리는 변경 가능합니다. 이번에는 컴파일러가 문자 배열 (여전히 char * 임)을 초기화 할 때 텍스트 세그먼트가 아닌 데이터 세그먼트를 가리키며 런타임에 안전하게 변경할 수 있습니다.


그러나 "읽기 전용 메모리"를 수정할 수있는 구현이있을 수 있습니까?
Pacerier

배열로 쓸 때 컴파일러는 초기화 된 문자열 데이터가 정적이거나 전역 인 경우 데이터 세그먼트에 배치합니다. 그렇지 않으면 (예를 들어 일반 자동 배열의 경우) 함수 main의 스택 프레임에서 스택에 배치됩니다. 옳은?
SE

27

문자열에 쓸 때 왜 세그먼테이션 오류가 발생합니까?

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에서는 문자열 리터럴 또는 리터럴에서 생성 된 각 멀티 바이트 문자 시퀀스에 값이 0 인 바이트 또는 코드가 추가됩니다. 그런 다음 멀티 바이트 문자 시퀀스를 사용하여 시퀀스를 포함하기에 충분한 정적 스토리지 기간 및 길이의 배열을 초기화합니다. 문자열 리터럴의 경우 배열 요소에는 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";

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"은 상수를 수정하는 데 사용되는 포인터입니다.

두 번째 코드에서 "string"은 배열 이니셜 라이저입니다.

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

"str"은 스택에 할당 된 배열이며 자유롭게 수정할 수 있습니다.


1
스택, 또는 데이터 세그먼트에있는 경우 str글로벌 또는 static.
Gauthier

12

"whatever"첫 번째 예제의 컨텍스트에서 유형 은 const char *(const가 아닌 char *에 할당하더라도), 따라서 시도하거나 쓰지 않아야 함을 의미합니다.

컴파일러는 문자열을 메모리의 읽기 전용 부분에 배치하여이를 시행하므로 쓰기는 segfault를 생성합니다.


8

이 오류 또는 문제를 이해하려면 먼저 포인터와 배열의 차이점을 알아야하므로 여기에서 먼저 차이점을 설명합니다.

문자열 배열

 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

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

6

@matli가 링크 한 C FAQ는 아직 언급하지 않았으므로 명확하게 설명합니다. 문자 배열을 초기화하는 것 이외의 다른 곳에서 문자열 리터럴 (소스에서 큰 따옴표로 묶인 문자열)을 사용 하는 경우 (예 : @ Mark의 두 번째 예 (정확하게 작동)는이 문자열이 컴파일러에 의해 특수 정적 문자열 테이블 에 저장되는데 , 이는 기본적으로 익명 인 (변수는 "name"이없는 전역 정적 변수 (물론 읽기 전용)를 만드는 것과 유사합니다. "). 읽기 전용 부분은 중요한 부분이며, 왜 @ 마크의 첫 번째 코드 예제 세그먼테이션 폴트 (segfault)이다.


우리가 쓰는 int *b = {1,2,3) 것처럼 쓸 수 있을까요char *s = "HelloWorld" 있습니까?
Suraj Jain

4

그만큼

 char *str = "string";

line은 포인터를 정의하고 리터럴 문자열을 가리 킵니다. 리터럴 문자열은 쓸 수 없으므로 다음을 수행하십시오.

  str[0] = 'z';

세그 결함이 발생합니다. 일부 플랫폼에서는 리터럴이 쓰기 가능한 메모리에있을 수 있으므로 segfault가 표시되지 않지만 상관없이 유효하지 않은 코드 (정의되지 않은 동작이 발생 함)입니다.

라인 :

char str[] = "string";

문자 배열을 할당 하고 리터럴 문자열을 해당 배열에 복사 하여 완전히 쓸 수 있으므로 후속 업데이트는 문제가되지 않습니다.


우리가 쓰는 int *b = {1,2,3) 것처럼 쓸 수 char *s = "HelloWorld"있습니까?
Suraj Jain

3

"string"과 같은 문자열 리터럴은 아마도 실행 파일의 주소 공간에 읽기 전용 데이터로 할당 될 것입니다 (컴파일러를 주거나 가져옵니다). 당신이 그것을 만질 때, 그것은 당신이 수영복 지역에 있다는 것을 놀라게하고 세그 결함으로 알려줍니다.

첫 번째 예에서는 해당 const 데이터에 대한 포인터를 얻습니다. 두 번째 예에서는 const 데이터의 복사본으로 7 자 배열을 초기화합니다.


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" . 컴파일러는 쓸 수는 없지만 읽을 수있는 메모리의 위치에 문자열 리터럴을 넣을 수 있습니다. (실제로 a const char *를 할당하기 때문에 경고가 발생했을 것입니다.char * 입니다. 경고가 비활성화되었거나 방금 무시 했습니까?)

두 번째로, 전체 액세스 권한을 가진 메모리 인 배열을 만들고를 사용하여 초기화합니다 "string". 당신은 char[7](문자 6, 종료 '\ 0'에 대한 6)을 만들고 있으며 원하는대로 무엇이든 할 수 있습니다.


@ 페루 치오? 예 const접두사 변수를 읽기 전용으로 설정
EsmaeelE

C 문자열 리터럴에는 char [N]not 형식 const char [N]이 있으므로 경고가 없습니다. (적어도 gcc에서이를 변경할 수 있습니다 -Wwrite-strings.)
melpomene

0

문자열이

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

첫 번째 경우, 리터럴은 'a'가 범위 안에 들어 오면 복사됩니다. 여기서 'a'는 스택에 정의 된 배열입니다. 이는 문자열이 스택에 생성되고 해당 데이터가 일반적으로 읽기 전용 인 코드 (텍스트) 메모리에서 복사됨을 의미합니다 (구현 특정, 컴파일러는이 읽기 전용 프로그램 데이터를 읽기 / 쓰기 가능 메모리에도 배치 할 수 있음) ).

두 번째 경우, p는 스택 (로컬 범위)에 정의되고 다른 곳에 저장된 문자열 리터럴 (프로그램 데이터 또는 텍스트)을 참조하는 포인터입니다. 일반적으로 그러한 메모리를 수정하는 것은 좋은 습관이 아니며 권장되지 않습니다.


-1

첫 번째는 수정할 수없는 상수 문자열입니다. 두 번째는 초기화 된 값을 가진 배열이므로 수정할 수 있습니다.


-2

액세스 할 수없는 메모리에 액세스하려고하면 분할 오류가 발생합니다.

char *str 수정 불가능한 문자열에 대한 포인터입니다 (segfault를 얻는 이유).

반면 char str[]배열이며 수정할 수 있습니다 ..

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.