Автор: (c)Крис Касперски ака мыщъх
Препроцессор в Си - великая вещь, однако его возможности существенно ограничены и зачастую, чтобы осуществить задуманное, приходится извращаться не по-детски. Достаточно часто программистам требуется получить случайное число, уникальное для каждого билда, но не меняющееся от запуска программы к запуску. Существует множество решений этой проблемы. В частности, линкер 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__ не будет изменен, мы получим то же самое число, что и в предыдущем билде. В некоторых случаях это неприемлемо, в некоторых - напротив, даже очень желательно (т.е. сгенерированное число изменяется только в случае изменения файла).
Листинг 1. Использование макроса __TIMESTAMP__ для генерации случайного числа, уникального для каждого билда.
Рассмотрим следующий код (см. листинг 2), вполне типичный для начинающих. Что в нем неправильно?
Листинг 2. Пример неправильного использования char*, неявно закладывающийся на особенности поведения компилятора.
Опытным программистам известно, что стандарт ANSI Cи позволяет компиляторам самостоятельно решать, должен ли тип char быть знаковым или нет, поэтому если число укладывается в диапазон [0...127], мы вправе использовать char - программа будет работать, независимо от наличия знака. В противном случае следует явно специфицировать тип, указывая перед char каким ему быть - signed или unsigned.
Компилятор Microsoft Visual C++ по умолчанию всегда выбирает unsigned char, поэтому данная программа будет работать правильно, однако стоит откомпилировать ее с помощью Borland C++, как все изменится и мы получим совершенно неожиданный результат. Компилятор по умолчанию устанавливает char в signed, в результате чего строковый литерал 'я' превращается в число -17 и условие (*s < 'я') окажется в косяках, что наглядно подтверждает дизассемблерный листинг, приведенный ниже:
_TEXT:00000000 _foo proc near ; CODE XREF: _main+4vp _TEXT:00000000 _TEXT:00000000 arg_0 = dword ptr 8 _TEXT:00000000 _TEXT:00000000 push ebp _TEXT:00000001 mov ebp, esp _TEXT:00000003 mov eax, [ebp+arg_0] _TEXT:00000006 cmp byte ptr [eax], -17 ; 'я' _TEXT:00000009 jge short loc_20 ; знаковое сравнение! _TEXT:0000000B mov eax, 0EBE2h _TEXT:00000010 _TEXT:00000010 loc_20: ; CODE XREF: _foo+9^j _TEXT:00000010 pop ebp _TEXT:00000011 retn _TEXT:00000011 _foo endp
Листинг 3. Результат работы компилятора Borland C++ - переменная char *s трактуется как signed char*.
Самое подлое коварство данной проблемы заключается в том, что она не проявляется на английских программах, поскольку символы английского алфавита сосредоточены в первой половине ASCII-таблицы и потому знака "минус" в них просто не возникает! А вот после русификации (или работе с русскими файлами данных) она неожиданно вылезает в самых разных местах, разваливая программу и порождая трудноуловимые баги.
Начинающие программисты постоянно задают мне один и тот же вопрос - как выйти из двух и более циклов сразу? Средствами структурного программирования никак не получается. То есть, получается, конечно, но приходится использовать флаги, проверяемые в каждом цикле, что не только громоздко, ненадежно, ненаглядно, но еще и непроизводительно. Современные процессоры не любят ветвлений и каждая лишняя проверка сжирает кучу тактов, особенно на нерегулярных переходах, которые невозможно предсказать.
Выход состоит в использовании горячо критикуемого "goto", который обвиняют в неструктурности и вообще "идеологической неправильности". Действительно, при злоупотреблении goto, программа превращается в "спагетти" и ее становится совершенно невозможно отлаживать, поскольку непонятно, как мы вообще попали в данный блок кода и какая зараза совершила сюда переход, но... сравните два следующих фрагмента кода:
Листинг 4. Выход из трех циклов c использованием оператора goto.
Листинг 5. Выход из трех циклов без использования оператора goto.
Не кажется ли вам, что листинг 4 намного более нагляден и в нем гораздо труднее совершить ошибку, чем в "идеологически правильном" листинге 5? Увы! В некоторых случаях использование goto строго запрещено принятыми корпоративными правилами кодирования, против которых не попрешь. Вот такая, значит, бюрократия.
Когда возможностей, предоставляемых языком Си, оказывается недостаточно (например, требуется прочитать значение регистра - счетчика команд), программисты обычно прибегают к ассемблерным вставкам.
Проблема в том, что способ оформления ассемблерных вставок не стандартизован и каждый компилятор делает это по-своему. К тому же, даже в рамках x86-процессоров существует, как минимум, два ассемблерных синтаксиса - Intel (поддерживаемый Windows-компиляторами) и AT&T, поддерживаемый, например, GCC.
Одно из решений состоит в переводе ассемблерной вставки в машинный код (что очень удобно делать в Hiew) с последующим размещением ее в локальном массиве, указатель на который преобразуется в указатель на функцию, запускаемую на выполнение с передачей аргументов через стек по тому или иному соглашению.
Практический пример использования такого трюка на практике приведен ниже:
Листинг 6. Пример вставки машинного кода в С-программу.
Единственный существенный недостаток данного метода в том, что на осях с неисполняемым стеком он не работает и приходится вызывать системно-зависимые функции для установки соответствующих атрибутов доступа к памяти - VirtualProtect() на Windows и mprotect() на UNIX, вокруг которых приходится делать свои "обертки" (они же "врапперы", от английского "wrapper"), вызываемые перед передачей управления на функции foo(), но и в этом случае у нас нет никаких гарантий, что ось позволит выполнить код. В частности, некоторые UNIX-подобные системы на процессорах, не поддерживающих биты NX/XD (атрибуты исполнения кода на уровне страниц), размещают стек в области памяти, управляемой селектором, устанавливающим права доступа только на чтение/запись (без возможности исполнения) и потому игнорирующие вызов mprotect(..., PROT_EXEC).