Почему функция "noreturn" возвращается?

Я читаю этот вопрос об атрибуте noreturn, который используется для функций, которые не возвращаются вызывающему.

Затем я сделал программу в C.

#include <stdio.h>
#include <stdnoreturn.h>

noreturn void func()
{
        printf("noreturn func\n");
}

int main()
{
        func();
}

И сгенерированная сборка кода с помощью this:

.LC0:
        .string "func"
func:
        pushq   %rbp
        movq    %rsp, %rbp
        movl    $.LC0, %edi
        call    puts
        nop
        popq    %rbp
        ret   // ==> Here function return value.
main:
        pushq   %rbp
        movq    %rsp, %rbp
        movl    $0, %eax
        call    func

Почему функция func() возвращается после предоставления атрибута noreturn?

Ответ 1

Спецификаторы функций в C являются подсказкой для компилятора, степень принятия определяется реализацией.

Прежде всего, спецификатор функции _Noreturn (или noreturn, используя <stdnoreturn.h>) является подсказкой для компилятора о теоретическом обещании программиста о том, что эта функция никогда не вернется. Основываясь на этом обещании, компилятор может принимать определенные решения, выполнять некоторые оптимизации для генерации кода.

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

  • с помощью и явного выражения return
  • достигнув конца тела функции

поведение undefined. Вы НЕ ДОЛЖНЫ возвращаться от функции.

Чтобы сделать это ясно, использование noreturn спецификатора функции не останавливает форму функции, возвращающуюся к вызывающему. Это обещание, сделанное программистом компилятору, позволить ему еще большую свободу создавать оптимизированный код.

Теперь, если вы сделали обещание раньше и позже, решите нарушить это, результатом будет UB. Компиляторы рекомендуются, но не требуются, для получения предупреждений, когда функция _Noreturn, по-видимому, способна вернуться к вызывающему абоненту.

Согласно главе §6.7.4, C11, пункт 8

Функция, объявленная с помощью спецификатора функции _Noreturn, не должна возвращаться к вызывающему абоненту.

и, пункт 12, (обратите внимание на комментарии!!)

EXAMPLE 2
_Noreturn void f () {
abort(); // ok
}
_Noreturn void g (int i) { // causes undefined behavior if i <= 0
if (i > 0) abort();
}

Для C++ поведение довольно похоже. Цитата из главы §7.6.4, C++14, параграф 2 (акцент мой)

Если вызывается функция f, где f ранее объявлялся с атрибутом noreturn и f в конечном итоге возвращается, поведение undefined. [Примечание. Функция может завершиться путем исключения исключения. -конец примечание]

[Примечание. Реализациям рекомендуется выдать предупреждение, если функция с пометкой [[noreturn]] может вернуть. -end note]

3 [Пример:

[[ noreturn ]] void f() {
throw "error"; // OK
}
[[ noreturn ]] void q(int i) { // behavior is undefined if called with an argument <= 0
if (i > 0)
throw "positive";
}

-end пример]

Ответ 2

Почему функция func() возвращает после предоставления атрибута noreturn?

Потому что вы написали код, который сказал ему.

Если вы не хотите, чтобы ваша функция возвращалась, вызовите exit() или abort() или аналогичный, чтобы он не возвращался.

Что еще сделала бы ваша функция, кроме возврата после того, как она вызвала printf()?

Стандарт C в 6.7.4 Спецификаторы функций, параграф 12 содержит, например, пример noreturn функция, которая может фактически возвратиться - и маркирует поведение как undefined:

ПРИМЕР 2

_Noreturn void f () {
    abort(); // ok
}
_Noreturn void g (int i) {  // causes undefined behavior if i<=0
    if (i > 0) abort();
}

Короче говоря, noreturn - это ограничение на вы на ваш код - он сообщает компилятору "МОЙ код никогда не вернется" . Если вы нарушите это ограничение, все это на вас.

Ответ 3

noreturn - это обещание. Вы говорите компилятору: "Это может быть или не быть очевидным, но я знаю, основываясь на том, как я написал код, что эта функция никогда не вернется". Таким образом, компилятор может избежать настройки механизмов, которые позволят функции правильно возвращаться. Оставляя эти механизмы, можно позволить компилятору генерировать более эффективный код.

Как функция не может вернуться? Например, если бы он назывался exit().

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

  • Будьте "хороши" для вас и выясните способ возврата функции в любом случае.
  • Измените код, который, когда функция ненадлежащим образом вернется, сбой или ведет себя произвольно непредсказуемыми способами.
  • Дайте вам предупреждение или сообщение об ошибке, указывающее, что вы нарушили свое обещание.

Компилятор может выполнить 1, 2, 3 или некоторую комбинацию.

Если это похоже на поведение undefined, это потому, что оно есть.

Суть в программировании, как в реальной жизни, заключается в следующем: не делайте promises, которую вы не можете сохранить. Кто-то, возможно, принял решения на основе вашего обещания, и плохие вещи могут произойти, если вы нарушите свое обещание.

Ответ 4

Атрибут noreturn - это обещание, которое вы делаете компилятору о своей функции.

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

Однако, если вы пишете это:

noreturn void func(void)
{
    printf("func\n");
}

int main(void)
{
    func();
    some_other_func();
}

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

Ответ 5

