- PVSM.RU - https://www.pvsm.ru -

Примечания к статье «Как писать на С в 2016 году»

Примечания к статье «Как писать на С в 2016 году» - 1
На самом деле так выглядел бы Ассемблер, если бы он был оружием, но с C тоже надо быть предельно аккуратным

От переводчика:
Данная публикация является переводом статьи-ответа на текст «How to C in 2016» [1]. Перевод последнего был опубликован мной [2] в пятницу и вызвал, местами, неоднозначную реакцию сообщества. Наводку на данный «ответ», для поддержания обсуждения вопроса уже в рамках Хабра, дал пользователь CodeRush [3], за что ему отдельное спасибо.

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

Используйте отладчик

Пункт №1, который вы, наверняка, игнорируете, а зря, — запускать для каждой строки кода отладчик уровня ядра прямо на этапе написания. Если вы задействуете потенциал данного инструмента только для решения особо сложных задач, вы, определенно, допускаете ошибку.

Я имею в виду использование таких IDE, как Visual Studio, XCode, или Eclipse. Если в данном случае вы работаете только с редактором (без возможности отладки), значит, вы плохо делаете свое дело. Я упоминаю об этом нюансе, потому что очень многие пишут коды в редакторах, в которых не предусмотрена функция отладки. И я не исключение.

Это важно при программировании на всех языках, но на С особенно. При повреждении памяти вам понадобится дамп структур и содержимого памяти для того, чтобы обнаружить ошибку. Почему х выдает какое-то странное 37653? Команда printf() для отладки стиля не прояснит ситуацию, но заглянув в данные утилиты hexdump в стеке, вы обязательно поймете, как переписывался определенный объем информации.

Не забывайте об отладке своего кода

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

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

Активно защищайте код

Однажды мне довелось работать над проектом, руководители которого во избежание сбоя в программе решили ставить catch(...) (в C ++ ) везде. Исключения и даже случаи повреждения памяти просто незаметно маскировались, и программа продолжала функционировать. Разработчикам казалось, что они снижали уязвимость кода. Они думали, что защитный стиль программирования – отличное решение.

Но это не защита, а глупость. Где логика в том, чтобы скрывать ошибки, обнаружить которые впоследствии гораздо сложнее?

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

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

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

Код должен быть качественным

Что касается этого пункта, такие вещи, как модульное, регрессионное тестирование и даже фаззинг постепенно становятся нормой. Если у вас есть проект с открытым исходным кодом, вам просто необходимо запросить опцию make test, чтобы провести его качественный анализ. С ее помощью вы сможете запустить модульное тестирование кода с высоким покрытием. Такой подход считается стандартом для крупных проектов с открытым исходным кодом. Я не шучу: модульное тестирование с высоким покрытием кода должно быть отправной точкой для каждого нового проекта. Вы заметите это во всех моих серьезных разработках с открытым исходным кодом. Тут я начинаю писать модульные тексты уже на начальном этапе, причем один за другим (правда, я ленивый, и поэтому не могу похвастаться высоким покрытием кода).

Фаззинг посредством AFL – явление относительно новое, но, протестировав данный механизм, вы поймете, насколько он эффективен в процессе идентификации багов в различных проектах с открытым кодом. Язык программирования С бьет все рекорды, когда речь идет об угрозе парсинга сигналов внешних источников. Раньше нередко возникали сбои в программах из-за плохо отформатированных файлов или никудышных пакетов сети. Но в 2016 году никто не станет терпеть подобную ерунду. Если вы не уверены в том, что, независимо от вводимых данных, программа, наверняка, будет работать корректно, значит, вы где-то ошиблись.

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

Забудьте о глобальных переменных

Когда я работаю над проектами с открытым исходным кодом на C/C ++, именно глобальные переменные не дают мне жить. Вам трудно проводить отладку проекта и задавать параметры многопоточности, потому что вы устроили настоящий рассадник глобальных переменных. Минимизируйте их использование – и рефакторинг кода станет куда эффективнее.

