Издательский дом ООО "Гейм Лэнд"ЖУРНАЛ ХАКЕР #105, СЕНТЯБРЬ 2007 г.

Трюки от Крыса

Крис Касперски

Хакер, номер #105, стр. 122

Этот выпуск трюков в некотором смысле особенный. А особенный он потому, что юбилейный (в шестнадцатеричной нотации). Мыщъх долго готовился к этому знаменательному событию, отбирая самые вкусные трюки, но… в конце концов их оказалось столько (и один вкуснее другого), что пришлось просто подкинуть монетку и выбрать четыре трюка наугад.

Трюк 1: обход префикса «_»

Си-соглашение о передаче параметров (обычно обозначаемое как cdecl от C Declaration), которому подчиняются все Си-функции, если только их тип не специфицирован явно, заставляет компилятор помещать префикс «_» перед именем каждой функции, чтобы линкер мог определить, что он имеет дело именно с cdecl, а не, скажем, с stdcall.

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

Ладно, а как быть, если текст программы уже кишит функциями с префиксами «_», что, в частности, любит делать Microsoft, отмечая таким образом нестандартные функции, отсутствующие в ANSI C? Переделывать программу, заменяя знаки подчеркивания чем-нибудь другим, себе дороже. Хорошо, если она вообще потом соберется. А если даже и соберется, то нет гарантий, что не появится кучи ошибок в самых разных местах.

И вот тут на помощь нам приходит трюкачество. А именно — макросы. Допустим, мы имеем функцию _f() и хотим избавиться от знака подчеркивания. Как это мы делаем? Да очень просто:

Избавляемся от префиксов «_» через макросы

#define _f() x_f()

x_f();

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

Трюк 2: динамические массивы

Известно, что язык Си не поддерживает динамических массивов. Ну не поддерживает и все тут. Хоть тресни. Хоть убейся о «газель». Хоть грызи зубами лед. А динамические массивы все равно нужны. Функции семейства malloc не в счет, поскольку они выделяют именно блок памяти, а не массив, что совсем не одного и то же.

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

Структура, реализующая динамический массив

struct string

{

int length; // длина строки

char data [1]; // память, зарезервированная для строки

};

Элемент length хранит длину строки, а char data [1] - это не сама строка (как можно подумать поначалу), а место, зарезервированное под нее. Осталось только научиться работать с этим хозяйством.

Рассмотрим следующий фрагмент кода, реализующий динамический массив:

Практический пример использования динамических массивов

// некая строка с динамическим массивом внутри

string* p2 = ...

...

// выделение памяти, необходимой для строки размером p2->length

// минус один заранее зарезервированный байт

struct string s = malloc (sizeof (struct string) + p2->length - 1);

// инициализация элемента структуры length

s->length = p2->length;

// копирование строки из p2 в s

strncpy (s->data, p2->data, p2->length);

...

// освобождение s

free (s);

...

Ну и в чем здесь прикол? А в том, что язык Си, с его вольностями в трактовке типов, позволяет нам выделить блок памяти произвольной длины и натянуть на него структуру string. При этом первые ячейки займет элемент length типа int, а остальное — данные строки, длина которой может и не совпадать с data [1]. Действуя таким образом, мы можем, например, имитировать PASCAL-строки. Однако следует сказать, что с С++ этот трюк не работает, точнее, работает, но дает непредсказуемый результат, и потому применять его крайне опасно, это может позволить себе только опытный программист).

Трюк 3: экономия памяти

Допустим, нам потребовалось выделить три локальные переменные типа char и еще один массив типа char[5]. Ну потребовалось, что тут такого? Хорошо, тогда попробуй ответить на вопрос: сколько байт мы при этом израсходовали? Голос из толпы: «Восемь!» Всего восемь байт?! Это что же за компилятор такой у тебя?! Берем MS VC (впрочем, с тем же успехом можно взять и любой другой) и компилируем следующий код:

Функция с тремя переменными типа char и одной char[5]

foo()

{

char a;

char b;

char c;

char d[5];

}

 Смотрим на откомпилированный код, дизассемблированный IDA Pro (советую при этом крепко держаться за стул):

Откомпилированный результат

.text:00000000 _foo proc near

.text:00000000 push ebp

