Подтвердить что ты не робот

Функция, не вызываемая в коде, вызывается во время выполнения

Как можно вызвать следующую программу never_called, если она никогда не будет вызывается в коде?

#include <cstdio>

static void never_called()
{
  std::puts("formatting hard disk drive!");
}

static void (*foo)() = nullptr;

void set_foo()
{
  foo = never_called;
}

int main()
{
  foo();
}

Это отличается от компилятора компилятором. Компиляция с помощью Clang with оптимизация включена, функция never_called выполняется во время выполнения.

$ clang++ -std=c++17 -O3 a.cpp && ./a.out
formatting hard disk drive!

Компиляция с GCC, однако, этот код просто сбой:

$ g++ -std=c++17 -O3 a.cpp && ./a.out
Segmentation fault (core dumped)

Версия компиляторов:

$ clang --version
clang version 5.0.0 (tags/RELEASE_500/final)
Target: x86_64-unknown-linux-gnu
Thread model: posix
InstalledDir: /usr/bin
$ gcc --version
gcc (GCC) 7.2.1 20171128
Copyright (C) 2017 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
4b9b3361

Ответ 1

Программа содержит поведение undefined, как разыменование нулевого указателя (т.е. вызов foo() в главном, не присваивая ему действительный адрес заранее) является UB, поэтому стандарты не налагаются.

Выполнение never_called во время выполнения - идеальная действительная ситуация, когда Повреждение undefined было удалено, оно так же корректно, как и просто сбой (например, при компиляции с GCC). Хорошо, но почему Кланг это делает? если ты скомпилируйте его с отключением оптимизации, программа больше не будет выводить "форматирование жесткого диска" и просто сбой:

$ clang++ -std=c++17 -O0 a.cpp && ./a.out
Segmentation fault (core dumped)

Сгенерированный код для этой версии выглядит следующим образом:

main:                                   # @main
        push    rbp
        mov     rbp, rsp
        call    qword ptr [foo]
        xor     eax, eax
        pop     rbp
        ret

Он пытается сделать вызов функции, для которой foo указывает, и как foo инициализируется с помощью nullptr (или если у него нет инициализации, это все равно будет иметь место), его значение равно нулю. Здесь undefined поведение было поражено, поэтому все может произойти вообще и программа оказывается бесполезным. Как правило, обращение к такому недействительному адресу приводит к ошибкам ошибок сегментации, поэтому сообщение мы получаем, когда выполнение программы.

Теперь рассмотрим одну и ту же программу, но скомпилируем ее с оптимизациями:

$ clang++ -std=c++17 -O3 a.cpp && ./a.out
formatting hard disk drive!

Сгенерированный код для этой версии выглядит следующим образом:

set_foo():                            # @set_foo()
        ret
main:                                   # @main
        push    rax
        mov     edi, .L.str
        call    puts
        xor     eax, eax
        pop     rcx
        ret
.L.str:
        .asciz  "formatting hard disk drive!"

Интересно, что каким-то образом оптимизация изменила программу так, что main вызывает std::puts напрямую. Но почему Кланг это сделал? И почему set_foo скомпилирован в одну инструкцию ret?

Вернемся к стандарту (N4660, в частности) на мгновение. Какие это говорит о undefined поведении?

3.27 undefined поведение [defns.undefined]

для которого этот документ не предъявляет требований

[Примечание: поведение Undefined может, когда этот документ опускает любое явное определение поведения или , когда программа использует ошибочную конструкцию или ошибочные данные. Допустимые диапазоны поведения undefinedот полностью игнорируя ситуацию с непредсказуемыми результатами, чтобы во время перевода или выполнение программы документированным образом характерной для окружающей среды (с выпуском или без диагностическое сообщение), до прекращения перевода или исполнения (с выдача диагностического сообщения). Многие ошибочные программные конструкции не порождают поведение undefined; они должны быть диагностированы. Оценка постоянного выражения никогда не демонстрирует поведения явно указанный как undefined ([expr.const]). - конечная нота]

Акцент на мой.

Программа, которая демонстрирует поведение undefined, становится бесполезной, поскольку все он сделал до сих пор и не будет иметь никакого смысла, если он содержит ошибочные данные или конструкции. Помня об этом, помните, что компиляторы могут полностью игнорировать случай, когда поведение undefined , и это фактически используется как обнаруженные факты при оптимизации программа. Например, конструкция типа x + 1 > x (где x - целое число со знаком) будет скомпилирована для true, даже если значение x неизвестно во время компиляции. Обоснование заключается в том, что компилятор хочет оптимизировать действительные случаи, и единственный способ для того, чтобы эта конструкция была действительной, - это если она не вызывает арифметику переполнение (т.е. если x != std::numeric_limits<decltype(x)>::max()). Эта это новый научный факт в оптимизаторе. Исходя из этого, конструкция всегда подтверждается.

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

