c-tricks-11h

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

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

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

Зачем может понадобится создавать статический массив нулевой длинны? Выражение типа «char c[0]» не имеет смысла! Однако… в некоторых ситуациях оно бывает _очень_ полезно. Допустим, мы имеем определение DATA_LEN с допустимыми значениями от 0 (no data) до… XXL. Тогда конструкция «char c[DATA_LEN]» при DATA_LEN == 0 приведет к ошибке компиляции, даже если мы не собираемся обращаться к массиву «c» по ходу исполнения программы. А усложнять алгоритм, добавляя лишние ветвленияи загромождая листинг командами препроцессора — не хочется.

Вся хитрость в том, что если обернуть статический массив структурой, то компилятор, проглотит ее не задумываясь! Как раз то, что нам нужно!!! Рассмотрим следующий код:

#define DATA_LEN0 нет данных struct ZERO структура со статическим массивом нулевой длинны

{

char c[DATA_LEN]; массив нулевой длины }; main() { объявляем структуру с массивом нулевой длины struct ZERO zero; печатаем размер структуры ZERO и ееэкземпляра zero printf(»%x %x\n», sizeof(struct ZERO),sizeof(zero)); присваиваем значение первой ячейке массива нулевой длины!!!

*zero.c = 0x69;

выводим это значение на экран printf(«0%Xh\n», *zero.c); } Листинг 1 статический массив нулевой длинны на стеке Этот код компилируется всеми компиляторами без исключения, причем, работает _правильно_ (хотя и не обязан это делать). В частности, при компиляции Си-программы, Microsoft Visual C++ утверждает, что размер структуры ZERO равен 4 байтам, но если изменить расширение файла с «.c» на «.cpp», мы получим… 1 байт. GCC во всех случаях дает нам 0 байт,что логично, но неправильно, поскольку _все_ 32‑битные компиляторы _реально_ резервируют как минимум 4 байта под локальные переменные любых видов, т. к. это необходимо для выравнивания стека (и дизассемблерные листинги наглядно подтверждают это!). Следовательно, мы можем не только создавать статические массивы нулевой длины, но еще и (пускай не без предосторожностей) использовать их. Например, в качестве вступительных тестов для новичков. Шутка! Но своя доля истины в ней есть. Обычно программисты, не желающие, чтобы их отстранили от проекта, добавляют в исходный код немного «черной магии». Программа работает вопреки здравому смыслу, совершенно непостижимому для окружающих! А вот если переместить структуру ZERO в статическую область памяти (секцию данных), то размер, резервируемый под массив нулевой длины, сразу же сократиться до одного байта, поскольку выравнивать переменные в статической памяти нет никакой необходимости, но в нашем распоряжении останется по меньшей мере один байт, который можно задействовать под производственные нужды ;) ===== трюк второй — динамические массивы на куче ===== Функции семейства malloc() обязаны корректно обрабатывать нулевой аргумент, возвращая валидный указатель на блок памяти нулевой длинны. Вот что говорит MSDN по этому поводу «If size is 0, malloc allocates a zero-length item in the heap and returns a valid pointer to that item» (Если размер [выделяемой памяти] равен нулю, функция malloc выделяет блок памяти нулевой длины в куче и возвращает указатель на него). То есть, создавать массив нулевой длинны на куче мы можем без всяких извращений со структурами. Вот только обращаться к созданному массиву (по стандарту) никак не можем. Стандарт допускает только проверку указателя на нуль, сравнение двух указателей, освобождение памяти, ну и, естественно, реаллокацию. Однако, стандарт предполагает, а компилятор располагает. Давайте выясним: сколько же всего в _действительности_ выделяется байт при создании массива нулевой длинны? #define DATA_LEN0 нет данных

main()

