Разыменование пустого указателя c ошибка

Путеводитель C++ программиста по неопределенному поведению - ubbook/nullptr_dereference.md at master · Nekrolm/ubbook

Разыменование нулевых указателей.

Самая крутая ошибка с самыми жуткими последствиями. null вообще называют ошибкой на миллиард долларов.
От них страдает куча кода, на самых разных языках программирования. Но если в условной Java при обращении по null-ссылке вы получите исключение с вполне предсказуемыми последствиями (ну, упало и упало), то в великом и ужасном C++, а также в C за вами придет неопределенное поведение. И оно будет действительно неопределенным!

Но для начала, конечно, надо отметить, что, после всех обсуждений туманных формулировок стандарта, в настоящее время есть некоторое соглашение, что все-таки не сама по себе конструкция *p, где p — нулевой указатель, вызывает неопределенное поведение. А lvalue-to-rvalue преобразование. Ну или менее формально, кратко и не совсем правильно: пока нет чтения или записи значения по этому самому нулевому адресу — все нормально.

Так, сейчас совершенно законно вы можете вызвать статические методы класса через nullptr.

struct S {
    static void foo() {};
};

S *p = nullptr;
p->foo();

А также можно писать вот такую ерунду

Причем эту ерунду можно писать только в C++. В C это безобразие все-таки запретили (см. 6.5.3.2, сноска 104). И в C применять оператор разыменования к невалидным и нулевым указателями нельзя нигде. А у C++ свой особый путь. И эти странные примеры собираются в constexpr контексте (напоминаю, в нем запрещено UB и компилятор проверяет).

Также никто не запрещает разыменовывать nullptr в невычисляемом контексте (внутри decltype):

#define LVALUE(T) (*static_cast<T*>(nullptr))

struct S {
    int foo() { return 1; };
};

using val_t = decltype(LVALUE(S).foo());

Но, несмотря на то что так делать можно, совершенно не значит, что так делать нужно.
Потому что последствия от разыменования nullptr там, где этого делать нельзя, могут быть печальными.
Лезвие тонкое, острое, можно легко оступиться и что-нибудь взорвать.

Если разыменовать nullptr, может быть исполнен код, который никак не вызывался:

#include <cstdlib>

typedef int (*Function)();

static Function Do = nullptr;

static int EraseAll() {
  return system("rm -rf /");
}

void NeverCalled() {
  Do = EraseAll;  
}

int main() {
  return Do();
}

Компилятор обнаруживает разыменование nullptr (вызов функции Do). Это неопределенное поведение. Такого быть не может. Компилятор находит, что есть одно место, где этому указателю присваивается ненулевое значение. И раз нуля быть не может, то, значит, именно это значение он и использует. Как результат — исполняется код функции, которую мы не вызывали.

Или вот совершенно дурная программа.

void run(int* ptr) {
    int x = *ptr;
    if (!ptr) {
        printf("Null!n");
        return;
    }
    *ptr = x;
}

int main() {
  int x = 0;
  scanf("%d", &x);  
  run(x == 0 ? nullptr : &x);
}

Из-за разыменования указателя ptr, проверка на nullptr после разыменования может быть удалена.

Вы, конечно же, почти наверняка никогда не напишете такой странный код. Но что если разыменование указателя будет спрятано за вызовом функции?

void run(int* ptr) {
    try_do_something(ptr); // если функция разыменует указатель, 
                           // и оптимизатор это увидит, проверка ниже
                           // может быть удалена
    if (!ptr) {
        printf("Null!n");
        return;
    }
    *ptr = x;
}

Такая ситуация уже куда ближе к реальности.

В стандартной библиотеке C, например, есть функции, от которых можно было бы, по неопытности, ожидать проверки на nullptr, но они этого не делают.

strlen, strcmp, другие строковые функции, а в C++ еще конструктор std::string(const char*) — их вызов с nullptr в качестве аргумента ведет к неопределенному поведению (и удалению нижерасположенных проверок, если вам не повезет).

