Pull to refresh

Нечего на зеркало пенять, коли рожа кривая

Reading time5 min
Views7.7K
Original author: John Graham
Одной из самый угнетающих вещей для каждого программиста является осознание того, что все ваше время тратится не на создание чего нибудь полезного, а на устранение проблем, которые мы же сами и создаем.

Этот процесс называется отладка. Каждый день, каждый программист предстает перед тем фактом, что когда он пишет код — он создает и ошибки в коде. И как только он понимает, что его программа не работает, он должен искать проблемы, которые сам же и создал.

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

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

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

Однако, что бы не вызвало ошибку, ключевые шаги при поиске проблемы всегда следующие:
  • Найти наименьшую закономерность, при которой ошибка полностью воспроизводится. С гейзенбагами это может оказаться сложным, но даже небольшой процент испытаний, при которых ошибка воспроизводится является значимым.
  • Автоматизировать процесс испытаний. Намного лучше, когда можно запускать тест снова и снова. Можно даже сделать его частью программы, когда ошибка будет устранена — это не допустит появления ошибки вновь.
  • Искать причину, пока не будет найдена её основа. До тех пор, пока вы не найдете истинную причину возникновения ошибки, вы не можете с уверенностью говорить, что вы её исправили. С гейзенбагами можно очень просто сбиться с толку, полагая, что вы исправили ошибку, после того как она вдруг исчезнет после того что вы сделаете в процессе поиска.
  • Исправить причину и проверить с помощью шага 2.

Недавно, в Hacker News появилась стаьтя — Если у вас гейзенбаг в С — значит проблемма в вашем оптимизаторе компилятора. Это очень неверное суждение.

Компилятор, который вы используете, вероятно, используют тысячи людей, в то время как вашу программу используете скорее всего только вы. Как вы думаете, что, компилятор или ваша программа работает наиболее стабильно?

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

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

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

В статье выше есть один определенно не завершенный пример:

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

Все что вы знаете когда меняете уровни оптимизации — это то, что уровни меняются независимо от того, появляется ошибка или нет. Это не говорит вам о том, что оптимизатор работает неверно. Вы не нашли искомую причину ошибки.

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

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

#include <stdlib.h>

int a()
{
    int ar[16];
    ar[20] = (getpid() % 19 == 0);
}

int main(int argc, char * argv[])
{
    int rc[16];
    rc[0] = 0;
    a();
    return rc[0];
}


Скомпилируйте эту программу с помощью gcc под Mac OS X с помощью следующего Makefile (я сохранил код в файле odd.c).

CC=gcc
CFLAGS=

odd: odd.o


И вот пример скрипта, который запускает программу 20 раз и выводит результат:

#!/bin/bash
for i in {0..20}
do
    ./odd ; echo -n "$? "
done
echo


Если вы запустите этот скрипт, вы будете ожидать строку нулей, поскольку rc[0] никогда не получает значений отличных от нуля. Однако вот пример работы программы:

$ ./test
0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0


Если вы опытный программист C, вы увидите как я сделал чтобы появлялась единица, и почему она появляется в разных местах. Но давайте теперь попробует отладить программу с помощью printf:

[...]
rc[0] = 0;
printf( "[%d]", rc[0] );
a();
[...]


Теперь, когда вы запустите программу, ошибка исчезнет.

$ ./test
[0]0 [0]0 [0]0 [0]0 [0]0 [0]0 [0]0 [0]0 [0]0 [0]0 [0]0
[0]0 [0]0 [0]0 [0]0 [0]0 [0]0 [0]0 [0]0 [0]0 [0]0


Выглядит странно, поэтому вы перемещаете printf в другое место:

[...]
rc[0] = 0;
a();
printf( "[%d]", rc[0] );
[...]


и получаете тот же странный результат с исчезновением ошибки. И то же самое произойдет если вы отключите оптимизатор и даже без printf ошибка не будет появляться:

$ make CFLAGS=-O3
gcc -O3 -c -o odd.o odd.c
gcc odd.o -o odd

$ ./test
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0


Это все происходит, потому что ф-я a() выделяет память для 16-ти Integer элементов. И тут же записывает после конца массива либо 1 либо 0 в зависимости от того, делится ли PID процесса на 19 или нет. В конечном итогде она записывается в rc[0] из-за расположения в стеке.

Добавление printf или изменение уровня оптимизации меняет расположение кода и исключает неверное обращение к rc[0]. Но, будьте осторожны, ошибка не ушла. Единица просто записалась в другую ячейку памяти.

Т.к. C очень восприимчив к этому типу ошибок, важно использовать хорошие инструменты для проверки таких проблем. Например статический анализатор кода splint и анализатор памяти valgrind помогают устранить массу мерзких ошибок. И вы должны разрабатывать свои приложения с максимальным уровнем ошибок и устранять их все.

Только если вы сделаете все что нужно, вы можете начать подозревать чужой код. Но даже если вы начали это делать — проверьте ещё раз все шаги, чтобы установить истинную причину ошибки. К сожалению, в большинстве случаев, большая часть ошибок — ваша.
Tags:
Hubs:
+42
Comments42

Articles