私は 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()
が戻るのはなぜですか?
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"; }
—例の終了]
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();
}
つまり、noreturn
はrestrictionであり、youがyourコード-コンパイラーに指示します「MYコードは戻りません」。あなたがその制限に違反した場合、それはすべてあなた次第です。
noreturn
は約束です。コンパイラーに、「明らかな場合もそうでない場合もありますが、Iは、コードの記述方法に基づいて、この関数は決して戻らないことを知っています。」そうすれば、コンパイラーは、関数が適切に戻ることを可能にするメカニズムのセットアップを回避できます。これらのメカニズムを除外すると、コンパイラーがより効率的なコードを生成できる場合があります。
どうして関数は戻らないのですか? 1つの例は、代わりにexit()
を呼び出した場合です。
しかし、関数が返らないことをコンパイラーに約束し、コンパイラーが関数が適切に戻ることができるように手配していない場合、does戻る、コンパイラは何をするべきですか?基本的に3つの可能性があります。
コンパイラーは、1、2、3、または何らかの組み合わせを行う場合があります。
これが未定義の動作のように聞こえる場合、それはそうなっているからです。
実際のプログラミングと同様に、最終的な結論は次のとおりです。守れない約束をしないでください。他の誰かがあなたの約束に基づいて決定を下したかもしれません、そしてあなたがあなたの約束を破ると悪いことが起こるかもしれません。
noreturn
属性は、関数についてコンパイラにyoを約束するものです。
このような関数からdoを返す場合、動作は未定義ですが、これは正気のコンパイラがret
ステートメントを削除することでアプリケーションの状態を完全に混乱させることを意味しません。特に、コンパイラーは多くの場合、戻り値が実際に可能であると推定することさえできるためです。
ただし、これを書く場合:
noreturn void func(void)
{
printf("func\n");
}
int main(void)
{
func();
some_other_func();
}
コンパイラーがsome_other_func
を完全に削除することは完全に合理的です。
他の人が述べたように、これは古典的な未定義の動作です。 func
は戻らないと約束しましたが、とにかく戻すようにしました。それが壊れるとき、あなたはピースを拾うことができます。
コンパイラはfunc
を通常の方法でコンパイルしますが(noreturn
にもかかわらず)、noreturn
は関数の呼び出しに影響します。
これは、アセンブリリストで確認できます。コンパイラは、main
でfunc
が返されないと想定しています。そのため、call func
の後のすべてのコードを文字通り削除しました( https://godbolt.org/g/8hW6ZR を参照してください)。アセンブリのリストは切り捨てられず、call func
の直後で終了します。コンパイラはその後のコードは到達不能だと想定するためです。したがって、func
が実際に戻ると、main
はmain
関数に続くがらくた(パディング、即値定数、または00
バイトの海)の実行を開始します。 。繰り返しますが、非常に未定義の動作です。
これは推移的です-可能なすべてのコードパスでnoreturn
関数を呼び出す関数は、それ自体がnoreturn
と見なされます。
this による
_Noreturnと宣言された関数が戻る場合、動作は未定義です。これを検出できる場合は、コンパイラ診断をお勧めします。
この関数が決して戻らないようにすることは、プログラマーの責任です。関数の最後でexit(1)。
ret
は、単に関数が呼び出し元にcontrolを返すことを意味します。したがって、main
はcall func
を実行し、CPUは関数を実行し、ret
を使用して、CPUはmain
の実行を継続します。
編集
したがって、それは 判明 、noreturn
はmakeを返しません。関数はまったく戻りません。単なる指定子です。これは、コンパイラにこの関数のコードが、関数が返らないように書かれていることを伝えます。したがって、ここで行うべきことは、この関数が実際に制御を呼び出し先に戻さないようにすることです。たとえば、その中でexit
を呼び出すことができます。
また、この指定子について読んだことを考えると、関数が呼び出しポイントに戻らないことを確認するために、anothernoreturn
関数を使用し、後者が常に実行されるようにして(未定義の動作を回避するため)、UB自体を引き起こさないようにします。
リターン関数は必要ないため、エントリのレジスタを保存しません。最適化が容易になります。たとえば、スケジューラルーチンに最適です。
ここの例を参照してください: https://godbolt.org/g/2N3THC そして違いを見つけます
TL:DR:gccによる最適化の失敗です。
noreturn
は、関数が返さないというコンパイラーへの約束です。これにより、最適化が可能になります。特に、ループが終了しないことをコンパイラーが証明するのが難しい場合、または戻る関数を通るパスがないことを証明するのが難しい場合に役立ちます。
GCCは、デフォルトの-O0
(最小最適化レベル)が使用されているように見える場合でも、func()
が戻った場合、main
を既に最適化して関数の終わりから外れます。
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);
}
(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 ext
で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
を保存/復元せずに使用できます。
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
存在する他のすべての命令は、最適化されていません。 ext
がnoreturn
と宣言されている場合、まさにそれが得られます。
戻り値で終わる 基本ブロック は、到達しないと見なされます。