﻿# Разыменовывание нулевого указателя приводит к неопределённому поведению

Ненароком я породил большую дискуссию, касающуюся того, допустимо ли использовать в Си/Си\+\+ выражение &P\-\>m\_foo, если P является нулевым указателем\. Программисты разделились на два лагеря\. Одни уверенно доказывали, что так писать нельзя, другие столь же уверенно утверждали, что можно\. Приводились различные аргументы и ссылки\. И я понял, что нужно внести окончательную ясность в этот вопрос\. Для этого я обратился к экспертам Microsoft MVP и разработчикам Visual C\+\+, общающимся через закрытый список рассылки\. Они помогли подготовить эту статью, и я представляю её всем желающим\. Для нетерпеливых: этот код не корректен\.

![0306_Reflections_Null_Pointer_Dereferencing2_ru/image1.png](https://import.viva64.com/docx/blog/0306_Reflections_Null_Pointer_Dereferencing2_ru/image1.png)

## Напомню историю обсуждений

Все началось со [статьи](https://pvs-studio.ru/ru/blog/posts/cpp/0299/) о проверке ядра Linux с помощью анализатора PVS\-Studio\. Но сама проверка ядра тут ни причём\. Дело в том, что в статье я привёл следующий фрагмент из кода Linux:

```cpp
static int podhd_try_init(struct usb_interface *interface,
        struct usb_line6_podhd *podhd)
{
  int err;
  struct usb_line6 *line6 = &podhd->line6;

  if ((interface == NULL) || (podhd == NULL))
    return -ENODEV;
  ....
}
```

Я назвал этот код опасным, так как посчитал, что здесь имеет место [неопределённое поведение](https://pvs-studio.ru/ru/blog/terms/0066/)\. 

По этому поводу я получил много возражений от читателей и даже одно время был готов поддаться на их убедительные речи в письмах и комментариях\. Например, в качестве доказательства корректности кода приводили устройство макроса [offsetof](https://en.wikipedia.org/wiki/Offsetof), который часто реализован так:

```cpp
#define offsetof(st, m) ((size_t)(&((st *)0)->m))
```

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

Хотя я и доверчивый, но стараюсь проверять информацию\. Я начал разбираться с этой темой и в результате написал небольшую статью: "[Размышления над разыменованием нулевого указателя](https://pvs-studio.ru/ru/blog/posts/cpp/0301/)"\.

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

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

## О языке Си

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

Вот что говорится про оператор взятия адреса '&' в стандарте C99 \(Раздел 6\.5\.3\.2 "Операторы взятия адреса и разыменовывания"\):

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

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

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

Если "lvalue\-выражение не указывает на объект при своем вычислении, возникает неопределенное поведение" \(Стандарт C99, Раздел 6\.3\.2\.1 "Lvalue\-выражения, массивы и указатели функций"\):

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

Ещё раз кратко:

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

## О языке Си\+\+

В языке С\+\+ всё обстоит точно также\. Выражение '&podhd\-\>line6' является неопределенным поведением в языке C\+\+ в том случае, если 'podhd' \- нулевой указатель\.

С толку немного сбивает дискуссия на WG21 \([232\. Is indirection through a null pointer undefined behavior?](http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_active.html)\), на которую я ссылался в предыдущей статье\. Там настаивают, будто бы такое выражение не является неопределенным поведением\. Однако никто так и не нашел никаких правил в стандартах C\+\+, которые разрешали бы использовать "podhd\-\>line6", когда "podhd" \- нулевой указатель\.

Указатель "podhd" нарушает основное ограничение \(Раздел 5\.2\.5/4, второй пункт в списке\) о том, что он должен указывать на объект\. Ни один объект в C\+\+ не может иметь адреса nullptr\.

## Итого

```cpp
struct usb_line6 *line6 = &podhd->line6;
```

Этот код является некорректным в языке Си и Си\+\+, если указатель podhd равен 0\. Если указатель равен 0, то возникает неопределённое поведение\.

То, что программа может работать, является везением\. Неопределённое поведение может проявить себя, как угодно\. В том числе, программа может работать так, как хотел программист\. Это один из частных случаев, но не более того\.

Так писать нельзя\. Указатель должен быть проверен до разыменования\.

## Разное в дополнение

* При рассмотрении идиоматической реализации [offsetof\(\)](https://en.wikipedia.org/wiki/Offsetof) следует учитывать, что компилятору разрешено использовать непереносимые приемы для реализации этой функциональности\. Тот факт, что в реализации библиотеки в компиляторе используется константа нулевого указателя при реализации offsetof\(\), вовсе не означает, что в пользовательском коде можно без опаски применять '&podhd\-\>line6' в случае, когда'podhd' является нулевым указателем\.
* GCC может \(и делает это\) проводить оптимизацию, основываясь на предположении, что никакого неопределенного поведения возникнуть не может, и убрать в данном случае проверки указателей на ноль \- поэтому ядро компилируется с набором ключей, указывающих компилятору не делать этого\. Например, эксперты в качестве примера ссылаются на статью "[What Every C Programmer Should Know About Undefined Behavior \#2/3](http://blog.llvm.org/2011/05/what-every-c-programmer-should-know_14.html)"\.
* Возможно, вам также будет интересно узнать, что подобным образом нулевой указатель был задействован в эксплойте ядра с помощью TUN/TAP\-драйва\. Некоторые могут решить, будто эти два примера имеют мало общего, поскольку во втором случае есть существенное отличие: в баге TUN/TAP\-драйвера вместо простого взятия адреса поля структуры, к которому обращался нулевой указатель, это поле было явно взято в качестве значения для инициализации переменной\. Однако с точки зрения стандарта C взятие адреса поля с помощью нулевого указателя также является неопределенным поведением\.
* А есть ли какая\-та ситуация, когда при P \=\= nullptr мы напишем &P\-\>m\_foo и всё будет хорошо? Да, например это может быть аргументом оператора sizeof: sizeof\(&P\-\>m\_foo\)\.

## Благодарности

В подготовке статьи мне помогли эксперты, сомневаться в компетенции которых нет повода\. Я благодарен за помощь в написании статьи следующим людям:

* **Майкл Бёрр** — горячий поклонник языка C/C\+\+ и специалист по системному и встроенному ПО, в том числе службам Windows, работе с сетями и драйверам устройств\. Активно участвует в жизни сообщества [Stack Overflow](https://stackoverflow.com/users/12711/michael-burr), отвечая на вопросы программистов по C и C\+\+ \(а иногда и на некоторые простые вопросы по C\#\)\. Имеет 6 наград Microsoft MVP в номинации Visual C\+\+\.
* **Билли О'Нил** — разработчик ПО на C\+\+ \(преимущественно\) и активный участник сообщества [Stack Overflow](https://stackoverflow.com/users/82320/billy-oneal)\. Является инженером\-разработчиком ПО в подразделении по совершенствованию систем безопасности Microsoft \(Trustworthy Computing Team\)\. До этого работал в нескольких компаниях, занимающихся безопасностью ПО, в числе которых \- Malware Bytes и PreEmptive Solutions\.
* **Джованни Диканио** — программист, специализирующийся на разработке ОС Windows\. Автор статей для программистов по C\+\+, OpenGL и другим темам в ряде итальянских компьютерных журналов\. Также писал код для некоторых открытых проектов\. Джованни помогает коллегам, давая советы по решению программистских проблем, связанных с C и C\+\+, на форумах [Microsoft MSDN](https://social.msdn.microsoft.com/profile/giovanni%20dicanio/), а с некоторых пор \- и на Stack Overflow\. Имеет 8 наград Microsoft MVP в номинации Visual C\+\+\.
* **Габриэль** **Дус** **Рейс** — главный инженер\-разработчик ПО Microsoft\. Также является исследователем и долгосрочным участником C\+\+\-сообщества\. Одно из направлений его научных интересов и исследований \- средства разработки надежного ПО\. До того, как прийти в Microsoft, работал старшим преподавателем в Техасском Университете A&M \(Texas A&M University\)\. В 2012 году Доктор Дус Рейс был отмечен премией Национального Научного Фонда \(National Science Foundation CAREER Award\) за проведенное им исследование компиляторов надежного ПО в области вычислительной математики и за образовательную деятельность\. Является членом комитета по стандартизации языка C\+\+\.

## Дополнительные ссылки

1. Wikipedia\. [Неопределённое поведение](https://ru.wikipedia.org/wiki/%D0%9D%D0%B5%D0%BE%D0%BF%D1%80%D0%B5%D0%B4%D0%B5%D0%BB%D1%91%D0%BD%D0%BD%D0%BE%D0%B5_%D0%BF%D0%BE%D0%B2%D0%B5%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5)\.
1. A Guide to Undefined Behavior in C and C\+\+\. Part [1](https://blog.regehr.org/archives/213), [2](https://blog.regehr.org/archives/226), [3](https://blog.regehr.org/archives/232)\.
1. Wikipedia\. [offsetof](https://en.wikipedia.org/wiki/Offsetof)\.
1. LLVM Blog\. [What Every C Programmer Should Know About Undefined Behavior \#2/3](http://blog.llvm.org/2011/05/what-every-c-programmer-should-know_14.html)\.
1. Дискуссия на сайте Stack Overflow\. [Is dereferencing a pointer that's equal to nullptr undefined behavior by the standard?](https://stackoverflow.com/questions/28573215/is-dereferencing-a-pointer-thats-equal-to-nullptr-undefined-behavior-by-the-sta)