Что такое безопасный язык программирования?


54

Безопасные языки программирования (PL) набирают популярность. Интересно, каково формальное определение безопасного PL. Например, C небезопасен, но Java безопасен. Я подозреваю, что свойство «safe» должно применяться к реализации PL, а не к самой PL. Если это так, давайте обсудим определение безопасной реализации PL. Мои собственные попытки формализовать это понятие привели к странному результату, поэтому я хотел бы услышать другие мнения. Пожалуйста, не говорите, что у каждого PL есть небезопасные команды. Мы всегда можем взять безопасное подмножество.


Комментарии не для расширенного обсуждения; этот разговор был перемещен в чат .
Жиль "ТАК - перестань быть злым"

«мы всегда можем взять безопасное подмножество» Как вы можете быть уверены, что полученный язык все еще завершен по Тьюрингу? (что обычно подразумевается под «языком программирования»)
effeffe

«свойство« safe »должно применяться к реализации PL, а не к самому PL» - вы можете вызвать сейф PL, если существует безопасная реализация.
Дмитрий Григорьев

Ответы:


17

Когда мы называем язык «безопасным» в некотором отношении , это формально означает, что есть доказательство того, что ни одна правильно сформированная программа на языке не может сделать то, что мы считаем опасным. Слово «безопасный» также используется менее формально, но именно здесь люди понимают, что означает ваш вопрос. Есть много разных определений свойств, которые мы хотим, чтобы «безопасный» язык имел.

Несколько важных из них:

  • Определение Эндрю Райтом и Матиасом Феллайзеном «правильности типа» , которое во многих местах (включая Википедию) приводится в качестве общепринятого определения «безопасности типов», и их доказательство того, что подмножество ML соответствует этому определению в 1994 году.

  • Майкл Хикс перечисляет здесь несколько определений «безопасности памяти» . Некоторые представляют собой списки типов ошибок, которые не могут возникать, а некоторые основаны на обработке указателей как возможностей. Java гарантирует, что ни одна из этих ошибок не возможна (если вы явно не используете функцию, помеченную unsafe), если сборщик мусора будет управлять всеми выделениями и освобождениями. Rust дает ту же гарантию (опять же, если вы явно не пометите код как unsafe) через свою систему аффинных типов, которая требует, чтобы переменная была либо принадлежала, либо заимствована, прежде чем ее можно будет использовать не более одного раза.

  • Аналогично, потокобезопасный код обычно определяется как код, который не может отображать определенные виды ошибок, связанных с потоками и общей памятью, включая гонки данных и взаимоблокировки. Эти свойства часто применяются на уровне языка: Rust гарантирует, что гонки данных не могут происходить в его системе типов, C ++ гарантирует, что его std::shared_ptrумные указатели на одни и те же объекты в нескольких потоках не будут удалять объект преждевременно или не удалят его при последней ссылке поскольку он уничтожен, C и C ++ дополнительно имеют atomicвстроенные в язык переменные с атомарными операциями, которые гарантированно обеспечивают определенную согласованность памяти при правильном использовании. MPI ограничивает межпроцессное взаимодействие явными сообщениями, а OpenMP имеет синтаксис для обеспечения безопасности доступа к переменным из разных потоков.

  • Свойство, что память никогда не утечет, часто называют безопасным для пространства. Автоматическая сборка мусора является одной из языковых функций, чтобы гарантировать это.

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

  • Функциональные языки имеют большое количество инвариантов, которые гарантированно поддержит любая правильно сформированная программа, например, что чистые функции не могут вызывать побочные эффекты. Они могут или не могут быть описаны как «безопасные».

  • Некоторые языки, такие как SPARK или OCaml, предназначены для облегчения проверки правильности программы. Это может или не может быть описано как «безопасный» от ошибок.

  • Доказательство того, что система не может нарушать формальную модель безопасности (отсюда и фраза «Любая система, которая доказуемо безопасна, вероятно, не является»).


1
Это может или не может быть описано как «безопасный» от ошибок. Не могли бы вы уточнить это немного? Что вы подразумеваете под "от жуков"?
scaaahu

2
@scaaahu Вот пример веб-сайта, который ссылается на программное обеспечение, формально признанное правильным как «доказуемо безопасное». В этом контексте оно относится к программному обеспечению, предотвращающему столкновение воздушных судов, поэтому оно означает защиту от столкновений.
Дэвислор

