2017-01-25 1 views
3

は、「短期のストレージ」によると、「ステップバイアセンブリ言語の手順」の第8章(第3版):長期的な変数保存にスタックを使用すべきですか?

スタックは短期的なものを隠しておくための場所を考慮すべきです。スタックに格納されているアイテムには名前がありません。一般的には、それらが置かれた逆の順序でスタックから取り出さなければなりません。最後に、まず、忘れないでください。 LIFO!

私の知るところでは、Cコンパイラは基本的にすべてをスタックで使います。これは、スタックが短期間と長期間の両方で変数を格納する最良の方法であることを意味しますか?それとも良い方法がありますか?

  • ヒープが、それは遅いです:

    私は考えることができる選択肢があります。

  • 静的変数ですが、それはプログラムの全寿命を延ばすことになり、多くのメモリを浪費する可能性があります。
+1

、7秒はデータの永遠です。 – danny117

+1

あなたの本はアセンブリ言語に関するものであり、C –

+0

@ M.Mではありません。私は理解していますが、Cコンパイラの一般的な操作方法を例として使用していました。 –

答えて

1

Cコンパイラは基本的にすべてのスタックを使用します。まあ、実際には、doやdidntにはレジスタがたくさんあるのでスタック重い命令セットがあります。したがって、それは命令セットの設計の一部です。純粋なコンパイラ設計では、パラメータを渡したり情報を返すためのルールは何ですか? ISAにレジスタをたくさん持つ呼び出し規約の中にはスタックが重いものや、いくつかのレジスタを使用している可能性があり、多くのパラメータがある場合はスタックに依存することがあります。

次に、グローバルでのようなことは悪いことを学校で教えられているプログラマーに伝えます。今度は重いプログラマーを積み重ねるという習慣がありますが、関数の概念は小規模でなければならず、12ポイントフォントの印刷されたページに収まらなければなりません。ネストされた関数であることがあります。ネストされた構造上の1つの構造体へのポインタまたは同じ値またはそのバリエーションが何度も何度も繰り返し渡されます。スタックを大量に使いすぎると、関数の入れ子の深さや変数を渡したり格納したりするためのスタックの使用のために、その変数のコピーが数十または数百になる可能性があります。特定のプログラミング言語とはまったく関係がありませんが、教育者の意見(一部のケースでは、書類のグレード化を容易にし、より良いプログラムを作成する必要がない)や習慣とは関係しています。

レジスタが十分にあり、呼び出し規約でその使用を許可し、オプティマイザを使用している場合は、スタック使用量を大幅に減らすことができますが、プログラマはここでも習慣に関与し、不必要なスタック消費、およびインライン化が不可能なネストは、スタック上の項目の重複、またはプログラムの全寿命の間スタックに残っている構造または項目を依然として引き起こす可能性があります。

ローカルグローバルと呼ばれるグローバルおよび静的ローカルは、スタック上にない.dataにあります。 main()レベルで変数や構造体を作成するプログラマがあり、スタックの重大な呼び出し規約の場合でも、より効率的に使用されている可能性のあるパラメータ渡しの消費をコストをかけて入れ子にするあなたはまだ静的なグローバルがはるかに安いだろう、すべてのレベルのポインタを燃やしている参照を渡す、ローカルのグローバルは、あなたがそのトップレベルで静的ではないローカルと同じ金額を費やしています。グローバル変数や静的なローカル変数のほうがコストがかかるということは簡単に言えませんが、プログラミング言語の習慣や変数の選択によりますが、消費量がはるかに少ないと主張します。トラブルに巻き込まれる。しかし、たとえば、マイクロコントローラの作業や、リソースに極端に制約がある他の埋め込み作業を行う場合は、グローバルのみを使用すると成功のチャンスが増え、メモリ使用量はほぼ固定されます。入れ子にされ、インライン化されない関数のアドレス。これは少し極端ですが、練習ではレジスタに最適化され、スタックを使用しないというかなり良い機会がある地元住民を使うことができます。重いローカルな使用か多量のグローバルな使用が実際に少ないメモリを消費するかどうかは、非常にプログラマ、プロセッサ、コンパイラによって異なります。重いローカルでの使用は一時的な使用に過ぎない可能性がありますが、制約のあるシステムでは、スタックをプログラムやヒープにクラッシュさせないようにするために必要な分析は、安全性を確保するために多くの作業を必要とします。ローカル変数に重い場合にスタック使用量に劇的な影響を与えます。スタックの使用状況を即座に検出するためのスキームは、新しいアプリケーションの高水準コードを追加しなくても、その領域をさらに消費する多くのリソースを必要とします。