Как уже упоминалось, это классическое поведение undefined. Вы обещали, что func не вернется, но вы все равно вернетесь. Вы можете подобрать кусочки, когда это сломается.

Хотя компилятор компилирует func обычным способом (несмотря на ваш noreturn), noreturn влияет на вызывающие функции.

Это можно увидеть в списке сборки: компилятор предположил в main, что func не вернется. Поэтому он буквально удалил весь код после call func (см. Для себя https://godbolt.org/g/8hW6ZR). Список сборок не усечен, он буквально заканчивается после call func, потому что компилятор принимает любой код после того, как это будет недоступно. Таким образом, когда func действительно возвращается, main собирается начать выполнение любой дерьма после функции main - будь то заполнение, немедленные константы или море байтов 00. Опять же - поведение undefined.

Это транзитивно - функция, вызывающая функцию noreturn во всех возможных кодах кода, сама может считаться noreturn.

Ответ 6

Согласно this

Если объявленная функция _Noreturn возвращается, поведение undefined. Рекомендуется диагностика компилятора, если это можно обнаружить.

Ответственность программиста заключается в том, чтобы убедиться, что эта функция никогда не возвращается, например. выход (1) в конце функции.

Ответ 7

ret просто означает, что функция возвращает управление обратно вызывающему. Итак, main выполняет call func, CPU выполняет эту функцию, а затем, с ret, ЦП продолжает выполнение main.

Изменить

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

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

Ответ 8

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

См. пример здесь: https://godbolt.org/g/2N3THC и укажите разницу

Ответ 9

TL: DR: это пропущенная оптимизация gcc.


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

GCC уже оптимизирует main, чтобы упасть с конца функции, если func() возвращает, даже с по умолчанию -O0 (минимальный уровень оптимизации), который выглядит так, как вы использовали.

Вывод для func() сам по себе может считаться пропущенной оптимизацией; он может просто опустить все после вызова функции (поскольку вызов не возвращается, это единственный способ, которым может быть сама функция noreturn). Это не отличный пример, так как printf является стандартной функцией C, которая, как известно, нормально возвращается (если вы не setvbuf, чтобы дать stdout буфер, который будет segfault?)

Использует другую функцию, о которой компилятор не знает.

void ext(void);

//static
int foo;

_Noreturn void func(int *p, int a) {
    ext();
    *p = a;     // using function args after a function call
    foo = 1;    // requires save/restore of registers
}

void bar() {
        func(&foo, 3);
}

( Код + x86-64 asm в проводник компилятора Godbolt.

Выход gcc7.2 для bar() интересен. Он встраивает func() и удаляет мертвое хранилище foo=3, оставляя только:

bar:
    sub     rsp, 8    ## align the stack
    call    ext
    mov     DWORD PTR foo[rip], 1
   ## fall off the end

Gcc по-прежнему предполагает, что ext() будет возвращаться, иначе он может иметь только хвост ext() с jmp ext. Но gcc не отменяет функции noreturn, потому что теряет информацию backtrace для таких вещей, как abort(). Очевидно, что их инкрустировать нормально.

Gcc мог бы оптимизироваться, опуская хранилище mov после call. Если ext возвращается, программа закрывается, поэтому нет никакой точки, генерирующей какой-либо из этого кода. Clang делает эту оптимизацию в bar()/main().


func сам по себе более интересен, и большая пропущенная оптимизация.

gcc и clang оба излучают почти одно и то же:

func:
    push    rbp            # save some call-preserved regs
    push    rbx
    mov     ebp, esi       # save function args for after ext()
    mov     rbx, rdi
    sub     rsp, 8          # align the stack before a call
    call    ext
    mov     DWORD PTR [rbx], ebp     #  *p = a;
    mov     DWORD PTR foo[rip], 1    #  foo = 1
    add     rsp, 8
    pop     rbx            # restore call-preserved regs
    pop     rbp
    ret

Эта функция может считать, что она не возвращается, и используйте rbx и rbp без сохранения/восстановления.

Gcc для ARM32 на самом деле делает это, но все еще испускает инструкции для возврата в противном случае чисто. Таким образом, функция noreturn, которая действительно возвращается на ARM32, нарушит ABI и вызовет проблемы с отладкой в ​​вызывающем или более позднем. (Undefined позволяет это, но это по крайней мере проблема качества реализации: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=82158.)

Это полезная оптимизация в случаях, когда gcc не может доказать, возвращается ли функция или не возвращается. (Это явно вредно, когда функция просто возвращает. Gcc предупреждает, когда она уверена, что функция noreturn вернется.) Другие целевые архитектуры gcc этого не делают; это также пропущенная оптимизация.

Но gcc не заходит достаточно далеко: оптимизация команды возврата (или замена ее незаконной инструкцией) позволит сохранить размер кода и гарантировать шумный сбой вместо молчащего повреждения.

И если вы собираетесь оптимизировать ret, оптимизируя все, что только нужно, если функция вернется, имеет смысл.

Таким образом, func() может быть скомпилирован с:

    sub     rsp, 8
    call    ext
    # *p = a;  and so on assumed to never happen
    ud2                 # optional: illegal insn instead of fall-through

Каждая другая презентация представляет собой пропущенную оптимизацию. Если ext объявлен noreturn, это именно то, что мы получаем.

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