.text:00000001 mov ebp, esp

.text:00000003 sub esp, 14h

.text:00000006 mov esp, ebp

.text:00000008 pop ebp

.text:00000009 retn

.text:00000009 _foo endp

Откуда тут взялось 14h (20) байт локальной памяти?! Все очень просто. Компилятор в угоду производительности самопроизвольно выравнивает все переменные по границе двойного слова. Итого мы получаем 3*max(1,4)+max(5,8)=12+8=20. Вот они, наши 20 «оптимизированных» байт вместо ожидаемых пяти.

А что делать, если нам не нужна такая «оптимизация»?! Все просто: гоним переменные в структуру, предварительно отключив выравнивание соответствующей прагмой компилятора. К примеру, у MS VC за это отвечает ключевое слово «#pragma pack( [ n] )», где n – желаемая кратность выравнивания, в данном случае равная единице, то есть выравнивание производится по границе одного байта, то есть не производится вовсе.

Переписанный код будет выглядеть приблизительно так:

Оптимизированный вариант с отключенным выравниванием

#pragma pack( 1 )

struct bar

{

char a;

char b;

char c;

char d[5];

};

foo()

{

struct bar baz;

}

Смотрим на откомпилированный код, дизассемблированный все той же IDA Pro.

Дизассемблированный код с отключенным выравниванием

.text:00000000 _foo proc near

.text:00000000 push ebp

.text:00000001 mov ebp, esp

.text:00000003 sub esp, 8

.text:00000006 mov esp, ebp

.text:00000008 pop ebp

.text:00000009 retn

.text:00000009 _foo endp

Вот оно! Вот они, наши 8 ожидаемых байт вместо непредвиденных 20! Однако скорость доступа к переменным за счет отключения выравнивания слегка упала. Но с невыравненными данными процессоры научились эффективно бороться еще во времена Pentium II, а вот если данные не влезут в кэш первого уровня, тогда падения производительности действительно не избежать.

Трюк 4: загадка чистых виртуальных методов

В предыдущих выпусках этой рубрики мы не касались вопросов приплюснутого Си, но по случаю юбилея сделаем исключение. Как известно, в любом учебнике по Си++ черным по белому написано, что невозможно создать экземпляр (instantiate) класса, имеющего чистый виртуальный метод (pure virtual method), при условии, что он никогда не вызывается. В этом, собственно говоря, и заключается суть концепции абстрактных классов.

Но в действительности не всему написанному стоит верить, и приплюснутый Си открывает достаточно большие возможности для трюкачества. Поставленную задачу можно решить, например, так:

Трюковый код, создающий экземпляр объекта с чистой виртуальной функцией, которая никогда не вызывается

class base

{

public:

base();

virtual void f() = 0;

};

class derived : public base

{

public:

virtual void f() {}

};

void G(base& b){}

base::base() {G(*this);}

main()

{

derived d;

}

После компиляции (использовался компилятор Microsoft Visual C++) мы увидим (смотри предыдущий код), что когда создается экземпляр d, конструктор base::base вызывает функцию G, передавая ей в качестве указателя this указатель на base, но не на derived, что, собственно говоря, и требовалось доказать.

Результат компиляции трюкового кода компилятором MS Visual C++ 6.0

.text:00000005 public: __thiscall Base::Base(void) proc near

.text:00000005 ; CODE XREF: Derived::Derived(void)+A?p

.text:00000005

.text:00000005 var_4 = dword ptr -4

.text:00000005

.text:00000005 push ebp

.text:00000006 mov ebp, esp

.text:00000008 push ecx

.text:00000009 mov [ebp+var_4], ecx ; this (base::base)

.text:0000000C mov eax, [ebp+var_4]

.text:0000000F mov dword ptr [eax], offset const Base::`vftable'

.text:00000015 mov ecx, [ebp+var_4]

.text:00000018 push ecx

.text:00000019 call G(Base &)

.text:0000001E add esp, 4

.text:00000021 mov eax, [ebp+var_4]

.text:00000024 mov esp, ebp

.text:00000026 pop ebp

.text:00000027 retn

.text:00000027 public: __thiscall Base::Base(void) endp

Содержание
ttfb: 88.454008102417 ms