今、アセンブリ言語の本を読んでいます。コンパイラの本ではありません。コンパイラのプログラマの習慣は、閉じ込められた、制御された、または他の何らかの言葉をもう少し言えます。出力をデバッグし、あなたの正気を保つために、コンパイラはスタックを前面に、最後にスタックフレームを基本的に混乱させることがよくあります。あなたはしばしばそれらを参照してください、関数を介してすべてのものを削除して、同じアイテムのオフセットを変更したり、フレームポインタとして別のレジスタを焼くので、スタックミッド関数を混乱させることができますが、 xまたは変数yに渡された値は、そのスタックポインタまたはフレームポインタと同じオフセットに維持されます。アセンブリ言語のプログラマもそうすることを選択するかもしれませんが、スタックを比較的短期間のソリューションとして使用することもできます。ソートの、アップフロント、使用されている

unsigned int more_fun (unsigned int); 
unsigned int fun (unsigned int a) 
{ 
    return(more_fun(a)+a+5); 
} 

00000000 <fun>: 
    0: e92d4010 push {r4, lr} 
    4: e1a04000 mov r4, r0 
    8: ebfffffe bl 0 <more_fun> 
    c: e2844005 add r4, r4, #5 
    10: e0840000 add r0, r4, r0 
    14: e8bd4010 pop {r4, lr} 
    18: e12fff1e bx lr 

スタックフレームのアプローチの作成:

だから、スタックを使用するように、例えばコンパイラを強制的に書かれたコードをこれを取りますスタック上のレジスタをプッシュし、バックエンドでレジスタを解放/復元します。そのレジスタのmid関数をローカルストレージに使用します。ここでの呼び出し規約では、r4を保持しなければならないことが指示されているので、次の関数downは下のすべてのネスティングを保持し、この関数に戻るとr4はそれをどのように残したかです(r0はパラメータが来て、この場合)は揮発性であり、各関数はそれを破壊する可能性があります。

それはあなたの代わりに

push {lr} 
push {r0} 
bl more_fun 
add r0,r0,#5 
pop {r1} 
add r0,r0,r1 
pop {lr} 
bx lr 

を持つことができます設定し、この命令の現在の規則は、他の、必ず2つのレジスタのスタックプッシュとポップは、このために、4つの個別のものよりも安価であるよりも、一つの方法安いです違反が我々が2つの加算を行うことを回避する命令セット、同じ数のレジスタを使用する。この場合、コンパイラのアプローチは「安価」です。 e92d4010プッシュ{R4、LR} 4:ebfffffeのBL 0 8関数はdidntのは、一時的に記憶するためのスタックを使用する必要があることが書かれていたが、何場合

unsigned int more_fun (unsigned int); 
unsigned int fun (unsigned int a) 
{ 
    return(more_fun(a)+5); 
} 

は 0を生成する(命令セットに応じて) :e8bd4010ポップ{R4、LR} C:e2800005追加R0、R0、#5 10:e12fff1e BX LR

、その後、あなたは私に言うが、それはなかったです。これは部分的には呼び出し規約の一部であり、バスが64ビット幅の場合(これはARMのために頻繁に使用される)、またはそうでない場合でも、その追加のレジスタに数百から数百クロックかかるトランザクションにクロックを1つ追加し、大きなコストではなく、64ビット幅の場合、単一のレジスタのプッシュ/ポップは実際にはあなたを節約しません。同様に、64ビット幅のバスを使用する場合は64ビット境界に配置してください。ここではコンパイラはr4を選択しましたが、r4はここでは保存されていません。コンパイラがこれに関連する他のstackoverflowの質問に見られるようにスタックを整列させるために選んだレジスタです。それがr4を選んだ場合。

しかし、スタックのアライメントとコンベンションを超えて(古いコンパイラを掘り下げてr4がlrだけでないことを示すことができます)。このコードでは、入れ子関数の呼び出しの後でmathが入力パラメータを保持する必要はありませんでした。変数がmore_fun()に入ると、変数aは破棄できます。

アセンブリ言語のプログラマとしては、レジスタをたくさん使うようにしたいと思うかもしれません。命令セットや、おそらく多くの命令でメモリオペランドを直接使うことができるx86 CISCの習慣によると思いますあなたはパフォーマンスコストにもかかわらず、その習慣を開発します。しかし、あなたは、あなたが本が

push {r0} 
ldr r0,[r2] 
ldr r1,[r0] 
pop {r0} 

か何かをあなたに言っている何ので、あなたが最終的に崖の秋、すべてのレジスタが使用されており、1より多くを必要とすることが可能な限り多くのレジスタを使用するよう努めている場合そのように、レジスタを使い果たし、二重間接を行う必要があった。それともあなたは中間変数を必要とし、あなたは、単にどれも余裕が残っていない持っているので、あなたは一時的にコンパイル言語でスタック

push {r0} 
add r0,r1,r2 
str r0,[r3] 
pop {r0} 

