C 문자열 리터럴이 읽기 전용 인 이유는 무엇입니까?


29

읽기 전용 문자열 리터럴의 장점은 무엇입니까? (-ies / -ied)

  1. 발에 몸을 쏠 수있는 또 다른 방법

    char *foo = "bar";
    foo[0] = 'd'; /* SEGFAULT */
  2. 한 줄에 단어의 읽기-쓰기 배열을 우아하게 초기화 할 수 없음 :

    char *foo[] = { "bar", "baz", "running out of traditional placeholder names" };
    foo[1][2] = 'n'; /* SEGFAULT */ 
  3. 언어 자체를 복잡하게합니다.

    char *foo = "bar";
    char var[] = "baz";
    some_func(foo); /* VERY DANGEROUS! */
    some_func(var); /* LESS DANGEROUS! */

메모리를 절약 하시겠습니까? RAM이 부족했을 때 컴파일러는 비슷한 문자열을 병합하여 메모리 사용을 최적화하려고 시도했던 그 어느 시점 (지금 소스를 찾을 수 없음)을 읽었습니다.

예를 들어 "more"및 "regex"는 "moregex"가됩니다. 오늘날 디지털 블루 레이 품질 영화 시대에도 이것이 사실입니까? 임베디드 시스템은 여전히 ​​제한된 리소스 환경에서 작동하지만 여전히 사용 가능한 메모리 양이 크게 증가했다는 것을 알고 있습니다.

호환성 문제? 읽기 전용 메모리에 액세스하려고하는 레거시 프로그램이 충돌하거나 발견되지 않은 버그로 계속 될 것이라고 가정합니다. 따라서 레거시 프로그램은 문자열 리터럴에 액세스하려고하지 않아야하므로 문자열 리터럴에 쓰도록 허용해도 유효하고 해킹되지 않은 휴대용 레거시 프로그램 에는 영향을 미치지 않습니다 .

다른 이유가 있습니까? 내 추론이 잘못 되었습니까? 새로운 C 표준에서 읽기 / 쓰기 문자열 리터럴을 변경하거나 최소한 컴파일러에 옵션을 추가하는 것이 합리적입니까? 이전에이 문제가 고려 되었습니까, 아니면 내 "문제"가 너무 작고 중요하지 않은 사람입니까?


12
컴파일 된 코드에서 문자열 리터럴이 어떻게 보이는지 살펴 보았습니다 .

2
내가 제공 한 링크에 포함 된 어셈블리를보십시오. 바로 거기에 있습니다.

8
널 종료로 인해 "moregex"예제가 작동하지 않습니다.
dan04

4
상수를 덮어 쓰지 않으려면 값이 변경되기 때문입니다. 다음에 동일한 상수를 사용하려고 할 때 달라집니다. 컴파일러 / 런타임은 어딘가에서 상수를 가져와야하며 어디에서나 수정해서는 안됩니다.
Erik Eidt

1
'그래서 문자열 리터럴은 RAM이 아닌 프로그램 메모리에 저장되며 버퍼 오버플로로 인해 프로그램 자체가 손상됩니까?' 프로그램 이미지도 RAM에 있습니다. 정확하게 말하면, 문자열 리터럴은 프로그램 이미지를 저장하는 데 사용 된 동일한 RAM 세그먼트에 저장됩니다. 문자열을 덮어 쓰면 프로그램이 손상 될 수 있습니다. MS-DOS와 CP / M 시절에는 메모리 보호 기능이 없었으며 이와 같은 작업을 수행 할 수 있었고 대개 심각한 문제가 발생했습니다. 첫 번째 PC 바이러스는 그러한 트릭을 사용하여 프로그램을 수정하여 하드 드라이브를 실행할 때 하드 드라이브를 포맷했습니다.
Charles E. Grant

답변:


40

역사적으로 (아마도 일부를 다시 작성하여), 그 반대였습니다. 1970 년대 초반 (아마도 PDP-11 ) 프로토 타입 배아 C (아마도 BCPL )를 실행하는 최초의 컴퓨터 에는 MMU메모리 보호 기능 이 없었습니다 (대부분의 오래된 IBM / 360 메인 프레임 에 존재 함 ). (리터럴 스트링 또는 기계 코드를 처리하는 것을 포함) 메모리의 모든 바이트가 잘못된 프로그램이 덮어 쓰기 할 수 있도록 (일부 변경 프로그램 상상 %/A의 printf와 (3) 형식 문자열). 따라서 리터럴 문자열과 상수를 쓸 수있었습니다.

