アセンブリ言語の学習
アセンブリ言語は、コンピュータプログラミングの最低ソフトウェアレベルであるマシンコードと同等の、人間が読める言語です。
コンピュータはすべてのプログラムを数字として理解し、さまざまな異なる数字をコンピュータに指示して異なる操作を行わせますが、これは人間が消費するには(ましてや執筆には)あまりにも面倒なことです。 そのため、人間はマシン・コードとほぼ1対1に対応するアセンブリ言語を使用してプログラミングを行います。
Cプログラミング言語を学びます。 C (または C++) を知っていれば、アセンブリはより簡単になる。
C# と Java は良い言語だが、C# と Java のコードがアセンブリ言語でどのように見えるかを示すツールはほとんどない。 (実際、Java と C# は、実際のハードウェア CPU 命令セット アーキテクチャとは異なり、かけ離れた独自のバイト コード アセンブリ言語を持っています。)
C (と C++) コードからは、コンパイラーを使ってアセンブリ言語がどのように見えるかを示すことが可能です。 C 言語は低レベルの言語であり、C 言語の各構成要素がどのようにアセンブリ言語に変換されるかがわかれば、アセンブリ言語がわかるようになります! アセンブリ言語として知っておきたいC言語の構成要素の例は以下の通りです。
C の構成要素 | Assembly Language Equivalent |
---|---|
ローカル変数 | CPUレジスタまたはスタックのメモリ位置 |
グローバル変数 | access globalまたはaccess globalを使用する。 constメモリロケーション |
配列参照 | 加算とメモリアクセスのパターン |
goto | |
if 文 | if-でのパターン。を手に入れる。 条件付きテスト&分岐命令 |
while ループ | if- のパターン。goto |
アサイメント | レジスタやメモリの変更 |
演算命令 | |
パラメー タ パス | レジスタまたはスタックメモリの変更 |
パラメータ受信 | レジスタまたはスタックメモリへのアクセス |
関数 call | コール命令 |
関数 entry | プロローグパターン |
関数 exit | epilogue パターン |
戻り値 | レジスタの修正とエピローグ |
アセンブリ言語と他のプログラミング言語の違い
アセンブリ言語では、。 CPUのハードウェアのリソースを丸裸にすることになります。 (さらに、C言語では1行で書けることが、アセンブリでは数行かかります。)
-
CPUレジスタやコールスタックといったものを意識するようになります – これらのリソースは、プログラムが動作するために管理しなければなりません (他の言語では、これらのリソースは自動的に管理されています。) これらのリソースに対する認識は、関数呼び出しや戻り値に重要なものです。 (CPU のレジスタはメモリに比べれば微々たるものですが、超高速で、その性能も安定しています。)
-
アセンブリ言語には構造化文がありません – したがって、すべての制御フロー構造は、「後書き」スタイルでプログラムされなければなりません (他の言語では、コンパイラが構造化文を後書きに翻訳します)。 if-gotoスタイルでは、ターゲットとしてラベルを使用します。 (C 言語は goto とラベルの使用をサポートしていますが、プログラマは構造化ステートメントを好むことが多いので、C 言語では goto とラベルをあまり見かけません。)
Assembly Language vs. Machine Code
Assembly language is meant for human to read and write.
Labels
アセンブリ言語においては、多くのラベルを使用することになります。 ラベルの使用は機械語では数値に変換され、ラベルは機械語の中で消えてしまう。 ラベルがなければ、小さな変更でも実際には大きな変更になってしまいます。
プロセッサは、オペランドがどこにあるか、おそらくデータ オペランドまたはコード ロケーションがどこにあるかを知る必要があります。 たとえば、コード位置の場合、通常、プロセッサは現在実行中のコード位置からそのアイテムがどれだけ離れているかを知りたがりますが、これは pc 相対アドレッシングと呼ばれます。 ここで、Nはスキップする(またはしない)命令のサイズまたは数、つまりNはthen-partのサイズであることを表します。 もし、then-part に命令を追加すれば、スキップする命令の数は増える(then-part から命令を削除すれば、減る)。
擬似命令
アセンブリ言語のほとんどの命令は、マシン・コード命令に1対1で翻訳されます。 しかし、時には機械語に直接変換されないものをアセンブラに指示しなければならないことがあります。 典型的な例は、次の命令がコードであること (通常 .text
命令を使用) とデータであること (通常 .data
命令を使用) をアセンブラに伝えることです。 通常の命令と同様に、これらの命令は一般的に独自の行に書かれますが、機械語命令を直接生成するのではなく、アセンブラに通知します。
Writing Assembly Language
C では、文を書きます。これらの文は次々と連続的に実行し、デフォルトでは、何らかの制御フロー構造 (loop, if) でフローを変更するまで連続的です。 C 言語では、ステートメントは変数に影響を及ぼし、別々のステートメントが変数を通じて互いに通信すると考えることができます。 命令が多くなればなるほど(命令が単純になればなるほど)、必然的に変数も多くなりますが、多くはある命令から次の命令への伝達に使われる非常に短命な一時的なものです。
Basic Assembly Langauge Programming
私たちはレジスタ選択と命令選択を交互に行っています。 アセンブリ言語の命令はかなり単純なので、複数の命令を、そう、CPUレジスタの変数としての値を介して相互に接続して使用する必要があることがよくあります。
レジスタの選択
レジスタを選択するには、どのレジスタがビジーで、どのレジスタがフリーかというメンタルモデルが必要です。 この知識により、我々は一時的な(短時間の)結果を保持するために使用する空きレジスタを選択することができる。 そのレジスタは最後に使うまでビジー状態のままで、その後フリーに戻ります。フリーになったら、まったく別のことに再利用できます。 (ある意味では、プロセッサはプログラムの意図を知らないので、私たちがこの絶え間ない再利用を行っていることを知りません。)
命令選択
使用するリソースが確定したら、必要とする特定の命令を選択できます。 たとえば、MIPS と RISC V は、比較 & 分岐命令を提供する類似のアーキテクチャですが、これらは 2 つの CPU レジスタを比較することしかできません。 そのため、文字列中のあるバイトが特定の文字 (改行など) であるかどうかを確認したい場合、compare & 分岐命令を使用する前に、改行の値 (数値定数) を CPU レジスタに提供 (ロード) しなければならないのです。 これらの関数はメモリを共有しなければならないが、メモリは膨大であるため、仲良くやっていける。 しかし、1つのCPUには限られた固定数のレジスタ(32や16など)があり、すべての関数はこれらのレジスタを共有しなければなりません!
それをうまくやるために、ソフトウェア設計者はこれらの限られた固定リソースを共有するためのルールと考えられる慣習を定義します。 さらに、これらの規約は、ある関数 (呼び出し側) が別の関数 (呼び出し側) を呼び出すために必要です。 この規則は呼び出し規約と呼ばれ、呼び出し側/呼び出される側のために、引数や戻り値をどこに置くか/見つけるかを特定するものである。 呼び出し規約は、アプリケーションバイナリインターフェイスの一部で、関数が機械語で外の世界(他の関数)とどのように通信すべきかを大まかに示しています。 呼び出し規約はソフトウェアでの使用のために定義されています (ハードウェアは気にしません)。
これらのドキュメントでは、パラメータ渡し、レジスタ使用、スタック割り当て/解放、および戻り値で何を行うべきかが示されています。 これらのドキュメントは、命令セット プロバイダまたはコンパイラ プロバイダによって公開され、さまざまな場所からのコードが適切に相互運用できるようによく知られています。 公開された呼び出し規約は、ハードウェアがそれを知らないとしても、与えられたものとして受け取られるべきです。
そのようなドキュメントがなければ、これらのこと (パラメーター渡しなど) をどのように行うかわからないことでしょう。 標準の呼び出し規約を使用する必要はありませんが、シミュレータ上で実行される単純なおもちゃのプログラム以外のものに独自のものを考案するのは非現実的です。 独自の呼び出し規約をロールバックすることは、システムやライブラリを呼び出すような、現実的なプログラムで行う必要のある、他のコードとの相互運用性がないことを意味します。 ただやみくもに、命令の後に命令を実行するだけです (本当に速い)。 たとえば、レジスタがソフトウェアの規約によって分割されたり、特定の用途に割り当てられていても、ハードウェアにとっては、ほとんどのレジスタは等価です。 たとえば、最近のほとんどのCPUは何らかの形でスタックポインタを備えています。 MIPS および RISC V では、スタックポインタは、ソフトウェア規約の定義による以外は、他と区別されない普通のレジスタで、どのレジスタを選択するかは任意ですが、実際に選択され、この選択は公表されているので、どのレジスタであるかは皆知っているのです。 一方、x86 では、スタックポインタは専用の push
& pop
命令でサポートされているため(コールやリターン命令と同様に)、スタックポインタレジスタには論理的に1つの選択肢しかない。
Register Partioning
他のレジスタを何千もの関数で共有するために、ソフトウェアの慣習で、通常4つのセットに分割されます。
- パラメータを渡すためのレジスタ
- 戻り値を受け取るためのレジスタ
- 「揮発性」レジスタ
「不揮発性」レジスタ
パラメータ & 戻り値レジスタ
パラメータレジスタはパラメータの引渡しに使われます!
戻り値レジスタは関数の戻り値を保持し、関数が完了すると、呼び出し側に制御の流れを返す前に、その戻り値を適切なレジスタに置くことがその仕事となります。
Volatile & Non-Volatile Registers
揮発性レジスタは、そのレジスタの以前の使用や値を気にせずに、任意の関数で自由に使用することができるものです。 これは、命令シーケンスを相互に接続するために必要なテンポラリに適しており、また、ローカル変数として使用することも可能です。 しかし、これらのレジスタはどの関数でも自由に使用できるため、関数呼び出しの間、その値を保持することはできません(ソフトウェア規約による)! したがって、関数が返された後、呼び出し元はこれらのレジスタにどのような値が格納されているのかを知ることができません。
関数呼び出しにまたがる必要がある変数には、不揮発性レジスタがあります (そしてスタック メモリも)。 これらのレジスタは (呼び出し規約を遵守するソフトウェアによって) 関数呼び出しにまたがって保存されます。 したがって、ある関数が高速なCPUレジスタに変数を持ちたいが、その変数が呼び出しにまたがって生きている(呼び出し前に設定し、呼び出し後に使用する)場合、呼び出し元に戻ったときにレジスタの前の値が復元されるという(呼び出し規約の定義による)要件で不揮発性レジスタを選択することができます。 これには、レジスタを再利用する前に不揮発性レジスタの現在値を(スタック・メモリに)保存し、復帰時に(スタック・メモリから前の値を使用して)復元することが必要です
。