c-tricks-0Dh

сишные трюки\\ (0xD выпуск)

крис касперски ака мыщъх, a.k.a.souriz, a.k.a.nezumi, a.k.a.elraton, no-email

Препроцессор в Си — великая вещь, однако, его возможности существенно ограничены и зачастую, чтобы осуществить задуманное приходится извращаться не по детски. Достаточно часто программистам требуется получить случайное число, уникальное для каждого билда, но не меняющееся от запуска программы к запуску. Существует множество решений этой проблемы. В частности, линкер 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) вполне типичный для начинающих. Что в нем неправильно?

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:00000006cmpbyte ptr [eax], -17; 'я'

_TEXT:00000009jgeshort 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-таблицы и потому знака «минус» в них просто не возникает! А вот после русификации (или работе с русскими файлами данных) он неожиданно вылезает в самых разных местах, разваливая программу и порождая трудноуловимые баги.

Начинающие программисты постоянно задают мне один и тот же вопрос: как выйти из двух и более циклов сразу? Средствами структурного программирования никак не получается. То есть, получается, конечно, но приходится использовать флаги, проверяемые в каждом цикле, что не только громоздко, ненадежно, ненаглядно, но еще и непроизводительно. Современные процессоры не любят ветвлений, и каждая лишняя проверка сжирает кучу тактов, особенно на нерегулярных переходах, которые невозможно предсказать.

Выход состоит в использовании горячо критикуемого «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 строго запрещено принятыми корпоративными правилами кодирования, против которых не попрешь. Вот такая, значит, бюрократия.

Когда возможностей, предоставляемых языком Си, оказывается недостаточно (например, требуется прочитать значение регистра-счетчика команд) программисты обычно прибегают к ассемблерным вставкам.

Проблема в том, что способ оформления ассемблерный вставок не стандартизован и каждый компилятор делает это по своему. К тому же, даже в рамках 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).