Можно ли изменить строковый литерал в C? - PullRequest
4 голосов
/ 24 июня 2019

У меня недавно был вопрос, я знаю, что указатель на постоянный массив, инициализированный так, как это показано в приведенном ниже коде, находится в области .rodata и что эта область доступна только для чтения. Тем не менее, я видел в паттерне C11, что запись в этом адресе памяти будет неопределенной. Я знал, что компилятор Borland Turbo-C может писать, куда указывает указатель, потому что процессор работал в реальном режиме в некоторых системах того времени, таких как MS-DOS? Или это не зависит от режима работы процессора? Есть ли какой-либо другой компилятор, который пишет в указатель и не принимает никаких сбоев памяти при использовании процессора в защищенном режиме?

#include <stdio.h>

int main(void) {
    char *st = "aaa";
    *st = 'b'; 
    return 0;
}

В этом коде, компилируемом с Turbo-C в MS-DOS, вы сможете записывать в память

Ответы [ 5 ]

5 голосов
/ 25 июня 2019

Как уже указывалось, попытка изменить постоянную строку в C приводит к неопределенному поведению.Для этого есть несколько причин.

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

Другая причина заключается в том, что строка может использоваться совместно.Многие компиляторы (например, gcc) заметят, когда одна и та же буквальная строка появляется более одного раза в модуле компиляции, и будут использовать одно и то же хранилище для нее.Поэтому, если программа изменяет один экземпляр, это может повлиять и на другие.

Нет также необходимости в этом, поскольку тот же намеченный эффект может быть легко достигнут с помощью статического массива символов.Например:

#include <stdio.h>

int main(void) {
    static char st_arr[] = "aaa";
    char *st = st_arr;
    *st = 'b'; 
    return 0;
}

Это именно то, что пытался сделать размещенный код, но без какого-либо неопределенного поведения.Это также занимает столько же памяти.В этом примере строка "aaa" используется в качестве инициализатора массива и не имеет собственного хранилища.Массив st_arr занимает место строки констант из исходного примера, но (1) он не будет помещен в постоянную память и (2) он не будет использоваться совместно с любыми другими ссылками на строку.Так что это безопасно изменить, если на самом деле это то, что вы хотите.

3 голосов
/ 25 июня 2019

Чтобы добавить правильные ответы выше, DOS работает в реальном режиме, поэтому нет памяти только для чтения. Вся память плоская и доступная для записи. Следовательно, запись в литерал была хорошо определена (как это было в любой переменной типа const) в то время.

3 голосов
/ 25 июня 2019

Вы спрашиваете, может ли платформа вызывать неопределенное поведение. Ответ на этот вопрос - да.

Но вы также спрашиваете, определяет ли платформа это поведение. На самом деле это не так.

По некоторым советам по оптимизации компилятор объединит строковые константы, так что запись в одну константу будет записывать в другие использования этой константы. Я использовал этот компилятор один раз, он вполне мог объединять строки.

Не пишите этот код. Это не хорошо. Вы пожалеете о написании кода в этом стиле, когда перейдете на более современную платформу.

3 голосов
/ 25 июня 2019

Ваш литерал "aaa" создает статический массив из четырех константных символов 'a', 'a', 'a', '\ 0' в анонимном месте и возвращает указатель на первый символ 'a', приведенный к типу char *.

Попытка изменить любой из четырех символов - неопределенное поведение. Неопределенное поведение может делать что угодно: от изменения символа по своему усмотрению, от претензий на изменение символа, бездействия или сбоев.

Это в основном то же самое, что статический const char anonymous [4] = {'a', 'a', 'a', '\ 0'}; char * st = (char *) & anonymous [0];

2 голосов
/ 27 июня 2019

Существует ли какой-либо другой компилятор, который пишет в указатель и не принимает никаких сбоев памяти при использовании процессора в защищенном режиме?

Как некоторые компиляторы GCC могут изменитьуказатель на константный символ?

GCC 3 и более ранние версии поддерживали gcc -fwriteable-strings, чтобы позволить вам скомпилировать старый K & R C там, где это было явно допустимо, согласно https://gcc.gnu.org/onlinedocs/gcc-3.3.6/gcc/Incompatibilities.html.(Это неопределенное поведение в ISO C и, следовательно, ошибка в программе ISO C).Эта опция будет определять поведение присвоения, которое ISO C оставляет неопределенным.

Руководство по GCC 3.3.6 - C Параметры диалекта

-fwritable-strings
Сохранять строковые константы в сегменте данных для записи и не унифицировать их,Это для совместимости со старыми программами, которые предполагают, что они могут записывать в строковые константы.

Запись в строковые константы - очень плохая идея;«Константы» должны быть постоянными.

GCC 4.0 убрал эту опцию ( примечания к выпуску );последняя серия GCC3 была gcc3.4.6 в марте 2006 года. Хотя, по-видимому, в этой версии глючил .

gcc -fwritable-strings будет обрабатывать строковые литералы как неконстантные массивы анонимных символов (см.ответ @ gnasher), поэтому они идут в раздел .data вместо .rodata и, таким образом, связываются с сегментом исполняемого файла, который сопоставлен страницам чтения + записи, а не только для чтения.(Исполняемые сегменты в основном не имеют ничего общего с сегментацией x86, это всего лишь отображение памяти start + range из исполняемого файла в память.)

И это отключило бы объединение повторяющихся строк, поэтому char *foo() { return "hello"; } и char *bar() { return "hello"; } будет возвращать разные значения указателя вместо объединения идентичных строковых литералов.


Связанные:

...