1
Я принимаю этот ответ, потому что в нем перечислены виды безопасности. Тип, который я имел в виду, это безопасность памяти.
Beroal

Хотя в этом ответе перечислены некоторые полезные ссылки и множество примеров, большинство из них полностью запутаны. Сборка мусора гарантирует, что память никогда не будет протекать или не использовать «небезопасные» блоки автоматически, что дает вам безопасность или переполнение со знаком, являющееся неопределенным поведением, потому что компиляторы С должны серьезно поддерживать некоторые странные процессоры? И просто краткое слово для Ada / SPARK, который является единственным из упомянутых языков, который серьезно относится к безопасности.
VTT

93

Не существует формального определения «безопасного языка программирования»; это неформальное понятие. Скорее, языки, которые утверждают, что обеспечивают безопасность, обычно предоставляют точное формальное заявление о том, какая безопасность заявляется / гарантируется / предоставляется. Например, язык может обеспечивать безопасность типов, безопасность памяти или некоторые другие подобные гарантии.


13
Как добавление, если мы говорим о C против Java, как о публикации OP: безопасность памяти гарантируется в Java, а не в C. Безопасность типов обеспечивается обоими по-своему. (Да, многие люди, читающие это, уже знают это, но, возможно, некоторые не знают).
Вальфрат

17
@ Walfrat Это часть этого. Java также не имеет неопределенного поведения, чего мы и ожидаем от языка, который называет себя «безопасным». Что касается систем типов, я не думаю, что сильная статическая система типов - это то, что люди обычно называют «безопасными». Динамически типизированные языки, такие как Python, как правило, «безопасны» в конце концов.
Макс Барраклоф

2
Мое определение безопасности типов - это проверка компиляции, которая это обрабатывает. Это не может быть формальным определением. Обратите внимание, что я сказал «тип безопасности», а не «безопасно». Для меня «безопасный» язык относится к «моему» определению «безопасности типов и памяти», и я думаю, что он может быть самым распространенным. Конечно, я не говорю о некоторой ловушке, такой как указатель отражения / пустоты в Си, с которой компиляция не может справиться. Другое возможное определение safe - это программа, которая не завершается с ошибкой сегмента, как унитарный указатель в C. Подобные вещи обычно предоставляются в Python и Java.
Вальфрат

7
@Walfrat Все, что вас заводит, это язык, в котором синтаксис четко определен. Это не гарантирует, что выполнение четко определено - и сколько раз я видел сбой JRE, я могу вам сказать, что как система это не «безопасно». В C, с другой стороны, MISRA приложила усилия, чтобы избежать неопределенного поведения, чтобы получить более безопасное подмножество языка, и компиляция C в ассемблер намного лучше определена. Так что это зависит от того, что вы считаете «безопасным».
Грэм

5
@MaxBarraclough - «У Java также нет неопределенного поведения» - у Java нет неопределенного поведения в том смысле, в каком он используется в спецификациях C в определении языка (хотя он позволяет некоторому коду создавать значения, которые не имеют единственного предопределенного значения, например, для доступа переменная, которая изменяется в другом потоке, или посредством доступа к doubleили longкогда она изменяется в другом потоке, что не гарантирует, что не будет получено половина одного значения, смешанного каким-то неуказанным способом с половиной другого), но спецификация API однако имеет неопределенное поведение в некоторых случаях.
Жюль

41

Если вы сможете получить экземпляр « Типов и языков программирования» Бенджамина Пирса , во введении у него будет хороший обзор различных точек зрения на термин «безопасный язык».

Одна из предлагаемых интерпретаций этого термина, которая может вас заинтересовать:

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

Таким образом, я бы не хотел использовать термин «небезопасный» для обозначения реализации языка программирования. Если неопределенный термин в языке вызывает различное поведение в разных реализациях, одна из реализаций может привести к тому, что поведение продукта будет более ожидаемым, но я бы не назвал его «безопасным».


7
Конечно, проблема остановки говорит о том, что, независимо от языка, всегда будут программы, поведение которых не предсказуемо из определения языка. Таким образом, любое определение, которое зависит от «предсказать поведение каждой программы в языке», в корне неверно для любого языка, полного по Тьюрингу.
MSalters

15
@MSalters Это популярное недопонимание проблемы остановки. Неразрешимость проблемы остановки означает, что невозможно механически вывести поведение произвольной программы на языке, полном по Тьюрингу. Но возможно, что для любой данной программы поведение предсказуемо. Просто вы не можете создать компьютерную программу, которая делает такой прогноз.
Жиль "ТАК - перестань быть злым"

