Печать нулевых указателей с% p является неопределенным поведением?


93

Является ли неопределенное поведение печатью нулевых указателей с помощью %pспецификатора преобразования?

#include <stdio.h>

int main(void) {
    void *p = NULL;

    printf("%p", p);

    return 0;
}

Вопрос относится к стандарту C, а не к реализациям C.


На самом деле не думайте, что кто-то (включая комитет C) слишком заботится об этом. Это довольно искусственная проблема, не имеющая (или почти не имеющая) практического значения.
P__J поддерживает женщин в Польше

это так, как printf только отображает значение и не касается (в смысле чтения или записи заостренного объекта) - не может быть указатель UB i имеет допустимое значение для своего типа ( допустимое значение
NULL

3
@PeterJ скажем, что то, что вы говорите, является правдой (хотя в стандарте явно указано иное), только тот факт, что мы обсуждаем это, делает вопрос действительным и правильным, поскольку похоже, что приведенная ниже часть стандарта делает Обычному разработчику очень трудно понять, что, черт возьми, происходит .. Значение: вопрос не заслуживает голоса против, потому что эта проблема требует разъяснения!
Питер Варо


2
@PeterJ, тогда это другая история, спасибо за разъяснения :)
Питер Варо

Ответы:


93

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


Код в вопросе демонстрирует четко определенное поведение.

Поскольку [7.1.4] является основой вопроса, давайте начнем с этого:

Каждое из следующих утверждений применяется, если иное явно не указано в подробных описаниях, которые следуют: Если аргумент функции имеет недопустимое значение ( например, значение вне домена функции или указатель вне адресного пространства программы, или нулевой указатель , [... другие примеры ...] ) [...] поведение не определено. [... другие заявления ...]

Это корявый язык. Одна интерпретация состоит в том, что элементы в списке являются UB для всех библиотечных функций, если они не отменены отдельными описаниями. Но список начинается с «например», что указывает на то, что он иллюстративный, а не исчерпывающий. Например, в нем не упоминается правильное завершение строк нулевым символом (критично для поведения, например strcpy).

Таким образом, ясно, что цель / область действия 7.1.4 просто состоит в том, что «недопустимое значение» приводит к UB ( если не указано иное ). Мы должны изучить описание каждой функции, чтобы определить, что считается «недопустимым значением».

Пример 1 - strcpy

[7.21.2.3] говорит только следующее:

В strcpyфункция копирует строку , указанную s2(включая завершающий нулевой символ) в массив , на который указывает s1. Если копирование происходит между перекрывающимися объектами, поведение не определено.

В нем нет явного упоминания нулевых указателей, но и нет упоминания о нулевых терминаторах. Вместо этого из «строки, на которую указывает s2» делается вывод, что единственными допустимыми значениями являются строки (то есть указатели на массивы символов с завершающим нулем).

Действительно, эту закономерность можно увидеть в отдельных описаниях. Еще несколько примеров:

  • [7.6.4.1 (fenv)] сохранить текущую среду с плавающей точкой в указываемого объекта путемenvp

  • [7.12.6.4 (frexp)] хранить целое число в Int указываемого объекта с помощьюexp

  • [7.19.5.1 (fclose)] поток указал наstream

Пример 2 - printf

[7.19.6.1] говорит следующее %p:

p- Аргумент должен быть указателем на void. Значение указателя преобразуется в последовательность печатаемых символов способом, определяемым реализацией.

Null - допустимое значение указателя, и в этом разделе явно не упоминается, что null - это особый случай, а также то, что указатель должен указывать на объект. Таким образом определяется поведение.


1. Если автор стандартов не явится, или если мы не сможем найти что-то похожее на документ с обоснованием, который проясняет ситуацию.


Комментарии не подлежат расширенному обсуждению; этот разговор был перемещен в чат .
Бхаргав Рао