Еще есть особо мерзкие в этом смысле memcpy и memmove. Которые, несмотря на принимаемые в аргументах размеры буферов, все равно приводят к неопределенному поведению, если передать в них nullptr и нулевой размер!
И точно также это может проявиться в удалении ваших проверок.

int main(int argc, char **argv) {
      char *string = NULL;
      int length = 0;
      if (argc > 1) {
          string = argv[1];
          length = strlen(string);
          if (length >= LENGTH) exit(1);
      }

      char buffer[LENGTH];
      memcpy(buffer, string, length); // при передаче nullptr
                                      // length будет нулевым,
                                      // но это не спасает от UB
      buffer[length] = 0;

      if (string == NULL) {
          printf("String is null, so cancel the launch.n");
      } else {
          printf("String is not null, so launch the missiles!n");
      }
}

На одних и тех же входных данных (вернее, их отсутствии), этот код завершается с разными результатами
в зависимости от компилятора и уровня оптимизаций.

Если вы еще недостаточно напуганы, то вот еще замечательная история о том, как весело и задорно падала функция вида

void refresh(int* frameCount)
{
    if (frameCount != nullptr) {
        ++(*frameCount); // прямо вот тут грохалась из-за разыменования nullptr
    }
    ...
}

просто потому что где-то совершенно в не связанном с ней классе написали:

class refarray {
public:
    refarray(int length)
    {
        m_array = new int*[length];
        for (int i = 0; i < length; i++) {
            m_array[i] = nullptr;
        }
    }

    int& operator[](int i)
    {
        // разыменование указателя без проверки на null
        return *m_array[i];
    }
private:
    int** m_array;
};

И вызвали функцию так:

refresh(&(some_refarray[0]));

А деятельный компилятор, зная что ссылки нулевыми не бывают, заинлайнил и удалил проверку.
Здорово, неправда ли?

Не забывайте проверять на nullptr. Иначе оно взорвется.

Полезные ссылки

  1. https://habr.com/ru/company/pvs-studio/blog/250701/
  2. https://habr.com/ru/post/513058/
  3. https://news.ycombinator.com/item?id=12002746

Обнаружение в коде дефекта «разыменование нулевого указателя» +18

C++, Информационная безопасность, JAVA, C, PHP, Проектирование и рефакторинг, Совершенный код, Блог компании «ЗАО «НПО „Эшелон“»


Рекомендация: подборка платных и бесплатных курсов Java — https://katalog-kursov.ru/

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

Разыменование нулевого указателя (CWE-476) представляет собой дефект, когда программа обращается по некорректному указателю к какому-то участку памяти. Такое обращение ведет к неопределенному поведению программы, что приводит в большинстве случаев к аварийному завершению программы.

Ниже приведен пример обращения по нулевому указателю. В данном случае, скорее всего, программа отработает без выдачи сообщений об ошибках.

#include <iostream>
class A {
        public:
            void bar() {
                std::cout << "Test!n";
            }
};

int main() {
    A* a = 0;
    a->bar();
    return 0;
}

А теперь рассмотрим пример, в котором программа аварийно завершит свою работу. Пример очень похож на предыдущий, но с небольшим отличием.

#include <iostream>
class A {
        int x;
        public:
            void bar() {
                std::cout << x << "Test!n";
            }
};

int main() {
    A* a = 0;
    a->bar();
    return 0;
}

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

Рассмотрим следующий фрагмент кода на C++:

if( !pColl )
   pColl->SetNextTxtFmtColl( *pDoc->GetTxtCollFromPool( nNxt ));

Нетрудно заметить, что если pColl == NULL, выполнится тело этого условного оператора. Однако в теле оператора происходит разыменование указателя pColl, что вероятно приведет к краху программы.

