Почему строковые литералы 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! */

Экономия памяти? Я где-то читал (не мог найти источник сейчас), когда давным-давно, когда оперативной памяти было мало, компиляторы пытались оптимизировать использование памяти, объединяя похожие строки.

Например, «more» и «regex» станут «moregex». Это все еще верно сегодня, в эпоху цифровых фильмов качества Blu-Ray? Я понимаю, что встроенные системы по-прежнему работают в среде ограниченных ресурсов, но объем доступной памяти значительно увеличился.

Проблемы с совместимостью? Я предполагаю, что унаследованная программа, которая будет пытаться получить доступ к памяти только для чтения, либо аварийно завершит работу, либо продолжит работу с необнаруженной ошибкой. Таким образом, ни одна унаследованная программа не должна пытаться получить доступ к строковому литералу, и поэтому разрешение записи в строковый литерал не нанесет вреда действующим, не взломанным, переносимым унаследованным программам.

Есть ли другие причины? Мои рассуждения неверны? Было бы разумно рассмотреть возможность изменения строковых литералов для чтения-записи в новых стандартах C или хотя бы добавить опцию в компилятор? Было ли это рассмотрено раньше или мои "проблемы" слишком незначительны и незначительны, чтобы кого-то беспокоить?

Мариус Макиаускас
источник
12
Я предполагаю, что вы смотрели, как строковые литералы выглядят в скомпилированном коде ?
2
Посмотрите на сборку, содержащуюся в приведенной мной ссылке. Это прямо там.
8
Ваш пример "moregex" не будет работать из-за нулевого завершения.
Ден04
4
Вы не хотите переписывать константы, потому что это изменит их значение. В следующий раз, когда вы захотите использовать одну и ту же константу, она будет другой. Компилятор / среда выполнения должны получать константы откуда-то, и где бы вы ни находились, вам не разрешается изменять.
Эрик Эйдт
1
«То есть строковые литералы хранятся в памяти программы, а не в ОЗУ, и переполнение буфера может привести к повреждению самой программы?» Образ программы тоже находится в оперативной памяти. Точнее, строковые литералы хранятся в том же сегменте ОЗУ, который использовался для хранения образа программы. И да, перезапись строки может повредить программу. Во времена MS-DOS и CP / M не было защиты памяти, вы могли делать такие вещи, и это обычно вызывало ужасные проблемы. Первые компьютерные вирусы использовали такие хитрости, чтобы модифицировать вашу программу, чтобы она форматировала ваш жесткий диск, когда вы пытались его запустить.
Чарльз Э. Грант

Ответы:

40

Исторически (возможно, переписывая его части), это было наоборот. На самых первых компьютерах начала 1970-х годов (возможно, PDP-11 ) с прототипом эмбрионального C (возможно, BCPL ) не было ни MMU, ни защиты памяти (которая существовала на большинстве старых мэйнфреймов IBM / 360 ). Таким образом , каждые байты памяти ( в том числе и обработках буквенных строк или машинный код) могут быть перезаписаны по ошибочной программе (представьте себе программу , изменяя некоторые , %чтобы /в Е () 3 строку формата). Следовательно, буквенные строки и константы были доступны для записи.

Будучи подростком в 1975 году, я запрограммировал в парижском музее Пале-де-ла-Декуверт на старые компьютеры эпохи 1960-х годов без защиты памяти: у IBM / 1620 была только базовая память, которую можно было инициализировать с помощью клавиатуры, поэтому вам пришлось набрать несколько десятков из цифр для чтения исходной программы на перфорированных лентах; CAB / 500 имел магнитную память барабана; Вы можете отключить запись некоторых треков через механические переключатели рядом с барабаном.

Позднее компьютеры получили какую-то форму блока управления памятью (MMU) с некоторой защитой памяти. Было устройство, запрещающее процессору перезаписывать какую-то память. Таким образом, некоторые сегменты памяти, особенно сегмент кода (также известный как .textсегмент), стали доступны только для чтения (кроме операционной системы, которая загрузила их с диска). Для компилятора и компоновщика было естественным помещать литеральные строки в этот сегмент кода, и литеральные строки стали доступны только для чтения. Когда ваша программа пыталась их перезаписать, это было плохо, неопределенное поведение . А наличие в виртуальной памяти сегмента кода, доступного только для чтения, дает значительное преимущество: несколько процессов, выполняющих одну и ту же программу, используют одну и ту же оперативную память ( физическую память).страниц) для этого сегмента кода (см. MAP_SHAREDфлаг для mmap (2) в Linux).

Сегодня дешевые микроконтроллеры имеют некоторую постоянную память (например, Flash или ROM) и хранят там свой код (а также буквенные строки и другие константы). А настоящие микропроцессоры (например, в вашем планшете, ноутбуке или настольном компьютере) имеют сложный блок управления памятью и механизм кэширования, используемый для виртуальной памяти и подкачки . Таким образом, сегмент кода исполняемой программы (например, в ELF ) представляет собой память, отображаемую как доступный только для чтения, разделяемый и исполняемый сегмент (с помощью mmap (2) или execve (2) в Linux; кстати, вы можете дать директивы ldчтобы получить доступный для записи фрагмент кода, если вы действительно этого хотите). Написание или злоупотребление это, как правило, ошибка сегментации .