7
@ Джайлс: Это не так. Предположим, что существует доказательство отсутствия завершения для каждой программы без завершения. Затем вы можете перечислить доказательства не прекращения, чтобы найти, останавливается ли данная программа. Так что проблема остановки решаема. Противоречие. Таким образом, некоторые неразрывные программы не являются доказуемо неразрывными.
Кевин

9
@ Жиль: я прекрасно осознаю тот факт, что многие программы тривиально доказывают, останавливаются они или нет. Но утверждение здесь буквально о поведении каждой программы. Доказательство теоремы Остановки показывает, что существует хотя бы одна программа, для которой это не так. Это просто неконструктивное доказательство, оно не скажет вам, какая программа неразрешима.
MSalters

8
@MSalters Я думаю, что подразумевается, что утверждение касается мелкомасштабного поведения программы, а не крупномасштабного, эмерджентного поведения. Например, возьмем гипотезу Коллатца . Отдельные этапы алгоритма просты и хорошо определены, но возникающее поведение (сколько итераций до остановки и, если вообще так происходит), совсем не так. - «Прогноз» используется здесь неформально. Это может быть лучше написано как «знать, как будет выполняться любое заданное утверждение в произвольной программе».
RM

18

Сейф не бинарный, это континуум .

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

  • Безопасность памяти: язык и его реализация предотвращают множество ошибок, связанных с памятью, таких как использование после освобождения, двойное освобождение, доступ за пределы допустимого, ...
  • Безопасность типов: язык и его реализация предотвращают множество ошибок, связанных с типами, таких как непроверенные приведения, ...

Это не единственные классы ошибок, которые предотвращают языки, свобода гонки данных или тупика довольно желательна, доказательства правильности довольно приятны и т. Д ...

Однако просто некорректные программы редко считаются «небезопасными» (только с ошибками), а термин «безопасность» обычно зарезервирован для гарантий, влияющих на нашу способность рассуждать о программе. Таким образом, C, C ++ или Go, имеющие неопределенное поведение, небезопасны.

И, конечно же, есть языки с небезопасными подмножествами (Java, Rust, ...), которые целенаправленно разграничивают области, в которых разработчик отвечает за поддержание языковых гарантий, а компилятор находится в режиме «от руки». Языки, как правило, все еще называют безопасными , несмотря на это практическое определение.


7
Я бы сказал, что это решетка.
PatJ

1
Большинство реализаций языков программирования имеют небезопасные функции (например, Obj.magicв Ocaml). И на практике это действительно нужно
Василий Старынкевич

4
@BasileStarynkevitch: Действительно. Я думаю, что любой язык с FFI обязательно содержит некоторый уровень небезопасности, так как вызов функции C потребует «закрепления» объектов GC и ручного обеспечения совпадения сигнатур на обеих сторонах.
Матье М.

15

Хотя я не согласен с ответом DW, я думаю, что он оставляет одну часть «безопасного» без внимания.

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

Следует отметить, что эти разные понятия, следовательно, имеют разные классы ошибок, и эти классы не являются взаимоисключающими, и эти классы не охватывают все формы ошибок. Просто чтобы взять два примера DW, вопрос о том, содержит ли определенная область памяти определенный объект, является одновременно вопросом безопасности типов и безопасности памяти.

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


10
Это довольно не по теме для этого сайта, потому что разработка программного обеспечения на самом деле не наука, но я не согласен с вашим эмпирическим утверждением. Использование хороших библиотек не спасет вас на небезопасных языках, потому что вы не защищены от их неправильного использования. Безопасные языки дают вам больше гарантий от автора библиотеки и дают больше уверенности в том, что вы используете их правильно.
Жиль "ТАК - перестань быть злым"

3
Я с MSalters по этому вопросу. - «Безопасные языки позволяют получить больше гарантий от автора библиотеки и дают больше уверенности в том, что вы используете их правильно». Это не sequitur для всех практических целей.
Капитан Жираф

9

Принципиальное различие между C и Java заключается в том, что если не использовать некоторые легко идентифицируемые возможности Java (например, те, которые находятся в Unsafeпространстве имен), то каждое возможное действие, в том числе «ошибочные», будет иметь ограниченный диапазон возможных результатов. , Хотя это ограничивает то, что можно делать в Java - по крайней мере, без использования Unsafeпространства имен, оно также позволяет ограничить ущерб, который может быть вызван ошибочной программой или, что более важно, программой, которая будет правильно обрабатывать допустимые файлы, но не особо защищены от ошибочных.

