Почему язык С не помешает вам делать ошибки

в 10:48, , рубрики: C, Блог компании Mail.Ru Group, никто не читает теги, отладка, Программирование, стандарты
Почему язык С не помешает вам делать ошибки - 1

Если вкратце: потому что мы так сказали.

:)

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

Встреча Комитета по языку С — которую сначала планировали провести в германском Фрайбурге, но не срослось по понятным причинам, — завершилась 7 августа. Она прошла хорошо, мы продвинулись по всем фронтам. Да, мы действительно продвигаемся, уверяю вас, и язык С не умер.

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

И всё же я это сказал (что язык С никогда не помешает вам делать ошибки), а значит должен обосновать. Можем посмотреть на тысячи CVE и связанные с ними тикеты с кучей кода на С. Либо можем привлечь MISRA, чтобы она энергично проверяла каждую, даже самую мелкую фичу С на возможность неправильного использования (привет, объявления прототипа K&R…) или наличие более сложных и забавных ошибок, относящихся к портируемости и неопределённому поведению. Но вместо этого почитаем первоисточник — что сказал сам Комитет.

О, пора брать попкорн?!

Нет, дорогой читатель, отложи попкорн. Как и в случае со всеми процедурами ISO, я не могу цитировать чьи-либо слова, и эта статья не для того, чтобы покрывать кого-то позором. Но я объясню, почему то, что мы легко можем посчитать плохим поведением в соответствующем стандартам документе ISO C, никогда не будет исключено. И начнём с документа за авторством доктора Филиппа Клауса Краузе (Dr. Philipp Klaus Krause):

N2526, используйте const для библиотечных данных, которые не будут модифицированы.

N2526 — очень простой документ.

Некоторые данные, возвращаемые библиотекой, являются константами нравственно, по духу и даже по своей реализации. Записывать в них — это ошибка и неопределённое поведение, так что давайте перестанем дразнить друг друга и окольцуем этого шалуна! …

Согласен, говорится не совсем о том же, но я уверен, что идея покажется тебе разумной, дорогой читатель. Когда этот документ поставили на голосование, против него почти не было голосов. Позднее несколько человек сильно возразили, потому что это предложение ломало старый код. Конечно, это плохо: даже у меня перехватывает дыхание, когда я задумываюсь, добавлять ли const? В языке С нет ABI, на который может повлиять нововведение. С (его реализации) даже не обращает внимание на квалификаторы (qualifier), как же мы можем что-то поломать?! Давайте разберёмся, почему некоторые считают, что это будет критическим изменением.

Язык С

Или, как мне нравится называть его, «Типобезопасность — это для языков-неудачников». Да, слишком многословно, так что остановимся на «С». Возможно, вы удивились, почему я говорю, что в языках вроде С нет типобезопасности. Ведь вот же:

struct Meow {
    int a;
};

struct Bark {
    double b;
    void* c;
};

int main (int argc, char* argv[]) {
    (void)argc;
    (void)argv;

    struct Meow cat;
    struct Bark dog = cat;
    // error: initializing 'struct Bark' with an expression of incompatible type 'struct Meow'

    return 0;
}

Буду честен: для меня это выглядит как сильная типобезопасность, Джим! А так всё становится ещё пикантнее:

#include <stdlib.h>

struct Meow {
    int a;
};

struct Bark {
    double b;
    void* c;
};

int main (int argc, char* argv[]) {
    (void)argc;
    (void)argv;

    struct Meow* p_cat = (struct Meow*)malloc(sizeof(struct Meow));
    struct Bark* p_dog = p_cat;
    // :3
    
    return 0;
}

Да, стандарт С позволяет двум совершенно независимым типам указателей ссылаться друг на друга. Большинство компиляторов предупредят об этом, но стандарт требует принимать этот код, если только вы не раскрутите -Werror -Wall -Wpedantic и т.д., и т.д., и т.д.

На самом деле, компилятор может без явного приведения типов принимать вот что:

  • volatile (кому вообще нужны эти семантики?!)
  • const (записывайте сюда любые данные «только для чтения»!)
  • _Atomic (потокобезопасность!)

