что происходит в других местах, таким образом отслеживая ошибки. Их можно было так поймать.
Но вот ту, о которой я говорю, поймать было нельзя. Пока я не написал ту программу с тяжелым умножением/делением, благодаря которой стало видно, с какой частотой происходит ошибка. Вместо того чтобы падать раз в пару дней, теперь система рушилась раз в пару минут. И как только мы получили то, что обрушивает машину, у нас появился шанс это найти.
Сейбел: Сегодня некоторые говорят: “Да, конечно, у ассемблера больше всего шансов повредить память из-за программных ошибок, но и Си склонен к этому больше, чем некоторые другие языки”. Можно сделать так, что указатели будут указывать черт знает куда, и получится выход за границу массива. Вы не считаете, что это проблема?
Томпсон: Нет, ее можно обойти языковыми оборотами. Некоторые пишут хрупкий код, а некоторые - очень крепкий структурно, и тут все зависит от человека. Думаю, хрупкий код можно написать на любом языке. Я определяю хрупкий код так: допустим, требуется добавить функциональность, так вот в хорошем коде ее нужно добавить только в одно место, а в хрупком придется затронуть сразу десять мест.
Сейбел: Если появляется брешь в безопасности из-за переполнения буфера, то что вы можете ответить на критику Си и C++, где утверждается, что они частично за это ответственны, что многих проблем можно было бы избежать, если использовать язык, который проверяет границы массивов или в котором есть сборка мусора?
Томпсон: Ошибки есть ошибки. Вы пишете код с ошибками, просто потому что вы так пишете. Если язык безопасен в смысле безопасности времени выполнения, то операционная система упадет, а не переполнит буфер, став при этом уязвимой. Например, ping of death - атака на IP-стек операционной системы. Мне кажется, что будут еще атаки такого рода. Это будут не атаки типа “полностью захватить машину и стать суперпользователем”. Это будут ping of death.
Сейбел: Но ведь есть разница между отказом в обслуживании и уязвимостью, когда вы можете стать суперпользователем и делать, что захотите.
Томпсон: Есть две возможности стать суперпользователем: одна состоит в переполнении буфера, а другая - в том, чтобы указать программе сделать то, чего она делать не должна. В большинстве случаев реализуется вторая возможность, а не переполнение буфера. Вы можете стать суперпользователем без всяких переполнений. Так что ваш аргумент не работает. Все, что нужно сделать, - это уговорить
Сейбел: Ладно. Не будем говорить о том, что ведет к краху программы, уязвимости или чему-то подобному; есть такой класс ошибок, которые случаются в Си и C++, но почему-то не случаются, скажем, в Java. Есть ли для определенных типов приложений какие-то преимущества, весомые по сравнению с тем, что эти ошибки будут происходить и вызывать неприятности?
Томпсон: Думаю, этот класс ошибок доставляет не так уж много проблем. Разумеется, каждый раз когда я пишу один из вызовов функции, которая не проверяет длину строки, strcpy и тому подобное, я знаю, что фактически пишу ошибку. Но каким-то образом я принимаю решение о том, оправданна ли эта ошибка, нужно ли экономить аргументы. Сейчас обычно я их экономлю. Но имеется семантическая проблема: если вы обрезаете строку и используете обрезанную строку, то появляется новая проблема. Ошибка по-прежнему там, просто буфер еще не переполнен.
Сейбел: Какие инструменты вы используете при отладке?
Томпсон: В основном вывод на печать. При разработке программы я размещаю очень много операторов вывода на печать. И пока я их не убрал или не закомментировал, вывод на печать достаточно надежен. Мне редко приходится возвращаться к этому снова.
Сейбел: И что именно вы печатаете?
Томпсон: Все, что нужно, все, что удобно иметь под рукой. Инварианты. Но по большей части я печатаю во время разработки. Так я и выполняю отладку. Я не пишу программы с чистого листа - я беру программу и изменяю ее. Даже в большой программе я вывожу: “main, left, right, print, hello”. Да, “hello” - это не то, что я ожидаю от этой программы. Я вывожу на печать то, что ожидаю увидеть, и отлаживаю эту часть. При разработке я запускаю программу двадцать раз в час.
Сейбел: Вы печатаете инварианты; а используете ли вы утверждения, которые проверяют эти инварианты?
Томпсон: Редко. Мне проще убедить себя, что они правильны, и либо закомментировать вывод на печать, либо отбросить их.
Сейбел: Почему же тогда вам проще напечатать, что инвариант верен, чем использовать assert для автоматической проверки?
Томпсон: Потому что при печати вы действительно видите, что происходит, а не частные значения, и вы печатаете к тому же много всего, не только инварианты. Просто я делаю вот так. Я не предлагаю это как общую парадигму. Но сам я всегда делаю так.
Сейбел: Когда мы говорили о том, как вы создаете программы, вы упоминали процесс разработки снизу вверх. Вы строите эти кирпичики отдельно друг от друга?
Томпсон: Иногда.
Сейбел: А вы пишете тестовые модули для тестирования низкоуровневых функций?
Томпсон: Да, я так часто поступаю. Все зависит от программы, над которой я работаю. Если программа - это транслятор из А в Б, то я предложу целый спектр возможных А и соответствующих Б. Для регрессионного тестирования исполню все варианты А и проверю, насколько им будут соответствовать Б. Компилятор, транслятор, поиск регулярных выражений. Что-то вроде этого. Но есть программы, которые совсем не похожи на эти. Я никогда много не занимался тестированием и в этих программах мало понимаю. Я проведу несколько проверок, но они редко будут значительными, потому что, например, их будет тяжело проводить внутри самой программы. Главным образом это будут просто регрессионные тесты.
Сейбел: Под более сложными для тестирования программами вы понимаете, например, драйверы устройств и сетевые протоколы?
Томпсон: Вообще, они ведь запускаются каждый раз, когда вы запускаете систему.
Сейбел: То есть, по-вашему, таким образом можно избавиться от части ошибок?
Томпсон: Да, конечно. Что может быть лучше для тестирования операционной системы, чем постоянно запускать ее?
Сейбел: Еще один важный аспект программирования - оптимизация. Одни приступают к оптимизации с самого начала. Другие предпочитают сначала написать код, а потом уже думать о дальнейшей оптимизации. Каков ваш подход?
Томпсон: В первый раз я делаю программу насколько возможно простой. И очень часто этого достаточно. Строить очень сложный алгоритм для того, что никогда не будет запущено, просто глупо. Это трата времени и порождение ошибок. И поддерживать это невозможно, потому что придется исписать математическими формулами 50 страниц, чтобы рассказать другому парню, что ты, собственно, делаешь.
То, что сделано просто и методом грубой силы, 99% времени будет работать отлично. Если вы действительно создаете инструмент, который используется часто и местами проявляет квадратичную сложность, то иногда приходится вложиться в него как следует. Но чаще всего нет. Чем проще, тем лучше.
Сейбел: Некоторым просто нравится доводить код до блеска - так сказать, код для кода.
Томпсон: Да мне тоже нравится, но здесь во многом ради кода приходится жертвовать алгоритмом. Я имею в виду, что обычно сложный алгоритм требует сложного кода. А я лучше буду использовать простой алгоритм и простой код, чем какой-нибудь ужас. А если нужно как-то кратко охарактеризовать мой код, то могу сказать, что он простой, легко меняющийся и маленький. Ничего особо