Комментарии 22
Ожидаемая правильная работа программы — это просто один из вариантов неопределённого поведения
А какие бывают варианты неопределенного поведения?
Ой, ну разные. Например, запустится игра:
Ещё один пример неопределенного поведения: курьёз с ANSI-директивой «#pragma». Согласно спецификации языка компиляторам предоставлена полная свобода при обработке этой конструкции. До версии 1.17 компилятор GCC при нахождении в исходном коде этой директивы пытался запустить Emacs с игрой «Ханойские башни».
То есть, неопределенное поведение — это набор вариантов, один из которых — попытка запустить Emacs?
Насколько мне известно, это называется unspecified behaviour.
А неопределенное поведение потому и названо так, что оно не определено. И никаких вариантов нет.
Насколько мне известно, это называется unspecified behaviour.
А неопределенное поведение потому и названо так, что оно не определено. И никаких вариантов нет.
Определение undefined behavior из Стандарта C++03 (1.3.12)
[...], на которое этот Стандарт не налагает никаких требований — допускается любое поведение, в том числе и запуск Emacs. В принципе, никто не мешает разработчикам компилятора специально сделать неопределенное поведение разнообразным — например, по пятницам программа будет запускать Emacs, а по остальным — выполнять действие, приводящее к аварийному завершению программы, но пользователи не очень это оценят.
behavior, such as might arise upon use of an erroneous program construct or erroneous data, for which this International Standard imposes no requirements.
[...], на которое этот Стандарт не налагает никаких требований — допускается любое поведение, в том числе и запуск Emacs. В принципе, никто не мешает разработчикам компилятора специально сделать неопределенное поведение разнообразным — например, по пятницам программа будет запускать Emacs, а по остальным — выполнять действие, приводящее к аварийному завершению программы, но пользователи не очень это оценят.
Да, это я и имел ввиду. При этом ни о каких вариантах речи нет. Спасибо за ссылку на пункт стандарта.
В Стандарте действительно не сказано ничего о вариантах неопределенного поведения. В статье, очевидно, речь идет о возможных вариантах работы конкретного результата компиляции программы конкретной версией конкретного компилятора с конкретными настройками. Одни компиляторы вообще неспособны к описанной оптимизации, у других она включается в зависимости от настроек (в достаточно новом gcc она включается автоматически, начиная с уровня -O2). В зависимости от этих факторов и возможны разные варианты работы программы.
Однако, меня смущает то, что, хотя все говорят про неопределённое поведение, нигде нет точного разъяснения на этот счёт.
В стандарте C++03 есть разъяснение. Пункты 1.9/4 и 5.2.5/3.
Это я видел. Было 1.9/4: Certain other operations are described in this International Standard as undefined (for example, the effect of dereferencing the null pointer).
А что с новым то стандартом? Почему они застеснялись прежнего описания и теперь там иной пример 1.9/4: Certain other operations are described in this International Standard as undefined (for example, the effect of attempting to modify a const object).
А что с новым то стандартом? Почему они застеснялись прежнего описания и теперь там иной пример 1.9/4: Certain other operations are described in this International Standard as undefined (for example, the effect of attempting to modify a const object).
А как изменение примера могло повлиять на поведение? Пример могли вообще удалить, его присутствие необязательно.
Какого-то нового описания разыменования нулевого указателя не появилось, поэтому по данному вопросу все так же, как и было. Пункт 1.9/4 устанавливает неопределенное поведение.
Какого-то нового описания разыменования нулевого указателя не появилось, поэтому по данному вопросу все так же, как и было. Пункт 1.9/4 устанавливает неопределенное поведение.
Но мне так и не понятно, что ответить. :) Мне продолжат писать письма, что в статье ошибка и я/анализатор не прав. :)
Прошу помощи. Кто сможет написать бронебойный текст вида: данный код корректен/не корректен, так как согласно современному стандарту C++11 бла… бла… см. пункты такой и такой. Буду благодарен.
Прошу помощи. Кто сможет написать бронебойный текст вида: данный код корректен/не корректен, так как согласно современному стандарту C++11 бла… бла… см. пункты такой и такой. Буду благодарен.
Это не те пункты. В Стандарте C++03 нужное определение дано в 1.3.12
imposes no requirements означает «не налагает никаких требований» — допускается любое поведение.
undefined behavior [defns.undefined]
behavior, such as might arise upon use of an erroneous program construct or erroneous data, for which this International Standard imposes no requirements.
imposes no requirements означает «не налагает никаких требований» — допускается любое поведение.
Я думаю, стоит задать вопрос в gcc-help. Там живые разработчики компилятора могут ответить, что они думают на этот счёт.
Прислали ещё следующее мнение:
Я прочитал вашу статью «Размышления над разыменованием нулевого указателя» и предыдущую «PVS-Studio покопался во внутренностях Linux (3.18.1)».
Обнаружил два заблуждения:
· При оптимизации компилятор рассуждает так. Вот здесь указатель разыменовывается: podhd->line6. Ага, программист знает что делает. Значит указатель здесь точно не равен нулю. Отлично, запомним это.
· Любое разыменование нулевого указателя — это неопределённое поведение.
Собака здесь зарыта в том, что разыменование указателя a->b это две последовательных операции:
· Вычисление значения lvalue;
· Использование объекта lvalue.
В случае с NULL ничто не мешает вычислить lvalue, и согласно стандарту его вычислят здесь нет неопределенного поведения. А вот в момент использования объекта lvalue компилятор уже с полным основанием может полагать, что в функцию передан ненулевой указатель, либо порадовать нас неопределенным поведением в противном случае. Если lvalue используется в выражении указателей, то нет основания полагать, что исходный указатель был NULL.
Похоже, что вы просто столкнулись с багом программы PVS-Studio, когда она некорректно выдает предупреждение V595 в случае вычисления lvalue без реального использования объекта.
в статье же есть комментарий
Прежде всего вспомним, что сишные компиляторы используются в мире эмбеддеров, там struct и union — весьма частые гости. Конструкция offsetof в указанном виде нормально работает типами struct и union. Если коротко — макрос offsetof успешно выдает смещение элементов структур вида offsetof(POINT,str_my_point.x). Вот пример разжевывающей статьи и ее перевод на русский.
Если эта реализация макроса offsetof вас смущает — покажите как без нее обойтись в случаях со struct'ами и union'ами. Просто чтоб вычисляло смещение и компилировалось без ошибок — как?
Прежде всего вспомним, что сишные компиляторы используются в мире эмбеддеров, там struct и union — весьма частые гости. Конструкция offsetof в указанном виде нормально работает типами struct и union. Если коротко — макрос offsetof успешно выдает смещение элементов структур вида offsetof(POINT,str_my_point.x). Вот пример разжевывающей статьи и ее перевод на русский.
Если эта реализация макроса offsetof вас смущает — покажите как без нее обойтись в случаях со struct'ами и union'ами. Просто чтоб вычисляло смещение и компилировалось без ошибок — как?
Спасибо за ссылку на перевод, полезная информация.
После прочтения вспомнил что несколько лет назад набрел как-то на статью где описывалась очень странная по их мнению поведение gcc и способы обхода этой баги. То есть проблема там была не надуманная а реальная, взятая с какой-то либы. За точность не ручаюсь, но с учетом того что прочитал теперь понял для чего там использовался этот offset, точнее использовался для объяснения проблемы. Итак.
Была у них структура, для упрощения приведу ключевые элементы которые влияли на результат, да и не вспомню какие там точно поля другие были:
typedef struct A_
{
bool t;
long long s;
} A;
Тут имеем структура sizeof(A) == 12. В одной из функции в библиотеке структура инициализировалась последовательно присваивая ее поля. Потом эта другому экземпляру этой структуры присваивалась та которая была уже заполненная. После чего по какой-то логике, зачем и почему не помню и не хочу :), для сравнения структур, точнее идентичности полей, использовалась «memcmp()».
Внимательные сишные эмбедерские комрады сразу заподозрили неладное с этой структурой, и не зря. Для тех кто считает битики и байтики экономя на всем, видят тут что в структуре будет использовано выравнивание из-за «bool».
Написал примерчик, как раз было очень удобно использовать этот «offsetof()»:
GCC 32 — ideone.com/qc3G4B
A struct size: 12 — Paddind A struct: 3 — memcmp res: 1
B struct size: 12 — Paddind B struct: 0 — memcmp res: 0
MSVC 2013 x64:
A struct size: 16 — Paddind A struct: 7 — memcmp res: -1
B struct size: 16 — Paddind B struct: 4 — memcmp res: 0
Но у меня еще остался вопрос если выравнивание идет по 4 байт, в случае как с MSVC, memcmp все равно выдает 0, то есть едентичность, тут я пока не разобрался, специфика реализации? Хотя GCC в этом случае так же вернет 0.
Ну и разрабам PVS. Можете записать в свой «todo list». Баг очень специфический, но это баг из реального кода библиотеки. Навряд такое в ядре найдешь, но кто знает.
А ну и как разрабы те решили проблему? написали макрос по типу "#define our_super__struct_memcmp(a, b)", где сравнивали поля структуры по отдельности.
После прочтения вспомнил что несколько лет назад набрел как-то на статью где описывалась очень странная по их мнению поведение gcc и способы обхода этой баги. То есть проблема там была не надуманная а реальная, взятая с какой-то либы. За точность не ручаюсь, но с учетом того что прочитал теперь понял для чего там использовался этот offset, точнее использовался для объяснения проблемы. Итак.
Была у них структура, для упрощения приведу ключевые элементы которые влияли на результат, да и не вспомню какие там точно поля другие были:
typedef struct A_
{
bool t;
long long s;
} A;
Тут имеем структура sizeof(A) == 12. В одной из функции в библиотеке структура инициализировалась последовательно присваивая ее поля. Потом эта другому экземпляру этой структуры присваивалась та которая была уже заполненная. После чего по какой-то логике, зачем и почему не помню и не хочу :), для сравнения структур, точнее идентичности полей, использовалась «memcmp()».
Внимательные сишные эмбедерские комрады сразу заподозрили неладное с этой структурой, и не зря. Для тех кто считает битики и байтики экономя на всем, видят тут что в структуре будет использовано выравнивание из-за «bool».
Написал примерчик, как раз было очень удобно использовать этот «offsetof()»:
GCC 32 — ideone.com/qc3G4B
A struct size: 12 — Paddind A struct: 3 — memcmp res: 1
B struct size: 12 — Paddind B struct: 0 — memcmp res: 0
MSVC 2013 x64:
A struct size: 16 — Paddind A struct: 7 — memcmp res: -1
B struct size: 16 — Paddind B struct: 4 — memcmp res: 0
Но у меня еще остался вопрос если выравнивание идет по 4 байт, в случае как с MSVC, memcmp все равно выдает 0, то есть едентичность, тут я пока не разобрался, специфика реализации? Хотя GCC в этом случае так же вернет 0.
Ну и разрабам PVS. Можете записать в свой «todo list». Баг очень специфический, но это баг из реального кода библиотеки. Навряд такое в ядре найдешь, но кто знает.
А ну и как разрабы те решили проблему? написали макрос по типу "#define our_super__struct_memcmp(a, b)", где сравнивали поля структуры по отдельности.
А вот в случае когда структура копируется в другую посредством простого присваивания, а не инициализации полями, memcmp() возвращает ожидаемый 0.
ideone.com/64eCVm
GCC:
A struct size: 12 — Paddind A struct: 3 — memcmp res: 0
B struct size: 12 — Paddind B struct: 0 — memcmp res: 0
MSVC 2013 x64:
A struct size: 16 — Paddind A struct: 7 — memcmp res: 0
B struct size: 16 — Paddind B struct: 4 — memcmp res: 0
Вот так.
ideone.com/64eCVm
GCC:
A struct size: 12 — Paddind A struct: 3 — memcmp res: 0
B struct size: 12 — Paddind B struct: 0 — memcmp res: 0
MSVC 2013 x64:
A struct size: 16 — Paddind A struct: 7 — memcmp res: 0
B struct size: 16 — Paddind B struct: 4 — memcmp res: 0
Вот так.
Ну и про сам «offset». MSVC версия прям вообще со всеми предосторожностями если используется С++…
#ifdef __cplusplus
#ifdef _WIN64
#define offsetof(s,m) (size_t)( (ptrdiff_t)&reinterpret_cast((((s *)0)->m)) )
#else /* _WIN64 */
#define offsetof(s,m) (size_t)&reinterpret_cast((((s *)0)->m))
#endif /* _WIN64 */
#else /* __cplusplus */
#ifdef _WIN64
#define offsetof(s,m) (size_t)( (ptrdiff_t)&(((s *)0)->m) )
#else /* _WIN64 */
#define offsetof(s,m) (size_t)&(((s *)0)->m)
#endif /* _WIN64 */
#endif /* __cplusplus */
#ifdef __cplusplus
#ifdef _WIN64
#define offsetof(s,m) (size_t)( (ptrdiff_t)&reinterpret_cast((((s *)0)->m)) )
#else /* _WIN64 */
#define offsetof(s,m) (size_t)&reinterpret_cast((((s *)0)->m))
#endif /* _WIN64 */
#else /* __cplusplus */
#ifdef _WIN64
#define offsetof(s,m) (size_t)( (ptrdiff_t)&(((s *)0)->m) )
#else /* _WIN64 */
#define offsetof(s,m) (size_t)&(((s *)0)->m)
#endif /* _WIN64 */
#endif /* __cplusplus */
На всякий случай добавлю замечание.
Тут проблему можно еще решить обнулением участка памяти созданного объекта структуры, в простонародье:
memset(&r1, 0, sizeof(A));
memset(&r2, 0, sizeof(A));
ideone.com/OaipoS
Тут проблему можно еще решить обнулением участка памяти созданного объекта структуры, в простонародье:
memset(&r1, 0, sizeof(A));
memset(&r2, 0, sizeof(A));
ideone.com/OaipoS
Что за жесть? Почему бы просто не проинициализировать при создании:
structTypeName r1 = { 0 };
structTypeName r2 = { 0 };
structTypeName r1 = { 0 };
structTypeName r2 = { 0 };
memset() гарантировано зануляет память структуры включая неиспользуемое место из-за выравнивания(padding). А вот скобочки до принятия стандарта С11(6.7.9-10) НЕ гарантировало обнуление padding и тогда memcmp() могло работать не как ожидалось, а в случае если структуру отправлять по сети или запиcывать в файл — возможно будет включен мусор.
Вот поэтому во всех, почти во всех, сишных библиотеках, ядрах..., структуры заполняют нулями с помощью memset().
Вот поэтому во всех, почти во всех, сишных библиотеках, ядрах..., структуры заполняют нулями с помощью memset().
Продолжение: habrahabr.ru/company/pvs-studio/blog/250701/
Зарегистрируйтесь на Хабре, чтобы оставить комментарий
Размышления над разыменованием нулевого указателя