1975 년 십대, I 메모리 보호없이 오래된 1960 년대 시대의 컴퓨터에서 파리의 팔레 드 라 메디 테라 박물관 코딩 : / 1620 IBM은 여러 수십 입력했다, 그래서 키보드를 통해 초기화 할 수 있습니다 - 어떤 단지 코어 메모리를했다 펀치 테이프의 초기 프로그램을 읽을 수있는 자릿수; CAB / 500 에는 자기 드럼 메모리가있었습니다. 드럼 근처의 기계식 스위치를 통해 일부 트랙을 쓰지 못하게 할 수 있습니다.

나중에 컴퓨터에는 메모리 보호 기능이있는 메모리 관리 장치 (MMU)가 있습니다. CPU가 어떤 종류의 메모리를 덮어 쓰지 못하게하는 장치가있었습니다. 따라서 일부 메모리 세그먼트, 특히 코드 세그먼트 (일명 .text세그먼트)는 읽기 전용이되었습니다 (디스크에서로드 한 운영 체제 제외). 컴파일러와 링커에서 리터럴 문자열을 해당 코드 세그먼트에 넣는 것이 자연스럽고 리터럴 문자열은 읽기 전용이되었습니다. 당신의 프로그램이 그것들을 덮어 쓰려고 시도했을 때, 그것은 정의되지 않은 행동 이었습니다. 가상 메모리에 읽기 전용 코드 세그먼트가 있으면 동일한 장점을 제공합니다 . 동일한 프로그램을 실행하는 여러 프로세스 가 동일한 RAM ( 실제 메모리)을 공유합니다.해당 코드 세그먼트에 대한 페이지) ( Linux의 mmap (2)MAP_SHARED대한 플래그 참조 ).

오늘날 저렴한 마이크로 컨트롤러 에는 읽기 전용 메모리 (예 : 플래시 또는 ROM)가 있으며 코드 (및 리터럴 문자열 및 기타 상수)를 유지합니다. 또한 태블릿, 랩톱 또는 데스크탑과 같은 실제 마이크로 프로세서에는 정교한 메모리 관리 장치와 가상 메모리페이징에 사용되는 캐시 장치가 있습니다. 의 코드 세그먼트 그래서 실행 프로그램 (예에서 ELF가 ) 메모리 읽기 전용, 공유, 그리고에 의해 실행 가능한 세그먼트 (로 매핑됩니다 의 mmap (2) 또는 에서 execve (2) 리눅스, BTW 당신을 지시 줄 수있는 신분증을정말로 원한다면 쓰기 가능한 코드 세그먼트를 얻으려면). 쓰기 또는 악용은 일반적으로 세그먼테이션 오류 입니다.

따라서 C 표준은 바로크 식입니다. 법적으로 (역사적 이유로 만) 리터럴 문자열은 const char[]배열이 아니라 char[]덮어 쓸 수 없는 배열입니다.

BTW, 현재 사용 가능한 언어 중 문자열 리터럴을 덮어 쓸 수있는 언어는 거의 없습니다 (역사적으로 쓸 수있는 리터럴 문자열이 최근 4.02에서 동작이 변경되어 현재 읽기 전용 문자열을 갖는 Ocaml조차도).

현재 C 컴파일러는 마지막 5 바이트 (종료 널 바이트 포함) 를 최적화하고 보유 "ions"하고 "expressions"공유 할 수 있습니다.

파일에 C 코드를 컴파일하려고 foo.c으로 gcc -O -fverbose-asm -S foo.c생성 된 어셈블러 파일 내부 및 모양을 foo.s하여 GCC