Да, если мы говорим о системе отладки/ регистрации статуса, обращаться к глобальным переменным даже нужно, а в остальных случаях – просто забудьте об их существовании.

Немного ООП, добавим функциональное программирование и чуть-чуть Java

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

В объектно-ориентированном программировании нас интересует то, как концепция структуры объединяет данные и методы, которые необходимы для обработки информации. Для struct Fooba вы создаете ряд функций, которые сводятся к foo_xxxx(). К вашим услугам конструктор foo_create(), деструктор foo_destroy() и море функций, определяющих структуру.

Самое главное: описывать struct Foobar в файле C, а не в файле заголовка. Пусть функции будут общедоступными, но вот точный формат структуры, желательно, скрыть. Как правило, на структуры даются прямые ссылки, что особенно важно для библиотек, экспорт заголовков которых сказывается на совместимости бинарных интерфейсов приложений (так как меняется размер структуры). Если необходимо экспортировать структуру, в качестве первого параметра указывайте версию или размер.

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

Кроме того, никто не отменял несколько хороших идей и в функциональном программировании, а именно те из присущих ему функций, которые не имеют «побочных эффектов». Это опции, рассчитанные на то, что есть исходные данные и определенная информация на выходе. Никакой самодеятельности. Большинство написанных вами функций должны выглядеть именно так. Если же вы сочиняете что-то вроде void foobar(void); — ждите неприятностей.

В списке инструментов, заметно усложняющих жизнь, упоминаются и глобальные переменные. В эту же категорию попадают и вызовы системы, снижающие ее производительность. Глобальные переменные, по сути, похожи на переменные, скрытые глубоко в теле структуры, которые вы вызываете через int foobar(struct Xyz *p);. В результате приходится копаться в недрах р, чтобы найти необходимые параметры. Проще, когда все это лежит на поверхности, тогда запрос формируется по типу foobar(p->length, p->socket->status, p->bbb). Да, в этом случае приходится работать с длинными, раздражающими списками параметров, но так вместо сложной структуры функция foobar() зависит от простых типов.

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

С – язык программирования систем низкого уровня, но, если не считать явно сложных случаев, постарайтесь им не злоупотреблять. Вместо конкретных приемов С используйте инструменты, с помощью которых ваш код будет жизнеспособным в более широкой среде, пусть даже и похожей на условия С. Так вы сможете интегрировать его с JavaScript, Java, C # и др.

Придется обойтись без арифметики указателей. Да, в 1980-х она позволила существенно ускорить коды, но с 1990-х от нее больше никакой пользы, особенно, в случае с современными оптимизирующими компиляторами. Арифметика указателей только снижает читабельность кода. Почти всякий раз, когда проект с открытым исходным кодом подвергается воздействию кибермошенников (Hearbleed, Shellshock, и т.д.), ищите причину в коде с арифметикой указателей. В качестве альтернативы обратите внимание на переменные целочисленных индексов и проанализируйте соответствующие массивы данных, как если бы вы писали это на Java.

Такой идеальный подход к написанию кода подразумевает и отказ от парсинга сетевых протоколов/форматов файлов для структур/целых. Да, пособие по сетевому протоколированию советует использовать что-то вроде noths(*(short*)p), только вот совет этот уже во время написания книги был не самым удачным, а сегодня и вовсе неуместен. Анализируйте целочисленные параметры, как на Java: p[0] * 256 + p[1]. Вам кажется, что закрепив упакованную структуру на поверхности, ее проще будет анализировать – не тут-то было.

Блокируйте небезопасные функции

Хватит использовать устаревшие функции из разряда strcpy() и spritnf(). Если я нахожу уязвимости, скорее всего, они именно здесь. Более того, с таким набором аудит вашего кода стоит гораздо дороже, ведь нужно просмотреть каждую из вышеупомянутых функций, чтобы убедиться, что буфер не переполнен. Возможно, вы уверены, что с буфером все в порядке, но мне придется долго и нудно это проверять. Нет, чтобы писать strlcpy()/strcpy_s(), и snprintf()/sprintf_s().

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

