c-triks

сишные трюки от мыщъх'а

крис касперски ака мыщъх no-email

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

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

j = (flag?sin:cos)(x);

Листинг 1 enigma

хороший тест на значение языка, кстати. _что_ данный код делает понятно и без подсказки, достаточно открыть описание оператора «?», но объяснить _как_ он это делает, может только гуру.

при использовании _имени_ функции в качестве выражения, компилятор возвращает указатель на нее, таким образом, в зависимости от значения flag'а будет выбран либо тот, либо иной указатель, заключенный, согласно правилам языка, в круглые скобки, и ожидающий аргументов, в роли которых в данном случае выступает (x).

а теперь попробуйте переписать этот хак на классический манер и сравните размер программы и откомпилированного кода.

вопрос: сколько времени потребуется рядовому программисту, чтобы понять, что происходит с переменными x и y?

x ^= y ^= x ^=y;

Листинг 2 порождение тьмы

давайте разобьем это выражение на три: «x ^= y; y ^= x; x ^= y;», для наглядности, записав их так:

1) x = x XOR y;

2) y = y XOR x;

3) x = x XOR y;

Листинг 3 срывая вуаль тьмы

а это уже чистая математика получается! повторное наложение И исключающего ИЛИ, независимо от порядка аргументов, как известно, дает исходный результат, но в строках 1) и 2) аргументы меняются местами, следовательно, после выполнения шага 2) в переменной y окажется x, а сам x будет содержать «смесь» (xXORy), из которой на шаге 3) «изымается» прежний x и остается чистый y. короче, происходит обмен значений двух переменных без привлечения третьей.

красиво? красиво! но увы, по скорости и объему машинного кода сильно проигрывает стандартному «tmp = x; x = y; y = tmp;», поэтому пользоваться данным хаком не рекомендуется.

изобилие фигурные скобок сильно раздражает и возникает естественное стремление записать весь statement одной строкой. рассмотрим типичный фрагмент кода, написанный «правильным» программистом:

if (n > 1)

{

printf(«%d\n», x); n=0; a++;

}

Листинг 4 общепринятый вариант

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

if (n >1) printf(«%d\n», x), n = 0, a++;

Листинг 5 хакерский вариант

фигурные скобки «волшебным» образом исчезают, а вместе с ними исчезает и точка из знака «точка с запятой», в результате чего наглядность листинга значительно повышается.

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

вот еще один типичный пример кода, написанный «правильным» программистом:

x = 0;

for (a = 0; a < n; a++)

{

if (f()) x++;

}

Листинг 6 инициализация переменной перед входом в цикл (широко распространенный вариант)

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

for (a = 0, x = 0; a < n; a++)

{

if (f()) x++;

}

Листинг 7 инициализация переменной перед входом в цикл (хакерский вариант)

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

sum = 0;

for (a = 0; a < n; a++) sum += f(a);

Листинг 8 подсчет суммы («канонический» вариант)

а что, если избавиться от тела цикла, поместив весь код в заголовок? любой хакер без труда сделает это:

for (a = 0, sum = 0; a < n; sum += f(a), a++);

Листинг 9 подсчет суммы (хакерский вариант)

а вот еще более оптимизированный вариант:

for (a = 0, sum = 0; a < n; sum += f(a++));

Листинг 10 инкремент, совмещенный с передачей аргумента (суперхакерский вариант)

общеизвестно, что 1)

Листинг 12 «правильный» вариант проверки диапазона

для хакеров очевидно, что проверка на положительное значение x избыточна и от нее легко избавиться, переписав код так:

if 2)

Листинг 13 хакерский вариант проверки диапазона

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

вот, например:

if (a)

{

x += f(a); if (n < MAX) n++;

}

Листинг 14 фрагмент «правильной» программы

первым делом освобождаемся от оператора if, преобразуя его в выражение: 3);

Листинг 15 фрагмент «хакерской» программы

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

программирование под windows требует включения огромных include-файлов, заметно снижающих скорость трансляции, что раздражает. но ведь необходимые функции можно объявить и самостоятельно! следить за соблюдением прототипов необязательно, главное указать компилятору, чтобы он вызывал их по соглашению stdcall:

#include <windows.h> int __stdcall MessageBoxA(); main() { MessageBoxA(0,«hello,sailor»,«hello»,0); } Листинг 16 хакерский вариант объявления функций
(компиляция: cl.exe file_name.c USER32.lib) ===== хак N7 отказ от кучи и динамической памяти ===== любовь программистов к динамической памяти с позиции здравой логики ничем, кроме мракобесия необъяснима. куча — это тормоза и потенциальные утечки, про борьбу с которыми написано столько, что… хакеры активно используют статические массивы, выделяемые операционной системой на еще стадии загрузки файла, а динамическая память выделяется/освобождается каждый раз! рассмотрим следующий «пионерский» код, который можно встреть даже в профессиональных программах. f(char *s) { p = malloc(sizeof(s) + 1); … if (something_goes_wrong) return -1; ошибка! преждевременныйвыход … free(p); return val; } Листинг 17 популярный, но не слишком удачный способ выделения памяти кажущаяся потребность в динамической памяти объясняется переменным размером строки s, передаваемой функции f(). и все было бы хорошо, если бы не коварная ошибка, приводящая к преждевременному выходу из функции без освобождения! а вот «хакерский» вариант, использующий статическую память вместо хипа: f(char *s) { static char p[MAX_POSSIBLE_SIZE];** if ((sizeof(s)+1)> MAX_POSSIBLE_SIZE) return -1; … if (something_goes_wrong) return -1; … return val; } Листинг 18 хакерский вариант выделения памяти мы увеличили производительность, избавились от проблем с утечками, но… сделали функцию нерентабельной. в практическом плане это означает невозможность рекурсии (но в данном случае функция заведомо не рекурсивна) и запрет на одновременный вызов функции из двух и более потоков, иначе в статическом массиве образуется «мешанина» данных и наступит крах, предотвратить который можно либо путем принудительной синхронизации (критические секции, мутанты), либо через локальную память потока, известную под аббревиатурой TSL, впрочем, учитывая корявость поддержки этой самой TSL нынешними компиляторами, ни один здравомыслящий хакер ни за что ей не воспользуется. ===== хак N8 — программирование без RTL ===== по умолчанию си-программы собираются вместе с библиотекой времени исполнения (она же RTL), занимающий до черта килобайт и обеспечивающий работу функций типа spritnf. но ведь Windows NT уже включает в себя RTL, реализованную в NTDLL.DLL, так зачем же нам еще одна? чтобы собрать программу без RTL, достаточно назвать главную функцию не main, а как-нибудь иначе, например, _start. умные линкеры сами поймут, что это точка входа, глупым (к которым, в частности относится MS LINKER) потребуется указывать точку входа явно: cl.exe /c file_name.c /Ox link.exe file_name.obj /ENTRY:start /SUBSYSTEM:WINDOWS USER32.lib Листинг 19 сборка программы, приведенной в листинге 16 без RTL сравнивая размеры программы с RTL и без нее, мы практически не обнаружим разницы в размерах, поскольку минимальная кратность выравнивания в 9x составляет 4 Кб, она же используется линкером по умолчанию. в NT минимальная кратность составляет всего 16 байт, но линкер отказывается собирать такой файл, пока мы не притворимся, что собираем драйвер: link.exe file_name .obj /ENTRY:start USER32.lib /DRIVER /ALIGN:16 Листинг 20 сборка программы с минимальным выравниванием на примере листинга 16, при компиляции MS VC 6 с RTL размер исполняемого файла составляет – 24.576 КБайт, без RTL – 16.384 Кбайт и, наконец, без RTL с минимальным выравниванием — 816 байт. как говорится, почувствуйте разницу!

1)
unsignedchar) 0xFF == (signedchar) -1), соответственно, на 32-разрядных платформах ((unsignedint) 0xFFFFFFFF == (signedint) -1). естественно, написать -1 намного быстрее и надежнее, чем пересчитывать F'ы, рискуя пропустить один из них или переборщить. вот конкретный пример хакерского кода: unsigned int a; for (a = 0; a < -1UL; a++) printf(«%x\n»,a); Листинг 11 гибридный хакерский цикл со знаковыми и без знаковыми параметрами с точки зрения «нормального» прикладного программиста этот код вообще не должен работать, поскольку (0 > -1) и цикл не выполнится ни разу. но ведь это не обычный -1, а с суффиксом UL, что равносильно конструкции ((unsignedint) -1), после преобразования которой мы получим 0xFFFFFFFF. развивая мысль дальше: можно не только сократить исходный текст, но и оптимизировать машинный код. возьмем конструкцию вида: signed char x, y; if ((x > 0) && (x < y
2)
unsigned int) x < y
3)
n < MAX) && n++), и передает его функции f() как «сверхплановый» аргумент: if (a) x += f(a, ((n < MAX) && n++