Различия

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

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

articles:c-tricks-0dh [2017/09/05 02:55] (текущий)
Строка 1: Строка 1:
 +====== c-tricks-0Dh ======
 +<​sub>​{{c-tricks-0Dh.odt|Original file}}</​sub>​
 +
 +====== сишные трюки\\ (0xD выпуск) ======
 +
 +крис касперски ака мыщъх, a.k.a.souriz,​ a.k.a.nezumi,​ a.k.a.elraton,​ no-email
 +
 +===== совет 1 — вычисления rand() на стадии компиляции =====
 +
 +Препроцессор в Си — великая вещь, однако,​ его возможности существенно ограничены и зачастую,​ чтобы осуществить задуманное приходится извращаться не по детски. Достаточно часто программистам требуется получить случайное число, уникальное для каждого билда, но не меняющееся от запуска программы к запуску. Существует множество решений этой проблемы. В частности,​ линкер ulink использует штамп времени,​ содержащийся в заголовке PE-файла,​ однако,​ этот способ системно-зависимый и что самое неприятное неработающий на некоторых UNIX-подобных осях, где ELF-заголовок вообще не проецируется на адресное пространство процесса.
 +
 +Некоторые программисты поступают так: подключают включаемый файл x-file.h директивой '#​include "​x-file.h"',​ создают простую утилиту на Си, генерирующую x-file.h следующего содержания:​ "#​define X_RAND 0xXXXXXXXX",​ где 0xXXXXXXXX — случайное число, возвращаемое функцией rand(), а в makefile-файл вставляют команды компиляции этой вспомогательной утилиты,​ ее линковку и запуск. Затем, после создания x-file.h можно собирать файл проекта. Достоинство этого трюка в его переносимости,​ а недостаток — в излишней громоздкости. Сгенерировать уникальное для каждого билда число можно и проще!
 +
 +Компиляторы,​ придерживающиеся ANSI Си, имеют в своем "​словарном запасе"​ макросы __DATE__ и __TIME__, возвращающие дату и время компиляции файла соответственно. Оба значения представлены в строковом формате,​ от которого приходится избавляться путем вычисления хеш-суммы по алгоритму CRC32 или любому другому. Для достижения большей случайности полученное число можно передать функции srand() с последующим вызовом rand().
 +
 +Как вариант,​ можно использовать макрос __TIMESTAMP__,​ возвращающий штамп даты/​времени последней модификации компилируемого файла в виде 32-битного целого,​ что изваляет нас от необходимости вычисления CRC32, однако,​ если файл, содержащий __TIMESTAMP__ не будет изменен,​ мы получим тоже самое число, что и в предыдущем билде. В некоторых случаях это неприемлемо,​ в некоторых,​ напротив,​ даже очень желательно (т. е. сгенерированное число изменяется только в случае изменения файла).
 +
 +int x_rand;
 +
 +main()
 +
 +{
 +
 +srand(__TIMESTAMP__);​
 +
 +x_rand = rand();
 +
 +
 +
 +}
 +
 +Листинг 1 использование макроса __TIMESTAMP__ для генерации случайного числа, уникального для каждого билда
 +
 +===== совет 2 – строковые литералы и тип char =====
 +
 +Рассмотрим следующий код (см. листинг 2) вполне типичный для начинающих. Что в нем неправильно?​
 +
 +foo(char *s)
 +
 +{
 +
 +if (*s < '​я'​) return '​ты';​
 +
 +}
 +
 +Листинг 2 пример неправильного использования char*, неявно закладывающийся на особенности поведения компилятора
 +
 +Опытным программистам известно,​ что стандарт ANSI Cи позволяет компиляторам самостоятельно решать должен ли тип char быть знаковым или нет, поэтому,​ если число укладывается в [0, 127] мы вправе использовать char — программа будет работать независимо от наличия знака. В противном случае следует явно специфицировать тип, указывая перед char каким ему быть — signed или unsigned.
 +
 +Компилятор Microsoft Visual C++ по умолчанию всегда выбирает unsigned char, поэтому данная программа будет работать правильно,​ однако,​ стоит откомпилировать ее с помощью Borland C++, как все измениться и мы получим совершенно неожиданный результат. Компилятор по умолчанию устанавливает char в signed, в результате чего строковой литерал '​я'​ превращается в число -17 и условие (*s < '​я'​) окажется в косяках,​ что наглядно подтверждает дизассемблерный листинг,​ приведенный ниже:
 +
 +_TEXT:​00000000 _fooproc near; CODE XREF: _main+4↓p
 +
 +_TEXT:​00000000
 +
 +_TEXT:​00000000 arg_0= dword ptr  8
 +
 +_TEXT:​00000000
 +
 +_TEXT:​00000000pushebp
 +
 +_TEXT:​00000001movebp,​ esp
 +
 +_TEXT:​00000003moveax,​ [ebp+arg_0]
 +
 +**_TEXT:​00000006********cmp********byte ptr [eax], -17****; '​****я****'​**
 +
 +**_TEXT:​00000009********jge********short loc_20****; ****знаковое сравнение!**
 +
 +_TEXT:​0000000Bmoveax,​ 0EBE2h
 +
 +_TEXT:​00000010
 +
 +_TEXT:​00000010 loc_20:; CODE XREF: _foo+9↑j
 +
 +_TEXT:​00000010popebp
 +
 +_TEXT:​00000011retn
 +
 +_TEXT:​00000011 _fooendp
 +
 +Листинг 3 результат работы компилятора Borland C++, переменная char *s трактуется как signed char*
 +
 +Самое подлое коварство данной проблемы заключается в том, что она не проявляется на английских программах,​ поскольку символы английского алфавита сосредоточены в первой половине ASCII-таблицы и потому знака "​минус"​ в них просто не возникает! А вот после русификации (или работе с русскими файлами данных) он неожиданно вылезает в самых разных местах,​ разваливая программу и порождая трудноуловимые баги.
 +
 +===== совет 3 — выход из нескольких циклов сразу =====
 +
 +Начинающие программисты постоянно задают мне один и тот же вопрос:​ как выйти из двух и более циклов сразу? Средствами структурного программирования никак не получается. То есть, получается,​ конечно,​ но приходится использовать флаги, проверяемые в каждом цикле, что не только громоздко,​ ненадежно,​ ненаглядно,​ но еще и непроизводительно. Современные процессоры не любят ветвлений,​ и каждая лишняя проверка сжирает кучу тактов,​ особенно на нерегулярных переходах,​ которые невозможно предсказать.
 +
 +Выход состоит в использовании горячо критикуемого "​goto",​ который обвиняют в неструктурности и вообще "​идеологической неправильности"​. Действительно,​ при злоупотреблении goto, программа превращается в "​спагетти"​ и ее становится совершенно невозможно отлаживать,​ поскольку непонятно как мы вообще попали в данный блок кода и какая зараза совершила сюда переход,​ но… сравните два следующих фрагмента кода:
 +
 +for(…)
 +
 +{
 +
 +for (…)
 +
 +{
 +
 +for(…)
 +
 +{
 +
 +if (…) goto to_exit;
 +
 +}
 +
 +}
 +
 +} to_exit:
 +
 +Листинг 4 выход из трех циклов c использованием оператора goto
 +
 +int to_exit = 0;
 +
 +for(…)
 +
 +{
 +
 +for(…)
 +
 +{
 +
 +for(…)
 +
 +{
 +
 +if (…)
 +
 +{
 +
 +to_exit = 1;
 +
 +break;
 +
 +}
 +
 +}
 +
 +if (to_exit) break;
 +
 +
 +
 +}
 +
 +if (to_exit) break;
 +
 +}
 +
 +Листинг 5 выход из трех циклов без использования оператора goto
 +
 +Не кажется ли вам, что листинг 4 намного более нагляден и в нем гораздо труднее совершить ошибку,​ чем в "​идеологически правильном"​ листинге 5?​ Увы! В некоторых случаях,​ использование goto строго запрещено принятыми корпоративными правилами кодирования,​ против которых не попрешь. Вот такая, значит,​ бюрократия.
 +
 +===== совет 4 — переносимые ассемблерные вставки =====
 +
 +Когда возможностей,​ предоставляемых языком Си, оказывается недостаточно (например,​ требуется прочитать значение регистра-счетчика команд) программисты обычно прибегают к ассемблерным вставкам.
 +
 +Проблема в том, что способ оформления ассемблерный вставок не стандартизован и каждый компилятор делает это по своему. К тому же, даже в рамках x86-процессоров существует как минимум два ассемблерных синтаксиса –Intel (поддерживаемый Windows-компиляторами) и AT&T, поддерживаемый,​ например,​ GCC.
 +
 +Одно из решений состоит в переводе ассемблерной вставки в машинный код (что очень удобно делать в hiew'​е) с последующим размещением ее в локальном массиве,​ указатель на который преобразуется в указатель на функцию,​ запускаемую на выполнение с передачей аргументов через стек по тому или иному соглашению.
 +
 +Практический пример использования такого трюка на практике приведен ниже:
 +
 +int (*foo)();
 +
 +bar()
 +
 +{
 +
 +// объявляем массив и заполняем его машинным кодом
 +
 +char shell[]="​\x0F\x31";//​ RDTSC
 +
 +
 +
 +// преобразуем указатель на массив в указатель на функцию foo
 +
 +foo = (int(*)())shell;​
 +
 +
 +
 +// вызываем функцию foo, возвращая результат ее выполнения
 +
 +// для простоты результат усекается до 32-бит, передаваемыхв регистр EAX,
 +
 +// старшие 32-бита,​ помещаемые командой RDTSC в регистр EDX мы отбрасываем
 +
 +return foo();
 +
 +}
 +
 +main()
 +
 +{
 +
 +int a;
 +
 +a = bar();
 +
 +}
 +
 +Листинг 6 пример вставки машинного кода в си-программу
 +
 +Единственный существенный недостаток данного метода в том, что на осях с неисполняемым стеком он не работает и приходится вызывать системно-зависимые функции для установки соответствующих атрибутов доступа к памяти — VirtualProtect()на Windows и mprotect()на UNIX, вокруг которых приходится делать свои "​обертки"​ (они же "​врапперы"​ от английского wrapper), вызываемые перед передачей управления на функции foo(), но и в этом случае у нас нет никаких гарантий,​ что ось позволит выполнить код. В частности,​ некоторые UNIX-подобные системы на процессорах не поддерживающих биты NX/XD (атрибуты исполнения кода на уровне страниц) размещают стек в области памяти,​ управляемой селектором,​ устанавливающим права доступа только на чтение/​запись (без возможности исполнения) и потому игнорирующие вызов mprotect(,,​PROT_EXEC).
 +
 +