c-tricks-10h

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

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

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

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

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

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

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

#define _f() x_f()

x_f();

Листинг 1 избавляемся от префиксов знака подчеркивания через макросы

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

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

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

struct string

{

intlength; длина строки chardata [1]; память зарезервированная для строки

};

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

Элемент «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);

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

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

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

foo()

{

char a;

char b;

char c;

char d[5];

}

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

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

.text:00000000 _fooproc near

.text:00000000pushebp

.text:00000001movebp, esp

.text:00000003subesp, 14h

.text:00000006movesp, ebp

.text:00000008popebp

.text:00000009 retn

.text:00000009 _fooendp

Листинг 5 откомпилированный результат листинга 4

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

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

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

#pragma pack( 1 )

struct bar

{

char a;

char b;

char c;

char d[5];

};

foo()

{

struct bar baz;

}

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

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

.text:00000000 _fooprocnear

.text:00000000pushebp

.text:00000001movebp, esp

.text:00000003subesp, 8

.text:00000006movesp, ebp

.text:00000008popebp

.text:00000009retn

.text:00000009 _fooendp

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

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

В предыдущих выпусках этой рубрики мы не касались вопросов приплюснутого си, но на случай юбилея сделаем исключение. Как известно, в любом учебнике по Си++ черным по белому написано, что невозможно создать экземпляр (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;

}

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

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

.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:00000005pushebp .text:00000006movebp, esp .text:00000008pushecx .text:00000009mov[ebp+var_4], ecx; this (base::base) .text:0000000Cmoveax, [ebp+var_4] .text:0000000Fmovdword ptr [eax], offset const Base::`vftable' .text:00000015movecx, [ebp+var_4] .text:00000018pushecx .text:00000019callG(Base &) .text:0000001Eaddesp, 4 .text:00000021moveax, [ebp+var_4] .text:00000024movesp, ebp .text:00000026popebp .text:00000027retn .text:00000027 public: thiscall Base::Base(void) endp

Листинг 9 результат компиляции «трюкового» кода компилятором MS Visual C++ 6.0