마지막 으로 C 의 의미론 은 충분히 복잡 하며 (캡처하려는 CompCert & Frama-C 에 대해 자세히 읽어보십시오 ) 쓰기 가능한 상수 리터럴 문자열을 추가하면 프로그램이 더 약하고 덜 안전 해지며 더 약해집니다. 정의 된 동작)으로 인해 향후 C 표준이 쓰기 가능한 리터럴 문자열을 허용 할 가능성은 거의 없습니다. 반대로 const char[]그들은 도덕적으로해야 할 것처럼 배열을 만들 것 입니다.

또한 여러 가지 이유로, 가변 데이터는 상수 데이터보다 컴퓨터 (캐시 일관성), 개발자가 코딩, 이해하기가 어렵습니다. 따라서 대부분의 데이터 (특히 리터럴 문자열)를 변경하지 않는 것이 좋습니다. 함수형 프로그래밍 패러다임 에 대해 자세히 알아보십시오 .

IBM / 7094의 이전 Fortran77 일에서 버그는 상수 를 변경할 수도 있습니다 .2를 참조하여 전달 된 인수를 수정 한 CALL FOO(1)경우 FOO구현이 다른 발생을 1로 2로 변경했을 수 있습니다. 나쁜 버그, 찾기가 매우 어렵습니다.


이것이 문자열을 상수로 보호하는 것입니까? const표준으로 정의되어 있지 않더라도 ( stackoverflow.com/questions/2245664/… )?
Marius Macijauskas

첫 번째 컴퓨터에 읽기 전용 메모리 가 없었 습니까? 램보다 훨씬 저렴하지 않습니까? 또한 RO 메모리에 넣더라도 UB가 잘못 수정하려고 시도하지는 않지만 OP를 수행하지 않으면 해당 신뢰를 위반합니다. 예를 들어 모든 문자 1가 갑자기 2재미 처럼 행동 하는 포트란 프로그램을보십시오 .
Deduplicator

1
박물관의 십대 시절, 나는 1975 년에 오래된 IBM / 1620 및 CAB500 컴퓨터에서 코딩했습니다. ROM도 없었습니다 : IBM / 1620에는 코어 메모리가 있었고 CAB500에는 마그네틱 드럼이있었습니다 (일부 트랙은 기계식 스위치로 쓰기 가능하도록 비활성화 될 수 있음)
Basile Starynkevitch

2
코드 세그먼트에 리터럴을 넣으면 런타임이 아닌 컴파일 타임에 초기화가 수행되므로 프로그램의 여러 사본에서 문자를 공유 할 수 있습니다.
Blrfl

@Deduplicator 글쎄, 정수 상수를 변경할 수있는 BASIC 변형을 실행하는 기계를 보았습니다 (예 : "byref"인수 전달 또는 간단한 let 2 = 3작동 여부). 이것은 물론 많은 재미 (단어의 드워프 요새 정의에서)를 가져 왔습니다. 나는 인터프리터가 어떻게 이것을 허용하도록 설계되었는지 전혀 모른다.
루안

2

컴파일러는 결합 할 수 "more""regex"는 후에 전자는 널 바이트를 가지고 있기 때문에 e반면 후자는있다 x, 그러나 많은 컴파일러는 완벽하게 일치하는 문자열 리터럴을 결합 것이고, 일부는 공통의 꼬리를 공유 문자열 리터럴 일치합니다. 따라서 문자열 리터럴을 변경하는 코드는 완전히 다른 목적으로 사용되지만 동일한 문자를 포함하는 다른 문자열 리터럴을 변경할 수 있습니다.

C의 발명 이전에 FORTRAN에서도 비슷한 문제가 발생했다. 인수는 항상 값이 아닌 주소로 전달되었다. 따라서 두 개의 숫자를 추가하는 루틴은 다음과 같습니다.

float sum(float *f1, float *f2) { return *f1 + *f2; }

상수 값 (예 : 4.0)을로 전달하려는 sum경우 컴파일러는 익명 변수를 만들어로 초기화합니다 4.0. 동일한 값이 여러 함수에 전달되면 컴파일러는 동일한 주소를 모든 함수에 전달합니다. 결과적으로 매개 변수 중 하나를 수정 한 함수에 부동 소수점 상수가 전달되면 프로그램의 다른 곳에서 해당 상수의 값이 변경되어 "변수가 없습니다. 상수가 없습니다." '티".

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