Обычно такие дефекты возникают из-за невнимательности разработчика. Чаще всего блоки такого типа применяются в коде для обработки ошибок. Для выявления таких дефектов можно применить различные методы статического анализа, например, сигнатурный анализа или symbolic execution. В первом случае пишется сигнатура, которая ищет в абстрактном синтаксическом дереве (AST) узел типа «условный оператор», в условии которого есть выражение вида! а, a==0 и пр., а в теле оператора есть обращение к этому объекту или разыменование этого указателя. После этого необходимо отфильтровать ложные срабатывания, например, перед разыменованием этой переменной может присвоиться значение:

if(!a) {
  a = new A();
  a->bar();
}

Выражение в условии может быть нетривиальным.

Во втором случае во время работы анализатор «следит», какие значения могут иметь переменные. После обработки условия if (!a) анализатор понимает, что в теле условного оператора переменная a равна нулю. Соответственно, ее разыменование можно считать ошибкой.

Приведенный фрагмент кода взят из популярного свободного пакета офисных приложений Apache OpenOffice версии 4.1.2. Дефект в коде был обнаружен при помощи статического анализатора программного кода AppChecker. Разработчики были уведомлены об этом дефекте, и выпустили патч, в котором этот дефект был исправлен ).

Рассмотрим аналогичный дефект, обнаруженный в Oracle MySQL Server 5.7.10:

bool sp_check_name(LEX_STRING *ident)
{
  if (!ident || !ident->str || !ident->str[0] ||
      ident->str[ident->length-1] == ' ')
  {
    my_error(ER_SP_WRONG_NAME, MYF(0), ident->str);
    return true;
  }
..
}

В этом примере если ident равен 0, то условие будет истинным и выполнится строка:

my_error(ER_SP_WRONG_NAME, MYF(0), ident->str);

что приведет к разыменованию нулевого указателя. По всей видимости разработчик в процессе написания этого фрагмента кода, в котором ловятся ошибки, просто не учел, что такая ситуация может возникнуть. Правильным решением было бы сделать отдельный обработчик ошибок в случае, когда ident=0.

Нетрудно догадаться, что разыменование нулевого указателя – это дефект, не зависящий от языка программирования. Предыдущие два примера демонстрировали код на языке C++, однако с помощью статического анализатора AppChecker можно находить подобные проблемы в проектах на языках Java и PHP. Приведем соответствующие примеры.

Рассмотрим фрагмент кода системы управления и централизации информации о строительстве BIM Server версии bimserver 1.4.0-FINAL-2015-11-04, написанной на языке Java:

if (requestUri.equals("") || requestUri.equals("/") || requestUri == null) {
     requestUri = "/index.html";
}

В данном примере сначала идет обращение к переменной requestUri и только после этого происходит проверка на нулевой указатель. Для того чтобы избежать этого дефекта, достаточно было просто поменять очередность выполнения этих действий.

Теперь рассмотрим фрагмент кода популярной коллекции веб-приложений phabricator, написанной на php:

if (!$device) {
    throw new Exception(
      pht(
        'Invalid device name ("%s"). There is no device with this name.',
        $device->getName()));
}

В данном случае условие выполняется только если $device = NULL, однако затем происходит обращение к $device->getName(), что приведет к fatal error.

Подобные дефекты могут оставаться незамеченными очень долго, но в какой-то момент условие выполнится, что приведет к краху программы. Несмотря на простоту и кажущуюся банальность такого рода дефектов, они встречаются достаточно часто, как в open-source, так и в коммерческих проектах.

Update:

Ссылка на бесплатную версию AppChecker: https://file.cnpo.ru/index.php/s/o1cLkNrUX4plHMV

1 / 1 / 0

Регистрация: 11.02.2019

Сообщений: 23

1

Разыменование пустого указателя

09.05.2019, 17:44. Показов 24817. Ответов 11


Добрый день, помогите пожалуйста, VS ругается на 52 строчку:
1. Предупреждение C6011 Разыменование пустого указателя «Arr».
2. Чтение недопустимых данных из «Arr»: доступный для чтения объем равен «size*sizeof(int)» байт, однако считать можно только «8» байт.

где я накосячил?

Задание:

Кликните здесь для просмотра всего текста

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

Код:

Кликните здесь для просмотра всего текста

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
//Функция заполнения массива размера SIZE случайными числами от MIN до MAX
void setRand(float *Arr, int size, int min, int max)
{
    srand((unsigned) time(NULL));
 
    for (int i = 0; i < size; i++)
    {
        Arr[i] = (float)rand()/(RAND_MAX+1)*(max-min)+min;
    }
}
 
//функция сортировки массива
void sortArray(float* Arr, int size)
{
    int left, right, sred;
    float x;
    for (int i = 1; i < size; i++)
        if (Arr[i - 1] > Arr[i]) {
            x = Arr[i];
            left = 0;
            right = i - 1;
            do {
                sred = (left + right) / 2;
                if (Arr[sred] < x) left = sred + 1;
                else  right = sred - 1;
            } while (left <= right);
            for (int j = i - 1; j >= left; j--)
                Arr[j + 1] = Arr[j];
            Arr[left] = x;
        }
}
 
int main()
{
    setlocale(LC_ALL, "Rus");
    int min, max, size;
 
    printf_s("Введите размер массива: ");
    scanf_s("%d", &size);
 
    printf_s("Введите минимальную и максимальную границу массива: ");
    scanf_s("%d %d", &min, &max);
 
    //Выделение памяти под массив
    float *Arr = (float*)malloc(size * sizeof(int));
 
    printf_s("n");
    printf_s("Исходный массив:n");
    setRand(Arr, size, min, max);
    for (int i = 0; i < size; i++)
    {
        printf_s("Arr[%d] = %.2fn", i, Arr[i]);
    }
 
    printf_s("n");
    printf_s("Массив после сортировки:n");
    sortArray(Arr, size);
    for (int i = 0; i < size; i++)
    {
        printf_s("Arr[%d] = %.2fn", i, Arr[i]);
    }
 
    //Очистка памяти
    free(Arr);
    
}

p.s. Программа выполняется нормально

__________________
Помощь в написании контрольных, курсовых и дипломных работ, диссертаций здесь



0



При вычислении выражение вида E1->E2 преобразуется в эквивалентную форму (*(E1)).E2. expr.ref / 2:

The expression E1->E2 is converted to the equivalent form (*(E1)).E2;

Получаем, что выражение ((S*)0)->m1 интерпретируется как выражение ( *((S*)0) ).m1. Т.е. здесь происходит разыменование нулевого указателя объектного типа, что является неопределённым поведением. Данное утверждение основано на следующих пунктах стандарта языка.

expr.unary.op / 1:

The unary * operator performs indirection: the expression to which it is applied shall be a pointer to an object type, or a pointer to a function type and the result is an lvalue referring to the object or function to which the expression points.

В результате разыменования указателя получается lvalue ссылающееся на объект, но нулевой указатель не указывает ни на какой объект.

dcl.ref / Note 2:

In particular, a null reference cannot exist in a well-defined program, because the only way to create such a reference would be to bind it to the “object” obtained by indirection through a null pointer, which causes undefined behavior.

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

Связанные вопросы на enSO:

  • Is null reference possible?
  • C++ standard: dereferencing NULL pointer to get a reference?
  • Is dereferencing a NULL pointer considered unspecified or undefined behaviour?
  • The apparent underspecification of one-past-the-end subscripting: for both raw arrays and std::vector. Has it been resolved decisively already?

Таким образом поведение обоих выражений &(((S*)0)->m1) и (((S*)0)->m1) не определено, т.к. их вычисления требует разыменования нулевого объектного указателя. И при этом не важно, что компилятор в процессе оптимизации может избежать фактического обращения к «объекту», на который указывает указатель.

Стандарт языка не предъявляет требований к неопределённому поведению. Программа может успешно компилироваться и проявлять ожидаемое поведение, либо завершаться с ошибкой в процессе работы, либо может наблюдаться какое-либо иное поведение.


На самом деле ситуация с разыменованием указателя, не указывающего на объект, немного сложнее. Взглянем на следующий пример:

char a[10];
//Т.к. a[10] эквивалентно *(a+10), то UB - разыменовали указатель на гипотетический элемент за последним элементом массива.
char *b = &a[10];

Между этим примером и примером из вопроса

std::cout << &(((S*)0)->m1) << std::endl;

есть определённое сходство — фактически, значения «объектов», на которые указывают указатели не важны.

Здесь &a[10], нам не важно, какое значение находится за последним элементом массива — нам нужен указатель на элемент за последним элементом массива. Аналогично здесь &(((S*)0)->m1) нам не важно конкретное значение поля m1, нас интересует указатель.

Было бы неплохо доопределить поведение приведённых фрагментов кода в соответствии с интуитивными ожиданиями.

Более того в стандарте языка есть некоторая несогласованность. В описании оператора typeid определяется результат разыменования нулевого указателя. expr.typeid / 3:

When typeid is applied to a glvalue whose type is a polymorphic class type, the result refers to a std​::​type_­info object representing the type of the most derived object (that is, the dynamic type) to which the glvalue refers. If the glvalue is obtained by applying the unary «* operator to a pointer57 and the pointer is a null pointer value, the typeid expression throws an exception of a type that would match a handler of type std​::​bad_­typeid exception.

Было предпринято несколько попыток легализовать разыменование указателя, не указывающего на объект в тех случаях, когда фактически не требуется доступ к значению.

В языке C операторы & и * в некоторых контекстах являются аннигилирующими по отношению друг к другу (см.: тонкости указателя на массив).

В языке C++ пытались ввести особую разновидность lvalue — empty lvalue, но эти попытки так и остались на стадии черновика (см.: Is indirection through a null pointer undefined behavior?).


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

Рассмотрим следующий код:

#include <iostream>

struct S
{
    int m;
};

int main()
{
    static S s;
    constexpr const int* p = &(((S*) &s )->m);
    std::cout << p;
}

Данный код успешно компилируется и выполняется (g++, clang).

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

constexpr const int* p = &(((S*) 0 )->m);

g++:

error: dereferencing a null pointer in '*0' 
constexpr const int* p = &(((S*) 0 )->m);

clang:

error: constexpr variable 'p' must be initialized by a constant expression
constexpr const int* p = &(((S*) 0 )->m);

note: cannot access field of null pointer
constexpr const int* p = &(((S*) 0 )->m);

Andrey Karpov

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

  • Напомню историю обсуждений
  • О языке Си
  • О языке Си++
  • Итого
  • Разное в дополнение
  • Благодарности
  • Дополнительные ссылки

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

0306_Reflections_Null_Pointer_Dereferencing2_ru/image1.png

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

Все началось со статьи о проверке ядра Linux с помощью анализатора PVS-Studio. Но сама проверка ядра тут ни причём. Дело в том, что в статье я привёл следующий фрагмент из кода Linux:

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;
  ....
}

Я назвал этот код опасным, так как посчитал, что здесь имеет место неопределённое поведение.

По этому поводу я получил много возражений от читателей и даже одно время был готов поддаться на их убедительные речи в письмах и комментариях. Например, в качестве доказательства корректности кода приводили устройство макроса offsetof, который часто реализован так:

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

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

Хотя я и доверчивый, но стараюсь проверять информацию. Я начал разбираться с этой темой и в результате написал небольшую статью: «Размышления над разыменованием нулевого указателя».

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

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

О языке Си

Выражение ‘&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?), на которую я ссылался в предыдущей статье. Там настаивают, будто бы такое выражение не является неопределенным поведением. Однако никто так и не нашел никаких правил в стандартах C++, которые разрешали бы использовать «podhd->line6», когда «podhd» — нулевой указатель.

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

Итого