Вы знаете, почему коды вдруг дают сбой при вводе определенных данных? Может, дело в их безопасности. И, кстати, почему ваш код взламывают злоумышленники? Будете все делать правильно – больше о подобных проблемах беспокоиться не придется.

В статье «Программирование на С в 2016 году» прозвучал совет везде использовать calloc(). Не спешите ему следовать, ведь в этом случае на многих платформах вы все равно столкнетесь с переполнением размеров переменной. Кроме того, привыкайте к функциям вроде realloc(), а заодно и reallocarray().

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

Долой странный код

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

Единственное, что можно назвать правильным «стилем» — сходство кода с его соратниками из Интернета. Это относится и к личным кодам, и к проектам с открытым исходным кодом, над которыми вы обычно работаете. Вам нужно всего лишь выбрать один из существующих известных стилей, вроде предлагаемых Linux, BSD, WebKit, или Gnu.

Среди преимуществ других языков программирования, особенно Python, можно отметить весьма небольшое количество общепринятых стилей, чего не скажешь о С. Так, например, при анализе уязвимости Hearbleed выяснилось, что OpenSSL использует скобки Whitesmiths – стиля, который в свое время был общепринятым, но сейчас встречается редко и выглядит странно. LibreSSL преобразовала его в формат BSD. Весьма неплохое решение: если ваш стиль на С слишком витиеват/устарел, возможно, пора сменить его на что-то распространенное/привычное.

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

Будущее за многоядерными процессорами

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

Скажите нет мьютексам и критическим секциям – они только усложняют ваш код. Да, это повышает безопасность продукта, но ведь страдает производительность. Так ваш код может летать на 2 или 3 ядрах, но если их больше, программа начинает тормозить. Важнее масштабируемости под разное количество ядер в рамках программирования на языке С может быть только обеспечение достойного уровня безопасности продукта.

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

Забудьте о true/false для success/failure

В статье «Программирование на С в 2016 году» [2] сказано, что success всегда равняется true. Чушь. True это true, а success это success. Не ставьте между ними знак равенства. Чтобы получить 0 в случае успешного результата и другое значение при отказе функции, придется написать сложнейший код.

Да, вот такая ерунда: стандарта нет, и вряд ли когда-либо появится. Вместо того, чтобы прислушиваться к вредному совету из «Программирования на С в 2016 году», посмотрите, как doc прекрасно справляется с задачей «долой странные коды». Автор считает, что если бы мы научили других делать так же, если бы не запутывали собственные коды, подавая тем самым хороший пример, тогда от проблемы не осталось бы и следа. Наивно. Программисты никогда не согласятся с этим стандартом. Вашему коду придется выживать в мире, полном неоднозначности, где и true, и 0 означают success, несмотря на изначально противоположные значения. Создание стандарта возможно только при однозначном определении показателей SUCCESS и FAILURE.

Если код выглядит так:

if (foobar(x,y)) {
      ...;
   } else {
      ...;
   }

Читая его, я ни за что не разберусь, что здесь success, а что failure. Вот вам и разнообразие стандартов. Лучше пишите так:  
 
  if (foobar(x,y) == Success) {
      ...;
   } else {
      ...;
   }

Немного о целых величинах

Автор статьи «Программирование на С в 2016 году» утверждает, что нет никаких оснований использовать классические int или unsigned, а вместо них лучше обращаться к int32_t и uint32_t. Нонсенс! Команды int и long являются общепризнанными для ввода исходных данных в большинстве функций библиотек, причем именно они обеспечивают своего рода защиту типов и уведомляют пользователей даже при запросе различных типов одного размера.

