LINUX.ORG.RU
ФорумTalks

Оптимизатор НАКАЗАЛ разработчика Clang за UB в коде

 ,


2

6

Изложение https://qinsb.blogspot.ru/2018/03/ub-will-delete-your-null-checks.html

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

Вот код, который моделирует поведение кода из коммита (Код покоцан движком блога, вырезаны аргументы шаблонов. Я не буду пытаться их восстановить, т.к. аргумент-тип у llvm::SmallVector может быть понят из контекста, где надо, а аргумент-число — не особо важен):

struct Foo {
  llvm::SmallVector Vals;
};
struct Bar {
  Foo *getLastFoo() { return Foos.empty() ? nullptr : Foos.back(); }
  llvm::SmallVector Foos;
  void *Pad;
  void doUB();
};
void __attribute__((noinline)) Bar::doUB() {
  if (getLastFoo()->Vals.empty())
    puts("Vals empty");
  else
    puts("Vals non-empty");
}
int main() {
  Bar b;
  b.doUB();
}

В SmallVector::back стоит assert на не-пустоту вектора (!empty()). Т.к. код этих методов инлайнится (в getLastFoo) и компилятор видит, что проверка на пустоту в assert-е находится в false-ветке тернарного оператора, в условии которого та же самая проверка уже сделана, то в assert подставляется константа true и, в общем, можно считать, что assert выкинут.

Далее. Компилятор видит, что результат вызова getLastFoo в Bar::doUB() всегда разыменовывается, поэтому он не может быть нулевым указателем. Следовательно, условие тернарного оператора в getLastFoo не может быть истинным и при инлайнинге оператор безусловно заменяется на свою false-ветку (Foos.back() (в котором уже нет проверки на empty, т.к. assert был выкинут ранее)).

SmallVector устроен так, что в пустом векторе back() указывает внутрь самого SmallVector, и, короче говоря, код интерпретирует кусок памяти объекта Bar b как объект типа struct Foo и выполняет проверку Vals.empty() с непредсказуемым результатом.

Автор заканчивает тем, что радуется новому юз-кейсу для опции -fno-delete-null-checks, но по-прежнему не считает, что эту опцию стоит использовать для решения проблем ядра linux.

(видимо, имеется в виду типичный говнокод ядра

struct foo* p = bar->p;

if (!bar) {
...
}
который ядрописатели не считают багом, а винят в своих кривых руках компиляторы)

Перемещено tailgunner из development

★★★

Последнее исправление: utf8nowhere (всего исправлений: 10)

Ответ на: комментарий от legolegs

А разве 0 и NULL - это не разные по смыслу вещи?

По смыслу они как раз одинаковые - невалидный указатель, но исторически NULL мог быть определен в значение, отличное от литерала 0.

tailgunner ★★★★★
()
Ответ на: комментарий от dikiy

что такое UB?

Undefined behaviour.

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

monk ★★★★★
()
Ответ на: комментарий от buddhist

И код должен будет упасть еще на первой строчке, разве нет?

В ядре нет защиты памяти. Поэтому может просто сложить мусор в p и успешно отработать. По воле компилятора. UB не обязывает программу падать.

monk ★★★★★
()
Ответ на: комментарий от buddhist

код должен будет упасть еще на первой строчке, разве нет?

Нет. Он упадёт только в том случае если указатель попадёт на несуществующую страницу памяти. Иначе p будет присвоено какое-то непонятное значение из ячейки bar + offsetof(..., p)

sergej ★★★★★
()
Ответ на: комментарий от monk

В ядре нет защиты памяти.

Есть. Более того, в ядре специально отлавливается обращение по нулевому указателю.

tailgunner ★★★★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.