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

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

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




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

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

Не верь глазам своим

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

Как это может работать?

static /vars/global/animals/cat;

/pub//demo/foo(int /args/mouse)
{
int /vars/local/animals/dog;
return /vars/local/animals/dog =
/vars/global/animals/cat + /args/mouse;
}

Шуточки сразу утихают, и слышится напряженный скрип возмущенных мозгов. Подсказка: здесь нет никаких define. И транслятор вполне банальный – Microsoft Visual C++. Чем не повод разыграть друзей, сказав, что мы хакнули компилятор и научили его понимать новые конструкции? Как говорится, не вешать DOS, гардемарины! Так ведь хрен его завесишь, даже если очень сильно постараться. Эх… были же времена. Тогда мыщъх писал свой собственный русификатор (просто так, чтобы поупражняться в программировании резидентов) и слегка изменил знакогенератор, заставив «_» отображаться как «/». Зачем? А просто надоело видеть имена в стиле «ModuleName_FuncName» – их приходилось давать по причине глобального пространства имен. А слеш очень даже позитивно выглядит!

Сейчас, конечно, знакогенератор так просто не изменишь и все шрифты не переделаешь, но этого и не надо! Современные текстовые редакторы и операционные системы поддерживают памминг, также называемый «таблицами перекодировки символов». В результате, мы можем заставить среду разработки отображать «_» как «/», но это не единственно возможный прикол.

Лет эдак десять назад, когда я только подбирался к win32 API, в качестве тренировки был написан простой текстовой редактор (чуть сложнее «блокнота»), позволяющий программировать на Си с использованием греческих символов. А что, очень даже удобно. Когда alpha, beta, gamma и прочие отображаются в «естественном» виде, наглядность листинга существенно повышается. Вопрос — как заставить транслятор понимать греческий алфавит? Никак не надо его заставлять! Пусть в тексте программы переменные записываются латиницей, превращаясь в греческую символику только на экране. Тестовому редактору ведь совсем не сложно найти строку «alpha» и отобразить ее как надо. Что, кстати, снимает проблему ввода символов с клавиатуры, попутно уменьшая количество ошибок (потому как неправильно записанные имена переменных типа alpfa в греческий уже не преобразуются).

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

Сложение, вычитание, умножение и деление… строк!

Поклонники приплюснутого Си постоянно хвастаются, что он позволяет переопределять стандартные операторы, – появляется возможность писать a + b вместо add(a, b). Дескать, это намного нагляднее. На самом деле, форма записи – вообще не вопрос. Программист - существо неприхотливое и ко всему привыкающее. Достоинство плюсов в том, что они позволяют иметь одну функцию на все случаи жизни (неважно, перекрыли стандартные операторы или нет).

Конечно, чистый Си тоже кое-что может: препроцессор позволяет создавать макросы, принимающие произвольные типы. Так, во всяком случае, написано в учебниках и действительно – #define add(a, b) ((a)+(b)) как бы работает и даже может принимать переменные типа char, int и float/double («как бы» – потому что макросы обладают кучей побочных эффектов, о которых мы уже не раз и не два говорили).

Но как быть, если нам нужно сложить две 64-битных переменные, а классический Си про таких отродясь не слыхивал? Можно, конечно, использовать расширения компилятора, но… если мы внезапно захотим увеличить разрядность переменных до 128 байт, то все рухнет и придется переписывать кучу кода в разных местах. Сложить два числа любой разрядности ни разу не проблема (будем складывать их побайтово, в столбик), проблема в том, как заставить компилятор или препроцессор работать с произвольными и заранее не известными типами. В плюсах это решается только путем шаблонов, поддержка которых до сих пор хромает на обе ноги, в смысле – на каждый из двух плюсов.