Традиционно компиляторы C обрабатывали бы многие действия стандартным образом в «нормальных» случаях, в то же время обрабатывая многие угловые случаи «способом, характерным для среды». Если бы кто-то использовал процессор, который бы замыкался и загорался, если происходило переполнение чисел, и хотел бы избежать возгорания процессора, нужно было бы написать код, чтобы избежать переполнения числа. Однако, если бы кто-то использовал процессор, который бы совершенно удачно урезал значения в виде дополнения до двух, не нужно было избегать переполнений в тех случаях, когда такое усечение приводило бы к приемлемому поведению.

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

 uint32_t test(uint16_t x)
 {
   if (x < 50000) foo(x);
   return x*x; // Note x will promote to "int" if that type is >16 bits.
 }

«Современный» C-компилятор, имеющий что-то похожее на вышесказанное, может заключить, что, поскольку вычисление x * x будет переполнено, если x больше 46340, он может выполнить вызов «foo» безоговорочно. Обратите внимание, что даже если было бы приемлемо прекратить ненормальное завершение программы, если x выходит за пределы диапазона, или если в таких случаях функция должна возвращать любое значение, вызов foo () с x вне диапазона может привести к повреждению, намного превышающему любая из этих возможностей. Традиционный C не обеспечил бы никакой защитной экипировкой сверх того, что поставил программист и соответствующая платформа, но позволил бы защитной экипировке ограничить ущерб от неожиданных ситуаций. Modern C обойдет любое защитное снаряжение, которое не на 100% эффективно держит все под контролем.


3
@DavidThornley: Возможно, мой пример был слишком тонким. Если int32 бита, то xбудет повышен до подписи int. Судя по обоснованию, авторы стандарта ожидали, что не странные реализации будут обрабатывать подписанные и неподписанные типы эквивалентным образом вне некоторых конкретных случаев, но gcc иногда «оптимизирует» способами, которые ломаются, если uint16_tпри uint16_tумножении на a получается результат, превосходящий INT_MAX. , даже когда результат используется в качестве значения без знака.
суперкат

4
Хороший пример. Это одна из причин, почему мы всегда (на GCC или Clang) должны компилировать -Wconversion.
Дэвислор

2
@Davislor: Ах, я только что заметил, что Godbolt изменил порядок, в котором перечислены версии компилятора, поэтому выбор последней версии gcc в списке дает самую последнюю, а не самую раннюю версию. Я не думаю, что предупреждение особенно полезно, так как оно склонно return x+1;отмечать множество ситуаций, например, которые не должны быть проблематичными, и приведение результата к uint32_t душит сообщение, не решая проблему.
суперкат

2
@supercat Исключать тесты бессмысленно, если компилятор должен вернуть тесты в другое место.
user253751

3
@immibis: директива «предположить допущение» может позволить компилятору заменить множество тестов или проверку, которая будет выполняться много раз в цикле, одним тестом, который может быть выведен за пределы цикла. Это лучше, чем требовать от программистов добавлять проверки, которые не нужны в машинном коде для программы, отвечающей требованиям, с целью обеспечения того, чтобы компилятор не «оптимизировал» проверки, необходимые для удовлетворения требований.
суперкат

7

В языке есть несколько уровней правильности. В порядке увеличения абстракции:

  • Немногие программы без ошибок (только те, для которых правильность может быть доказана). Другие уже упоминали, что сдерживание ошибок является поэтому самым конкретным аспектом безопасности. В этом отношении языки, работающие на виртуальной машине, такой как Java и .net, в целом безопаснее: программные ошибки обычно перехватываются и обрабатываются определенным образом. 1
  • На следующем уровне ошибки, обнаруженные во время компиляции, а не во время выполнения, делают язык более безопасным. Синтаксически правильная программа также должна быть максимально семантически правильной. Конечно, компилятор не может знать общую картину, так что это касается уровня детализации. Сильные и выразительные типы данных являются одним из аспектов безопасности на этом уровне. Можно сказать, что язык должен усложнять определенные ошибки(ошибки типа, вне доступа, неинициализированные переменные и т. д.). Информация о типе времени выполнения, такая как массивы, которые содержат информацию о длине, позволяет избежать ошибок. Я запрограммировал Ada 83 в колледже и обнаружил, что компиляция Ada-программы обычно содержит на порядок меньше ошибок, чем соответствующая C-программа. Просто возьмите способность Ады определять целочисленные типы, которые нельзя назначить без явного преобразования: целые космические корабли потерпели крушение из-за путаницы в футах и ​​метрах, чего можно было бы избежать с помощью Ады.

  • На следующем уровне язык должен обеспечивать средства, чтобы избежать стандартного кода. Если вам нужно написать свои собственные контейнеры, или их сортировку, или их конкатенацию, или если вы должны написать свои собственные, string::trim()вы будете делать ошибки. Поскольку уровень абстракции повышается, этот критерий включает в себя как сам язык, так и стандартную библиотеку языка.

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

  • Язык должен обеспечивать средства для модульности и сотрудничества. Сильные, тщательно продуманные пользовательские типы сверху помогают создавать выразительные API.