{

создаем три массива нулевой длины char *p1=malloc(DATA_LEN); char *p2=malloc(DATA_LEN); char *p3=malloc(DATA_LEN); создаем три массивадлинной в один байт

char *p4=malloc(1);

char *p5=malloc(1);

char *p6=malloc(1);

выводит указатель на созданные блоки на экран printf(«0%Xh\n0%Xh\n0%Xh\n\n0%Xh\n0%Xh\n0%Xh\n», p1, p2, p3, p4, p5, p6); } Листинг 2 измерительный прибор, определяющий реальный размер массивов нулевой длинны Откомпилировав программу с помощью Microsoft Visual C++ и запустив ее на выполнение, на мы получим следующий результат: 0300500h; \ 03004F0h; +- указатели на блоки нулевой длины 03004E0h; / 03004D0h; \ 03004C0h; +- указатель на блоки длиной в один байт 03004B0h; / Листинг 3 реально выделяемый размер динамических массивов Как видно, адреса выделяемых блоков планомерно уменьшаются на 10h байт, следовательно, каждый блок (состоящий из массива и служебных данных) занимает _намного_ больше, чем ничего. Более того, malloc(0) эквивалентно malloc(1). Определить размер актуальных данных динамического массива — несложно. Достаточно увеличивать аргумент, передаваемый malloc,до тех пор, пока разница между соседними указателями скачкообразно не увеличиться на некоторую величину. Эксперимент показывает, что минимальный размер выделяемого блока для Microsoft Visual C++ и 32-битных версий GCC составляет 10h байт, то есть malloc(0) работает _точно_ _также_ как и malloc(0xF). Естественно, никаких гарантий, что остальные компиляторы поведут себя аналогичным образом, у нас нет и никогда не будет, поэтому, вылезать за границы отведенного блока по любому не стоит. С другой стороны, выделив большое количество динамических массивов нулевого размера, не следует надеяться, что они не занимают драгоценной памяти и потому их можно не освобождать. Освобождать их нужно!!! Иначе память будет утекать со страшной скоростью! ===== трюк третий — оператор new ===== Практически все известные мне реализации Си++ компиляторов реализуют оператор new на основе malloc, поэтому, все, сказанное по отношению к malloc(0),справедливо и для new(0). Однако… кое-какие различия все-таки наблюдается и мне бы хотелось обратить на них читательское внимание. Прежде всего откроем Стандарт (см. «C++ Programming Language, Second Edition» секция 5.3.3), где Дохлый Страус прямо так и пишет: «This implies that an operator new() can be called with the argument zero. In this case, a pointer to an object is returned. Repeated such calls return pointers to distinct objects» («…отсюда следует, что оператор new() может вызываться с нулевым аргументом и возвращать валидный указатель на объект. Последовательный вызов new(0) возвращает указатели на различные объекты»). Дальше по тексту объясняется, что мы можем получить указатель на нулевой объект, сравнивать его с любым другим указателем, но вот обращение к объекту нулевой длинны стандартом… ну не то, чтобы запрещается, а отдается на откуп конкретным реализациям. Изучение исходных кодов RTL-библиотек различных компиляторов показывает, что new(0) в общем случае эквивалентно new(1) независимо от типа объекта. Вот, например, фрагмент кода из GCC: void* operator new(size_t size) реализация оператора new

{

если size равно нулю, принудительно устанавливаем размер в единицу if( size == 0 ) size = 1; продолжение функции … } Листинг 4 фрагмент кода из компилятора GCC, реализующий оператор new Оператор new в свою очередь опирается на RTL-библиотеку, общую как для Си, так и для Си++, а потому оператор new(1) в большинстве случаев эквивалентен new(0xF), что наглядно подтверждает следующая программа: main() { создаем символьный массив нулевой длины (Стандартом это допускается) char *c = new char[0]; получаем указатель на созданный объект нулевой длины (Стандартом это допускается) char *p = &c[0]; записываем в объект нулевой длинны число 0x69 (а вот этого Стандарт уже _не_ допускает!!!) *c=0x69; проверяем успешность записи числа, выводя его на экран printf(«0%Xh\n»,*c); } Листинг 5 демонстрация создания объекта размеров в 1 байт с помощью new char[0] Чтобы не быть голословным мыщъх приводит дизассемблерный фрагмент вышеупомянутой программы, откомпилированной Microsoft Visual C++ (heap_alloc – служебная функция, на которую опирается оператор new) : .text:00401B6F heap_allocproc near; CODE XREF: nh_malloc+B↑p .text:00401B6F .text:00401B6F arg_0 = dword ptr 8 .text:00401B6F .text:00401B6F pushesi .text:00401B70movesi, [esp+arg_0]; размер выделяемой памяти .text:00401B74cmpesi, dword_406630; выделяем больше 1016байт .text:00401B7Ajashort loc_401B87; если да, то - прыжок .text:00401B7C .text:00401B7Cpushesi; обрабатываем ситуацию .text:00401B7Dcall_sbh_alloc_block; с выделением ⇐1016байт .text:00401B82testeax, eax; памяти .text:00401B84popecx .text:00401B85jnzshort loc_401BA3; прыжок, если памяти нет .text:00401B87 .text:00401B87 loc_401B87:; CODE XREF: heap_alloc+B↑j .text:00401B87testesi, esi; выделяем ноль байт?! .text:00401B89jnzshort loc_401B8E; если не ноль, прыгаем .text:00401B8Bpush1; если ноль, увеличиваем .text:00401B8Dpopesi; аргумент на единицу .text:00401B8E .text:00401B8E loc_401B8E:; CODE XREF: heap_alloc+1A↑j .text:00401B8Eaddesi, 0Fh; округляем размер блока .text:00401B91andesi, 0FFFFFFF0h; на 10h в большую сторону** .text:00401B94pushesi; dwBytes .text:00401B95push0; dwFlags .text:00401B97pushhHeap; hHeap .text:00401B9Dcallds:HeapAlloc; выделяем блок памяти .text:00401BA3 .text:00401BA3 loc_401BA3:; CODE XREF: heap_alloc+16↑j .text:00401BA3popesi .text:00401BA4retn .text:00401BA4 heap_allocendp Листинг 6 дизассемблерный фрагмент функции __heap_alloc из Microsoft Visual C++, на которую опирается оператор new() и которая принудительно округляет выделяемый размер по границе 10h байт в большую строну, т.е. выделить менее 10h байт нам ни за что не удастся