1
"все же в нем не упоминаются нулевые терминаторы" является слабым в примере 1 - strcpy, поскольку в спецификации указано "копирует строку ". строка явно определяется как имеющая нулевой символ .
chux

1
@chux - Это в некотором роде моя точка зрения - нужно делать вывод о том, что действительно / недействительно, из контекста, а не предполагать, что список в 7.1.4 является исчерпывающим. (Однако существование этой части моего ответа имело несколько больше смысла в контексте комментариев, которые с тех пор были удалены, в которых утверждается, что strcpy был контрпримером.)
Оливер

1
Суть вопроса в том, как читатель интерпретирует такие как . Означает ли это, что некоторые примеры возможных недопустимых значений ? Означает ли это, что некоторые примеры всегда являются недопустимыми значениями ? Для протокола, я придерживаюсь первой интерпретации.
ninjalj

1
@ninjalj - Да, согласен. По сути, это то, что я пытаюсь передать здесь в своем ответе, то есть «это примеры типов вещей, которые могут иметь недопустимые значения». :)
Оливер

20

Краткий ответ

Да . Печать нулевых указателей со %pспецификатором преобразования имеет неопределенное поведение. Сказав это, я не знаю ни одной существующей соответствующей реализации, которая могла бы вести себя неправильно.

Ответ относится к любому из стандартов C (C89 / C99 / C11).


Длинный ответ

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

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

C99 / C11 §7.1.4 p1

