Чому буквені літери C рядки лише для читання?


29

Яка перевага рядкових літералів, що читаються лише для читання, виправдовує (-ее / -ій):

  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" не працюватиме через нульове припинення.
dan04

4
Ви не хочете писати константи, оскільки це змінить їх значення. Наступного разу, коли ви захочете використовувати ту саму константу, було б інакше. Компілятор / час виконання повинен джерела констант звідкись, і де б це не було, ви не повинні дозволяти змінювати.
Ерік Ейдт

1
"Отже, рядкові літерали зберігаються в пам'яті програми, а не оперативної пам'яті, а переповнення буфера призведе до пошкодження самої програми?" Зображення програми теж є в оперативній пам'яті. Якщо бути точним, рядкові літерали зберігаються в тому ж сегменті ОЗУ, який використовується для зберігання зображення програми. І так, перезапис рядка може пошкодити програму. Ще в часи MS-DOS і CP / M не було захисту пам’яті, ви могли робити такі речі, і це зазвичай спричиняло жахливі проблеми. Перші віруси на ПК використовували б подібні фокуси для модифікації вашої програми, щоб вона відформатувала ваш жорсткий диск, коли ви намагалися запустити його.
Чарльз Е. Грант

Відповіді:


40

Історично (можливо, переписуючи його частини), було навпаки. На перших комп’ютерах початку 1970-х (можливо, PDP-11 ) з прототиповим ембріональним C (можливо, BCPL ) не було ММУ та захисту пам'яті (що існувало на більшості старих мейнфреймів IBM / 360 ). Таким чином , кожні байти пам'яті ( в тому числі і обробках буквених рядків або машинний код) можуть бути перезаписані по помилковою програмі (уявіть собі програму , змінюючи деякі , %щоб /в Е () 3 рядок формату). Отже, буквальні рядки та константи були записані.

Будучи підлітком 1975 року, я зашифрував у музеї Palais de la Découverte в Парижі на старих комп’ютерах епохи 1960-х років без захисту пам’яті: IBM / 1620 мав лише основну пам’ять, яку можна було ініціалізувати через клавіатуру, тому вам довелося набрати кілька десятків цифр для читання початкової програми на перфорованих стрічках; CAB / 500 мав магнітну барабанну пам'ять; ви можете відключити запис деяких треків через механічні вимикачі біля барабана.

Пізніше комп'ютери отримали певну форму управління пам'яттю (MMU) з деяким захистом пам'яті. Був пристрій, забороняючи ЦП перезаписувати якусь пам’ять. Так деякі сегменти пам'яті, зокрема сегмент коду (він же .textсегмент), стали лише для читання (за винятком операційної системи, яка завантажила їх з диска). Було природно, що компілятор і лінкер розміщували прямолінійні рядки в цей кодовий сегмент, і буквальні рядки стали тільки для читання. Коли ваша програма намагалася їх перезаписати, це було погано, невизначеною поведінкою . І наявність у віртуальній пам'яті сегмента коду лише для читання дає значну перевагу: кілька процесів, що працюють в одній програмі, мають однакову оперативну пам'ять ( фізична пам'ятьсторінок) для цього кодового сегмента (див. MAP_SHAREDпрапор для mmap (2) в Linux).

Сьогодні дешеві мікроконтролери мають деяку пам'ять, доступну лише для читання (наприклад, їх Flash або ROM) і зберігають там свій код (а також буквальні рядки та інші константи). А справжні мікропроцесори (як у вашому планшетному ПК, ноутбуці чи настільному комп’ютері) мають складний блок управління пам’яттю та кеш- машини, що використовується для віртуальної пам’яті та підкачки . Таким чином, сегмент коду виконуваної програми (наприклад, в ELF ) відображається в пам'яті як сегмент, доступний лише для читання, доступний для виконання, і виконується (за допомогою mmap (2) або execve (2) в Linux; BTW можна давати директиви ldщоб отримати кодовий сегмент, який можна записати, якщо ви цього дуже хотіли). Написання або зловживання ним, як правило, є помилкою сегментації .

Отже стандарт C є бароковим: юридично (лише з історичних причин), буквальні рядки - це не const char[]масиви, а лише char[]масиви, які заборонено перезаписувати.

До речі, декілька сучасних мов дозволяють перезаписати рядкові літерали (навіть Ocaml, який історично і погано мав записувані рядки, що записуються, змінив таку поведінку останнім часом у 4.02, і тепер має рядки лише для читання).

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

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

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

Зауважте також, що з багатьох причин змінні дані важче обробляються комп'ютером (когерентність кешу), кодувати, розуміти розробником, ніж постійні дані. Тому бажано, щоб більшість ваших даних (і, зокрема, буквальних рядків) залишалися незмінними . Детальніше про парадигму функціонального програмування .

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


Це для захисту рядків як констант? Хоча вони не визначені як constу стандартних ( stackoverflow.com/questions/2245664/… )?
Marius Macijauskas

Ви впевнені, що на перших комп’ютерах не було пам'яті лише для читання? Хіба це не було значно дешевше барана? Крім того, розміщення їх у RO-пам’яті не спричиняє UB намагатися помилково їх модифікувати, але покладаючись на те, що ОП цього не робить, він порушує цю довіру. Дивіться, наприклад, програми Fortran, де всі буквальні 1зненацька поводяться так, як 2і такі веселі ...
Deduplicator

1
Будучи підлітком у музеї, я зашифрував у 1975 році на старих комп’ютерах IBM / 1620 та CAB500. Ні в якому не було ПЗУ: IBM / 1620 не мав ядрової пам'яті, а CAB500 мав магнітний барабан (деякі треки можна було відключити для запису за допомогою механічного перемикача)
Базиль Старинкевич

2
Також варто зазначити: Якщо розмістити літерали в сегменті коду, це означає, що їх можна ділити між декількома копіями програми, оскільки ініціалізація відбувається в час компіляції, а не час виконання.
Blrfl

@Deduplicator Добре, я бачив машину, яка працює з базовим варіантом, який дозволив вам змінювати цілі константи (я не впевнений, чи потрібно вам це робити, наприклад, передаючи аргументи "byref" або якщо простий let 2 = 3спрацював). Це призвело до безлічі FUN (у визначенні слова фортеця Карликів), звичайно. Я поняття не маю, як був розроблений перекладач, що це дозволяє, але це було.
Луаан

2

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

Аналогічне питання виникне у FORTRAN до винаходу C. Аргументи завжди передавалися за адресою, а не за значенням. Таким чином, звичайне додавання двох чисел було б рівнозначним:

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

У випадку, якщо хтось хотів передати постійне значення (наприклад, 4.0) sum, компілятор створив би анонімну змінну та ініціалізував би її 4.0. Якщо одне і те ж значення було передано декількома функціями, компілятор передав би однакову адресу всім їм. Як наслідок, якщо функція, яка змінила один з її параметрів, передала константу з плаваючою комою, значення цієї константи в іншому місці програми може бути змінено в результаті, що призводить до приказки "Змінні не будуть; константи не є 'т'.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.