Различия

Здесь показаны различия между двумя версиями данной страницы.

Ссылка на это сравнение

articles:c-triks [2017/09/05 02:55] (текущий)
Строка 1: Строка 1:
 +====== c-triks ======
 +<​sub>​{{c-triks.odt|Original file}}</​sub>​
 +
 +====== сишные трюки от мыщъх'​а ======
 +
 +крис касперски ака мыщъх no-email
 +
 +**си — это не только мощный,​ но еще и "​демократический"​ язык, свободный от глупых запретов и ограничений,​ сдерживающих полет хакерской мысли как это делают паскаль и си++. нестандартные приемы программирования высоко ценятся хакерами,​ но ненавистны "​законопослушным"​ программистам,​ называющих их хаками,​ но прежде чем судить,​ нужно увидеть хотя бы один хороший хак**
 +
 +===== хак N1 – функции под вопросом =====
 +
 +начнем с классики (см. листинг 1). на вопрос:​ что этот код делает большинство прикладных программистов убежденно отвечают:​ "не компилируются"​. те же из них, кто все-таки не поленится проверить и откомилировать,​ едут крышей и остаток дня проводят в размышлениях _как_ _такое_ вообще может работать!
 +
 +j = **(**flag?​sin:​cos**)**(x);​
 +
 +Листинг 1 enigma
 +
 +хороший тест на значение языка, кстати. _что_ данный код делает понятно и без подсказки,​ достаточно открыть описание оператора "?",​ но объяснить _как_ он это делает,​ может только гуру.
 +
 +при использовании _имени_ функции в качестве выражения,​ компилятор возвращает указатель на нее, таким образом,​ в зависимости от значения flag'​а будет выбран либо тот, либо иной указатель,​ заключенный,​ согласно правилам языка, в круглые скобки,​ и ожидающий аргументов,​ в роли которых в данном случае выступает (x).
 +
 +а теперь попробуйте переписать этот хак на классический манер и сравните размер программы и откомпилированного кода.
 +
 +===== хак N2 – обмен значений двух переменных =====
 +
 +вопрос:​ сколько времени потребуется рядовому программисту,​ чтобы понять,​ что происходит с переменными 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;", поэтому пользоваться данным хаком не рекомендуется.
 +
 +===== хак N3 – забытая запятая =====
 +
 +изобилие фигурные скобок сильно раздражает и возникает естественное стремление записать весь statement одной строкой. рассмотрим типичный фрагмент кода, написанный "​правильным"​ программистом:​
 +
 +if (n > 1)
 +
 +{
 +
 +printf("​%d\n",​ x); n=0; a++;
 +
 +}
 +
 +Листинг 4 общепринятый вариант
 +
 +а вот тот же самый код, написанный хакером. как говориться,​ сравните и найдите различия:​
 +
 +if (n >1) printf("​%d\n",​ x), n = 0, a++;
 +
 +Листинг 5 хакерский вариант
 +
 +фигурные скобки "​волшебным"​ образом исчезают,​ а вместе с ними исчезает и точка из знака "​точка с запятой",​ в результате чего наглядность листинга значительно повышается.
 +
 +постоянно разделяя операторы знаком точки с запятой,​ большинство программистов почему-то забывают об обыкновенной запятой,​ используемой для разделяющей выражений,​ но в си практически все что угодно может быть выражением!!!
 +
 +===== хак N4 – скрытые возможности цикла for =====
 +
 +вот еще один типичный пример кода, написанный "​правильным"​ программистом:​
 +
 +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 инкремент,​ совмещенный с передачей аргумента (суперхакерский вариант)
 +
 +===== хак N4 – со знаком или без =====
 +
 +общеизвестно,​ что ((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)) …
 +
 +Листинг 12 "​правильный"​ вариант проверки диапазона
 +
 +для хакеров очевидно,​ что проверка на положительное значение x избыточна и от нее легко избавиться,​ переписав код так:
 +
 +if ((unsigned int) x < y)) …
 +
 +Листинг 13 хакерский вариант проверки диапазона
 +
 +===== хак N5 – лишние аргументы =====
 +
 +си не требует соблюдения прототипов функций,​ что открывает большие возможности для трюкачества. поскольку,​ аргументы заносятся справа налево (т.е. на вершине стека оказывается крайний левый аргумент),​ а стек чистит материнская функция,​ то лишние аргументы попросту игнорируются. и вот тут начинается самое интересное. поскольку,​ в качестве аргументов допускается использовать выражения (а выражением в си, как уже говорилось,​ может быть практически все что угодно),​ мы получаем мощный инструмент для борьбы с фигурными скобками.
 +
 +вот, например:​
 +
 +if (a)
 +
 +{
 +
 +x += f(a); if (n < MAX) n++;
 +
 +}
 +
 +Листинг 14 фрагмент "​правильной"​ программы
 +
 +первым делом освобождаемся от оператора if, преобразуя его в выражение:​ ((n <​ MAX) &&​ n++),​ и передает его функции f() как "​сверхплановый"​ аргумент:​
 +
 +if (a) x += f(a, ((n < MAX) && n++));
 +
 +Листинг 15 фрагмент "​хакерской"​ программы
 +
 +расплатой за хакерство становится снижение читатебельности листинга,​ не говоря уже о том, что "​лишние"​ аргументы требуют дополнительного стекового пространства,​ к тому же порядок вычисления аргументов в си не определен,​ поэтому без лишней нужды прибегать к этиму трюком не стоит, а лучше воспользоваться оператором "​запятая",​ описанным в хаке N3.
 +
 +===== хак N6 – объявление функций вручную =====
 +
 +программирование под 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 байт. как говорится,​ почувствуйте разницу!
 +
 +