[...] Если аргумент функции имеет недопустимое значение (например, [...] нулевой указатель, [...] поведение не определено.

Примеры для функций (стандартной библиотеки), которые ожидают нулевые указатели как допустимые аргументы:

  • fflush() использует нулевой указатель для сброса «всех потоков» (которые применяются).
  • freopen() использует нулевой указатель для указания файла, «связанного в данный момент» с потоком.
  • snprintf() позволяет передавать нулевой указатель, когда n равно нулю.
  • realloc() использует нулевой указатель для выделения нового объекта.
  • free() позволяет передавать нулевой указатель.
  • strtok() использует нулевой указатель для последующих вызовов.

Если мы возьмем случай snprintf(), имеет смысл разрешить передачу нулевого указателя, когда «n» равно нулю, но это не относится к другим функциям (стандартной библиотеке), которые допускают аналогичный нулевой «n». Например: memcpy(), memmove(), strncpy(), memset(), memcmp().

Это не только указано во введении к стандартной библиотеке, но также еще раз во введении к этим функциям:

C99 §7.21.1 p2 / C11 §7.24.1 p2

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


Это намеренно?

Я не знаю, является ли UB %pс нулевым указателем на самом деле преднамеренным, но поскольку в стандарте явно указано, что нулевые указатели считаются недопустимыми значениями в качестве аргументов стандартных библиотечных функций, а затем он идет и явно указывает случаи, когда нулевой указатель указатель является действительным аргументом (snprintf, бесплатно, и т.д.), а затем он идет и еще раз повторяет требование аргументы действительными даже в нуле случаев «п» ( memcpy, memmove, memset), то я думаю , что это разумно предположить , что Комитет по стандартам C не слишком озабочен тем, чтобы такие вещи не были определены.


Комментарии не подлежат расширенному обсуждению; этот разговор был перемещен в чат .
Бхаргав Рао

1
@JeroenMostert: В чем смысл этого аргумента? Приведенная цитата из 7.1.4 довольно ясна, не правда ли? О чем спорить, «если прямо не указано иное», если не указано иное? Что можно спорить с тем фактом, что (несвязанная) библиотека строковых функций имеет похожую формулировку, поэтому формулировка не кажется случайной? Я думаю, что этот ответ (хотя и не очень полезный на практике ) является максимально правильным.
Дэймон

3
@Damon: Ваше мифическое оборудование не является мифом, существует множество архитектур, в которых значения, не представляющие действительные адреса, могут не загружаться в регистры адресов. Однако передача нулевых указателей в качестве аргументов функции по-прежнему требуется для работы на этих платформах в качестве общего механизма. Простое добавление одного в стек ничего не взорвет.
Jeroen Mostert

1
@anatolyg: В процессорах x86 адреса состоят из двух частей - сегмента и смещения. На 8086 загрузка сегментного регистра аналогична загрузке любого другого, но на всех последующих машинах он получает дескриптор сегмента. Загрузка недопустимого дескриптора вызывает ловушку. Много кода для 80386 и более поздних процессоров, однако, использует только один сегмент, и , таким образом , никогда не загружает сегментные регистры на всех .
supercat

1
Я думаю, все согласятся, что печать нулевого указателя %pне должна быть неопределенным поведением
ММ,

-1

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

Вопрос о том, вызывает ли что-то UB, редко бывает полезен сам по себе. Настоящие важные вопросы:

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

  2. Должны ли программисты иметь право ожидать, что качественные компиляторы для чего-либо, напоминающего обычные платформы, будут вести себя предсказуемым образом? В описанном сценарии я бы сказал, что да.

  3. Могут ли некоторые тупые авторы компиляторов растянуть интерпретацию Стандарта, чтобы оправдать совершение чего-то странного? Надеюсь, что нет, но не исключаю.

  4. Должны ли дезинфицирующие компиляторы кричать о поведении? Это будет зависеть от уровня паранойи пользователей; дезинфицирующий компилятор, вероятно, не должен по умолчанию кричать о таком поведении, но, возможно, должен предоставить возможность конфигурации на случай, если программы могут быть перенесены на «умные» / глупые компиляторы, которые ведут себя странно.

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


1. Программисты нередко обнаруживают, что предположения, сделанные современными / агрессивными оптимизаторами, расходятся с тем, что они считают «разумными» или «качественными». 2. Когда дело доходит до двусмысленности в спецификации, разработчики нередко расходятся во мнениях относительно того, какие свободы они могут себе позволить. 3. Что касается членов комитета по стандартам C, то даже они не всегда соглашаются с тем, что такое «правильная» интерпретация, не говоря уже о том, какой она должна быть. С учетом вышесказанного, чьей разумной интерпретации мы должны следовать?
Dror K.

6
Ответ на вопрос «вызывает ли этот конкретный фрагмент кода UB или нет» с диссертацией о том, что вы думаете о полезности UB или о том, как должны себя вести компиляторы, - плохая попытка дать ответ, тем более что вы можете скопировать и вставить это как ответ практически на любой вопрос о конкретном УБ. В ответ на вашу риторическую расцветку: да, действительно важно, что говорит стандарт, независимо от того, что делают некоторые разработчики компиляторов или что вы думаете о них, потому что стандарт - это то, с чего начинают и программисты, и разработчики компиляторов.
Jeroen Mostert

1
@JeroenMostert: ответ на вопрос «вызывает ли X неопределенное поведение?» Часто будет зависеть от того, что подразумевается под вопросом. Если программа считается имеющей неопределенное поведение, если стандарт не налагает никаких требований на поведение соответствующей реализации, то почти все программы вызывают UB. Авторы Стандарта четко позволяют реализациям вести себя произвольным образом, если программа слишком глубоко вкладывает вызовы функций, при условии, что реализация может правильно обрабатывать по крайней мере один (возможно, надуманный) исходный текст, который реализует ограничения перевода в Stadard.
supercat

@supercat: очень интересно, но есть ли printf("%p", (void*) 0)неопределенное поведение согласно Стандарту? Вызовы глубоко вложенных функций так же важны для этого, как и цена чая в Китае. И да, UB очень часто встречается в реальных программах - что из этого?
Jeroen Mostert

1
@JeroenMostert: Поскольку Стандарт позволяет тупой реализации рассматривать почти любую программу как имеющую UB, важным будет поведение не тупых реализаций. Если вы не заметили, я не просто написал копию / вставку об UB, но ответил на вопрос о %pкаждом возможном значении вопроса.
supercat
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.