Разыменование указателя, равного nullptr, неопределенному поведению по стандарту?

Автор блога поднял дискуссию о разыменовании нулевого указателя:

Я поставил несколько контраргументов здесь:

Его основная аргументация, цитирующая стандарт, такова:

Выражение '&podhd->line6' является неопределенным поведением в языке Си, когда 'podhd' является нулевым указателем.

Стандарт C99 говорит следующее об операторе адреса "&" (6.5.3.2 "Операторы адреса и косвенности"):

Операнд унарного оператора & должен быть либо указателем функции, результатом оператора [] или унарного *, либо lvalue, который обозначает объект, который не является битовым полем и не объявлен со спецификатором класса хранения регистра,

Выражение 'podhd->line6' явно не является обозначением функции, результатом оператора [] или *. Это выражение lvalue. Однако, когда указатель podhd равен NULL, выражение не обозначает объект, поскольку 6.3.2.3 "Указатели" говорят:

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

Когда "lvalue не обозначает объект при его оценке, поведение не определено" (C99 6.3.2.1 "Lvalue, массивы и указатели функций"):

Lvalue - это выражение с типом объекта или неполным типом, отличным от void; если lvalue не обозначает объект при его оценке, поведение не определено.

Итак, та же идея вкратце:

Когда -> был выполнен для указателя, он оценивается как lvalue, где нет объекта, и в результате поведение не определено.

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

Насколько я вижу, в разыменовании переменной-указателя, значение которой равно nullptrДаже мысли сравнения указателя на nullptr (или же (void *) 0) константа может исчезать при оптимизации в определенных ситуациях из-за указанных абзацев, но это выглядит как другая проблема, она не предотвращает разыменование указателя, значение которого равно nullptr, Обратите внимание, что я проверил другие SO вопросы и ответы, мне особенно нравится этот набор цитат, а также стандартные цитаты выше, и я не наткнулся на то, что явно выводит из стандарта, что если указатель ptr сравнивается равным nullptrРазыменование это было бы неопределенным поведением.

Самое большее, что я получаю, это то, что защита константы (или ее приведение к любому типу указателя) - это то, что есть UB, но ничего не говорит о переменной, которая немного равна значению, полученному из nullptr,

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

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

Если вывод таков, если ptr равно значению nullptr Разыменование это, безусловно, UB, другой вопрос:

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

3 ответа

Решение

Когда вы цитируете C, разыменование нулевого указателя является явно неопределенным поведением из этой стандартной цитаты (выделено мое):

(C11, 6.5.3.2p4) "Если указателю было присвоено недопустимое значение, поведение унарного оператора * не определено.102)"

102): "Среди недопустимых значений для разыменования указателя с помощью унарного оператора * есть нулевой указатель, адрес, неправильно выровненный для типа объекта, на который указывает объект, и адрес объекта после окончания его времени жизни".

Точно такая же цитата в C99 и аналогичная в C89 / C90.

C++

dcl.ref / 5.

Не должно быть ссылок на ссылки, массивов ссылок и указателей на ссылки. Объявление ссылки должно содержать инициализатор (8.5.3) за исключением случаев, когда объявление содержит явный спецификатор extern (7.1.1), является объявлением члена класса (9.2) в определении класса или является объявлением параметра или тип возврата (8.3.5); см. 3.1. Ссылка должна быть инициализирована для ссылки на действительный объект или функцию. [Примечание: в частности, пустая ссылка не может существовать в четко определенной программе, потому что единственный способ создать такую ​​ссылку - это привязать ее к "объекту", полученному косвенным путем через нулевой указатель, что вызывает неопределенное поведение. Как описано в 9.6, ссылка не может быть привязана непосредственно к битовому полю. - конец примечания]

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

Я уверен, что это говорит где-то еще в более актуальном контексте, но это достаточно хорошо.

Ответ на этот вопрос, который я вижу, относительно того, в какой степени значение NULL может быть разыменовано, заключается в том, что оно намеренно оставлено зависимым от платформы неопределенным образом, из-за того, что остается определенным реализацией в C11 6.3.2.3p5 и p6. Это в основном для поддержки автономных реализаций, используемых для разработки загрузочного кода для платформы, как указывает OP в своей опровержительной ссылке, но также имеет приложения для размещенной реализации.