Несколько ортогонально определение языка должно быть понятным; язык и библиотеки должны быть хорошо документированы. Плохая или отсутствующая документация приводит к плохим и неправильным программам.


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


1
+1 Для ясного послойного объяснения. Вопрос для вас: целые космические корабли разбились из-за путаницы в футах и ​​метрах, чего можно было бы избежать с помощью Ады. Вы говорите о том, что Mars Probe потерян из-за простой математической ошибки ? Ты случайно не знаешь язык, который они использовали для этого космического корабля?
scaaahu

2
@scaaahu Да, я думаю, что имел в виду это. Нет, я не знаю языка. На самом деле, читая отчет, кажется, что данные, отправленные зондом, были обработаны программным обеспечением на Земле, создав файл данных, который затем использовался для определения уровней тяги. Простая типизация языка не применима в этом сценарии. Кстати, у них было много проблем с наземным программным обеспечением и форматом файла данных, путаница, которая препятствовала раннему обнаружению проблемы. Таким образом, история не является прямым аргументом для строгой типизации, но все же предостерегающая история.
Питер - Восстановить Монику

1

Пожалуйста, не говорите, что у каждого PL есть небезопасные команды. Мы всегда можем взять безопасное подмножество.

На каждом языке, который я знаю, есть способы написания нелегальных программ, которые можно (компилировать и) запускать. И у каждого языка, который я знаю, есть безопасное подмножество. Итак, какой у тебя вопрос на самом деле?


Безопасность многомерна и субъективна.

В некоторых языках есть много операций, которые «небезопасны». У других таких операций меньше. В некоторых языках способ выполнения чего-либо по умолчанию небезопасен. В других способ по умолчанию безопасен. В некоторых языках существует явное «небезопасное» подмножество. В других языках такого подмножества нет вообще.

В некоторых языках «безопасность» относится исключительно к безопасности памяти - услуга, предлагаемая стандартной библиотекой, и / или среда выполнения, в которой нарушения доступа к памяти становятся трудными или невозможными. В других языках «безопасность» явно включает в себя безопасность потоков. На других языках «безопасность» относится к гарантии того, что программа не будет аварийно завершена (требование, которое включает недопущение каких-либо необработанных исключений). Наконец, во многих языках «безопасность» относится к безопасности типов - если система типов согласована определенным образом, она называется «надежной» (между прочим, Java и C # не имеют полностью звуковых систем типов).

А в некоторых языках все различные значения «безопасность» считаются подмножествами безопасности типов (например, Rust и Pony достигают безопасности потоков благодаря свойствам системы типов).


-1

Этот ответ немного шире. Слова «безопасность и безопасность» в последние десятилетия были изуродованы некоторыми политически ориентированными частями англоязычного общества, так что их общее использование почти не имеет определения. Тем не менее, по техническим предметам я все же возвращаюсь к определению «безопасность» и «безопасность» как: устройство, которое предотвращает непреднамеренное использование чего-либо или которое существенно затрудняет случайное использование, и состояние нахождения под защитой такого устройства ,
Таким образом, безопасный язык имеет какое-то устройство для ограничения определенного класса ошибок. Конечно, ограничения в некоторых случаях приводят к неудобствам или даже невозможности, и это не значит, что «небезопасные» языки приводят к ошибкам. Например, у меня нет защитных пробок на вилках, и в течение десятилетий мне удавалось без особых усилий избежать удара глазом во время еды. Конечно, меньше усилий, чем было бы потрачено на пробки. Таким образом, безопасность сопряжена с определенными затратами, о которых нужно судить. (пробка - это ссылка на персонажа Стива Мартина)

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.