アセンブリ言語の概要


移動:アセンブリ言語チュートリアルの概要


アセンブリ言語を学習するには、最初にレジスタとメモリモデルの2つの知識ポイントを理解する必要があります。

最初にレジスターを見てみましょう。CPU自体は計算のみを担当し、データの保存は担当しません。データは通常メモリに保存され、CPUはメモリに移動して、使用する必要があるときにデータの読み取りと書き込みを行います。ただし、CPUの計算速度は、メモリの読み取りおよび書き込み速度よりもはるかに高速です。速度が低下しないように、CPUには独自の第1レベルのキャッシュと第2レベルのキャッシュがあります。基本的に、CPUキャッシュは、読み取りと書き込みの速度が速いメモリと見なすことができます。

ただし、CPUキャッシュの速度はまだ十分ではなく、キャッシュ内のデータのアドレスは固定されておらず、CPUは読み取りと書き込みのたびにアドレス指定する必要があるため、速度が低下します。したがって、CPUには、キャッシュに加えて、最も一般的に使用されるデータを格納するための独自のレジスタ(レジスタ)もあります。つまり、最も頻繁に読み書きされるデータ(ループ変数など)がレジスターに配置され、CPUがレジスターの読み取りと書き込みを優先し、レジスターがメモリーとデータを交換します。


レジスターは、データを区別するためにアドレスに依存するのではなく、名前に依存します。各レジスタには独自の名前があり、どのレジスタからデータを取得するかをCPUに指示するため、速度が最も速くなります。一部の人々は、レジスタをCPUのゼロレベルキャッシュと見なします。

第四に、レジスタの種類

初期のx86CPUには8つのレジスタしかなく、それぞれに異なる目的がありました。現在、100を超えるレジスタがあり、それらはすべて汎用レジスタになっており、特に使用するように指定されていませんが、初期のレジスタの名前は保持されています。

EAX

EBX

ECX

EDX

知っている

ESI

EBP

ESP

上記の8つのレジスタのうち、最初の7つはすべて一般的です。ESPレジスタには、現在のスタックのアドレスを保存するという特定の目的があります。

32ビットCPUや64ビットCPUのような名前がよく見られますが、これらは実際にはレジスタのサイズを表しています。32ビットCPUのレジスタサイズは4バイトです。

5、メモリモデル:ヒープ

レジスターは少量のデータしか保存できません。ほとんどの場合、CPUは、メモリーと直接データを交換するようにレジスターに指示する必要があります。したがって、レジスタに加えて、メモリがデータを格納する方法も理解する必要があります。

プログラムの実行中、オペレーティングシステムはメモリのセクションを割り当てて、プログラムと操作によって生成されたデータを保存します。メモリのこのセクションには、開始アドレスと終了アドレスがあります(たとえば、0x1000から0x8000)。開始アドレスは小さい方のアドレスで、終了アドレスは大きい方のアドレスです。

プログラムの実行中に、動的メモリ占有要求(新しいオブジェクトの作成やmallocコマンドの使用など)の場合、システムは事前に割り当てられたメモリの一部をユーザーに割り当てます。特定のルールは、開始アドレスから開始することです。除算(実際には、開始アドレスには静的データが含まれますが、ここでは無視されます)。たとえば、ユーザーが10バイトのメモリを要求した場合、開始アドレス0x1000から割り当てられ、アドレス0x100Aまで続きます。22バイトが必要な場合は、0x1020に割り当てられます。

このメモリ領域をユーザーのアクティブな要求で割ったものをヒープと呼びます。開始アドレスから始まり、下位ビット(アドレス)から上位ビット(アドレス)に増加します。ヒープの重要な機能は、自動的に消えないことです。手動で解放するか、ガベージコレクションメカニズムによってリサイクルする必要があります。

第六に、メモリモデル:スタック

ヒープを除いて、他のメモリ使用量はスタックと呼ばれます。簡単に言えば、スタックは、実行中の関数によって一時的に占有されるメモリ領域です。

