Найди ошибку в коде программы
type
TForm1 = class(TForm)
pnl1: TPanel;
lbl1: TLabel;
lbl2: TLabel;
tmr1: TTimer;
tmr2: TTimer;
btn1: TButton;
lbl3: TLabel;
btn2: TButton;
btn3: TButton;
btn4: TButton;
btn5: TButton;
procedureFormShow(Sender: TObject);
procedure tmr1Timer(Sender: TObject);
procedure tmr2Timer(Sender: TObject);
procedure btn1Click(Sender: TObject);
procedure btn2Click(Sender: TObject);
procedure btn3Click(Sender: TObject);
procedure btn4Click(Sender: TObject);
procedure btn5Click(Sender: TObject);
private
< Private declarations >
public
< Public declarations >
end;
var
Form1: TForm1;
uses Unit2, Unit3, Unit4, Unit5, Unit6, Unit7;
procedure TForm1.FormShow(Sender: TObject);
begin
Form2.Showmodal;
end;
procedure TForm1.tmr1Timer(Sender: TObject);
begin
lbl1.Caption:=TimeToStr(time)
end;
procedure TForm1.tmr2Timer(Sender: TObject);
begin
lbl2.Caption:=DateToStr(Date)
end;
procedure TForm1.btn1Click(Sender: TObject);
begin
Form3.show
end;
procedure TForm1.btn2Click(Sender: TObject);
begin
Form6.show
end;
procedure TForm1.btn3Click(Sender: TObject);
begin
Form5.show
end;
procedure TForm1.btn4Click(Sender: TObject);
begin
Form4.show
end;
procedure TForm1.btn5Click(Sender: TObject);
begin
Form7.show
end;
Форма «Логин и палоль»
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;
type
TForm2 = class(TForm)
lbl1: TLabel;
lbl2: TLabel;
lbl3: TLabel;
edt1: TEdit;
lbl4: TLabel;
edt2: TEdit;
btn1: TButton;
private
< Private declarations >
public
< Public declarations >
procedure TForm2.btn1Click(Sender: TObject);
end;
var
Form2: TForm2;
i,p:Integer;
procedure TForm2.btn1Click(Sender: TObject);
begin
if edt1.text=’1234’ then i:=1 else i:=0;
if edt1.text=’1234’ then p:=1 else p:=0;
if (p+i)>1 then Form2.Close
elselbl4.caption:=’Неверныйлогинилипароль’
end;
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, Grids, DBGrids, ExtCtrls, DBCtrls, DB, ADODB;
type
TForm4 = class(TForm)
con1: TADOConnection;
tbl1: TADOTable;
ds1: TDataSource;
dbnvgr1: TDBNavigator;
dbgrd1: TDBGrid;
lbl1: TLabel;
private
< Private declarations >
public
< Public declarations >
end;
var
Form4: TForm4;
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, Grids, DBGrids, ExtCtrls, DBCtrls, DB, ADODB;
type
TForm3 = class(TForm)
con1: TADOConnection;
tbl1: TADOTable;
ds1: TDataSource;
dbnvgr1: TDBNavigator;
dbgrd1: TDBGrid;
lbl1: TLabel;
private
< Private declarations >
public
< Public declarations >
end;
var
Form3: TForm3;
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, Grids, DBGrids, ExtCtrls, DBCtrls, DB, ADODB;
type
TForm5 = class(TForm)
con1: TADOConnection;
tbl1: TADOTable;
ds1: TDataSource;
dbnvgr1: TDBNavigator;
dbgrd1: TDBGrid;
lbl1: TLabel;
private
< Private declarations >
public
< Public declarations >
end;
var
Form5: TForm5;
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, Grids, DBGrids, ExtCtrls, DBCtrls, DB, ADODB;
type
TForm6 = class(TForm)
con1: TADOConnection;
tbl1: TADOTable;
ds1: TDataSource;
dbnvgr1: TDBNavigator;
dbgrd1: TDBGrid;
lbl1: TLabel;
private
< Private declarations >
public
< Public declarations >
end;
var
Form6: TForm6;
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, Grids, DBGrids, ExtCtrls, DBCtrls, DB, ADODB;
type
TForm7 = class(TForm)
con1: TADOConnection;
tbl1: TADOTable;
ds1: TDataSource;
dbnvgr1: TDBNavigator;
dbgrd1: TDBGrid;
lbl1: TLabel;
private
< Private declarations >
public
< Public declarations >
end;
Искать ошибки в программах — непростая задача. Здесь нет никаких готовых методик или рецептов успеха. Можно даже сказать, что это — искусство. Тем не менее есть общие советы, которые помогут вам при поиске. В статье описаны основные шаги, которые стоит предпринять, если ваша программа работает некорректно.
Шаг 1: Занесите ошибку в трекер
После выполнения всех описанных ниже шагов может так случиться, что вы будете рвать на себе волосы от безысходности, все еще сидя на работе, когда поймете, что:
- Вы забыли какую-то важную деталь об ошибке, например, в чем она заключалась.
- Вы могли делегировать ее кому-то более опытному.
Трекер поможет вам не потерять нить размышлений и о текущей проблеме, и о той, которую вы временно отложили. А если вы работаете в команде, это поможет делегировать исправление коллеге и держать все обсуждение в одном месте.
Вы должны записать в трекер следующую информацию:
- Что делал пользователь.
- Что он ожидал увидеть.
- Что случилось на самом деле.
Шаг 3: Найдите строку, в которой проявляется ошибка
Если ошибка вызывает падение программы, попробуйте запустить её в IDE под отладчиком и посмотрите, на какой строчке кода она остановится. Совершенно необязательно, что ошибка будет именно в этой строке (см. следующий шаг), но, по крайней мере, это может дать вам информацию о природе бага.
Шаг 4: Найдите точную строку, в которой появилась ошибка
Как только вы найдете строку, в которой проявляется ошибка, вы можете пройти назад по коду, чтобы найти, где она содержится. Иногда это может быть одна и та же строка. Но чаще всего вы обнаружите, что строка, на которой упала программа, ни при чем, а причина ошибки — в неправильных данных, которые появились ранее.
Если вы отслеживаете выполнение программы в отладчике, то вы можете пройтись назад по стектрейсу, чтобы найти ошибку. Если вы находитесь внутри функции, вызванной внутри другой функции, вызванной внутри другой функции, то стектрейс покажет список функций до самой точки входа в программу (функции main() ). Если ошибка случилась где-то в подключаемой библиотеке, предположите, что ошибка все-таки в вашей программе — это случается гораздо чаще. Найдите по стектрейсу, откуда в вашем коде вызывается библиотечная функция, и продолжайте искать.
Шаг 5: Выясните природу ошибки
Ошибки могут проявлять себя по-разному, но большинство из них можно отнести к той или иной категории. Вот наиболее частые.
Если ваша ошибка не похожа на описанные выше, или вы не можете найти строку, в которой она появилась, переходите к следующему шагу.
Шаг 6: Метод исключения
Если вы не можете найти строку с ошибкой, попробуйте или отключать (комментировать) блоки кода до тех пор, пока ошибка не пропадет, или, используя фреймворк для юнит-тестов, изолируйте отдельные методы и вызывайте их с теми же параметрами, что и в реальном коде.
Попробуйте отключать компоненты системы один за другим, пока не найдете минимальную конфигурацию, которая будет работать. Затем подключайте их обратно по одному, пока ошибка не вернется. Таким образом вы вернетесь на шаг 3.
Шаг 7: Логгируйте все подряд и анализируйте журнал
Ваша задача состоит в том, чтобы вернуться к шагу 3, обнаружив, где проявляется ошибка. Также это именно тот случай, когда стоит использовать сторонние библиотеки для более тщательного логгирования.
Шаг 8: Исключите влияние железа или платформы
Замените оперативную память, жесткие диски, поменяйте сервер или рабочую станцию. Установите обновления, удалите обновления. Если ошибка пропадет, то причиной было железо, ОС или среда. Вы можете по желанию попробовать этот шаг раньше, так как неполадки в железе часто маскируют ошибки в ПО.
Если ваша программа работает по сети, проверьте свитч, замените кабель или запустите программу в другой сети.
Ради интереса, переключите кабель питания в другую розетку или к другому ИБП. Безумно? Почему бы не попробовать?
Если у вас возникает одна и та же ошибка вне зависимости от среды, то она в вашем коде.
Шаг 9: Обратите внимание на совпадения
- Ошибка появляется всегда в одно и то же время? Проверьте задачи, выполняющиеся по расписанию.
- Ошибка всегда проявляется вместе с чем-то еще, насколько абсурдной ни была бы эта связь? Обращайте внимание на каждую деталь. На каждую. Например, проявляется ли ошибка, когда включен кондиционер? Возможно, из-за этого падает напряжение в сети, что вызывает странные эффекты в железе.
- Есть ли что-то общее у пользователей программы, даже не связанное с ПО? Например, географическое положение (так был найден легендарный баг с письмом за 500 миль).
- Ошибка проявляется, когда другой процесс забирает достаточно большое количество памяти или ресурсов процессора? (Я однажды нашел в этом причину раздражающей проблемы «no trusted connection» с SQL-сервером).
Шаг 10: Обратитесь в техподдержку
Наконец, пора попросить помощи у того, кто знает больше, чем вы. Для этого у вас должно быть хотя бы примерное понимание того, где находится ошибка — в железе, базе данных, компиляторе. Прежде чем писать письмо разработчикам, попробуйте задать вопрос на профильном форуме.
Ошибки есть в операционных системах, компиляторах, фреймворках и библиотеках, и ваша программа может быть действительно корректна. Но шансы привлечь внимание разработчика к этим ошибкам невелики, если вы не сможете предоставить подробный алгоритм их воспроизведения. Дружелюбный разработчик может помочь вам в этом, но чаще всего, если проблему сложно воспроизвести вас просто проигнорируют. К сожалению, это значит, что нужно приложить больше усилий при составлении багрепорта.
Полезные советы (когда ничего не помогает)
Что вам точно не поможет
- Паника
Не надо сразу палить из пушки по воробьям. Некоторые менеджеры начинают паниковать и сразу откатываться, перезагружать сервера и т. п. в надежде, что что-нибудь из этого исправит проблему. Это никогда не работает. Кроме того, это создает еще больше хаоса и увеличивает время, необходимое для поиска ошибки. Делайте только один шаг за раз. Изучите результат. Обдумайте его, а затем переходите к следующей гипотезе. - «Хелп, плиииз!»
Когда вы обращаетесь на форум за советом, вы как минимум должны уже выполнить шаг 3. Никто не захочет или не сможет вам помочь, если вы не предоставите подробное описание проблемы, включая информацию об ОС, железе и участок проблемного кода. Создавайте тему только тогда, когда можете все подробно описать, и придумайте информативное название для нее. - Переход на личности
Если вы думаете, что в ошибке виноват кто-то другой, постарайтесь по крайней мере говорить с ним вежливо. Оскорбления, крики и паника не помогут человеку решить проблему. Даже если у вас в команде не в почете демократия, крики и применение грубой силы не заставят исправления магическим образом появиться.
Ошибка, которую я недавно исправил
Это была загадочная проблема с дублирующимися именами генерируемых файлов. Дальнейшая проверка показала, что у файлов различное содержание. Это было странно, поскольку имена файлов включали дату и время создания в формате yyMMddhhmmss . Шаг 9, совпадения: первый файл был создан в полпятого утра, дубликат генерировался в полпятого вечера того же дня. Совпадение? Нет, поскольку hh в строке формата — это 12-часовой формат времени. Вот оно что! Поменял формат на yyMMddHHmmss , и ошибка исчезла.
Начните с просмотра видео, чтобы познакомиться с принципами эффективной отладки и избежать распространенных ошибок.
Примеры в этой статье написаны на языке JavaScript, но принципы одинаковы для любого языка.
Тесты
Код на Хекслете проверяется с помощью автоматических тестов. Обычно они написаны на том же языке, на котором написан сам код. Общий принцип работы такого вида тестирования довольно прост. Тестируемая программа загружается в память и вызывается с разными параметрами, а тесты следят за тем, чтобы ее поведение соответствовало ожидаемому.
Когда код не проходит тесты, то обычно говорят что тесты упали. В этот момент начинается самое интересное. Необходимо понять, где и почему возникла ошибка. И вывод тестов в этом процессе играет ключевую роль, это главный помощник и проводник. Но необходим опыт, чтобы начать делать правильные выводы из того, что пишут тесты.
В первую очередь нужно классифицировать проблему. Ошибки в тестах можно грубо разделить на две категории:
- ошибки, которые выдает компилятор или интерпретатор: синтаксическая ошибка, ошибка типизации
- ошибочные утверждения.
Утверждения
Утверждение — это специальная функция, которая вызывает ваш код с определенными параметрами и проверяет, что он возвращает ожидаемый результат. Например:
Самое важное: если тесты упали на утверждении, это означает, что ваш код как минимум отработал, но его результат не соответствует ожидаемому. Причем часто бывает так, что часть утверждений проходит проверку, то есть код возвращает правильный результат, а часть — нет, обычно в пограничных случаях. В конечном итоге падение теста на утверждении говорит о том, что в коде логическая ошибка.
Ниже — пример вывода упавшего теста. То, насколько вывод подробный, зависит от вида утверждения и возможностей тестовой среды.
Вывод можно разделить на две части:
- Первая — описание того, что ожидалось от функции и что было получено. В нашем примере это строка AssertionError: 3 == 1 . Читается она следующим образом: «ожидалось, что функция вернет 3, но она вернула 1». Это уже хорошо, но еще хотелось бы увидеть, с какими параметрами была вызвана функция. И в этом нам поможет вторая часть вывода.
- Вторая часть называется backtrace, она содержит список функций, которые последовательно вызывались в коде. Порядок вывода, чаще всего, обратный: в начале то, что вызывалось последним.
В первую очередь нужно, начиная с конца, найти первое упоминание функции из файла, который похож на тестовый. Обычно его называние содержит слово test. В примере выше это at Object.<anonymous> (test.js:4:8) . В скобках указана строчка, на которой находится вызов этого утверждения. В данном случае — строчка 4.
Всё, что теперь остается, это зайти в соответствующий файл и посмотреть то, как вызывалась ваша функция.
Предупреждения компилятора и интерпретатора
Синтаксические ошибки
Самый простой тип ошибок. Такая ошибка говорит о том, что вы ошиблись в синтаксисе. Забыли запятую, скобку и тому подобные вещи. Такие ошибки легко находить и исправлять. Синтаксическая ошибка сопровождается текстом, по которому можно загуглить возможные причины.
Другие ошибки
Также ошибки содержат вывод backtrace, по которому можно найти то место, в котором возникла ошибка и попробовать его проанализировать.
Чистота и качество кода - важные критерии оценки работы программиста. Если код работает — это уже хорошо, но нельзя забывать о том, что он должен быть очищен от «мусора», быть логичным и понятным для других разработчиков. Ведь нередко работа одного программиста — это лишь часть кода крупной программной платформы, которую придется обслуживать в будущем. Скорее всего, другим людям.
В компаниях ревью кода обычно проводят сеньоры. Но их время — дорогое. Если хотя бы частично избавить сеньора от этой нагрузки, он может стать гораздо эффективней для компании. Чтобы при этом сохранять качество кода на высоком уровне, стоит использовать онлайн-сервисы оценки готового кода. Эта статья поможет вам выбрать один или несколько таких инструментов.
Reshift
- Интеграция с Github и Bitbucket.
- Пул-реквесты без переключения на другие дашборды во избежание путаницы.
- Умная маркировка проблемных мест.
- Отслеживание уязвимостей в каждой ветке.
- Показ критических уязвимостей перед мерджем с главной веткой.
Collaborator
Один из наиболее продвинутых инструментов ревью кода. Подойдёт как для работы команд, так и для отдельных разработчиков.
- Контроль за изменениями кода, определение проблем, создание комментариев.
- Создание правил и уведомлений на их основе.
- Кастомные поля, чеклисты, группы участников.
- Интеграция с 11 разными SCM и IDE, в том числе Eclipse и Visual Studio.
- Персонализированные ревью-отчеты.
Gerrit
Бесплатный онлайн-сервис проверки качества кода, который позволяет работать прямо в браузере, отклоняя или одобряя изменения. Сочетает в одной платформе багтрекер и инструмент ревью кода.
- Интеграция с Git — возможность управления репозиториями Git через Gerrit.
- Настраиваемая иерархия кода.
- Добавление комментариев при внесении изменений.
- Система голосований по вносимым изменениям
Codestriker
Ещё один неплохой open-source инструмент для ревью кода. Онлайн-сервис Codestriker позволяет быстро найти проблемы в коде и улучшить общее его качество.
- Фиксирование всех проблем, решений и комментариев в базе данных. Впоследствии к ней можно вернуться и посмотреть, что было сделано, какие изменения внесены.
- Интеграция с ClearCase, Bugzilla, CVS и не только
Crucible
Онлайн-приложение для ревью кода, поиска проблем, обсуждения изменений в отдельных ветках, шеринга данных и т.п. Crucible не бесплатный сервис. Есть две версии — для небольших команд и для корпораций. В первом случае нужно один раз заплатить $10, после чего становятся доступными безлимитные репозитории для 5 пользователей. Корпоративная версия стоит $1100, покупатель получает возможность открыть безлимитный репозиторий для 10 пользователей. Есть демо-доступ на 30 дней.
- Совместная работа как 2-3 программистов, так и больших групп разработчиков.
- Возможность ревью кода как до, так и после внесения изменений.
- Совместимость с SVN, Perforce и CVS.
Review Board
Ещё один бесплатный open-source инструмент, который применяется для ревью кода и отдельных документов. Можно попробовать демо-версию на сайте разработчика или же установить инструмент его на своём сервере. Хорош он тем, что даёт возможность лоб в лоб сравнить две версии кода — с изменениями и без — через простой интерфейс.
Существует сервис уже около десяти лет, и всё это время его создатели продолжают совершенствовать Review Board, добавляя новые функции и улучшая существующие.
- Простая интеграция в ClearCase, CVS, Perforce, Plastic.
- Выделение участков кода с проблемами или заданными параметрами.
- Возможность использовать инструмент для ревью кода как до, так и после внесения изменений.
GitHub
Наверное, нет разработчика, который бы не слышал о GitHub, но как автоматический ревьюер кода он известен гораздо меньше. Здесь у него есть две версии — бесплатная, с ограничением по количеству пользователей, и платная, от $7 в месяц.
В дополнение к обычным инструментам запроса на изменения, есть возможность проверят историю изменений, комментировать участки кода, разрешать простые конфликты при помощи веб-интерфейса. Кроме того, GitHub даёт возможность использовать и сторонние инструменты ревью кода.
- Сравнение фрагментов кода лоб в лоб.
- Просмотр истории отдельных фрагментов кода без просмотра всего документа — так называемый blame view.
- Создание white-листов по отдельным веткам.
Phabricator
Это целый набор open-source инструментов от Phacility, облегчающих работу по оценке кода. Можно использовать облачную версию, а можно загрузить всё на свой сервер. Если использовать второй вариант — ограничений нет. В случае же облачной версии нужно будет платить от $20 за пользователя в месяц. Верхняя планка - $1000 в месяц. Все платные предложения включают техническую поддержку, плюс 30-дневный пробный режим.
Rhodecode
Онлайн-инструмент, который поддерживает три версии систем контроля: Mercurial, Git и Subversion. Сервис не бесплатен. Цены начинаются с $8 в месяц за пользователя. Есть возможность заплатить сразу $75 за пользователя в год, что позволяет сэкономить пару десятков долларов. Если не хочется платить, можно загрузить community-edition, установив на своём сервере.
- Визуальный лог изменений.
- Онлайн-редактор кода.
- Интеграция с существующими проектами.
- Возможность совместной работы как нескольких разработчиков, так и больших команд.
Завершая подборку, повторим: описанные инструменты для ревью кода не призваны полностью заменить человека. Но они позволяют ускорить проверку во много раз, что даёт возможность значительно экономить время и ресурсы.
А какими инструментами пользуетесь вы? Ждём комментариев, поделитесь с коллегами :)
GNU Compiler Collection (обычно используется сокращение GCC) — набор компиляторов для различных языков программирования, разработанный в рамках проекта GNU. GCC является свободным программным обеспечением, распространяется фондом свободного программного обеспечения на условиях GNU GPL и GNU LGPL и является ключевым компонентом GNU toolchain. Проект написан на языке C и C++.
Компилятор GCC имеет хорошие встроенные диагностики, помогающие выявлять многие ошибки на этапе компиляции. Естественно, GCC собирается с помощью GCC и, соответственно, может выявлять ошибки в собственном коде. Дополнительно исходный код GCC проверяется с помощью анализатора Coverity. Да и вообще, думаю GCC проверялся энтузиастами с помощью многих анализаторов и других инструментов. Это делает поиск ошибок в GCC большим испытанием для анализатора кода PVS-Studio.
Примечание. Статья задержалась с выходом, и возможно какие-то ошибки уже исправлены. Но это не имеет значения: постоянно появляются новые ошибки, старые исчезают. Главное — статья показывает, что статический анализ может помогать программистам выявлять ошибки после их появления.
Предвидя дискуссию
Как я сказал во введении, я считаю GCC проектом с высоким качеством кода. Уверен, многие захотят поспорить. В качестве примера приведу цитату из Wikipedia на русском языке:
Некоторые разработчики OpenBSD, например Тео де Раадт и Отто Мурбек (Otto Moerbeek), критикуют GCC, называя его «громоздким, глючным, медленным и генерирующим плохой код».
Я считаю такие заявления необоснованными. Да, возможно, код GCC содержит много макросов, которые затрудняют его чтение. Но я никак не могу согласиться с заявлением о его глючности. Если бы GCC глючил, вообще бы нигде ничего не работало. Вы только вспомните, как много программ им компилируется и успешно работает. Создатели GCC делают огромную, сложную работу с большим профессионализмом. Спасибо им. Я рад, что могу протестировать работу PVS-Studio на таком высококачественном проекте.
Для тех, кто скажет, что код компилятора Clang всё равно круче, напомню: в нём PVS-Studio также находил ошибки: 1, 2.
PVS-Studio
Я проверил код GCC с помощью Alpha-версии анализатора PVS-Studio for Linux. Мы планируем начать выдавать заинтересовавшимся программистам Beta-версию анализатора в середине сентября 2016 года. Инструкцию о том, как стать одним из первых, кто сможет попробовать Beta-версию PVS-Studio for Linux на своём проекте, вы найдете в статье "PVS-Studio признаётся в любви к Linux".
Результаты проверки
К сожалению, я не могу выдать разработчикам компилятора полный отчёт. В нем пока слишком много мусора (ложных срабатываний), связанных с тем, что анализатор не полностью готов к встрече с миром Linux. Нужно проделать работу по уменьшению количества ложных предупреждений на типовые используемые конструкции. Попробую пояснить на одном простом примере. Многие диагностики не должны ругаться на выражения, относящиеся к макросам assert. Эти макросы бывают устроены весьма творчески и надо научить анализатор не обращать на них внимание. Но дело в том, что определяется макрос assert очень по-разному, и надо обучить анализатор всем типовым вариантам.
Поэтому разработчиков GCC прошу подождать выхода по крайней мере Beta-версии анализатора. Я не хочу испортить впечатление отчетом, сгенерированным недоделанной версией.
Классика (Copy-Paste)
Начнем мы с самой классической и распространённой ошибки, которая выявляется с помощью диагностики V501. Как правило, такие ошибки появляются из-за невнимательности при Copy-Paste или просто являются опечатками, допускаемыми при наборе нового кода.
Предупреждение анализатора PVS-Studio: V501 There are identical sub-expressions '!strcmp(a->v.val_vms_delta.lbl1, b->v.val_vms_delta.lbl1)' to the left and to the right of the '&&' operator. dwarf2out.c 1428
Быстро увидеть ошибки проблематично и следует внимательно присмотреться. Именно поэтому ошибка и не была выявлена при обзорах кода и рефакторинге.
Функция strcmp дважды сравнивает одни и те же строки. Мне кажется, второй раз следовало сравнивать не члены класса lbl1, а lbl2. Тогда корректный код должен выглядеть так:
Хочу отметить, что код, приведённый в статье, немного отформатирован, чтобы он занимал мало места по оси X. На самом деле, код выглядит так:
Ошибки, возможно, удалось бы избежать, если использовать «табличное» выравнивание кода. Например, ошибку было бы легче заметить, если отформатировать код так:
Подробнее я рассматривал такой подход в электронной книге "Главный вопрос программирования, рефакторинга и всего такого" (см. главу N13: Выравнивайте однотипный код «таблицей»). Рекомендую всем, кто заботится о качестве своего кода, познакомиться с приведённой здесь ссылкой.
Давайте рассмотрим ещё одну ошибку, которая, я уверен, появилась из-за Copy-Paste:
Предупреждение анализатора PVS-Studio: V519 The 'has_avx512vl' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 500, 501. driver-i386.c 501
В переменную has_avx512vl дважды подряд записываются различные значения. Это не имеет смысла. Я изучил код и обнаружил переменную has_avx512ifma. Скорее всего, именно она и должна инициализироваться выражением ebx & bit_AVX512IFMA. Тогда корректный код должен быть таким:
Опечатка
Продолжу испытание вашей внимательности. Посмотрите на код и, не подсматривая ниже, попробуйте найти ошибку.
Предупреждение анализатора PVS-Studio: V528 It is odd that pointer to 'char' type is compared with the '\0' value. Probably meant: *xloc.file == '\0'. ubsan.c 1472
Здесь программист случайно забыл разыменовать указатель в выражении xloc.file == '\0'. В результате указатель просто сравнивается с 0, т.е. с NULL. Никакого эффекта это не имеет, так как ранее такая проверка уже выполнялась: xloc.file == NULL.
Правильный вариант кода:
Хотя, давайте ещё немного улучшим код. Я рекомендую отформатировать выражение так:
Обратите внимание: теперь, если допустить ту же ошибку, шанс её заметить будет чуть-чуть выше:
Потенциальное разыменование нулевого указателя
Ещё этот раздел можно было бы назвать «стотысячный пример, почему макросы — это плохо». Я очень не люблю макросы и всегда призываю поменьше их использовать. Макросы затрудняют чтение кода, провоцируют появление ошибок, усложняют работу статическим анализаторам. Как мне показалось из недолгого общения с кодом GCC, его авторы очень любят макросы. Я замучался изучать, во что раскрывается тот или иной макрос и возможно поэтому пропустил немало интересных ошибок. Признаюсь, я иногда бываю ленив. Но пару ошибок, связанных с макросами, я всё-таки продемонстрирую.
Предупреждение анализатора PVS-Studio: V595 The 'odr_types_ptr' pointer was utilized before it was verified against nullptr. Check lines: 2135, 2139. ipa-devirt.c 2135
Если раскрыть макрос и убрать всё не относящееся к делу, мы получим следующий код:
В начале указатель разыменовывается, а потом проверяется. Приведёт это к беде на практике или нет, сказать сложно. Все зависит от того, может ли возникнуть ситуация, когда указатель действительно будет равен nullptr. Если такая ситуация невозможна, то следует удалить лишнюю проверку, которая будет вводить в заблуждение людей, поддерживающих код и анализатор кода. Если указатель может быть нулевым, то это серьёзная ошибка, которая требует ещё большего внимания и исправления.
Рассмотрим ещё один аналогичный случай:
Предупреждение анализатора PVS-Studio: V595 The 'list' pointer was utilized before it was verified against nullptr. Check lines: 1627, 1629. sched-int.h 1627
Чтобы увидеть ошибку, нам опять потребуется показать устройство макроса:
Раскрываем макрос и получаем:
И сейчас многие воскликнут: «Стоп, стоп! Здесь нет ошибки. Мы ведь просто получаем указатель на член класса. Никакого разыменования нулевого указателя здесь нет. Да, возможно код не аккуратен, но ошибки здесь нет!».
Всё не так просто. Здесь возникает неопределённое поведение. И то, что такой код может работать на практике, это просто везение. На самом деле, так писать нельзя. Например, оптимизирующий компилятор, увидев list->first, может удалить проверку if (list). Раз мы выполняли оператор ->, значит предполагается, что указатель не равен nullptr. Если это так, то проверять указатель не нужно.
Я написал целую статью на эту тему: "Разыменовывание нулевого указателя приводит к неопределённому поведению". Там как раз рассматривается аналогичный случай. Прежде чем спорить, прошу внимательно познакомиться с этой статьёй.
Впрочем, рассмотренная ситуация действительно сложна и неочевидна. Я допускаю, что могу быть всё-таки неправ и ошибки здесь нет. Однако, до сих пор мне никто не смог это доказать. Будет интересно услышать комментарии разработчиков GCC, если они обратят внимание на эту статью. Уж они-то точно должны знать, как работает компилятор и следует ли интерпретировать такой код как ошибочный, или нет.
Использование разрушенного массива
Предупреждение анализатора PVS-Studio: V507 Pointer to local array 'buf' is stored outside the scope of this array. Such a pointer will become invalid. hsa-dump.c 704
Строка формируется во временном буфере buf. Адрес этого временного буфера сохраняется в переменной name и используется далее в теле функции. Ошибка в том, что после записи буфера в переменную name, сам этот буфер будет уничтожен.
Использовать указатель на разрушенный буфер нельзя. Формально мы имеем дело с неопределённым поведением. На практике этот код может вполне успешно работать. Корректная работа программы — это один из вариантов проявления неопределенного поведения.
Выполнение одинаковых действий, независимо от условия
Анализатор выявил участок кода, который однозначно я не могу идентифицировать как ошибочный. Однако, крайне подозрительно выполнить проверку, а потом, независимо от её результата, выполнять одни и те же действия. Конечно, возможно, это задел на будущее и пока всё корректно, но проверить этот участок кода явно стоит.
Предупреждение анализатора PVS-Studio: V523 The 'then' statement is equivalent to the 'else' statement. tree-ssa-threadupdate.c 2596
Избыточное выражение вида (A == 1 || A != 2)
Предупреждение анализатора PVS-Studio: V590 Consider inspecting this expression. The expression is excessive or contains a misprint. gensupport.c 1640
Нас интересует условие: (alt < 2 || *insn_out == '*' || *insn_out != '@')
Его можно сократить до: (alt < 2 || *insn_out != '@')
Рискну предположить, что оператор != следует заменить на ==. Тогда код примет более осмысленный вид:
Обнуление не того указателя
Рассмотрим функцию, занимающуюся освобождением ресурсов:
Предупреждение анализатора PVS-Studio: V519 The 'bb_copy' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 1076, 1078. cfg.c 1078
Обратите внимание на эти 4 строчки кода:
Случайно дважды обнуляется указатель bb_copy. Правильный вариант:
Assert, который ничего не проверят
Неправильное условие, являющееся аргументом макроса gcc_assert не повлияет на корректность работы программы, но усложнит поиск ошибки, если таковая возникнет. Рассмотрим код:
Предупреждение анализатора PVS-Studio: V502 Perhaps the '?:' operator works in a different way than it was expected. The '?:' operator has a lower priority than the '<=' operator. dwarf2out.c 2053
Приоритет тернарного оператора ?: ниже, чем у оператора сравнения <=. Это значит, что мы имеем дело с условием вида:
Таким образом, второй операнд оператора && может принимать значение 0xffff или 0xffffffff. Оба эти значения обозначают истину, поэтому выражение можно упростить до:
Оператор ?: очень коварен и его лучше не использовать в сложных выражениях. Уж очень легко допустить ошибку. У нас собрано большое количество примеров таких ошибок, найденных анализатором PVS-Studio в различных открытых проектах. Подробнее об операторе ?: я писал в уже упомянутой ранее книге (см. главу N4: Бойтесь оператора ?: и заключайте его в круглые скобки).
Кажется, забыли про «cost»
Структура alg_hash_entry объявлена следующим образом:
В функции synth_mult программист решил проверить, тот ли это объект, который ему нужен. Для этого ему требуется сравнить поля структуры. Однако, кажется в этом месте допущена ошибка:
Предупреждение анализатора PVS-Studio: V501 There are identical sub-expressions 'entry_ptr->mode == mode' to the left and to the right of the '&&' operator. expmed.c 2573
Дубликаты присваиваний
Следующие участки кода, на мой взгляд, не представляют опасности и, кажется, дублирующееся присваивание можно просто удалить.
Предупреждение анализатора PVS-Studio: V519 The 'structures' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 842, 845. gengtype.c 845
Предупреждение анализатора PVS-Studio: V519 The 'nargs' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 39951, 39952. i386.c 39952
Последний случай более странный, чем остальные. Возможно, тут есть какая-то ошибка. Переменной steptype значение присваивается 2 или 3 раза. Это подозрительно.
Предупреждение анализатора PVS-Studio: V519 The 'steptype' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 5173, 5174. tree-ssa-loop-ivopts.c 5174
Заключение
Я рад, что написал эту статью. Теперь мне есть что отвечать на комментарии вида «PVS-Studio не нужен, так как все те же предупреждения выдаёт и GCC». Как видите, PVS-Studio очень мощный инструмент и превосходит по диагностическим возможностям GCC. Я не отрицаю, что в GCC реализованы отличные диагностики. Этот компилятор, при должной настройке, действительно выявляет много проблем в коде. Но PVS-Studio — это специализированный и быстро развивающийся инструмент, а это значит, он всегда будет лучше выявлять ошибки в коде, чем это делают компиляторы.
Приглашаю познакомиться с проверками других известных открытых проектов, посетив этот раздел нашего сайта. А также, тем, кто использует Twitter, последовать за мной @Code_Analysis. Я регулярно публикую ссылки на интересные статьи по программированию на языке C и C++, а также рассказываю о новых достижениях нашего анализатора.
Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Andrey karpov. Bugs found in GCC with the help of PVS-Studio.
Читайте также: