web-dev-qa-db-ja.com

「noreturn」関数が戻るのはなぜですか?

私は thisnoreturn属性についての質問を読みました。これは呼び出し元に戻らない関数に使用されます。

その後、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

noreturn属性を指定した後に関数func()が戻るのはなぜですか?

69
msc

Cの関数指定子は、コンパイラにとってhintであり、受け入れの程度は実装定義です。

まず、_Noreturn関数指定子(または、<stdnoreturn.h>を使用したnoreturn)は、理論的約束についてのコンパイラーへのヒントです。この関数は決して戻らないことをプログラマーが説明します。この約束に基づいて、コンパイラは特定の決定を下し、コード生成の最適化を実行できます。

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(emphasis mine)からの引用

fが以前にf属性で宣言され、noreturnが最終的に返される関数fが呼び出された場合、動作は未定義です。 [注:関数は例外をスローして終了する場合があります。 —注を終了]

[注:[[noreturn]]とマークされた関数が戻る可能性がある場合、実装は警告を発行することが推奨されます。 —注を終了]

3 [ Example:

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

—例の終了]

119
Sourav Ghosh

Noreturn属性を指定した後に関数func()が戻るのはなぜですか?

それを伝えるコードを書いたからです。

関数が戻りたくない場合は、exit()またはabort()などを呼び出して、戻りません。

関数は、printf()を呼び出した後に戻る以外に、どのelseを実行しますか?

C標準 in6.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();
}

つまり、noreturnrestrictionであり、youyourコード-コンパイラーに指示します「MYコードは戻りません」。あなたがその制限に違反した場合、それはすべてあなた次第です。

47
Andrew Henle

noreturnは約束です。コンパイラーに、「明らかな場合もそうでない場合もありますが、Iは、コードの記述方法に基づいて、この関数は決して戻らないことを知っています。」そうすれば、コンパイラーは、関数が適切に戻ることを可能にするメカニズムのセットアップを回避できます。これらのメカニズムを除外すると、コンパイラーがより効率的なコードを生成できる場合があります。

どうして関数は戻らないのですか? 1つの例は、代わりにexit()を呼び出した場合です。

しかし、関数が返らないことをコンパイラーに約束し、コンパイラーが関数が適切に戻ることができるように手配していない場合、does戻る、コンパイラは何をするべきですか?基本的に3つの可能性があります。

  1. とにかく関数を適切に返す方法を見つけてください。
  2. 関数が不適切に戻ると、クラッシュしたり、任意の予測不可能な方法で動作するコードを生成します。
  3. 約束を破ったことを警告またはエラーメッセージで知らせます。

コンパイラーは、1、2、3、または何らかの組み合わせを行う場合があります。

これが未定義の動作のように聞こえる場合、それはそうなっているからです。

実際のプログラミングと同様に、最終的な結論は次のとおりです。守れない約束をしないでください。他の誰かがあなたの約束に基づいて決定を下したかもしれません、そしてあなたがあなたの約束を破ると悪いことが起こるかもしれません。

25
Steve Summit

noreturn属性は、関数についてコンパイラにyoを約束するものです。

このような関数からdoを返す場合、動作は未定義ですが、これは正気のコンパイラがretステートメントを削除することでアプリケーションの状態を完全に混乱させることを意味しません。特に、コンパイラーは多くの場合、戻り値が実際に可能であると推定することさえできるためです。

ただし、これを書く場合:

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

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

コンパイラーがsome_other_funcを完全に削除することは完全に合理的です。

15
Groo

他の人が述べたように、これは古典的な未定義の動作です。 funcは戻らないと約束しましたが、とにかく戻すようにしました。それが壊れるとき、あなたはピースを拾うことができます。

コンパイラはfuncを通常の方法でコンパイルしますが(noreturnにもかかわらず)、noreturnは関数の呼び出しに影響します。