を使用し、いくつかの選択肢が最初のオフプロセッサ設計で始まる対の使用を積み重ね、飢えた命令セットがあります命令セットは、関数呼び出し命令と戻り命令、割り込み、および割り込みの戻りのために、設計によってスタックを使用するか、レジスタを使用して、スタックに保存する必要があるかどうかを選択できるようにします。命令セットは基本的にスタック使用に強制されますか、それともオプションですか。次のプログラミングの習慣は、彼らが教えたり、自分で開発したりすることができます重いまたは軽いスタックの使用、あまりにも多くの機能、あまりにも多くのネスティングをもたらすことができます、返信アドレスだけでは、スタックごとに小さなバイトを取るだろう、関数のサイズ、変数の数(可変サイズ)、関数内のコードに応じて、少し噛むか、爆発する可能性があります。オプティマイザを使用しないと大量のスタックが爆発してしまいますが、関数への行を1つ増やして崖の影響を受けないようにするには、レジスタをプッシュしているので、 1つ以上の行を追加することで、その崖の上での使用量を増やすことができます。最適化されていないスタック消費量は重いが、より線形です。レジスタを使用することはメモリ消費を減らす最良の方法ですが、コンパイラの出力をコーディングして見て、次のコンパイラが同じように動作することを期待しています。メモリ使用をより控えめにして、まだタスクを完了させるためにコードを書くことができます。 (intの代わりにcharを使うような小さな変数を使うと、16,32,64ビットのレジスタサイズの命令セットでは、必ずしもあなたを救うことはできません。レジスタの残りの部分を拡張またはマスクする追加の命令が必要になることがあります。命令セットとあなたのコード)そして、何らかの理由で眉をひそめて読みにくいグローバルがありますか?それは愚かです。彼らは賛否両論を持っています。あなたの消費ははるかに制御されています。反対意見はあります。多くの変数を使用すると、あなたはもっと多くを消費する変数を再利用しません。 、彼らは非静的な地元の人のように解放しません。静的なローカルは、範囲が限定された単なるグローバルであり、グローバル化を望んでいるが、それを避けることを恐れているとき、または再帰に主に関連する短いリストがある非常に特殊な理由があるときにのみ使用する。

ヒープはどのように遅くなりますか?ラムは一般的にラムですが、変数がスタック上やヒープ上にある場合、同じロードとストアが必要ですが、キャッシュはヒットしてミスしますが、操作することはできますが、時には時にはミスすることもあります。いくつかのプロセッサはスタック用のチップRAMを特別に持っていますが、今日の一般的なプロセッサの種類ではありません。これらのスタックは一般にかなり小さいです。または埋め込み/ベアメタルのデザインによっては、スタックを.dataやヒープとは別のRAMに置くこともできます。なぜなら、スタックを使用したいし、メモリが最も速いからです。しかし、これを読んでいるマシンでプログラムを実行すると、プログラム、スタック、および.data/heapは同じスロードラマ領域になります。とにかくメモリのコンパイル/オペレーティングシステムの使用である「ヒープ」は、割り当てと解放の問題がありますが、一度割り当てられると、パフォーマンスは.textと.dataと同じであり、多くのターゲットプラットフォームつかいます。スタックを使用すると、基本的にはmallocをやっていて、システムコールよりもオーバーヘッドが少なくて済みます。しかし、上記のスタックを使用したコンパイラのように、ヒープを効率的な方法で使用することができます.1つの命令で2つの要素をプッシュしてポップし、数十から数百クロックサイクルを節約できます。あなたはmallocし、より大きなものを少なくすることができます。そして、スタックを使用するのが理にかなっていないときは(構造体や配列のサイズ、構造体の配列のため)、それをします。

+0

ねえ、有益な答えをありがとう。私がちょっと掃除するのを助けてもらえますか? –

2

スタックは通常、関数呼び出しに引数をプッシュしたり、関数のローカル変数を格納したり、戻りアドレス(現在の関数から復帰して実行を開始する命令)のトラックを保持するために使用されます。しかし、どのように関数呼び出しを実装するかは、コンパイラの実装とcalling conventionsによって異なります。

Cコンパイラは真実ではありません基本的にすべて

のスタックを使用しています。 Cコンパイラはグローバル変数と静的変数をスタックに入れません。

これは、変数が短期間と長期間の両方で変数を格納する最良の方法であることを意味しますか?

スタックは、現在の関数が返された後に使用されない変数に使用する必要があります。はい、長期間スタックを使用することもできます。 main()のローカル変数は、プログラムの存続期間中も持続します。また、各プログラムのスタックには制限があることに注意してください。

ヒープですが、それは遅いです。

実行時に何らかの管理が必要なためです。アセンブリでヒープに割り当てる場合は、ヒープを自分で管理する必要があります。 C、C++などの高水準言語では、言語ランタイムとOSがヒープを管理します。あなたは組み立てにそれを持たないでしょう。

+0

btw、c/C++コードからアセンブリコードの生成について知りたい場合は、[gcc explorer](http://gcc.godbolt.org/)が面白いかもしれません。 – army007

関連する問題