Что за шрединбаг?


52

Эта вики-страница рассказывает:

Schrödinbug - это ошибка, которая проявляется только после того, как кто-то читает исходный код или использует программу необычным образом, замечая, что она никогда не должна была работать в первую очередь, после чего программа сразу перестает работать для всех, пока не будет исправлена. В Jargon File добавлено: «Хотя ... это звучит невозможно, это случается; в некоторых программах скрытые шрединг-баги скрывались годами».

То, о чем говорят, очень расплывчато ..

Может ли кто-нибудь привести пример того, как выглядит шрединбаг (например, в вымышленной / реальной ситуации)?


15
Обратите внимание, что цитата рассказана в шутку.

11
Я думаю, вы бы лучше поняли shrodinbug, если бы знали о коте Шредингера: en.wikipedia.org/wiki/Shrodingers_cat
Eimantas

1
@Eimantas Я на самом деле сейчас более запутан, но это интересная статья :)

Ответы:


82

По моему опыту картина такова:

  • Система работает, часто годами
  • Об ошибке сообщается
  • Разработчик исследует ошибку и находит немного кода, который кажется совершенно ошибочным, и заявляет, что он "никогда не работал"
  • Ошибка исправляется, и растет легенда о коде, который никогда не работал (но работал годами)

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

Итак, я собираюсь сказать, что ошибка, точно описанная (то есть наблюдение некорректного кода, перестает его работать), явно бессмысленна.

В действительности то, что произошло, - это одна из двух вещей:

1) Разработчик не полностью понял код . В этом случае код обычно является беспорядком, и где-то в нем есть большая, но неочевидная чувствительность к некоторому внешнему условию (скажем, конкретная версия ОС или конфигурация, которая определяет, как какая-то функция работает незначительным, но значимым образом). Это внешнее условие изменяется (скажем, при обновлении или изменении сервера, которое считается несвязанным), и при этом код нарушается.

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

В этой ситуации нужно понимать, что идея о том, что «это никогда не могло бы сработать», доказуемо ложна (потому что она сработала).

Это не значит, что переписывать - это плохо, часто это не так, хотя приятно знать, что именно не так, часто это отнимает много времени, и переписывание фрагмента кода часто происходит быстрее и позволяет вам быть уверенным, что вы исправили ошибки.

2) На самом деле это никогда не работало, просто никто никогда не замечал . Это удивительно часто, особенно в больших системах. В этом случае кто-то новый начинает и начинает смотреть на вещи так, как никто не делал раньше, или на изменения бизнес-процесса, которые привносят какой-то ранее незначительный крайний случай в основной процесс, и что-то, что действительно никогда не работало (или работало с некоторыми, но не со всеми время) найдено и сообщено.

Разработчик смотрит на это и заявляет, что «это никогда не могло бы сработать», но пользователи говорят: «ерунда, мы использовали это годами», и они в некотором роде правы, но считают, что это неуместно (и обычно не упоминают до разработчик находит точное условие , при котором указывают они идут «о да, мы делаем , что в настоящее время и не делали раньше») изменилось.

Здесь разработчик прав - он никогда не мог работать и никогда не работал.

Но в любом случае одна из двух вещей верна:

  • Утверждение «это никогда не сработало» верно и никогда не сработало - люди просто думали, что это сработало
  • Это сработало, и утверждение «это никогда не могло бы сработать» является ложным и объясняется (как правило, разумным) отсутствием понимания кода и его зависимостей.

1
Случается со мной так часто
Бытие

2
Отличное понимание реализма в этих ситуациях
StuperUser

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

1
@ Thaddee - я видел это раньше, но я также видел две ошибки в модулях кода, которые вызывали друг друга, уравновешивая друг друга, так что это действительно работало. Посмотрите на любой из них, и они были сломаны, но вместе они были в порядке.
Джон Хопкинс

7
@Jon Hopkins: у меня также есть случай 2 ошибок, отменяющих друг друга, и это действительно удивительно. Я нашел ошибку, произнес скандально известное утверждение «она никогда бы не сработала», посмотрел глубже, чтобы выяснить, почему она все равно работает, и обнаружил еще одну ошибку, которая исправляла первую, по крайней мере, в большинстве случаев. Я был действительно ошеломлен открытием и тем фактом, что только с ОДНОЙ ошибкой последствия были бы катастрофическими!
Алексис Дафреной