以下の例をご覧ください。

intmain(){int a = 2; int b = 3;}

上記のコードでは、システムがmain関数の実行を開始すると、そのためのフレームがメモリに作成され、すべての主要な内部変数(aやbなど)がこのフレームに格納されます。main関数の実行が終了すると、フレームが再利用され、すべての内部変数が解放され、スペースを占有しなくなります。

関数内で他の関数が呼び出された場合はどうなりますか?

intmain(){int a = 2; int b = 3; returnadd_a_and_b(a、b);}

上記のコードでは、add_a_and_b関数がmain関数内で呼び出されています。この行が実行されると、システムはadd_a_and_bの新しいフレームを作成して、その内部変数を格納します。つまり、mainとadd_a_and_bの2つのフレームが同時に存在します。一般的に、コールスタックにあるのと同じ数のフレームがあります。

add_a_and_bの実行が終了すると、そのフレームがリサイクルされ、システムは関数mainが実行を中断した場所に戻り、実行を続行します。このメカニズムにより、レイヤーごとの関数呼び出しが実現され、各レイヤーは独自のローカル変数を使用できます。

すべてのフレームはスタックに保存されます。フレームはレイヤーごとに重ね合わされるため、スタックはスタックと呼ばれます。「スタックにプッシュ」と呼ばれる新しいフレームを生成します。英語はプッシュです。スタックの回復は「ポップ」と呼ばれ、英語はポップです。Stackの特徴は、スタックにプッシュされた最新のフレームが最初にスタックから外れることです(最も内側の関数呼び出しが最初に操作を終了するため)。これは「ラストイン、ファーストアウト」データ構造と呼ばれます。関数の実行が終了するたびに、フレームが自動的に解放され、すべての関数の実行が終了し、スタック全体が解放されます。

スタックは、メモリ領域の終了アドレスから始まり、上位(アドレス)から下位(アドレス)に割り当てられます。たとえば、メモリ領域の終了アドレスは0x8000で、最初のフレームは16バイトと見なされ、次に割り当てられるアドレスは0x7FF0から始まり、2番目のフレームは64バイトが必要であると想定し、アドレスは0x7FB0に移動します。

7、CPU命令

7.1例

レジスタとメモリモデルを理解すると、アセンブリ言語が何であるかを確認できます。以下は簡単なプログラムの例です。c。

intadd_a_and_b(int a、int b){returna + b;} intmain(){returnadd_a_and_b(2,3);}

gccは、このプログラムをアセンブリ言語に変換します。

$ gcc-S example.c

上記のコマンドを実行すると、アセンブリ言語であり、数十行の命令を含むテキストファイルexample.sが生成されます。このように言えば、高レベルの言語を簡単に操作するために、最下層は数個または数十個のCPU命令で構成されている場合があります。CPUはこれらの命令を順番に実行して、このステップを完了します。

example.sを簡略化すると、次のようになります。

_add_a_and_b:push%ebx mov%eax、[%esp + 8] mov%ebx、[%esp + 12] add%eax、%ebx pop%ebx ret _main:push3push2call _add_a_and_b add%esp、8ret

元のプログラムの2つの関数add_a_and_bとmainが、2つのタグ_add_a_and_bと_mainに対応していることがわかります。各ラベルの中には、関数によって変換されたCPU実行プロセスがあります。

各行は、CPUによって実行される操作です。これは2つの部分に分かれており、そのうちの1つを例として取り上げます。

push%ebx

この行で、pushはCPU命令であり、%ebxは命令で使用される演算子です。CPU命令には、0個以上の演算子を含めることができます。

次に、アセンブラーを1行ずつ説明します。読んでいるときにページを上にスクロールしないように、このプログラムを別のウィンドウにコピーすることをお勧めします。

7.2プッシュコマンド

慣例に従い、プログラムは_mainタグから実行を開始します。このとき、スタック上にメイン用のフレームが作成され、スタックが指すアドレスがESPレジスタに書き込まれます。後でメインフレームに書き込むデータがある場合は、ESPレジスタに保存されているアドレスに書き込まれます。

