mirror of
https://github.com/Nekrolm/ubbook.git
synced 2026-06-09 13:14:18 +03:00
185 lines
9.3 KiB
Markdown
185 lines
9.3 KiB
Markdown
# Разыменование нулевых указателей.
|
||
|
||
Самая крутая ошибка с самыми жуткими последствиями. `null` вообще называют [ошибкой на миллиард долларов](https://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare/).
|
||
От них страдает куча кода, на самых разных языках программирования. Но если в условной `Java` при обращении по `null`-ссылке вы получите исключение с вполне предсказуемыми последствиями (ну, упало и упало), то в великом и ужасном C++, а также в C за вами придет неопределенное поведение. И оно будет действительно неопределенным!
|
||
|
||
Но для начала, конечно, надо отметить, что, после всех обсуждений туманных формулировок стандарта, в настоящее время есть некоторое [соглашение](http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_closed.html#315), что все-таки не сама по себе конструкция `*p`, где `p` — нулевой указатель, вызывает неопределенное поведение. А `lvalue-to-rvalue` преобразование. Ну или менее формально, кратко и не совсем правильно: пока нет чтения или записи значения по этому самому нулевому адресу — все нормально.
|
||
|
||
Так, сейчас совершенно законно вы можете вызвать статические методы класса через `nullptr`.
|
||
|
||
```C++
|
||
struct S {
|
||
static void foo() {};
|
||
};
|
||
|
||
S *p = nullptr;
|
||
p->foo();
|
||
```
|
||
|
||
А также можно писать вот такую ерунду
|
||
```C++
|
||
S* p = nullptr;
|
||
*p;
|
||
```
|
||
Причем эту ерунду можно писать только в C++. В C это безобразие все-таки запретили [(см. 6.5.3.2, сноска 104)](https://web.archive.org/web/20181230041359if_/http:/www.open-std.org/jtc1/sc22/wg14/www/abq/c17_updated_proposed_fdis.pdf). И в C применять оператор разыменования к невалидным и нулевым указателями нельзя нигде. А у C++ свой особый путь. И эти странные примеры [собираются](https://godbolt.org/z/zPx31e) в `constexpr` контексте (напоминаю, в нем запрещено UB и компилятор проверяет).
|
||
|
||
Также никто не запрещает разыменовывать `nullptr` в невычисляемом контексте (внутри `decltype`):
|
||
|
||
```C++
|
||
#define LVALUE(T) (*static_cast<T*>(nullptr))
|
||
|
||
struct S {
|
||
int foo() { return 1; };
|
||
};
|
||
|
||
using val_t = decltype(LVALUE(S).foo());
|
||
```
|
||
|
||
Но, несмотря на то что так делать _можно_, совершенно не значит, что так делать _нужно_.
|
||
Потому что последствия от разыменования `nullptr` там, где этого делать нельзя, могут быть печальными.
|
||
Лезвие тонкое, острое, можно легко оступиться и что-нибудь взорвать.
|
||
|
||
Если разыменовать `nullptr`, может быть исполнен код, который никак [не вызывался](https://godbolt.org/z/hPje47):
|
||
|
||
```C++
|
||
#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`). Это неопределенное поведение. Такого быть не может. Компилятор находит, что есть одно место, где этому указателю присваивается ненулевое значение. И раз нуля быть не может, то, значит, именно это значение он и использует. Как результат — исполняется код функции, которую мы не вызывали.
|
||
|
||
Или вот совершенно дурная программа.
|
||
```C++
|
||
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` после разыменования [может быть удалена](https://godbolt.org/z/c7YW9b).
|
||
|
||
Вы, конечно же, почти наверняка никогда не напишете такой странный код. Но что если разыменование указателя будет спрятано за вызовом функции?
|
||
|
||
```C++
|
||
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` и нулевой размер!
|
||
И точно также это может проявиться в удалении ваших проверок.
|
||
|
||
```C++
|
||
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");
|
||
}
|
||
}
|
||
```
|
||
|
||
На одних и тех же входных данных (вернее, их отсутствии), этот код завершается с [разными результатами](https://godbolt.org/z/zc4xGz)
|
||
в зависимости от компилятора и уровня оптимизаций.
|
||
|
||
Если вы еще недостаточно напуганы, то вот еще замечательная [история](https://devblogs.microsoft.com/oldnewthing/?p=97635) о том, как весело и задорно падала функция вида
|
||
|
||
```C++
|
||
void refresh(int* frameCount)
|
||
{
|
||
if (frameCount != nullptr) {
|
||
++(*frameCount); // прямо вот тут грохалась из-за разыменования nullptr
|
||
}
|
||
...
|
||
}
|
||
```
|
||
|
||
просто потому что где-то совершенно в не связанном с ней классе написали:
|
||
|
||
```C++
|
||
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;
|
||
};
|
||
```
|
||
|
||
И вызвали функцию так:
|
||
```C++
|
||
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
|