Объекты Нулевого Размера

В чем разница между следующими парами длин и указателей?

  
  
  
  
  
  
   

size_t len1 = 0; char *ptr1 = NULL; size_t len2 = 0; char *ptr2 = malloc(0); size_t len3 = 0; char *ptr3 = (char *)malloc(4096) + 4096; size_t len4 = 0; char ptr4[0]; size_t len5 = 0; char ptr5[];

Во многих случаях все пять выражений дадут один и тот же результат. В других их поведение может быть радикально иным.

Одним из очевидных отличий является возможность передать указатель на его освобождение, но мы не будем рассматривать это здесь.

Первый случай интересен, но он слишком отличается от остальных, поэтому мы его пока отложим.



маллок(0)

Поведение malloc(0) определяется стандартами.

Вы можете вернуть нулевой или уникальный указатель.

Второй вариант во многих реализациях реализуется путем внутреннего увеличения длины на единицу (которая затем обычно округляется до 16).

По правилам такой указатель нельзя разыменовать, но обычно несколько байт все равно выделяется, и поэтому такая программа не выйдет из строя.

Возврат NULL приводит к возможности возникновения интересной ошибки.

Часто возврат NULL из malloc считается ошибкой.



if ((ptr = malloc(len)) == NULL) err(1, "out of memory");

Если len равен нулю, это приведет к недопустимому сообщению об ошибке — если вы не добавите дополнительную проверку && len != 0. Вы также можете присоединиться к культу «без проверки malloc».

В OpenBSD malloc обрабатывает значение null по-другому.

Распределение данных нулевого размера возвращает фрагменты страниц, которые были защищены с помощью mprotect() с помощью ключа PROT_NONE. Попытка разыменовать такой указатель приведет к сбою.

Обратите внимание, что требования к уникальным указателям запрещают «читерство» при их использовании.



int thezero; void * malloc(size_t len) { if (len == 0) return &thezero; } void free(void *ptr) { if (ptr == &thezero) return; }

Эта реализация противоречива, поскольку последовательные вызовы будут возвращать одно и то же значение.

Поэтому второй случай аналогичен как первому, так и третьему, в зависимости от реализации.



Другие случаи

Если malloc не выдает ошибку, то варианты 3, 4 и 5 в большинстве случаев работают одинаково.

Основное отличие будет заключаться в использовании sizeof(ptr)/sizeof(ptr[0]), например в цикле.

Это приведет к неправильному ответу, правильному ответу или вообще ни к чему, обрываясь на этапе компиляции.

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

Это будет похоже на разницу между пустым массивом и отсутствующим массивом.

И точно так же пустая строка не равна пустой строке, хотя и занимает в памяти один байт.

нулевые объекты

Вернемся к первому варианту и нулевым объектам.

Рассмотрим следующий вызов:

memset(ptr, 0, 0);

Мы присваиваем ptr 0 0 байт. Какой из пяти перечисленных указателей позволит вам совершить такой вызов? 3, 4 и 5. 2-й – если это уникальный указатель.

Но что, если ptr равен NULL? В стандарте C в разделе «Использование библиотечных функций» сказано:

Если аргумент функции имеет недопустимое значение (значение находится за пределами домена функции, указатель указывает на память за пределами области действия программы или указатель имеет значение NULL), то поведение не будет определено.

В разделе «Соглашения для строковых функций» поясняется:
Если аргумент, объявленный как size_t n, указывает длину массива в функции, значение n может быть нулевым при вызове этой функции.

Если иное не указано в описании конкретной функции, значения аргументов указателя должны быть допустимыми.

Судя по всему, результат memset от 0 байт до NULL будет неопределенным.

В документации memset, memcpy и memmove не указано, что они могут принимать нулевые указатели.

В качестве контрпримера в описании snprintf говорится: «Если n равно нулю, ничего не записывается, а s может быть нулевым указателем».

В документации к функции чтения POSIX аналогичным образом описывается, что чтение нулевой длины не считается ошибкой, но реализация может проверять на наличие ошибок другие параметры — например, недопустимые указатели буфера.

Что на практике? Самый простой способ справиться с нулевой длиной в таких функциях, как memset или memcpy, — не зацикливаться и ничего не делать.

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

Проверка ненулевых, но недействительных указателей довольно сложна.

memcpy вообще этого не делает, что приводит к простому сбою программы.

Вызов чтения тоже ничего не проверяет. Он делегирует проверку функции копирования, которая запускает обработчик для обнаружения ошибок.

Хотя можно добавить проверку на ноль, такие указатели не более недействительны для этих функций, чем 0x1 или 0xffffffff, для которых не существует специальной обработки.



облом

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

Я решил поэкспериментировать, добавив в memcpy вывод ошибок и вывод, если указатель оказывается NULL, и установил новую библиотеку libc.

Feb 11 01:52:47 carbolite xsetroot: memcpy with NULL Feb 11 01:53:18 carbolite last message repeated 15 times

Неа, это не заняло много времени.

Интересно, что он там делает:

Feb 11 01:53:18 carbolite gdb: memcpy with NULL Feb 11 01:53:19 carbolite gdb: memcpy with NULL

Ясно понял.

Кажется, эти сообщения очень быстро надоедают. Возвращаем все как было.



Последствия

Я занялся этим вопросом, потому что на пересечении областей «не определено, но должно работать» и оптимизации компиляторов Си ничего хорошего не происходит. Умный компилятор может увидеть вызов memcpy, пометить оба указателя как действительные и удалить проверки на нулевое значение.



int backup; void copyint(int *ptr) { size_t len = sizeof(int); if (!ptr) len = 0; memcpy(&backup, ptr, len); }

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

Этот вопрос меня беспокоит, поскольку в прошлом я видел случаи, когда такая оптимизация разыменования и проверок приводила к дырам в безопасности.

Для программного обеспечения, не готового к такому высокому уровню соответствия, это довольно печальная новость.

Сначала мне не удалось убедить компилятор удалить проверку на null после «разыменования» memcpy, но это не значит, что этого не может произойти.

gcc 4.9 говорит, что эта проверка будет удалена при оптимизации.

В OpenBSD gcc 4.9 (который содержит множество исправлений) по умолчанию не удаляет проверку, даже с –O3, но включение «-fdelete-null-pointer-checks» удаляет проверки.

Не знаю, как насчет clang - первые тесты показывают, что не удаляет, но гарантий нет. По идее, он тоже сможет провести такую оптимизацию.

Теги: #указатели #нулевые указатели #программирование #C++

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.