Издательский дом ООО "Гейм Лэнд"ЖУРНАЛ ХАКЕР #97, ЯНВАРЬ 2007 г.

Трюки от Крыса

 

Хакер, номер #097, стр. 144

Программирование — это своего рода дзен, это поединок кода с мыслью. А поединок - это уже кун-фу. А кун-фу - это когда сначала ты не знаешь, что нельзя делать то-то; потом знаешь, что нельзя делать то-то; позже понимаешь, что иногда-таки можно делать то-то; ну а далее ты осознаешь, что, помимо того-то, существует еще 69 способов добиться желаемого и все они практически равноправны. Но когда тебя спрашивают: «Как мне добиться желаемого?», ты быстро перебираешь в уме эти 69 способов, прикидываешь то общее, что в них есть, вздыхаешь и говоришь: «Вообще-то, главное – гармония». А на вопрос обиженных учеников: «А как ее добиться?», ты отвечаешь: «Никогда не делайте то-то».

Проверка выделения памяти — ошибка или гениальная задумка

Та же истина, только в чуть-чуть более длинной интерпретации: есть несколько шагов обучения воинскому рукопашному искусству:

1. Новичка обучают небольшому количеству приемов. В каждой ситуацию ему говорят: делай так-то. И новичок оттачивает эти приемы до филигранной точности.

2. Ученику показывают, как известные ему приемы можно объединять, переходить от одной технике к другой и получать новые приемы.

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

4. Новичок, глядя на мастера, не может взять в толк, почему тот не использует ни один из известных приемов.

Применительно к программированию это означает, что грань между ошибкой и задумкой настолько тонка, что порой совсем не заметна. Возьмем классическую ситуацию с проверкой успешности выделения памяти. Можно ли считать следующий код правильным (отсутствие проверки на валидность указателя *s не принимается в расчет)?

zen(char *s)

{

char *p = malloc(strlen(s)+1));

strcpy(p,s);

free(p);

retu 0;

}

«Да здесь же грубейшая ошибка! — скажет начинающий. — Где гарантии, что malloc выделит память?!» И по-своему он будет прав, ведь такой гарантии у нас нет, и более опытные товарищи явно посоветуют воткнуть «if». И они тоже по-своему будут правы. Но только изначальный вариант окажется самым оптимальным среди всех возможных решений.

Программирование — это, прежде всего, учет рисков. Есть риск, что память не будет выделена, и эту ситуацию надо предусмотреть и обработать заранее. Но как мы ее можем обработать? И в каких ситуациях malloc может не выделить память? Чем нам реально поможет дополнительный «if»?! Если памяти нет, то нет никаких гарантий, что удастся сделать хоть что-то, даже вывести примитивный диалог, не говоря уже о том, чтобы корректно завершить работу, сохранив все данные.

Самое главное, что операционная система совместно с процессором отслеживает попытки обращения к нулевому указателю (а точнее, к первым 64 Кб адресного пространства), возбуждая исключение, которое мы можем поймать и обработать. При отсутствии обработчика на экране появляется знаменитый диалог с сообщением о критической ошибке. Но это лучше, чем «if (!p) retu ERROR;», поскольку если вызывающая функция забудет о проверке на ERROR, программа продолжит свою работу, но вряд ли эта работа будет стабильной. Последуют глюки или падения в весьма отдаленных от функции zen местах, и, даже имея на руках дамп памяти (или отчет «Доктора Ватсона»), можно угробить кучу времени на выяснение истинной причины аварии.

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

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

Еще один миф — проверка корректности указателей

Должна ли функция проверять корректность переданных ей аргументов? Кое-кто скажет: «Должна». Это зависит от спецификаций. В некоторых случаях все проверки можно переложить на материнскую функцию, в некоторых  нет. В частности, все ядерные native-API-функции и драйверы крайне осторожно относятся к передаче аргументов из прикладного адресного пространства, совершая множество телодвижений. Иначе и быть не может! В противном случае, передав некорректный указатель, пользователь мог бы нанести ядру серьезные увечья, что недопустимо!

А вот в рамках пользовательского пространства проверка аргументов материнской функцией политически более корректна, поскольку возможности дочерней функции в концептуальном плане весьма невелики. Если материнская функция при определенных обстоятельствах может передать невалидный указатель, то с таким же успехом она может проигнорировать ошибочный код завершения дочерней функции! Причем валидный и ненулевой указатель - это совсем не одно и тоже! Да, мы можем легко выявить нулевой указатель, но только толку с того… Указатель может и не равняться нулю, но указывать на недоступную область памяти. Обычно это происходит, когда нулевое значение, возращенное malloc, складывается с некоторым индексом и передается дочерней функции.

Если индекс меньше 10000h, то операционная система отловит такую ситуацию и выбросит исключение, а если нет? Вообще-то, существует целый легион API-функций типа IsBadReadPtr, IsBadWritePtr, IsBadStringPtr, позволяющих проверить, если у нас права доступа к данной ячейке (ячейкам) памяти или нет. Некоторые программисты их старательно используют и пихают во все функции, забывая о том, что, во-первых, исключение останавливает программу, явно сигнализируя об ошибке, а обработка ошибки в стиле «if (IsBadStringPtr(s)) retu ERROR» ее подавляет, постулируя, что материнская функция знает, что делать; во-вторых,  указатель может принадлежать чужой области памяти, наличие прав доступа к которой ничуть не смягчает последствия их реализации.

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

Освобождать или нет?

Каждому malloc должен соответствовать свой free. Это правило номер один, которое каждый новичок должен знать назубок и несоблюдение которого приводит к утечкам памяти. Однако освобождать память бывает не всегда удобно, а порой даже очень затруднительно. А что если… не освобождать! При всей внешней бредовости это весьма здравая идея. Действительно, частые выделения/освобождения памяти - это тормоза и рост фрагментации кучи. Лучше выделять память с запасом, используя ненужные блоки повторно без их освобождения. Это раз.

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

Заключение

Прежде чем воспользоваться приведенными здесь советами, перечитай азы кун-фу еще раз. Чтобы отступать от правил, сначала нужно научиться их соблюдать! И каждое отступление должно иметь под собой твердое основание и мотивацию. Отмазка в стиле: «Я не освобождаю память, не проверяю валидности указателей, потому что Настоящие Мастера этого не выполняют», совершенно не катит, потому что Мастера просчитывают те ситуации, возможность которых не ведома новичку. Но даже у Мастеров доминирует мотивация: «Рискнем, авось пронесет!».

Содержание
ttfb: 4.5452117919922 ms