これは、アセンブリリストで確認できます。コンパイラは、mainfuncが返されないと想定しています。そのため、call funcの後のすべてのコードを文字通り削除しました( https://godbolt.org/g/8hW6ZR を参照してください)。アセンブリのリストは切り捨てられず、call funcの直後で終了します。コンパイラはその後のコードは到達不能だと想定するためです。したがって、funcが実際に戻ると、mainmain関数に続くがらくた(パディング、即値定数、または00バイトの海)の実行を開始します。 。繰り返しますが、非常に未定義の動作です。

これは推移的です-可能なすべてのコードパスでnoreturn関数を呼び出す関数は、それ自体がnoreturnと見なされます。

11
nneonneo

this による

_Noreturnと宣言された関数が戻る場合、動作は未定義です。これを検出できる場合は、コンパイラ診断をお勧めします。

この関数が決して戻らないようにすることは、プログラマーの責任です。関数の最後でexit(1)。

8
ChrisB

retは、単に関数が呼び出し元にcontrolを返すことを意味します。したがって、maincall funcを実行し、CPUは関数を実行し、retを使用して、CPUはmainの実行を継続します。

編集

したがって、それは 判明noreturnmakeを返しません。関数はまったく戻りません。単なる指定子です。これは、コンパイラにこの関数のコードが、関数が返らないように書かれていることを伝えます。したがって、ここで行うべきことは、この関数が実際に制御を呼び出し先に戻さないようにすることです。たとえば、その中でexitを呼び出すことができます。

また、この指定子について読んだことを考えると、関数が呼び出しポイントに戻らないことを確認するために、anothernoreturn関数を使用し、後者が常に実行されるようにして(未定義の動作を回避するため)、UB自体を引き起こさないようにします。

6
ForceBru

リターン関数は必要ないため、エントリのレジスタを保存しません。最適化が容易になります。たとえば、スケジューラルーチンに最適です。

ここの例を参照してください: https://godbolt.org/g/2N3THC そして違いを見つけます

6
P__J__

TL:DR:gccによる最適化の失敗です。


noreturnは、関数が返さないというコンパイラーへの約束です。これにより、最適化が可能になります。特に、ループが終了しないことをコンパイラーが証明するのが難しい場合、または戻る関数を通るパスがないことを証明するのが難しい場合に役立ちます。

GCCは、デフォルトの-O0(最小最適化レベル)が使用されているように見える場合でも、func()が戻った場合、mainを既に最適化して関数の終わりから外れます。

func()自体の出力は、最適化されていないものと見なされる可能性があります。関数呼び出しの後にすべてを省略することができます(呼び出しが返されないことが、関数自体がnoreturnになる唯一の方法であるため)。 printfは正常に戻ることが知られている標準のC関数であるため、これは素晴らしい例ではありません(setvbufstdoutにセグメンテーション違反のバッファーを与える場合を除きます)。

コンパイラーが知らない別の関数を使用してみましょう。

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

Code + x86-64 asm on Godbolt compiler Explorer

bar()のgcc7.2出力は興味深いものです。 func()をインライン化し、foo=3デッドストアを削除し、以下を残します。

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

GCCはext()が戻ると想定していますが、そうでない場合は、jmp extext()を末尾に呼び出すだけです。ただし、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

この関数は、返らないと仮定し、rbxrbpを保存/復元せずに使用できます。

ARM32用のGccは実際にそれを行いますが、それでもそうでなければきれいに戻るための命令を発行します。したがって、ARM32で実際に返されるnoreturn関数は、ABIを破壊し、呼び出し元以降でデバッグが困難な問題を引き起こします。 (未定義の動作によりこれが可能になりますが、少なくとも実装品質の問題です。 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

存在する他のすべての命令は、最適化されていません。 extnoreturnと宣言されている場合、まさにそれが得られます。

戻り値で終わる 基本ブロック は、到達しないと見なされます。

2
Peter Cordes