54

Поскольку все упоминают код, который никогда не должен был работать, я приведу вам пример, с которым я столкнулся около 8 лет назад в умирающем проекте VB3, который был преобразован в .net. К сожалению, проект нужно было обновлять до тех пор, пока не будет завершена версия .net, и я был единственным, кто даже отдаленно понимал VB3.

Была одна очень важная функция, которую вызывали сотни раз для каждого расчета - она ​​рассчитывала ежемесячные проценты по долгосрочным пенсионным планам. Я воспроизведу интересные части.

Function CalculateMonthlyInterest([...], IsYearlyInterestMode As Boolean, [...]) As Double
    [about 30 lines of code]
    If IsYearlyInterestMode Then
        [about 30 lines of code]
        If Not IsYearlyInterestMode Then
            [about 30 lines of code (*)]
        End If
    End If
End Function

Часть, отмеченная звездочкой, имела самый важный код; это была единственная часть, которая сделала фактический расчет. Очевидно, это никогда не должно было работать, верно?

Потребовалось много отладки, но в конце концов я нашел причину: IsYearlyInterestModeбыло True, и Not IsYearlyInterestModeбыло также верно. Это потому, что где-то вдоль строки кто-то приводил его к целому числу, затем в функции, которая должна установить его в истинное значение, увеличивал его (если он Falseравен 0, для него было бы установлено значение 1, то есть VB True, поэтому я могу видеть логику там), затем бросьте его обратно в логическое значение. И я остался с состоянием, которое никогда не может случиться, и все же случается все время.


7
Эпилог: я никогда не исправлял эту функцию; Я только что исправил неисправный сайт вызовов, чтобы отправить 2, как и все остальные.
конфигуратор

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

1
@Pacerier: Чаще всего, когда код такой беспорядок, что он работает только случайно. В моем примере ни один разработчик не хотел IsYearlyInterestModeоценивать как истинное, так и не истинное; первоначальный разработчик, который добавил несколько строк (включая одну из них, на ifсамом деле не понимал, как это работает - просто так получилось, что было достаточно хорошо)
конфигуратор

16

Не знаю реального примера, но чтобы упростить его с примером ситуации:

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

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


4
Одно из объяснений состоит в том, что в программном обеспечении были случайные сбои, и никто не мог мысленно связать их вместе. Таким образом, эти ошибки считались естественными причинами (такими как случайные сбои оборудования). После того, как исходный код будет прочитан, люди теперь смогут связать все предыдущие случайные ошибки с этой единственной причиной и поймут, что она никогда не должна была работать в первую очередь.
Рувон

4
Второе объяснение состоит в том, что в программном обеспечении есть часть, которая реализована по схеме цепочки ответственности. Каждый обработчик написан надежным способом, несмотря на то, что один обработчик имеет критическую ошибку. Теперь первый обработчик всегда будет терпеть неудачу, но из-за того, что второй обработчик (который перекрывается в ответственности) пытается выполнить ту же задачу, общая операция, похоже, выполнена успешно. Если во втором модуле есть какие-либо изменения, такие как изменение области ответственности, это может привести к общему отказу, хотя настоящая ошибка находится в другом месте.
Рувон

13

Пример из жизни. Я не могу показать код, но большинство людей будут относиться к этому.

У нас есть большая внутренняя библиотека служебных функций, где я работаю. Однажды я ищу функцию, которая делает определенную вещь, и я Frobnicate()пытаюсь ее использовать. Э-э-э: получается, что Frobnicate()всегда возвращает код ошибки.

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

Это частый случай условия № 2, о котором упоминал Джон Хопкинс в своем ответе, и он удручающе распространен в больших внутренних библиотеках.


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

Да, но если программисты игнорируют коды возврата, это не вина библиотеки. (Кстати, когда вы в последний раз проверяли ретрокод printf()?)
JensG

Именно поэтому были изобретены проверенные исключения.
Кевин Крумвиде,

10

Вот настоящий Schrödinbug, который я видел в каком-то системном коде. Корневой демон должен общаться с модулем ядра. Таким образом, код ядра создает несколько файловых дескрипторов:

