[c] “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로 테스트되었습니다.



답변

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 배열로 처리하도록하는 옵션이있을 수 있습니다 (더 나은 오류 포착).


답변

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

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

printf("%p", str);

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

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


답변

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

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

배열로 쓰여질 때 컴파일러는 초기화 된 문자열 데이터를 데이터 세그먼트에 대신 배치합니다. 이는 전역 변수와 같은 곳에 있습니다. 데이터 세그먼트에 명령어가 없기 때문에이 메모리는 변경 가능합니다. 이번에는 컴파일러가 문자 배열 (여전히 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에서는 문자열 리터럴 또는 리터럴에서 생성 된 각 멀티 바이트 문자 시퀀스에 값이 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


답변

첫 번째 코드에서 “string”은 문자열 상수이며 문자열 상수는 종종 읽기 전용 메모리에 배치되므로 수정해서는 안됩니다. “str”은 상수를 수정하는 데 사용되는 포인터입니다.

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

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

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


답변

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

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


답변

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

문자열 배열

 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