Я не хочу сказать, что вы не должны иметь возможности всё это делать. Но когда пишешь на С — в котором очень легко создать функцию на 500-1000 строк с совершенно непонятными именами переменных, — Инфа Сотка, что вы работаете преимущественно с указателями, и вам вообще не хватает безопасности в том, что касается базового языка. Примечание: это нарушает ограничения, но уже написано столько старого кода, что каждая реализация каким-либо образом игнорирует квалификаторы, и из-за этого вашему коду не помешают компилироваться (спасибо, @fanf!)! В этой ситуации можно с помощью компилятора легко определить каждый потенциальный сбой, и вы получите предупреждения, но от вас не потребуют приводить типы ради того, чтобы вы дали компилятору понять, Что Вы Действительно Хотели Так Сделать. Хотя гораздо важнее то, что при этом человеческие существа, которые придут после вас, тоже не будут понимать, что вы намеревались сделать.

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

Теперь всё честно, верно? Если кто-то уберёт все эти предупреждения и флаги об ошибках, их не будет заботить, какие оплошности или глупости вы совершите. А это значит, что в конце концов эти предупреждения совершенно не имеют отношения к делу и безвредны в том, что касается соответствия стандарту ISO C. И всё же…

Мы рассматриваем критические изменения в предупреждениях

Да.

Это специальный ад, к которому привыкли С-разработчики и в меньшей степени С++-разработчики. Предупреждения раздражают, и, как показывает практика включения -Weverything или /W4, раздражают очень сильно. Скрытие предупреждений о переменных в глобальном пространстве имён (спасибо, теперь все заголовки и С-библиотеки превратились в проблему), использование «зарезервированных» имён (как говорят дети, «lol nice one xd!!«), а также «эта структура обладает паддингом, потому что вы использовали alignof» (…да, да, я знаю, что у неё есть паддинг, я явно попросил увеличить паддинг, ПОТОМУ ЧТО Я ИСПОЛЬЗОВАЛ alignof, МИСТЕР КОМПИЛЯТОР) — всё это отнимает кучу времени.

Но это же предупреждения.

Даже если они раздражают, они помогают избежать проблем. Тот факт, что я могу беззастенчиво игнорировать все квалификаторы и пренебрегать всеми видами безопасности чтения, записи, потоков и режима «только для чтения», является серьёзной проблемой, когда речь заходит о сообщении своих намерений и предотвращении багов. Даже старый синтаксис K&R приводил к багам в промышленных и правительственных кодовых базах, потому что пользователи делали что-то неправильно. И делали они так не потому, что паршивые программисты, а потому, что работают с кодовыми базами, которые зачастую старше них, и готовятся биться с тех. долгом размером в миллионы строк. Невозможно удержать в голове всю кодовую базу: для этого предназначены соглашения, статический анализ, высокоуровневые предупреждения и прочие средства. К сожалению,

всем хочется иметь код Без Предупреждений.

Это означает, что когда GCC-разработчик делает предупреждения более чувствительными к потенциально проблемным ситуациям, мейнтейнеры (не исходные разработчики) неожиданно получают от старого кода жирные логи на несколько гигабайтов, содержащие массу новеньких предупреждений и всякого разного. «Это идиотизм», — говорят они, — «код работал ГОДАМИ, так почему GCC теперь жалуется?». То есть, даже если добавить const в сигнатуру функции, даже если это нравственно, по духу и фактически правильно, этого будут избегать. «Ломать» людей означает «теперь они должны искать код, у которого сомнительные намерения». Это код, который может — под страхом Неопределённого Поведения — рушить ваш чип или повреждать вашу память. Но это уже другая проблема, сопутствующая профессии С-разработчика в наши дни.

Возраст как мера качества

Сколько людей хотя бы предполагали, что у sudo была такая примитивная уязвимость, как «-1 или целочисленное переполнение даёт доступ ко всему»? Сколько людей думали, что Heartbleed может стать настоящей проблемой? Сколько разработчиков игр отгружают «маленькие» stb-библиотеки, даже не применив к ним фазер и не понимая, что эти библиотеки содержат больше важных входных уязвимостей, чем можно представить? Я не критикую все эти разработки или их авторов: они оказывают нам жизненно важную помощь, от которой мир зависел десятилетиями, зачастую практически без чье-либо поддержки, пока не возникала какая-нибудь большая проблема. Но люди, которые боготворят эти разработки и ставят их у себя, потом источают из себя ядовитую поговорку, иллюстрируя ошибку выжившего:

Этот код такой старый и его использует столько людей, какие в нём могут быть проблемы?

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

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

Мда… отвратительно. Но что насчёт Стандарта C?

Проблема, которую я заметил во время моего (невероятно короткого) пребывания в роли участника встречи, заключается в том, мы ставим во главу угла обратную совместимость. Для тех, кто даже сегодня переходит на С, мы держимся за старые приложения и сценарии их применения и лишаем себя возможности улучшить безопасность, защищённость или искусность кода на С. Предложение доктора Краузе настолько лаконично, что практически бесспорно: если кому-то не нравятся предупреждения, он может их отключить. Эти предупреждения, а не ошибки, появляются не зря: абстрактная машина С не требует проводить диагностику, ISO C позволяет принимать такой код в строгих режимах сборки. И это помогло бы всему миру оторваться от API, которые открыто заявляют: «изменение содержимого, которое мы вам предоставляем, является неопределённым поведением».

И всё же мы поменяли своё мнение об этом документе после того, как в качестве причины было названо «мы не можем вводить новые предупреждения».

Аргумент против предложения звучал так: «написано так много кода, который сломается, если мы изменим эти сигнатуры». Это, повторюсь, ограничивает изменения в предупреждениях с точки зрения нарушения поведения (не забывайте, неявные преобразования, убирающие квалификаторы — даже _Atomic — полностью валидны по мнению ISO C, даже если они нарушают ограничения). Будь это так, каждый автор компилятора вводил бы что-то вроде Эпох в Rust, Только Для Предупреждений, чтобы дать людям «стабильный» эталонный набор для тестирования. Это предложение не ново: я читал подобные документы от инженеров Coverity о генерировании новых предупреждений и реакции пользователей на них. Трудно управлять «уверенностью» разработчиков относительно новых предупреждений и прочего. Приходится долго убеждать людей в их полезности. Даже Джону Кармаку пришлось потрудиться, чтобы получить от своих инструментов для статического анализа нужный набор предупреждений и ошибок, подходящий для его разработки, прежде чем он пришёл к выводу, что «безответственно это не использовать».

И всё же мы, Комитет, не согласились добавить const в четыре функции, возвращающие значения, потому что это добавило бы предупреждения в потенциально опасный код. Мы возражали против того, чтобы старый синтаксис K&R признали нерекомендуемым, несмотря на веские доказательства как невинных оплошностей, так и серьёзных уязвимостей, возникающих при передаче неправильных типов. Мы почти добавили в предпроцессор неопределённое поведение, просто чтобы костьми лечь, но сделать так, чтобы реализация С «вела себя как нужно». Из-за обратной совместимости мы всегда ходим по краю, чтобы не сделать очевидных ошибок. И это, дорогой читатель, пугает меня больше всего касательно будущего С.

Стандарт С не защищает вас

Не заблуждайтесь: не имеет значения, что говорят вам программисты или что вам нашёптывают. Управляющий Комитет по языку С действует очень ясно. Мы не добавим в ваш старый код новые предупреждения, даже если этот код может быть опасен. Мы не будем удерживать вас от ошибок, потому что это может пошатнуть представление о работе вашего старого кода, а это неправильно. Мы не будем помогать новичкам писать более качественный код на С. Мы не будем требовать, чтобы ваш старый код соответствовал какому-либо Стандарту. Каждая новая фича будет опциональной, потому что мы не можем представить, чтобы мы заставляли авторов компиляторов держаться более высокого стандарта или ожидали большего от разработчиков нашей стандартной библиотеки.

Мы позволим компилятору врать вам. Мы будем врать вашему коду. А когда всё пойдёт не так — возникнет ошибка, «ой фигня какая-то случилась», произойдёт утечка данных, — мы будем торжественно качать головой. Мы поделимся своими идеями и помолимся за вас, и скажем: «ну, стыд-то какой». И правда, стыд…

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

Автор: Макс

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js