Это полезно, поскольку позволяет тонны оптимизаций для удара в. Так далеко, так хорошо, но что произойдет, если x имеет максимальное значение во время выполнения? Ну, это поведение undefined, поэтому это бессмыслица, чтобы попытаться рассуждать о это, как может случиться, и стандарт не предъявляет никаких требований.

Теперь у нас достаточно информации, чтобы лучше изучить ваши неисправности программа. Мы уже знаем, что доступ к нулевому указателю равен undefined поведение, и то, что вызывает забавное поведение во время выполнения. Поэтому попробуйте понять, почему оптимизирован Clang (или технически LLVM) программа так, как она была.

static void (*foo)() = nullptr;

static void never_called()
{
  std::puts("formatting hard disk drive!");
}

void set_foo()
{
  foo = never_called;
}

int main()
{
  foo();
}

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

void set_foo();
int x = (set_foo(), 42);

Если вы пишете этот фрагмент до main, программа no более длинный демонстрирует поведение undefined, а сообщение "форматирование сложно дисковод! ", при этом оптимизация будет включена или выключена.

Итак, каков единственный способ использования этой программы? Там это set_foo функция, которая присваивает адрес never_called foo, поэтому мы можем найти что-то здесь. Обратите внимание, что foo обозначается как static, что означает имеет внутреннюю связь и не может быть доступен извне этого перевода Блок. Напротив, функция set_foo имеет внешнюю связь и может доступ извне. Если другая единица перевода содержит фрагмент как и выше, тогда эта программа станет действительной.

Прохладный, но никто не вызывает set_foo извне. Хотя это является тот факт, что оптимизатор видит, что единственный способ для этой программы - допустимо, если set_foo вызывается до main, в противном случае это только поведение undefined. Это новый научный факт, и он предполагает set_foo на самом деле называется. Основываясь на этих новых знаниях, другие оптимизации, которые удар может использовать его.

Например, когда constant складывание он видит, что конструкция foo() действительна только в том случае, если foo может быть правильно инициализирована. Единственный путь для этого - если set_foo вызывается вне этой единицы перевода, поэтому foo = never_called.

"Исправление мертвого кода" и могут обнаружить что если foo == never_called, то код внутри set_foo не нужен, поэтому он преобразуется в одну инструкцию ret.

Оптимизация встроенного расширения видит, что foo == never_called, поэтому вызов foo можно заменить с его телом. В итоге мы получим что-то вроде этого:

set_foo():
        ret
main:
        mov     edi, .L.str
        call    puts
        xor     eax, eax
        ret
.L.str:
        .asciz  "formatting hard disk drive!"

Это несколько эквивалентно выпуску Clang с оптимизациями. Конечно, то, что действительно сделал Кланг, может (и может) быть другим, но оптимизация, тем не менее, может прийти к такому же выводу.

Изучая вывод GCC с оптимизацией, кажется, что он не беспокоился об исследовании:

.LC0:
        .string "formatting hard disk drive!"
never_called():
        mov     edi, OFFSET FLAT:.LC0
        jmp     puts
set_foo():
        mov     QWORD PTR foo[rip], OFFSET FLAT:never_called()
        ret
main:
        sub     rsp, 8
        call    [QWORD PTR foo[rip]]
        xor     eax, eax
        add     rsp, 8
        ret

Выполнение этой программы приводит к сбою (сбою сегментации), но если вы вызываете set_foo в другой блок перевода до того, как основной будет выполнен, то эта программа больше не будет демонстрировать поведение undefined.

Все это может смутно измениться, поскольку все больше и больше оптимизируются, поэтому не полагайтесь на предположение, что ваш компилятор позаботится о коде, содержащем поведение undefined, он может просто повредить вас (и отформатировать ваши жесткий диск для реального!)


Я рекомендую вам читать Что каждый программист C должен знать о undefined Поведение и Руководство по undefined Поведение на C и С++, обе статьи очень информативны и могут помочь вам понять состояние дел.

Ответ 2

Если реализация не указывает на попытку вызвать указатель нулевой функции, она может вести себя как вызов произвольного кода. Такой произвольный код вполне может вести себя как вызов функции "foo()". Хотя в Приложении L стандарта C будут предложены реализации для различения "критического UB" и "некритического UB", а некоторые реализации С++ могут применять аналогичное различие, вызов недействительного указателя функции будет критическим UB в любом случае.

Обратите внимание, что ситуация в этом вопросе сильно отличается от, например,

unsigned short q;
unsigned hey(void)
{
  if (q < 50000)
    do_something();
  return q*q;
}

В последней ситуации компилятор, который не претендует быть "анализируемым", может распознать, что код будет вызываться, если q превышает 46 340, когда выполнение достигает инструкции return и, следовательно, может также вызвать do_something() безусловно. Хотя Приложение L плохо написано, казалось бы, намерение было бы запретить такие "оптимизации". В случае вызова недопустимого указателя функции, однако, даже простой код на большинстве платформ может иметь произвольное поведение.