Означают ли стандарты C и C++, что специальное значение в адресном пространстве должно существовать исключительно для представления значения нулевых указателей?

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

Если это может быть выведено из стандартов C и C++ (ответы могут быть нацелены на оба стандарта), то разыменование переменной указателя, значение которой равно nullptr (или же (void *)0) значение - неопределенное поведение, подразумевает ли это, что эти языки требуют, чтобы специальное значение в адресном пространстве было мертвым, что означает, что его нельзя использовать, кроме роли представления nullptr? Что если система имеет действительно полезную функцию или структуру данных по тому же адресу, который равен nullptr? Если это никогда не произойдет, потому что это обязанность автора компилятора выяснить неконфликтующее значение нулевого указателя для каждой системы, в которую компилируется компилятор? Или программист, которому нужен доступ к такой функции или структуре данных, должен быть доволен, программируя в "неопределенном режиме поведения" для достижения своих целей?

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

В этом блоге рассказывается о решении проблемной ситуации.

4 ответа

Решение

Означает ли это, что эти языки требуют, чтобы специальное значение в адресном пространстве было мертвым, что означает, что его нельзя использовать, кроме роли представления nullptr?

Нет.

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

Однако, если по этому адресу уже есть что-то, к чему не имеет доступа ни одна строго согласованная программа, то реализации разрешается поддерживать разыменование нулевого указателя для доступа к нему. Разыменование нулевого указателя в стандарте C не определено, поэтому реализация может заставить его делать все что угодно, в том числе и очевидное.

И стандарты C, и C++ понимают концепцию правила " как будто", которое в основном означает, что если для правильного ввода реализация не отличается от реализации, соответствующей стандарту, то она соответствует стандарту. Стандарт C использует тривиальный пример:

5.1.2.3 Выполнение программы

10 Пример 2. При выполнении фрагмента

char c1, c2;
/* ... */
c1 = c1 + c2;

"целочисленные продвижения" требуют, чтобы абстрактная машина повышала значение каждой переменной до int размер, а затем добавить два int и усечь сумму. Предусмотрено добавление двух char Это может быть выполнено без переполнения или с бесшумной упаковкой переполнения для получения правильного результата, фактическое выполнение должно давать только тот же результат, возможно, без продвижения по службе.

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

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


Хорошо известным примером является таблица векторов прерываний в реализациях DOS, которая находится по адресу ноль. Доступ к нему обычно осуществляется путем разыменования нулевого указателя. Стандарты C и C++ не охватывают, не должны и не могут охватывать доступ к таблице векторов прерываний. Они не определяют такое поведение, но и не ограничивают доступ к нему. Реализации должны быть и могут предоставлять расширения для доступа к нему.

Это зависит от того, что подразумевается под фразой "адресное пространство". Стандарт C использует фразу неформально, но не определяет, что это значит.

Для каждого типа указателя должно быть значение (нулевой указатель), которое сравнивается с указателем на любой объект или функцию. Это означает, например, что если тип указателя имеет ширину 32 бита, то может быть не более 232-1 допустимых ненулевых значений этого типа. Их может быть меньше, если некоторые адреса имеют более одного представления или если не все представления соответствуют действительным адресам.

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

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

Некоторые вещи на заметку:

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

В большинстве современных реализаций все типы указателей имеют одинаковый размер, и все они представляют нулевой указатель как все-ноль-бит, но есть веские причины, например, сделать указатели на функции шире, чем указатели на объекты, или сделать void* шире чем int*или используйте представление, отличное от нуля всех битов, для нулевого указателя.

Этот ответ основан на стандарте C. Большая часть этого также относится к C++. (Единственное отличие состоит в том, что в C++ есть типы указателей на члены, которые обычно шире обычных указателей.)

Да, это именно то, что это значит.

[C++11: 4.10/1]: [..] Константа нулевого указателя может быть преобразована в тип указателя; результат является нулевым значением указателя этого типа и отличается от любого другого значения указателя объекта или типа указателя функции. [..]

Значение нулевого указателя не должно быть 0x00000000, но он должен быть уникальным; нет другого способа заставить это правило работать.

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

Что если ОС помещает действительно полезную функцию или структуру данных по тому же адресу, который равен nullptr?

ОС не будет этого делать, но ее можно использовать.

Означает ли это, что эти языки требуют, чтобы специальное значение в адресном пространстве было мертвым, что означает, что оно непригодно для использования, кроме роли представления nullptr?

Да.

C имеет требования для нулевого указателя, которые отличают его от указателей объекта:

(C11, 6.3.2.3p3) "[...] Если константа нулевого указателя преобразуется в тип указателя, результирующий указатель, называемый нулевым указателем, гарантированно сравнивает неравный указатель с любым объектом или функцией."

Что если система имеет действительно полезную функцию или структуру данных по тому же адресу, который равен nullptr? Если это никогда не произойдет, потому что это обязанность автора компилятора выяснить значение неконфликтующего нулевого указателя для каждой системы, в которую компилируется компилятор?

Новый стандарт C Дерека М. Джонса предоставляет следующий комментарий к реализации:

Все биты ноль - это удобное представление времени выполнения константы нулевого указателя для многих реализаций, потому что это неизменно самый низкий адрес в памяти. (У транспортера INMOS [632] было адресное пространство со знаком, которое помещало ноль в середине.) Хотя в этом месте может быть информация о начальной загрузке программы, маловероятно, что какие-либо объекты или функции будут размещены здесь. Многие операционные системы оставляют это место хранения неиспользованным, потому что опыт показывает, что из-за сбоев в программе иногда значения записываются в местоположение, указанное константой нулевого указателя (среды, более ориентированные на разработчика, пытаются вызвать исключение при доступе к этому местоположению).

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

Другие вопросы по тегам