На самом деле, решение есть. Нам не понадобятся ни шаблоны, ни макросы. Запишем число в виде ASCII-строки и забудем о типах. Вот такой прикол. Или… не прикол? Собственно говоря, а зачем нам переводить число в текстовый вид? Пусть это будет массив байт, размер которого задан в первом байте (как в Pascal-строках). Тогда, если размер не превышает максимально допустимую разрядность процессора, просто кастим и складываем (вычитаем) строки как обычные числа; если же процессор не может обрабатывать числа такого размера (например, восьмерное слово) — складываем их в столбик. Байтами или двойными словами — это уже не суть важно. В итоге, мы получаем в свое распоряжение функцию, складывающую числа любой разрядности, причем складывающую максимально эффективно. Оптимизаторы удалят все ненужные ветвления, и накладные расходы окажутся невелики.

Причем, складывать можно не только числа, но еще и строки, и другие типы данных. Ведь мы уже имеем массив байт! И первый байт указывает его длину. А что если… вместе с длиной задавать еще и тип? Это же могут быть не только строки, но и вектора, и изображения. Да все, что угодно! Единственная проблема — для задания длины таких типов данных одного байта может не хватить, а резервировать четыре байта – расточительно. Хорошо, пусть старший бит нашего первого байта указывает, присутствует ли техбайтовое продолжение поля длины за ним или нет.

Складывать строки мы научились. А вычитать? Какой физический смысл может иметь операция s1 - s2? На первый взгляд никакого, однако программистам достаточно часто приходится искать в конце строки str подстроку substr и отрезать ее (например, отрывать символ переноса каретки). Это ли не вычитание строк? При желании можно придумать физические аналогии для умножения и деления, но увлекаться подобными трюками не стоит и объединять строки все-таки лучше функцией с именем strcat, а не оператором сложения.

Строки как устройство /dev/nul

Частенько случается так, что функция хочет получить указатель на переменную, куда она запишет информацию, которая нам не нужна. Например, количество прочитанных байт (как это делает ReadFile) или старые атрибуты доступа (как делает VirtualProtect). Иногда можно пойти на хитрость, передав нулевой указатель, что означает: «не нужно возвращать никаких данных», но далеко не все функции утруждают себя обработкой в такой ситуации. Вот и приходится захламлять листинг посторонними переменными. Вопрос даже не в производительности и не в экономии байт (хотя и это тоже). Это, скорее, проблема стиля и скорости написания программы. Классический Си не позволяет объявлять переменные по месту их использования и приходится каждый раз возвращаться в начало функции, чтобы задекорировать ненужный хлам. Прямо как на таможне ;).

А что если передать указатель не на переменную, а на строку, которую тут же объявить и откастить? К примеру, так: ReadFile(h, buf, 1, (DWORD*) "XXX", 0), где «XXX» и есть та самая строка, передаваемая функции вместо указателя на двойное слово, в которое предполагается записать количество прочитанных байт. Фокус вот в чем — компилятор исправно выделяет память под строку (четыре байта — три икса и завершающий нуль), размещает ее в записываемой области памяти (это по стандарту, так как строка передается по указателю) и заталкивает в стек указатель на «XXX» как аргумент. Кастинг (DWORD*) не обязателен, ибо классический Си не следит за соблюдением типов, однако компилятор обзывает нас матом, выдавая предупреждение. А зачем оно нам? Какие проблемы откастить? Тем более, всю конструкцию можно загнать в макрос.

Достоинство этого приема, конечно же, в ускорении программирования (не приходится мотаться по всему листингу), очищении программы от лишних переменных и повышении наглядности кода. Мы явно даем понять, что количество прочитанных байт никак не используем (в случае использования «правильных» переменных это далеко не очевидно). К тому же, строки размещаются в сегменте данных, уменьшая потребности в стековой памяти. Кто-то может сказать, что за подобные трюки нужно убивать. Или кастрировать. Или сначала кастрировать, а потом убивать. Или… нет, если сначала убить, а потом кастрировать, то это надругательство над трупом получится. Короче, я всех предупредил. Коллеги подобных шуток не любят. А все потому что мало кто интересуется работой компилятора…

Содержание