Издательский дом ООО "Гейм Лэнд"ЖУРНАЛ ХАКЕР 120, ДЕКАБРЬ 2008 г.

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

Крис Касперски




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

Комментарии, ремарки и помарки

Системы контроля версий как раз и создавались для того, чтобы обеспечить легкий, прозрачный и непротиворечивый механизм безопасной правки исходных текстов, – инвариантный по отношению к самому языку. Однако на практике системы контроля версий используются только для организации совместной работы над проектом, да и то не всегда. Уж слишком много телодвижений приходится совершать всякий раз, а программисты — люди ленивые.
Если нам необходимо временно отключить блок кода, намного проще будет его закомментировать, а потом удалить комментарии, подключая обратно. Быстро, дешево, сердито. Но – потенциально небезопасно, с точки зрения внесения новых ошибок и развала уже отлаженной программы. А потому прежде, чем идти дальше, сформулируем перечень требований, предъявляемый к механизмам отключения кода:

  • Легкость использования (никто не будет пользовать средство, требующее кучи телодвижений);
  • Вложенность (внутри отключаемого блока может находиться один или несколько ранее отключенных блоков);
  • Многоуровневость (если для отключения блока кода требуется исправить два и более несмежных фрагментов исходного текста, необходимо гарантировать корректное снятие блокировки. Это становится особенно актуально, если отключаются независимые блоки А, B, С – тогда при включении блока B возникает угроза подключения фрагментов, относящихся к блокам A и C. Что ведет к развалу программы);
  • Поддержка всех языковых конструкций (какой прок от инструмента, если он работает только с ограниченным набором языковых конструкций, например, не позволяет отключать ассемблерные вставки?!).

Удовлетворяют ли комментарии указанным требованиям? А вот и нет! Комментарии в стиле Си (/**/) очень удобны, поскольку позволяют отключать огромные блоки кода нажатием всего четырех клавиш, – к тому же они могут располагаться в любом месте строки, а не только в начале. Однако отсутствие поддержки вложенности создает серьезные проблемы. Например:

/* <-- ошибка! закомментированный
блок уже содержит /**/
for (a = 0; a < N; a++)
{
/*
for (b = 0; b < M; b++)
if (!strcmp(name_array[a],
vip_array[b])) continue;
*/

// DeleteFile(name_array[a]);
pritnf("%d %s\n", a, name_array[a]);

}
*/

Попытка выключить цикл for (a,,) ведет к ошибке компиляции — комментарии /**/ не могут быть вложенными. В таких случаях программисты используют альтернативу в виде «//», допускающую вложенность, но, увы, вручную проставляемую в начале каждой строки, что очень утомительно и, мягко говоря, не производительно, если, конечно, не использовать макросы, поддерживаемые средой разработки (а практически все среды разработки их поддерживают). Аналогично осуществляется и снятие комментариев.

И все бы хорошо, да вот неоднозначности с уровнем вложенности делают отключение блоков небезопасным. В нашем случае мы имеем три раздельных отключаемых блока кода. Во-первых, это заблокированная проверка принадлежности удаляемого файла к vip_array. Во-вторых, собственно, само удаление файла (заблокированное и замененное отладочной печатью через printf). И, в-третьих, комментарий, пытающийся отключить цикл for(a,,) со всем, что в нем находится.

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

Пусть следом за «//» идет цифра (или буква), указывающая принадлежность текущей комментируемой строки к блоку кода. Продвинутые среды разработки типа Microsoft Visual Studio поддерживают развитый макроязык. Он позволяет выполнять лексический анализ, удаляя только те комментарии, за которыми идет заданная буква/цифра.

Это может выглядеть, например, так:

Имитация многоуровневой структуры отключаемых блоков исходного кода посредством комментариев

//3for (a = 0; a < N; a++)
//3{
//3 //2
//3 //2for (b = 0; b < M; b++)
//3 //2if (!strcmp(name_array[a],
vip_array[b])) continue;
//3 //2
//3 //1// DeleteFile(name_array[a]);
//3pritnf("%d %s\n", a, name_array[a]);
//3 }

Проблема вложенности решена на 100%; проблема многоварианости — на 50% (после удаления комментария //1 мы также должны удалить, а точнее, временно заблокировать следующую за ним строку с отладочной печатью)… в целом, предложенная техника намного более удобна, и единственный серьезный недостаток — привязка программиста к конкретной среде с набором пользовательских макросов. Менее серьезный недостаток — ассемблерные вставки, как правило, не поддерживают Си/Си++ комментариев и потому должны обрабатываться отдельно, усложняя реализацию нашего макродвижка и сводя на нет его преимущества.

Директивы условной трансляции

Разработанные для поддержки многовариантного кода директивы условной трансляции оказались практически невостребованными (речь, разумеется, идет только о временном выключении кода). Это очень странно — директивы условной трансляции намного более эффективны, чем комментарии! Пример, приведенный ниже, доказывает этот тезис.

Директивы препроцессора, отключающие блоки кода

#define _D1_// блок _D1_ включен
//#define _D2_// блок _D2_ выключен
#define _D3_// блок _D3_ включен

#ifdef _D1_
for (a = 0; a < N; a++)
{

#ifdef _D2_
for (b = 0; b < M; b++)
if (!strcmp(name_array[a], vip_array[b])) continue;
#endif

#ifdef _D3_
DeleteFile(name_array[a]);
#else
pritnf("%d %s\n", a, name_array[a]);
#endif
}
#endif

Проблема вложенности решается сама собой; многовариантность поддерживается очень хорошо, позволяя нам включать/выключать определенные блоки и не затрагивая остальные, причем, при подключении «DeleteFile(name_array[a])» автоматически отключается отладочная печать – и наоборот. Риск развала программы уменьшается до нуля. Самое интересное, что директивы условной трансляции ничуть не хуже работают и с ассемблерными вставками!

Директивы препроцессора, отключающие ассемблерные инструкции внутри ассемблерных вставок

__asm{
xor eax,eax
#ifdef _D1_
PUSH file_name
CALL DeleteFile
#endif
}

Конечно, писать «#if def _Dx_» намного длиннее, чем «//» или «/**/», однако это не проблема — клавиатурные макросы на что? Про нежелание связаться с макросами мы уже говорили. Ну дамакросы – это еще ладно. Хуже всего, что отключенные блоки кода не попадают в релиз, и если у конечного пользователя программа начнет дико глючить, у нас не будет никакой возможности отключить их без перекомпиляции всего кода.

Ветвления

Финальный прием устраняет основные недостатки предыдущего трюка, добавляя к нему свои собственные достоинства, а достоинств у него… Короче, намного больше одного. Идея заключается в использовании конструкции if (_Dx_), а при необходимости и if (_Dx_) else.

Оператор «if», стоящий перед одиночным блоком кода, не требует замыкающего «#endif», что ускоряет процесс программирования и не так сильно загромождает листинг. Но это мелочь. Гораздо важнее, что если _Dx_ – константа (например, «1»), то оптимизирующий компилятор выбрасывает вызов if, удаляя лишний оверхид. Если же _Dx_ - переменная (глобальная, конечно), то компилятор оставляет ветвление «как есть», давая нам возможность управлять поведением программы. Если у пользователей возникнут проблемы из-за ошибки в плохо отлаженном блоке кода, то этот блок можно отключить (естественно, если значения флагов вынесены в конфигурационный файл или доступны через пользовательский интерфейс, но это уже несущественные детали реализации).
Пример использования ветвлений для отключения блоков кода приведен ниже:

Использование ветвлений для выключения блоков кода

#define _D1_0
// блок _D1_ выключен (ветвление в релиз не попадает)
#define _D3_1
// блок _D3_ включен(ветвление в релиз не попадает)
int_D2_1
// блок _D2_ включен(ветвление попадает в релиз!)

if (_D1_)
for (a = 0; a < N; a++)
{

if (_D2_)
for (b = 0; b < M; b++)
if (!strcmp(name_array[a], vip_array[b])) continue;
if (_D3_)
DeleteFile(name_array[a]);
else
pritnf("%d %s\n", a, name_array[a]);
}

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

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

#define _D1_0

if (_D1_)
__asm{
INT 03
}

Ветвления, конечно, тоже не лишены недостатков, но для временного выключения блоков кода они намного лучше, удобнее и продуктивнее, чем комментарии. Разумеется, существуют и другие средства. Взять хотя бы «return», позволяющий одним движением руки погасить блок кода до самого конца функции. Критикуемый GOTO – отличная штука, но только в малых дозах. Иначе программа превращается в настоящее спагетти, которое практически невозможно распутать.

Содержание
загрузка...
Журнал Хакер #151Журнал Хакер #150Журнал Хакер #149Журнал Хакер #148Журнал Хакер #147Журнал Хакер #146Журнал Хакер #145Журнал Хакер #144Журнал Хакер #143Журнал Хакер #142Журнал Хакер #141Журнал Хакер #140Журнал Хакер #139Журнал Хакер #138Журнал Хакер #137Журнал Хакер #136Журнал Хакер #135Журнал Хакер #134Журнал Хакер #133Журнал Хакер #132Журнал Хакер #131Журнал Хакер #130Журнал Хакер #129Журнал Хакер #128Журнал Хакер #127Журнал Хакер #126Журнал Хакер #125Журнал Хакер #124Журнал Хакер #123Журнал Хакер #122Журнал Хакер #121Журнал Хакер #120Журнал Хакер #119Журнал Хакер #118Журнал Хакер #117Журнал Хакер #116Журнал Хакер #115Журнал Хакер #114Журнал Хакер #113Журнал Хакер #112Журнал Хакер #111Журнал Хакер #110Журнал Хакер #109Журнал Хакер #108Журнал Хакер #107Журнал Хакер #106Журнал Хакер #105Журнал Хакер #104Журнал Хакер #103Журнал Хакер #102Журнал Хакер #101Журнал Хакер #100Журнал Хакер #099Журнал Хакер #098Журнал Хакер #097Журнал Хакер #096Журнал Хакер #095Журнал Хакер #094Журнал Хакер #093Журнал Хакер #092Журнал Хакер #091Журнал Хакер #090Журнал Хакер #089Журнал Хакер #088Журнал Хакер #087Журнал Хакер #086Журнал Хакер #085Журнал Хакер #084Журнал Хакер #083Журнал Хакер #082Журнал Хакер #081Журнал Хакер #080Журнал Хакер #079Журнал Хакер #078Журнал Хакер #077Журнал Хакер #076Журнал Хакер #075Журнал Хакер #074Журнал Хакер #073Журнал Хакер #072Журнал Хакер #071Журнал Хакер #070Журнал Хакер #069Журнал Хакер #068Журнал Хакер #067Журнал Хакер #066Журнал Хакер #065Журнал Хакер #064Журнал Хакер #063Журнал Хакер #062Журнал Хакер #061Журнал Хакер #060Журнал Хакер #059Журнал Хакер #058Журнал Хакер #057Журнал Хакер #056Журнал Хакер #055Журнал Хакер #054Журнал Хакер #053Журнал Хакер #052Журнал Хакер #051Журнал Хакер #050Журнал Хакер #049Журнал Хакер #048Журнал Хакер #047Журнал Хакер #046Журнал Хакер #045Журнал Хакер #044Журнал Хакер #043Журнал Хакер #042Журнал Хакер #041Журнал Хакер #040Журнал Хакер #039Журнал Хакер #038Журнал Хакер #037Журнал Хакер #036Журнал Хакер #035Журнал Хакер #034Журнал Хакер #033Журнал Хакер #032Журнал Хакер #031Журнал Хакер #030Журнал Хакер #029Журнал Хакер #028Журнал Хакер #027Журнал Хакер #026Журнал Хакер #025Журнал Хакер #024Журнал Хакер #023Журнал Хакер #022Журнал Хакер #021Журнал Хакер #020Журнал Хакер #019Журнал Хакер #018Журнал Хакер #017Журнал Хакер #016Журнал Хакер #015Журнал Хакер #014Журнал Хакер #013Журнал Хакер #012Журнал Хакер #011Журнал Хакер #010Журнал Хакер #009Журнал Хакер #008Журнал Хакер #007Журнал Хакер #006Журнал Хакер #005Журнал Хакер #004Журнал Хакер #003Журнал Хакер #002Журнал Хакер #001