Re:
(C11, 6.5.3.2p4) "Если указателю было присвоено недопустимое значение, поведение унарного оператора * не определено.102)"

102): "Среди недопустимых значений для разыменования указателя с помощью унарного оператора * есть нулевой указатель, адрес, неправильно выровненный для типа объекта, на который указывает объект, и адрес объекта после окончания его времени жизни".

Это сформулировано, как есть, на самом деле, потому что каждый из случаев в сноске НЕ может быть недействительным для конкретных платформ, на которые ориентирован компилятор. Если там есть дефект, его "недопустимое значение" должно быть выделено курсивом и квалифицировано как "определено реализацией". В случае выравнивания платформа может иметь доступ к любому типу, используя любой адрес, поэтому не имеет требований к выравниванию, особенно если поддерживается перенос адреса; и платформа может предполагать, что время жизни объекта заканчивается только после выхода из приложения, выделяя новый кадр с помощью malloc() для автоматических переменных при каждом вызове функции.

Для нулевых указателей во время загрузки у платформы могут быть ожидания, что структуры, используемые процессором, имеют определенные физические адреса, в том числе по адресу 0, и могут быть представлены как указатели объектов в исходном коде, или могут потребовать, чтобы функция, определяющая процесс загрузки, использовала базу адрес 0. Если стандарт не разрешает разыменования, такие как &podhd->line6, где платформе требуется, чтобы podhd имел базовый адрес 0, тогда для доступа к этой структуре потребуется язык ассемблера. Точно так же функции мягкой перезагрузки может понадобиться разыменовать 0-значный указатель как вызов пустой функции. Размещенная реализация может рассматривать 0 как основу исполняемого образа и отображать нулевой указатель в исходном коде после заголовка этого образа после загрузки в качестве структуры, которая должна быть по логическому адресу 0 для этого экземпляра виртуальной машины C.

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

Требование к (void *)0 больше, чем код компилятора, который гарантирует выражения, в которых источник использует (void *)0, явно или с помощью ссылки на NULL, что фактическое сохраненное значение будет тем, которое говорит, что на это нельзя указывать любые допустимые определения функций или объекты с помощью любого кода отображения. Это не должно быть 0! Аналогично, для приведений (void *)0 к (obj_type) и (func_type) они требуются только для получения назначенных значений, которые оцениваются как адреса, которые, как гарантирует компилятор, не используются тогда для объектов или кода. Разница с последним заключается в том, что они не используются, не недействительны, поэтому могут быть разыменованы определенным образом.

Код, который проверяет равенство указателей, затем проверяет, является ли один операнд одним из этих значений, а другой - одним из трех, а не просто одним и тем же битовым шаблоном, потому что это оценивает их с RTTI типа (null *), отличается от типов указателей void, obj и func на определенные объекты. Стандарт может быть более явным, это отдельный тип, если он не назван, потому что компиляторы используют его только внутренне, но я полагаю, это считается очевидным из-за того, что "нулевой указатель" выделен курсивом. По сути, imo, '0' в этих контекстах является дополнительным ключевым токеном компилятора, из-за дополнительного требования, чтобы он идентифицировал тип (null *), но не характеризуется как таковой, потому что это усложнит определение < идентификаторы>.

Это сохраненное значение может быть SIZE_MAX так же просто, как 0, для (void *)0, в испускаемом коде приложения, когда реализации, например, определяют диапазон от 0 до SIZE_MAX-4*sizeof(void *) дескрипторов виртуальной машины как действителен для кода и данных. Макрос NULL может быть даже определен как
(void *)SIZE_MAX, и компилятор должен выяснить из контекста, что он имеет ту же семантику, что и 0. Код приведения отвечает за то, что это выбранное значение в указателе <-> приведения указателя, и предоставить то, что подходит в качестве указателя на объект или функцию. Приведения к указателю <-> целое число, неявное или явное, имеют аналогичные требования проверки и предоставления; особенно в профсоюзах, где (u) поле intptr_t перекрывает поле (тип *). Переносимый код может защитить от компиляторов, которые не делают этого должным образом с явным выражением *(ptr==NULL?(Type *) 0: ptr).

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