次に、コードの最初の行が実行されます。

push3

プッシュ命令は、オペレーターをスタックに配置するために使用されます。ここでは、メインフレームに3を書き込みます。

見た目はシンプルですが、プッシュ命令には実際には事前操作があります。最初にESPレジスタのアドレスを取得し、そこから4バイトを減算してから、新しいアドレスをESPレジスタに書き込みます。スタックが高から低に発展するため、減算が使用されます。4バイトは、3のタイプがintであり、4バイトを占めるためです。新しいアドレスを取得した後、このアドレスの先頭の4バイトに3が書き込まれます。

push2

2行目も同じで、プッシュ命令はメインフレームに2を書き込み、位置は前に書き込んだ3に近くなります。このとき、ESPレジスタは4バイト(累積マイナス8)減算されます。

7.3呼び出し命令

3行目の呼び出し命令は、関数を呼び出すために使用されます。

_add_a_and_bを呼び出す

上記のコードは、add_a_and_b関数を呼び出すことを意味します。このとき、プログラムは_add_a_and_bタグを探し、関数の新しいフレームを作成します。

_add_a_and_bのコードは以下で実行されます。

push%ebx

この行は、EBXレジスタの値が_add_a_and_bフレームに書き込まれていることを示しています。これは、後でこのレジスタを使用する場合は、最初にその値を取り出し、使用後に書き戻すためです。

このとき、プッシュ命令はESPレジスタのアドレスから4バイトを減算します(累積的にマイナス12)。

7.4mov命令

mov命令は、レジスタに値を書き込むために使用されます。

mov%eax、[%esp + 8]

このコード行は、最初にESPレジスタのアドレスに8バイトを追加して新しいアドレスを取得し、次にこのアドレスに従ってスタックからデータをフェッチすることを意味します。前のステップによれば、ここで取り出されたものは2であると推定でき、次に2がEAXレジスタに書き込まれます。

コードの次の行も同じことをします。

mov%ebx、[%esp + 12]

上記のコードは、ESPレジスタの値に12バイトを加算し、このアドレスに従ってスタックからデータをフェッチします。今回は、3をフェッチしてEBXレジスタに書き込みます。

7.5追加コマンド

add命令は、2つの演算子を追加し、その結果を最初の演算子に書き込むために使用されます。

add%eax、%ebx

上記のコードは、EAXレジスタ(つまり2)の値をEBXレジスタ(つまり3)の値に加算して結果5を取得し、この結果を最初のオペレータEAXレジスタに書き込みます。

7.6popコマンド

pop命令は、スタックに書き込まれた最後の値(つまり、最小アドレスの値)をフェッチし、この値をオペレーターが指定した場所に書き込むために使用されます。

pop%ebx

上記のコードは、Stackによって最近書き込まれた値(つまり、EBXレジスタの元の値)を取り出し、この値をEBXレジスタに書き戻すことを意味します(追加がすでに行われているため、EBXレジスタは使用されません)。

pop命令は、ESPレジスタのアドレスにも4を追加することに注意してください。つまり、4バイトが回復されます。

7.7ret命令

ret命令は、現在の関数の実行を終了し、実行権を上位の関数に戻すために使用されます。つまり、現在の機能のフレームがリサイクルされます。

正しい

ご覧のとおり、この命令には演算子がありません。

add_a_and_b関数が実行を終了すると、システムはメイン関数が中断された場所に戻り、実行を続行します。

add%esp、8

上記のコードは、ESPレジスタのアドレスに手動で8バイトを追加し、それをESPレジスタに書き戻すことを意味します。これは、ESPレジスタがスタックの書き込み開始アドレスであるためです。前のポップ操作ではすでに4バイトが回復されています。ここでは、8バイトが回復されます。これは、すべての回復に相当します。

正しい

最後に、main関数が終了し、ret命令がプログラムの実行を終了します。

おすすめ

転載: blog.csdn.net/qq_36171263/article/details/96834171