int pipeFDs[1];

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

int pipeResult = pipe(pipeFDs);

Это не должно работать. pipe()записывает два файловых дескриптора в массив, но есть только место для одного. Но в течение семи лет он сделал работу; массив оказался до некоторого неиспользуемого пространства в памяти, которое превратилось в файловый дескриптор.

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


5

Следствием Schrödinbug является Heisenbug, который описывает ошибку, которая исчезает (или иногда появляется) при попытке исследовать и / или исправить ее.

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

На самом деле, это, как правило, вызвано тем или иным из следующего:

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

Оба подчеркивают важность тестирования кода релиза на оборудовании релиза, а также тестирование модуля / модуля / системы с использованием эмуляторов.


Почему я не заметил ответа S.Lote и комментария delnan прежде, чем я отправил это?
Андрей

Я немного испытал, но нашел пару из этого. Я работал в среде Android NDK. Когда отладчик обнаружил точку останова, он останавливал только потоки Java, а не потоки C ++, делая некоторые вызовы возможными, потому что элементы были инициализированы на C ++. Если оставить его без отладчика, код Java будет работать быстрее, чем C ++, и попытаться использовать значения, которые еще не были инициализированы.
MLProgrammer-CiM

Я обнаружил Heisenbug в нашем использовании API базы данных Django несколько месяцев назад: когда DEBUG = Trueимя «параметров» указывается на необработанный запрос SQL. Мы использовали его в качестве ключевого слова arg для ясности из-за длины запроса, который полностью прервался, когда пришло время перейти на бета-сайт, гдеDEBUG = False
Izkata

2

Я видел несколько Schödinbugs и всегда по той же причине:

Политика компании требовала, чтобы каждый должен был использовать программу.
Никто действительно не использовал это (главным образом потому, что не было никакого обучения для этого.)
Но они не могли сказать руководству об этом. Таким образом, все должны были сказать: «Я использую эту программу в течение 2 лет и никогда не сталкивался с этой ошибкой до сегодняшнего дня».
Программа действительно никогда не работала, за исключением меньшинства пользователей (включая разработчиков, которые ее написали).

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


1

У меня есть пример из моей собственной истории, это было около 25 лет назад. Я был ребенком, занимался программированием элементарной графики в Turbo Pascal. В TP была библиотека под названием BGI, которая включала в себя некоторые функции, которые позволяют копировать область экрана в блок памяти на основе указателя, а затем копировать его в другое место. В сочетании с блистанием на черно-белом экране его можно использовать для простой анимации.

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

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

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

Он никогда не должен был работать, он никогда не должен был работать (и на любой реальной ОС он не работал бы), но он все же работал, и как только он сломался - он остался сломанным.


0

Это происходит все время, когда люди используют отладчики.

Среда отладки отличается от реальной - без отладчика - производственной среды.

Работа с отладчиком может маскировать такие вещи, как переполнение стека, потому что кадры стека отладчика маскируют ошибку.


Я не думаю, что это относится к разнице между кодом, выполняемым в отладчике, и при компиляции.
Джон Хопкинс

26
Это не шрединбаг, это гейзенг .

@delnan: Это на грани, ИМО. Я считаю, что это неопределенная вещь, потому что существуют непостижимые степени свободы. Мне нравится резервировать heisenbug для вещей, в которых измерение одного действительно мешает другому (например, условия гонки, настройки оптимизатора, ограничения пропускной способности сети и т. Д.)
S.Lott

@ S.Lott: Ситуация, которую вы описываете, связана с изменением наблюдений, путаясь с кадрами стека или чем-то подобным. (Наихудший такой пример, который я когда-либо видел, состоял в том, что отладчик мирно и «правильно» выполнял бы загрузки недопустимых значений регистров сегмента в одношаговом режиме. В результате были получены некоторые подпрограммы в RTL, несмотря на загрузку указателя реального режима в защищенном режиме Так как он был только скопирован и не разыменован, он вел себя отлично.)
Loren Pechtel

0

Я никогда не видел настоящего клопа и не думаю, что они могут существовать - обнаружение, что это не сломает вещи.

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

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