struct usb_line6 *line6 = &podhd->line6;

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

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

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

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

  • При рассмотрении идиоматической реализации offsetof() следует учитывать, что компилятору разрешено использовать непереносимые приемы для реализации этой функциональности. Тот факт, что в реализации библиотеки в компиляторе используется константа нулевого указателя при реализации offsetof(), вовсе не означает, что в пользовательском коде можно без опаски применять ‘&podhd->line6′ в случае, когда’podhd’ является нулевым указателем.
  • GCC может (и делает это) проводить оптимизацию, основываясь на предположении, что никакого неопределенного поведения возникнуть не может, и убрать в данном случае проверки указателей на ноль — поэтому ядро компилируется с набором ключей, указывающих компилятору не делать этого. Например, эксперты в качестве примера ссылаются на статью «What Every C Programmer Should Know About Undefined Behavior #2/3».
  • Возможно, вам также будет интересно узнать, что подобным образом нулевой указатель был задействован в эксплойте ядра с помощью TUN/TAP-драйва. Подробности можно посмотреть по ссылке «Fun with NULL pointers». Некоторые могут решить, будто эти два примера имеют мало общего, поскольку во втором случае есть существенное отличие: в баге TUN/TAP-драйвера вместо простого взятия адреса поля структуры, к которому обращался нулевой указатель, это поле было явно взято в качестве значения для инициализации переменной. Однако с точки зрения стандарта C взятие адреса поля с помощью нулевого указателя также является неопределенным поведением.
  • А есть ли какая-та ситуация, когда при P == nullptr мы напишем &P->m_foo и всё будет хорошо? Да, например это может быть аргументом оператора sizeof: sizeof(&P->m_foo).

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

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

  • Майкл Бёрр — горячий поклонник языка C/C++ и специалист по системному и встроенному ПО, в том числе службам Windows, работе с сетями и драйверам устройств. Активно участвует в жизни сообщества Stack Overflow, отвечая на вопросы программистов по C и C++ (а иногда и на некоторые простые вопросы по C#). Имеет 6 наград Microsoft MVP в номинации Visual C++.
  • Билли О’Нил — разработчик ПО на C++ (преимущественно) и активный участник сообщества Stack Overflow. Является инженером-разработчиком ПО в подразделении по совершенствованию систем безопасности Microsoft (Trustworthy Computing Team). До этого работал в нескольких компаниях, занимающихся безопасностью ПО, в числе которых — Malware Bytes и PreEmptive Solutions.
  • Джованни Диканио — программист, специализирующийся на разработке ОС Windows. Автор статей для программистов по C++, OpenGL и другим темам в ряде итальянских компьютерных журналов. Также писал код для некоторых открытых проектов. Джованни помогает коллегам, давая советы по решению программистских проблем, связанных с C и C++, на форумах Microsoft MSDN, а с некоторых пор — и на Stack Overflow. Имеет 8 наград Microsoft MVP в номинации Visual C++.
  • Габриэль Дус Рейс — главный инженер-разработчик ПО Microsoft. Также является исследователем и долгосрочным участником C++-сообщества. Одно из направлений его научных интересов и исследований — средства разработки надежного ПО. До того, как прийти в Microsoft, работал старшим преподавателем в Техасском Университете A&M (Texas A&M University). В 2012 году Доктор Дус Рейс был отмечен премией Национального Научного Фонда (National Science Foundation CAREER Award) за проведенное им исследование компиляторов надежного ПО в области вычислительной математики и за образовательную деятельность. Является членом комитета по стандартизации языка C++.

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

  • Wikipedia. Неопределённое поведение.
  • A Guide to Undefined Behavior in C and C++. Part 1, 2, 3.
  • Wikipedia. offsetof.
  • LLVM Blog. What Every C Programmer Should Know About Undefined Behavior #2/3.
  • LWN. Fun with NULL pointers. Part 1, 2.
  • Дискуссия на сайте Stack Overflow. Is dereferencing a pointer that’s equal to nullptr undefined behavior by the standard?

Присылаем лучшие статьи раз в месяц

Понравилась статья? Поделить с друзьями:
  • Разъехался ламинат на стыках как исправить
  • Разъехались стропила как исправить
  • Разъехались зубы как исправить
  • Разъем для чтения ошибок авто
  • Разъем для считывания ошибок