Честно говоря, задать неверные значения целых несложно, в том числе и на 64 и 32-битных системах. Да, использование int для управления указателем сломает 64-битный код (вместо этого пишите intptr_t, ptrdiff_t или size_t), но, вы не поверите, как редко это случается на практике. Просто задайте mmap() для 4 первых гигабайт, пометив, что при загрузке эти страницы недействительны, проведите модульное/регрессионное тестирование — и вы быстро решите любую проблему. Причем мне вовсе не нужно вам объяснять, как это лучше сделать.
Что раздражает в коде больше всего, так это то, что программисты вечно спешат повторно определить типы целых величин. Завязывайте. Я понимаю, что u32 придает особый шарм коду, но меня этот элемент просто выводит из себя. А ведь именно мне придется читать код. Пожалуйста, замените его на что-нибудь стандартное, например, uint32_t или unsigned int. И, о ужас, хватит произвольно создавать типы целых вроде filesize. Я знаю, что вы хотите придать выбранному целому новый смысл, но не забывайте, что программирование на C рассчитано на «низкий уровень», а потому программисты просто умирают в процессе проверки таких перлов.

Используйте статический и динамический анализ

Если раньше специфика C сводилась к «уровням предупреждений» и опции «линт», на сегодняшний момент их место занял «статический анализ». С помощью Clang компиляторы радуют пользователей все новыми и новыми сообщениями, да и gcc старается на отставать. И, хотя многие не в курсе, компиляторы Microsoft также предлагают статический анализ на уровне Clang. Возможности XCode в сфере визуализации анализа Clang действительно впечатляют, хотя речь идет о тех же механизмах, что и в самом Clang.

Но это только общий статический анализ. А ведь есть еще много инструментов для обеспечения безопасности, которые поднимают статический анализ на более высокий уровень — Coverity, Veracode и HP Fortify.

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

Эти ужасные зависимости

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

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

Открытый исходный код тоже подразумевает определенные сложности. Зависимости редко документируют полностью, а вот недочетов в них – хоть отбавляй. В большинстве подобных случаев, в итоге, вы тратите море времени на установку двух несовместимых версий одной зависимости, чтобы, о чудо, скомпилировать необходимый код.

Чем меньше зависимостей, тем популярнее становится код. Для этого вам нужно всего лишь:

  • Удалить зависимости. Как правило, лишь 1% зависимостей приносит программисту пользу, остальные 99% — затрудняют работу.
  • Использовать только необходимый исходный файл. Вместо того, чтобы завязать все процессы на целой библиотеке OpenSSL (и ее зависимостях), просто добавьте файл sha2.c, если, конечно, вам не понадобятся другие функции OpenSSL.
  • Пусть источник всех зависимостей будет прямо в дереве. Например, Lua – шикарный скриптовый язык в 25kloc, которому просто необходимы обновления. Вместо того, чтобы бросать пользователей на произвол судьбы в погоне за соответствующей зависимостью lua-dev, укажите источник Lua в своем дереве.
  • Загружайте библиотеки в процессе запуска программы через dlopen(), не забывая про файлы их интерфейсов .h, которые также являются частью источника вашего проекта. Это значит, что у вас не будет проблем с той или иной зависимостью, пока она не нужна для функционирования библиотек. Или, если без нее никак, можете подготовить уведомления об ошибках с предусмотренными инструкциями по исправлению неполадок зависимости.

Разберитесь с неопределенными переменным на C

Скорее всего, вы не совсем понимаете, как работает язык C. Рассмотрим выражение (х + 1 < х). Действительный результат в этом случае 5. А все потому, что С не задает действия для x, если данная переменная получает максимальное целое значение и вы прибавляете 1, что приводит к ее переполнению. Многие компиляторы идентифицируют подобные явления, как переполнение в двоичном коде, так же, как и другие языки программирования (например, Java). Но, как известно, некоторые компиляторы автоматически классифицируют такого рода ситуации, как невозможные, и просто удаляют весь соответствующий код.

Таким образом, вам придется исходить не из того, как работает текущая версия вашего компилятора на С, а вникнуть в дополнительные аспекты, проанализировав, как на код могут отреагировать другие компиляторы на С.

Заключение

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

Автор: ragequit

Источник [4]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/sovety/110140

Ссылки в тексте:

[1] «How to C in 2016»: https://matt.sh/howto-c

[2] был опубликован мной: https://habrahabr.ru/company/inoventica/blog/275685/

[3] CodeRush: https://habrahabr.ru/users/coderush/

[4] Источник: https://habrahabr.ru/post/275823/