Таким образом, стандарт C является барочным: юридически (только по историческим причинам) буквальные строки не являются const char[]массивами, а являются только char[]массивами, которые запрещено перезаписывать.

Кстати, немногие современные языки позволяют перезаписывать строковые литералы (даже Ocaml, который исторически - и плохо - имел доступные для записи строковые литералы, изменил это поведение недавно в 4.02 и теперь имеет строки только для чтения).

Текущие компиляторы C способны оптимизировать, иметь "ions"и "expressions"делиться своими последними 5 байтами (включая завершающий нулевой байт).

Попробуйте скомпилировать ваш код C в файл foo.cс помощью GCCgcc -O -fverbose-asm -S foo.c и заглянуть внутрь сгенерированного файла ассемблера foo.s.

Наконец, семантика C достаточно сложна (узнайте больше о CompCert и Frama-C, которые пытаются ее перехватить), и добавление доступных для записи константных литеральных строк сделает ее еще более загадочной, а программы - более слабыми и еще менее защищенными (и с меньшим количеством определенное поведение), поэтому весьма маловероятно, что будущие стандарты Си будут принимать доступные для записи буквенные строки. Возможно, наоборот, они сделают их const char[]массивами, какими они должны быть морально.

Также обратите внимание, что по многим причинам изменяемые данные труднее обрабатывать на компьютере (когерентность кэша) для кодирования, чтобы понять, для разработчика, чем постоянные данные. Поэтому желательно, чтобы большинство ваших данных (и особенно буквенные строки) оставались неизменными . Узнайте больше о парадигме функционального программирования .

В старые времена Fortran77 в IBM / 7094 ошибка могла даже изменить константу: если вы CALL FOO(1)и если FOOслучалось, изменили свой аргумент, передаваемый по ссылке 2, реализация могла бы изменить другие вхождения 1 на 2, и это было действительно непослушная ошибка, довольно трудно найти.

Василий Старынкевич
источник
Это для защиты строк как констант? Даже если они не определены как constстандартные ( stackoverflow.com/questions/2245664/… )?
Мариус Макиаускас
Вы уверены, что на первых компьютерах не было постоянной памяти? Разве это не было значительно дешевле, чем баран? Кроме того, помещение их в RO-память не приводит к тому, что UB пытается ошибочно изменить их, но полагается, что OP не делает этого, а он нарушает это доверие. Посмотрите, например, программы на Фортране, где все литералы 1s внезапно ведут себя как 2s и такие забавные ...
Deduplicator
1
Будучи подростком в музее, я программировал в 1975 году на старых компьютерах IBM / 1620 и CAB500. Ни у одного из них не было ПЗУ: у IBM / 1620 была память ядра, а у CAB500 был магнитный барабан (некоторые дорожки можно было отключить для записи с помощью механического переключателя)
Бэзил Старынкевич,
2
Также стоит отметить: размещение литералов в сегменте кода означает, что они могут быть разделены между несколькими копиями программы, потому что инициализация происходит во время компиляции, а не во время выполнения.
Blrfl
@Deduplicator Хорошо, я видел машину, на которой выполнялся вариант BASIC, который позволял вам изменять целочисленные константы (я не уверен, что вам нужно было обмануть это, например, передать аргументы byref или просто let 2 = 3сработало). Это привело к большому удовольствию (в определении слова Dwarf Fortress), конечно. Я понятия не имею, как был разработан переводчик, чтобы он позволял это, но это было так.
Луаан
2

Компиляторы не могут объединяться "more"и "regex", поскольку первый имеет нулевой байт, eа у последнего есть x, но многие компиляторы будут сочетать строковые литералы, которые идеально совпадают, а некоторые также будут совпадать со строковыми литералами, которые имеют общий хвост. Код, который изменяет строковый литерал, может, таким образом, изменять другой строковый литерал, который используется для какой-то совершенно другой цели, но в нем содержатся одинаковые символы.

Подобная проблема возникла бы в FORTRAN до изобретения C. Аргументы всегда передавались по адресу, а не по значению. Процедура добавления двух чисел, таким образом, будет эквивалентна:

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

В случае, если нужно передать константное значение (например, 4.0) sum, компилятор создаст анонимную переменную и инициализирует ее 4.0. Если одно и то же значение было передано нескольким функциям, компилятор передал бы один и тот же адрес всем им. Как следствие, если функции, которая изменила один из своих параметров, была передана константа с плавающей запятой, значение этой константы в другом месте в программе могло бы быть изменено, что привело бы к поговорке «Переменные не будут; «т».

Supercat
источник