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

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

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

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

int main()
{
        func();
}

И сгенерировал сборку кода, используя этот:

.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?

71
msc 31 Авг 2017 в 15:31

8 ответов

Лучший ответ

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

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

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

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

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

Чтобы было понятно, использование спецификатора функции 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 в конечном итоге возвращает поведение не определено. [Примечание. Функция может завершить работу, вызвав исключение. -конец примечание]

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

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

- конец примера]

120
JDługosz 2 Сен 2017 в 06:21

Согласно этому

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

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

8
ChrisB 31 Авг 2017 в 12:39

Почему функция 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 - это ограничение , которое вы размещаете в вашем коде - это говорит компилятору "МОЙ код выиграл никогда не вернется ". Если вы нарушите это ограничение, это все на вас.

49
Andrew Henle 31 Авг 2017 в 12:46

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

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

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

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

11
nneonneo 31 Авг 2017 в 18:22

Функция no return не сохраняет регистры на записи, так как в этом нет необходимости. Это облегчает оптимизацию. Отлично подходит для рутины планировщика, например.

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

6
P__J__ 31 Авг 2017 в 13:13

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


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

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

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

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

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, потому что теряет информация о трассировке для таких вещей, как 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 и вызовет проблемы с отладкой в вызывающей или более поздней версии. (Неопределенное поведение позволяет это, но это, по крайней мере, проблема качества реализации: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=82158. )

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

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

И если вы собираетесь оптимизировать 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, это именно то, что мы получаем.

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

2
Peter Cordes 25 Сен 2017 в 16:04

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

< EM> Изменить

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

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

6
ForceBru 31 Авг 2017 в 12:52

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

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

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

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

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

Если это звучит как неопределенное поведение, это потому, что это так.

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

26
Steve Summit 31 Авг 2017 в 18:37