【コンピュータの構成原理】読書メモ 第5回 アセンブリ言語でプログラムの実際の構成を理解する

目次

初めに書きます

アセンブリ言語とネイティブコードの関係

アセンブリ言語のソースコード 

指令

アセンブリの基本構文

共通の組み立て説明書

動く

プッシュしてポップする

機能利用の仕組み

関数呼び出し

関数パラメータを渡して値を返す

グローバル変数

ローカル変数

プログラムフロー制御

ループ文

条件分岐

アセンブリ言語を通じてプログラムがどのように動作するかを理解する必要性

終わり

初めに書きます

  この記事は、本書『プログラムはどう動くか』(著:矢沢久雄)を読み進めてまとめたもので、本書の最後の読書メモでもあります。最初の 3 つのブログでは、本書の読書体験を紹介し、それぞれ第 1 章CPU 、第 4 章メモリ関連、第 5 章ディスク、第 8 章プログラムの知識をまとめています。詳細については、以下を参照してください。

[コンピュータ構成の原則] 読書メモ 第 1 回: プログラマーにとって CPU とは - CSDN ブログ 

[コンピュータ構成の原理] 読書メモ 第 2 号: Angular Memory の使用_Bossfrank のブログ - CSDN ブログ 

[コンピュータ構成の原理] 読書メモ 第 3 号: メモリとディスクの関係 - CSDN ブログ

 [コンピュータ構成の原則] 読書メモ 第 4 号: ソース ファイルから実行可能ファイルまで_Bossfrank のブログ - CSDN ブログ

   このブログでは、書籍『アセンブリ言語でプログラムの実際の構成を理解する』の第10章を、アセンブリ言語とローカルコードの関係、アセンブリ言語のソースコード(疑似命令、スタック機構、関数呼び出し)から紹介していきます。仕組み、ローカル変数/グローバル変数、プログラムフロー制御など)を2つの観点から紹介します。この章は、プログラム実行時の各レジスタの格納状況、プログラムの実行順序、関数の呼び出しスタックなどを理解するためには、アセンブリ言語の理解が基礎となる非常に内容が絞られた内容になっています。基礎となるアセンブリ言語を理解することは、高レベル プログラミング言語の作成とデバッグを学習する上でも非常に役立ちます。

アセンブリ言語とネイティブコードの関係

    一言で言えば、アセンブリ言語とネイティブコードは 1 対 1 に対応しますこのセクションの内容は比較的少ないですが、読者は次の質問に答えられれば重要なポイントを理解できるでしょう。

アセンブリ言語の目的は何ですか?

   ローカル コードを視覚的に表示するために使用されますCPUはローカルコードしか実行できませんが、ローカルコードの内容を調べることで、最終的にプログラムがどのように動作するのかが分かります。ただし、ローカル コードを直接開いて確認すると、バイナリ値のリストしか表示されません。そこで、各ローカルコードにその機能を表す英単語の略語(ニーモニック)を付けることでローカルコードを表現することができ、プログラマはプログラムの性質を理解しやすくなります。

アセンブリ言語は直接実行できますか?

  できません!アセンブリ言語で書かれたソース コードであっても、実行する前に最終的にはネイティブ コードに変換する必要があります変換を担当するプログラムはアセンブラと呼ばれ、変換プロセス自体はアセンブリと呼ばれます。ただし、アセンブリ言語で記述された.asmソース コードは、ローカル コードと 1 対 1 に対応します。したがって、ネイティブ コードをアセンブリ言語ソース コードに変換し直すこともできます。この機能を備えた逆変換プログラムを逆アセンブラと呼び、逆変換処理自体を逆アセンブラと呼びます。

 C言語などの高級プログラミング言語とローカルコードにはどのような関係があるのでしょうか?アセンブリ言語との関係は何ですか?

    C言語などの高級言語は、CPUで実行する前に最終的にローカルコードに変換する必要がありますが、C言語などの高級プログラミング言語にはローカルコードが存在しないことに注意してください。ローカルコードと1対1対応しているため、アセンブリ言語と1対1対応していないため、逆コンパイル(アセンブリ言語からC言語への変換)が難しくなります。ただし、ほとんどの C 言語コンパイラは、C 言語で書かれたソース コードをアセンブリ言語のソース コードに変換できます。

アセンブリ言語のソースコード 

   このセクションには多くの内容が含まれており、コード例が示されています。C 言語で書かれた次のサンプル.c ソース コードがあるとします。

//代码10-1 C语言实例源代码sample.c
// 返回两个参数值之和的函数
int AddNum(int a, int b)
{
    return a + b;
}
// 调用AddNum 函数的函数
void MyFunc()
{
    int c;
    c = AddNum(123, 456);
}

    この C 言語フラグメントは簡潔かつ包括的であり、次のセクションではこの C 言語コードのアセンブリ コードについて説明します。main 関数がないため、このコードは実行できず、アセンブリを学習するためにのみ必要であることに注意してください。コンパイラのアセンブリ関数を使用すると、対応するアセンブリコードsample.asmが次のように生成されます。

; コード 10-2、ソース コード sample.c をアセンブリ コード sample.asm に変換

_TEXT セグメント dword public use32 'CODE'
_TEXT 終了
_DATA セグメント dword public use32 'DATA'
_DATA 終了
_BSS セグメント dword public use32 'BSS'
_BSS 終了
DGROUP グループ _BSS,_DATA
_TEXT セグメント dword public use32 'CODE'
_AddNum proc Near
        ;
        ; int AddNum(int a, int b)
        ;
                ebp
                mov ebp,espをプッシュします
    。
    ; {     ; a + b を返します。     ;         mov eax,dword ptr [ebp+8]         add eax,dword ptr [ebp+12]     ;     ; }     ;         ポップ ebp         ret _AddNum endp










_MyFunc proc が近くにあります
    。
    ; void MyFunc()
    ;
        ebp
        mov ebp,espをプッシュします
    。
    ; {     ; int c;     ; c = AddNum(123, 456);     ;         プッシュ 456         プッシュ 123         call _AddNum         add esp,8     ;     ; }     ;         Pop ebp         ret _MyFunc endp _TEXT 終了end














   C言語のソースコードとアセンブリ言語に変換されたソースコードが重ねて表示されていることがわかります。そしてこれは、両者を比較研究するための優れた教材でもあります。このアセンブリ言語コードでは、コメントはセミコロン (;) の後に続きます。C言語のソースコードがコメントとなるため、Sample.asmを直接アセンブルしてネイティブコードに変換できますアセンブリ言語のソースコードを見たばかりだと混乱するかもしれませんが、以下で一つずつ説明していきますので、慌てないでください。

指令

      アセンブリ言語のソースコードは、ローカルコードに変換された命令(後述のオペコード)とアセンブラの疑似命令から構成されます。疑似命令は、アセンブラ (変換プログラム) にプログラムの構築およびアセンブリ方法を指示する役割を果たしますただし、疑似命令自体をアセンブルしてローカル コードに変換することはできません言い換えれば、ディレクティブには対応するネイティブ コードがありません。コード 10-3 に示すように、コード 10-2 で使用されている擬似命令を抽出します。

_TEXTセグメントdword public use32 'CODE'
_TEXT終了
_DATA セグメント dword public use32 'DATA'
_DATA 終了
_BSS セグメント dword public use32 'BSS'
_BSS 終了
DGROUP グループ _BSS,_DATA


_TEXT セグメント dword パブリック use32 'CODE'



_AddNum endp付近の _AddNum proc



_MyFunc endp付近の _MyFunc proc


_TEXT 終了
        終了

    セグメントディレクティブとエンドディレクティブで囲まれた部分は、プログラムを構成するコマンドやデータの集合に付けられた名前であり、セグメント定義と呼ばれます。プログラムにおいて、セグメント定義とは、プログラムのコマンドやデータなどの集合を指します。プログラムは複数のセグメント定義で構成されます。

    ソースコードの先頭には、_TEXT、_DATA、_BSS という 3 つのセクション定義が定義されています。_TEXT は命令のセグメント定義、_DATA は初期化済み (初期値あり) データのセグメント定義、_BSS は初期化されていないデータのセグメント定義ですこの種のセグメント定義と同様の名前と分割方法は、Borland C++ コンパイラによって自動的に割り当てられます。したがって、プログラムセグメント定義の構成順序は _TEXT、_DATA、_BSS となり、メモリの連続性も確保されますグループ ディレクティブは、2 つのセグメント定義 _BSS および _DATA が DGROUP という名前のグループにまとめられることを意味します。さらに、プログラムの実行中にスタックおよびヒープ メモリ空間が生成されます。

     _AddNum と _MyFun を囲む _TEXT セグメントと _TEXT の終わりは、_AddNum と _MyFunc が _TEXT セグメント定義に属していることを示します。そのため、ソースコード中に命令とデータが混在して書かれていても、コンパイルやアセンブル後は、きちんとセグメント定義されたネイティブコードに変換されます

    _AddNum procと_AddNum endpで囲まれた部分、_MyFuncprocとMyFunc endpで囲まれた部分が、それぞれAddNum関数とMyFunc関数のスコープを表します。コンパイル後、関数名の前にはアンダースコア (_) が付きます。これは、Borland C++ コンパイラの要件です。C言語で記述されたAddNum関数は、内部的には_AddNumという名前で扱われます。疑似命令 proc と endp で囲まれた部分が手続きのスコープを表します。アセンブリ言語では、このC 言語に相当する関数の形式をプロシージャと呼びます

    最後のendディレクティブはソースコードの終わりを示します。

アセンブリの基本構文

   簡単に言うと、アセンブリ言語の構文は、オペコード + オペランドです。アセンブリ言語では、mov に類似した命令を「オペコード」 (オペコード) と呼びます。 どのような形式のオペコードを使用できるかは、CPU の種類によって決まります。 表 10-1 は、前のコード例を示しています 10 -2 で使用されるオペランドはすべて32ビットx86系CPUで使用されるオペコードで、命令の対象となるメモリアドレスやレジスタを「オペランド」(オペランド)と呼び、オペランドにはレジスタ名やメモリなどのアドレスや定数などが指定されます。アセンブリ ステートメントに複数のオペランドがある場合は、カンマ「,」で区切る必要があります。

   注: ネイティブ コードは、メモリにロードされるまで実行できません。メモリには、ネイティブ コードを構成する命令とデータが保存されます。プログラムの実行中、CPU はメモリから命令とデータを読み取り、それらを CPU 内のレジスタに格納して処理します(図 10-2)。

  レジスタはCPU内の記憶領域です。しかし、レジスタは命令やデータを格納する機能だけでなく、演算機能も持っていますx86 シリーズ CPU のレジスタの主な種類と役割を表 10-2 に示します (表中のレジスタ名はすべて e で始まり、e は拡張を意味し、32 ビットレジスタは 16 ビットレジスタを拡張したものです)。レジスタの名前は、アセンブリ言語のソース コードを通じてオペランドに割り当てられます。メモリ上の記憶領域はアドレス番号によって区別されますCPU 内のレジスタは、eax および ebx という名前で区別されます。また、CPU 内部にはプログラマが直接操作できないレジスタがありますたとえば、正負の演算結果やオーバーフローの状態を示すフラグ レジスタやオペレーティング システム固有のレジスタは、プログラマが作成したプログラムで直接操作することはできません。

共通の組み立て説明書

   アセンブリ言語では、アセンブリ言語の構文はオペコード + オペランドであり、一般的なオペコードには、データをレジスタやメモリに格納する mov 命令が含まれます。スタックのプッシュやポップを行うpush命令やpop命令、蓄積するadd命令、関数呼び出しに関わるcallやretなど。この記事では、Code 10-2 に含まれるオペコード命令に基づいて 1 つずつ説明します。

動く

    mov 命令は非常に一般的で、レジスタやメモリにデータを格納するために使用されます。mov 命令の 2 つのオペランドは、それぞれデータの格納場所と読み出し元を指定するために使用されます。オペランドには、レジスタ、定数、ラベル (アドレスの前に追加)、および角括弧([]) で囲まれたこれらの内容を指定できます。角括弧で囲まれていない内容を指定すると、値が処理されることを意味し、括弧で囲まれた内容を指定すると、角括弧内の値がメモリ アドレスとして解釈され、メモリアドレスに対応する値が読み書きされます以下に例を示します。

コード:

mov ebp,esp
mov eax,dword ptr [ebp+8]

説明: mov ebp,esp では、esp レジスタの値が ebp レジスタに直接格納されます。esp レジスタの値が 100 の場合、ebp レジスタの値も 100 になります。mov eax,dwordptr [ebp+8] の場合、ebp レジスタの値に 8 を加算した値がメモリアドレスとして解釈されますebp レジスタの値が 100 の場合、アドレス 100 + 8 = 108 のデータが eax レジスタに格納されます。d word ptr (ダブルワードポインタ) は、指定されたメモリアドレスから 4 バイトのデータを読み取ることを意味しますこのように、アセンブリ言語では修飾子 dword ptr がオペランドに追加されることがあります。

プッシュしてポップする

   プログラムの実行中、メモリ内にスタックと呼ばれるデータ領域が割り当てられます。スタックにデータが格納されると、メモリの下位(アドレス番号の大きい)から上位(アドレス番号の小さい)に向かって徐々に蓄積され、読み出す際には上から下へスムーズに進みます(図10-) 3)。

   スタックとは一時的にデータを格納しておく領域で、プッシュ命令やポップ命令によりデータの格納と読み出しを行うのが特徴です。スタックにデータを格納することを「プッシュ」といい、スタックからデータを読み出すことを「ポッピング」といいます。32 ビット x86 シリーズ CPU では、32 ビット (4 バイト) のデータを 1 回のプッシュまたはポップで処理できます。プッシュ命令とポップ命令のオペランドは 1 つだけです。このオペランドは、「メモリのどのアドレス番号をプッシュまたはポップするか」を指定することなく、「何をプッシュするか(どのデータをスタックにプッシュするか)、何をポップするか(スタックをポップした結果がどこに格納されるか)」を表します。これは、スタックを読み書きするためのメモリアドレスがespレジスタ(スタックポインタ)で管理されているためです。プッシュ命令とポップ命令の実行後、espレジスタの値は自動的に更新される(プッシュ命令は-4、ポップ命令は+4)ため、プログラマはメモリアドレスを指定する必要はありません。

機能利用の仕組み

関数呼び出し

   このセクションでは、AddNum 関数を呼び出す MyFunc 関数のアセンブリ言語部分から始めて、関数呼び出しメカニズムを説明します。MyFunc関数の処理内容は以下のとおりです。

;コード 10-4 関数呼び出しに関連するアセンブリ コード

_MyFunc proc Near
    Push ebp; ebp レジスタの値をスタックに格納 (1)
    mov ebp,esp; ebp レジスタの値を ebp レジスタに格納 (2)
    プッシュ 456; 456 をスタックに格納 (3)
    プッシュ 123 ; 123 をスタックに追加します (4)
    _AddNum を呼び出します; AddNum 関数を呼び出します (5)
    add ebp,8; esp レジスタの値に 8 を加算します (6)
    Pop ebp; スタックから値を読み取り、ebp に格納しますregister (7)
    ret; MyFunc 関数を終了し、呼び出し元に戻ります (8)
_MyFunc endp

   (1)、(2)、(7)、(8)の処理はC言語のすべての関数に共通であり、後ほどAddNum関数の処理内容を示す際に説明します。レジスタ ebp の値は、関数の入口でスタックにプッシュされ (1)、関数の出口でスタックからポップアウトされます (コード リスト 10-4 (7))。これは C 言語の要件です。これは、関数呼び出しの前後で ebp レジスタの値が変わらないことを保証するために行われます。

   (3) から (6) の部分が関数呼び出しメカニズムの中核です。(3) と (4) は、AddNum 関数に渡されたパラメーターをスタックにプッシュすることを表します。C言語のソースコードでは関数AddNum(123,456)と記述されていますが、スタックにプッシュされると456,123、つまり末尾の値の順になります。最初にスタックにプッシュされますこれはC言語のルールです。(5)のcall命令は、オペランドで指定したAddNum関数が存在するメモリアドレスにプログラムフローをジャンプしますアセンブリ言語では、関数名は関数が配置されているメモリ アドレスを表しますAddNum 関数が処理された後、プログラム フローは行番号 (6) に戻る必要があります。call 命令の実行後、 call 命令の次の行(6)のメモリ アドレス(関数呼び出し後に返されるメモリ アドレス) が自動的にスタックにプッシュされますこの値は、AddNum 関数の処理の最後に ret 命令によってスタックからポップされ、プログラム フローは(6)に戻ります

  (6) パートは、スタックに格納されている 2 つのパラメータ (456 と 123) を破棄します。これは、前のブログの第 5 章で説明したスタック クリーニングプロセスです。Pop 命令を 2 回使用することでも実現できますが、esp レジスタに 8 を加算する方が効率的です (1 回の処理で済みます)。スタックに数値を入出力する場合、数値の単位は4バイト(32ビット)となりますしたがって、スタックアドレス管理を担当するespレジスタに2×4の8を加算することで、popコマンドを2回実行したのと同じ効果が得られます。実際にはメモリ上のデータは残っていますが、esp レジスタの値がデータ格納アドレスよりも前のデータ位置に更新されている限り、データは破壊されたことになります( esp レジスタの値が先頭を表すためです)。スタックの要素、つまり、スタック内のデータの最上位ビットのメモリ アドレス、esp + 8 は、スタックの上部をスタックの下部に 2 データ単位移動することに相当します)。

     プッシュ命令とポップ命令は、スタックへのデータのプッシュおよびスタックからのデータのポップを 4 バイト単位で行う必要があります。したがって、AddNum 関数の呼び出し前後のスタックの状態変化は図 10-4 に示されます。4 バイト未満の値 123 と 456 を格納する場合も、4 バイトのスタック領域を占有します。

  ちなみに、コード10-1ではc = AddNum(123, 456)という文がありますが、これはAddNum関数の戻り値を変数cに代入するという意味ですが、アセンブリ言語ではcは関係ありません。アセンブリコードは、コンパイラに最適化機能が備わっており、コンパイルされたプログラムをより高速に実行し、ファイルサイズを小さくすることが目的ですAddNum関数の戻り値を格納する変数cは後ほど使用されないため、コンパイラは「この処理には意味がない」と判断し、対応するアセンブリ言語コードを生成しません。

関数パラメータを渡して値を返す

    以下では、AddNum 関数のソース コード部分 (コード 10-5) を通じてパラメーターを受け取り、戻り値を返すメカニズムを紹介します。

;コード 10-5 関数内の処理

_AddNum proc Near
        Push ebp (1)
        mov ebp,esp (2)
        mov eax,dword ptr [ebp+8] (3)
        add eax,dword ptr [ebp+12] (4)
        Pop ebp (5)
        ret (6)
_AddNum 末尾 

     ebp レジスタの値は (1) でスタックにプッシュされ、(5) でスタックからポップアウトされます。これは主に、関数内で使用されているebpレジスタの内容を関数呼び出し前の状態に戻すためのものです。関数の処理に入る前はebpレジスタがどこで使われているか分かりませんが、ebpレジスタは関数内でも使われるため、一時的に値が保存されます。

  (2)ではスタックアドレスを管理するespレジスタの値をebpレジスタに代入します。これは、mov 命令内の角括弧内のパラメータで esp レジスタを指定できないためです。したがって、ここでは esp を直接使用するのではなく、 ebp レジスタを使用してスタックの内容を読み書きする方法を使用します

(3) [ebp+8] でスタックに格納されている最初のパラメータ 123 を指定し、eax レジスタに読み込みます (eax は演算を担当する累積レジスタです)。また、スタックの内容の読み取りは、pop 経由だけではないこともわかります (pop はスタックの先頭の内容しか読み取ることができず、スタックをポップします。このとき、[ebp+4] はメモリ アドレスを表します)スタックの一番上にあります。つまり、関数に入る前にプッシュされた ebp の値です)。

  (4)のadd命令により、eaxレジスタの現在の値と第2パラメータを加算した結果がeaxレジスタに格納されます。[ebp+12] は 2 番目のパラメータ 456 を指定するために使用されます。C言語では関数の戻り値は eax register で返さなければならないというルールもあります。ただし、ebp レジスタとは異なり、eax レジスタの値を元の状態に復元する必要はありません (eax レジスタはポインタと同等ではないため、以前に保存された値は後で使用されるときに上書きされ、特別なクリアリングが必要です)。(6)のret命令実行後、関数の戻り先のメモリアドレスが自動的にスタックからポップされるため、コードリスト10-4の(6)に戻ります。このセクションの核心は、関数のパラメータがスタックを介して渡され、戻り値が register を介して返されることです

     図 10-5 に、AddNum 関数の開始時と終了時のスタックの状態変化を示します。図 10-4 と図 10-5 を (a) (b) (c) (d) (e) (f) の順に見ると、関数呼び出し処理中のスタックの状態変化が分かります。(a)の状態ではAddNum関数にジャンプするため、(a)と(b)は同じです。同様に、(d) の状態でも呼び出し元に処理がジャンプしますので、(d) と (e) は同じになります。(f)の状態では、クリーニングが行われる。スタックの最上位データ アドレスは常に esp レジスタに格納されます

図 10-4 を次のように再生します。 

グローバル変数

   ご存知のとおり、C言語では関数の外で定義された変数をグローバル変数、関数の内部で定義された変数をローカル変数と呼びます。グローバル変数はソース コードのどの部分からでも参照できますが、ローカル変数は変数が定義されている関数内でのみ参照できます。アセンブリ言語のソースコードを通して、グローバル変数とローカル変数の違いを比較してみましょう。コード 10-6 は、ローカル変数とグローバル変数を使用する C 言語コードです。

//代码10-6 使用了全局变量和局部变量的C语言代码
// 定义被初始化的全局变量
int a1 = 1;
int a2 = 2;
int a3 = 3;
int a4 = 4;
int a5 = 5;
// 定义没有初始化的全局变量
int b1, b2, b3, b4, b5;
// 定义函数
void MyFunc()
{
    // 定义局部变量
        int c1, c2, c3, c4, c5, c6, c7, c8, c9, c10;
    // 给局部变量赋值
    c1 = 1;
    c2 = 2;
    c3 = 3;
    c4 = 4;
    c5 = 5;
    c6 = 6;
    c7 = 7;
    c8 = 8;
    c9 = 9;
    c10 = 10;
    // 把局部变量的值赋给全局变量
    a1 = c1;
    a2 = c2;
    a3 = c3;
    a4 = c4;
    a5 = c5;
    b1 = c6;
    b2 = c7;
    b3 = c8;
    b4 = c9;
    b5 = c10;
}

    リスト 10-6 をアセンブリ言語ソース コードに変換すると、結果はリスト 10-7 のようになります。説明の都合上、アセンブリ言語のソースコードの一部を省略し、一部のセグメント定義の構成順序を変更し、コメントを削除しています。

   前述したように、コンパイルされたプログラムはセグメント定義と呼ばれるグループにグループ化されます。コード 10-7 の (1) に示すように、初期化されたグローバル変数は _DATA という名前のセグメント定義にまとめられ、初期化されていないグローバル変数は (2) に示すように _BSS という名前のセグメント定義にまとめられます命令は (3) のように _TEXT という名前のセグメント定義にまとめられますこれらのセクション定義の名前は、Borland C++ コンパイラの使用仕様によって決まります。_DATAセグメントと_DATA終了、_BSSセグメントと_BSS終了、_TEXTセグメントと_TEXT終了、これらは各セグメントの定義範囲を示すディレクティブです。

_DATAセクションの定義内容

(4) の _a1 ラベル dword は、ラベル _a1 を定義します。ラベルは、セグメント定義の開始位置を基準とした相対位置を表します。_a1 は _DATA セクション定義の先頭にあるため、相対位置は 0 になります。_a1 はグローバル変数 a1 に相当します。コンパイルされた関数名と変数名にはアンダースコアが追加されます。これは Borland C++ の要件でもあります。(5) の dd 1 は、初期値 1 を格納する 4 バイトのメモリ空間を割り当てるアプリケーションを指します。dd (define double word) は、ダブルワードデータが定義されていることを意味します。つまり、4 バイトのメモリ空間が に適用されます

_BSSセクション定義内容

ここではグローバル変数 b1 ~ b5 に相当するラベル _b1 ~ _b5 を定義します。(6) db 4 dup(?) は、
4 バイトのフィールドが割り当てられているが、値がまだ決定されていないことを意味します (ここでは ? で表します)。db (define byte) は、1 バイトの長さのメモリ空間があることを意味します。したがって、db 4 dup(?) の場合、4 バイトのメモリ空間になりますここでの dd 4 と混同しないように皆さんも注意してください。db 4 dup(?) は、1 バイトの長さの 4 つのメモリ空間を表します。また、dd 4 は、
2 バイト (= 4 バイト) のメモリ空間に格納されている値が 4 であることを意味します。

   _DATA、_BSSのセグメント定義では、グローバル変数用のメモリ空間が確保されています。したがって、グローバル変数は、プログラムの最初から最後まですべての部分で使用できますグローバル変数のセグメント定義を初期化の有無で2つに分けているのは、Borland C++ではプログラム実行時に初期化されていないグローバル変数のフィールド(_BSSセグメント定義)が0に設定されるためです。初期化のためです。メモリの特定の範囲がすべて 0 に設定されている限り、集約を通じて初期化を実装するのが簡単であることがわかります。

ローカル変数

ローカル変数は、それが定義されている関数内でのみ使用できるのは    なぜですか? これは、ローカル変数がレジスタやスタックに一時的に格納されるためです。本章の前半で述べたように、関数内で使用されているスタックは関数の処理後に初期状態に戻されるため、ローカル変数の値は破棄され、レジスターが他の目的に使用される可能性もあります。したがって、ローカル変数は、関数処理の実行中にレジスタおよびスタックに一時的にのみ格納されます。

   リスト 10-6 では、10 個のローカル変数が定義されています。これは、ローカル変数がスタック上だけでなくレジスターにも格納されることを示します。c1~c10に必要な領域を確保するため、レジスタが空いている場合はレジスタを使用し、レジスタ空間が不足している場合はスタックを使用します。

_TEXTセクションの定義内容

(7) は MyFunc 関数のスコープを表します。MyFunc関数内で定義したローカル変数に必要なメモリ空間は、可能な限りレジスタに確保されます。レジスターにスペースがある限り、コンパイラーはそれを使用します。レジスタを使用するとメモリに比べて
アクセス速度が速いため、より高速に処理を行うことができます

(8)はローカル変数をレジスタに割り当てる部分です。ローカル変数を定義するだけでは不十分で、ローカル変数に値を代入する場合にのみ、レジスタのメモリ領域に値が割り当てられます。(8)は、5つのローカル変数c1~c5にそれぞれ1~5の値を代入することに相当します。eax、edx、ecx、ebx、esi は、Pentium などの x86 シリーズの 32 ビット CPU レジスタの名前です (表 10-2 を参照)。どのレジスタを使用するかについては、コンパイラが決定します。この場合、レジスタは単に変数の値を格納するために使用され、それ自体の役割とは何の関係もありませんx86系CPUが持つレジスタの中には、プログラムが動作できるレジスタが十数個あります。その中で、無料のものはせいぜい数えるほどしかありません。したがって、ローカル、割り当て可能なレジスタが不足します。この場合、ローカル変数はスタック上に割り当てられたメモリ空間に適用されます

    (8)では、ローカル変数c1~c5にレジスタを割り当てた後、使用可能なレジスタの数が不足します。したがって、(9) に示すように、残りの 5 つのローカル変数 c6 ~ c10 がスタックのメモリ空間に割り当てられます。関数エントリ (10) の追加 esp,-20 は、スタック データの格納場所にある esp レジスタ (スタック ポインタ) の値から 20 を減算することを指します。これは、変数を格納するために 20 バイトのスペースを空けることに相当します。内部変数 c6 ~ c10 を確実にスタック上に置くためには、int 型ローカル変数 5 個分(4 バイト×5 = 20 バイト)の領域を確保する必要があります。

    (11)のmov ebp,esp処理とは、現在のespレジスタの値をebpレジスタにコピーすることを指す。(11) が必要な理由は、関数終了時の move esp,ebp 処理 (12) により、esp レジスタの値を元の状態に戻し、割り当てられたスタック領域を解放するためです。スタックで使用されていたものは消えます。これはスタックのクリーンアップ プロセスでもあります。レジスタを使用する場合、レジスタが他の目的に使用されると、ローカル変数は自動的に消えます(図 10-6、この図は非常に優れています)。

 (9)の5行のコードはスタック領域に値を代入する部分です。スタックからメモリ空間を申請する前に、esp レジスタの値は mov ebp,esp プロセスの助けを借りて ebp レジスタに保存されるため、[ebp - 4]、[ebp - 8]、[ [ebp - 12]、[ebp - 16]、[ebp - 20] この形式では、要求された 20 バイトのスタック メモリ空間を、それぞれ 4 バイトの 5 つの空間に分割して使用できます (図 10-7)。たとえば、mov dword ptr [ebp - 4], 6 in (9) は、割り当てを要求されたメモリ空間の下端 (ebp レジスタで示される位置) から 4 バイト先のアドレス ([ebp - 4] ) を表します。 ) )、値 6 の 4 バイトのデータが格納されます。

  ローカル変数の値をグローバル変数に代入する際には、レジスタが仲介として使用されます。たとえば、演算 b1 = c6 では、次のように 2 行のステートメントが使用されます。

mov eax,dword ptr [ebp-4]
mov dword ptr [_b1],eax

   ebp-4 の位置、つまり値が 6 の c6 変数のデータを eax に格納し、eax のデータを eax のアドレスに格納するという意味は理解できるはずです。グローバル変数 b1 により代入が実現されます。なぜ dword ptr [_b1]、dword ptr [ebp-4] を移動しないのでしょうか? あるメモリ アドレス空間の値を別のメモリ スペースに直接格納できないためだと思います。レジスタを使用する必要があります。 CPU

プログラムフロー制御

ループ文

   C言語などの高級プログラミング言語で実装されるループのフロー制御文に対応したアセンブリ言語の処理方法を紹介します。C 言語のコード例 10-8 は次のとおりです。

//代码10-8 执行循环处理的C语言源代码
// 定义MySub 函数
void MySub()
{
    // 不做任何处理
}
// 定义MyFunc 函数
Void MyFunc()
{
    int i;
    for (i = 0; i < 10; i++ )
    {
        // 重复调用MySub 函数10 次
        MySub();
    }
}

アセンブリ言語への変換をコード 10-9 に示します。

       ;コード 10-9 ループ文のアセンブリコード  

        xor ebx, ebx ; eax レジスタを 0 にクリア
@4 call _MySub ; // MySub 関数を呼び出す
        inc ebx ; // ebx レジスタの値に 1 を加算
        cmp ebx,10 ; // ebx レジスタの値を 10 と比較
        jl short @ 4; //10 未満の場合は @4 にジャンプ

 C言語のfor文は、ループカウンタの初期値(i=0)、ループの継続条件(i<10)、ループカウンタの更新(i++)を括弧内に指定してループ処理を行います。対照的に、アセンブリ言語のソース コードでは、ループは比較命令 (cmp) とジャンプ命令 (jl は未満の場合ジャンプを意味します) によって実装されます。

コードリスト10-9の内容順に説明していきます。
   MyFunc 関数で使用される唯一のローカル変数は i であり、変数 i には ebx レジスタのメモリ空間が割り当てられます。for 文のかっこ内の i = 0; は xor ebx, ebx に変換されますxor 命令は、左から 1 番目のオペランドと右から 2 番目のオペランドに対して XOR 演算を実行し、その結果を最初のオペランドに格納します。第 1 オペランドと第 2 オペランドの両方に ebx が指定されているため、同じ値に対する XOR 演算になります。つまり、ebx レジスタの現在の値が何であっても、結果は 0 でなければなりません。mov 命令を使用した mov ebx,0 は同じ結果を取得しますが、xor 命令は mov 命令よりも高速ですこのとき、コンパイラの最適化機能も有効になります。

    ebx レジスタの値が初期化された後、call 命令を通じて MySub 関数 (_MySub) が呼び出されます。MySub 関数から戻った後、inc 命令によってこの処理はfor文のi++に相当します。

    次の行の cmp 命令は、第 1 オペランドと第 2 オペランドの値を比較する命令です。cmp ebx,10 は C 言語の i<10 の処理に相当し、ebx レジスタの値と 10 を比較することを意味します。アセンブリ言語による比較命令の結果は、CPU のフラグレジスタに格納されます

    ただし、フラグレジスタの値をプログラムから直接参照することはできません。では、プログラムはどのようにして比較結果を決定するのでしょうか? 実際、アセンブリ言語には複数のジャンプ命令があり、これらのジャンプ命令はフラグ レジスタ の値に基づいてジャンプが必要かどうかを決定しますたとえば、最後の行の jl は、ジャンプのみを意味します。つまり、jl short @4 は、以前に実行した比較命令の結果が「小さい」場合、@4 ラベルにジャンプすることを意味します。

条件分岐

条件分岐の実装方法はループ処理の実装方法と同様で、cmp命令やjump命令も使用されます。コード リスト 10-11 は、変数 a の値に基づいてさまざまな関数 (MySub1 関数、MySub2 関数、MySub3 関数) を呼び出すための C 言語ソース コードです。

//代码10-11 条件分支结构的C语言代码
// 定义MySub1 函数
void MySub1()
{
    // 不做任何处理
}
// 定义MySub2 函数
void MySub2()
{
    // 不做任何处理
}
// 定义MySub3 函数
void MySub3()
{
    // 不做任何处理
}
// 定义MyFunc 函数
void MyFunc()
{
    int a = 123;
    // 根据条件调用不同的函数
    if (a > 100)
    {
        MySub1();
    }
    else if (a < 50)
    {
        MySub2();
    }
    else
    {
        MySub3();
    }
}

   次のようにコード 10 ~ 11 をアセンブリ コードに変換します。

; コード 10-11 の MyFunc 関数をアセンブリ言語に変換した結果

_MyFunc proc は
        プッシュ ebp の近くにあります。
        mov ebp、特に;

        mov eax,123; eax レジスタに 123 を格納
        cmp eax,100; eax レジスタの値を 100 と比較
        jle short @8; 100 より小さければ @8 ラベルにジャンプ
        call _MySub1; MySub1 関数を呼び出す
        jmp short @ 11 ; @11 label にジャンプ
@8: cmp eax,50 ; eax レジスタの値と 50 を比較
        jge short @10 ; 50 より大きい場合は @10 label にジャンプ
        call _MySub2 ; MySub2 関数を呼び出す
        jmp short @11 ; @11 ラベル
@10 にジャンプします _MySub3 ; MySub4 関数を呼び出します
@11: Pop ebp
        ret
_MyFunc endp

     コード 10-12 では、比較結果が小さい場合にジャンプする jle (以下のジャンプ)、比較結果が大きい場合にジャンプする jge (以上の場合にジャンプ)、および比較結果に関わらず無条件にジャンプする 3 つのジャンプ命令が使用されています。 jmp を転送します。これらのジャンプ命令の前に比較に使用される cmp 命令があり、比較結果はフラグレジスタに保存されます。ここにはコメントが追加されています。読者はプログラムの流れに従ってください。C言語のソースコードの処理の流れは全く同じではありませんが、処理結果は同じであることは誰でも知っているはずです。さらに、eax レジスタが変数 a を表すことに注意してください。 

アセンブリ言語を通じてプログラムがどのように動作するかを理解する必要性

   アセンブリ言語のソースコードから得られる知識は、場合によってはバグの原因を見つけるのにも役立ちます。このセクションでは、アセンブリ言語とローカル コードの間の 1 対 1 の対応により、アセンブリ言語を理解していれば、プログラムがどのように実行されるかを理解しやすくなり、根本的なバグのいくつかを解決できることを示す例を示しますサンプル コード 10-13 は、2 つの関数が同じグローバル変数の値を更新する C 言語プログラムです。

//代码10-13 两个函数更新同一个全局变量数值的C语言程序
// 定义全局变量
int counter = 100;
// 定义MyFunc1 函数
void MyFunc1()
{
    counter *= 2;
}
// 定义MyFunc2 函数
void MyFunc2()
{
    counter *=2;
}

  コード 10-13 の counter *= 2; の一部をアセンブリ言語ソース コードに変換した後の結果は、コード 10-14 に示されているようになります。

; グローバル変数の値をアセンブリ言語のソースコードに変換した結果

mov eax,dword ptr[_counter]; counter の値を eax レジスタに読み取ります
add eax,eax; eax レジスタの値を元の値の 2 倍に拡張します
mov dword ptr[_counter],eax; の値を格納します各レジスターをカウンター中央に入れる

   C言語ソースコードのcounter *= 2;命令は、アセンブリ言語ソースコード、つまり実際に実行されるプログラムでは3つの命令に分割されます。counter *= 2; だけを見ると、counter の値が元の値の 2 倍に直接拡張されたように見えます。ただし、実際に実行されるのは「eaxレジスタにcounterの値を読み込む」「eaxレジスタの値を2倍にする」「eaxレジスタの値をcounterに書き込む」という3つの処理です。

   マルチスレッド処理では、アセンブリ言語で記述されたコードが1行実行されるたびに、別のスレッド(関数)に処理が切り替わることがあります。したがって、MyFunc1 関数がカウンタ 100 の値を読み取った後、その 2 倍の値 200 をカウンタに書き込む前に、MyFunc2 関数がたまたまカウンタ 100 の値を読み取ったと仮定すると、結果は次のようになります。カウンタは 200 になります (図 10-8):

    このバグを回避するには、関数単位やC言語ソースコード単位でのスレッド切り替えを禁止するロック方式を使用することができます。ロックを行うと、特定のスコープ内の処理が完了するまで他の機能に処理が切り替わりません。なぜ MyFunc1 関数と MyFunc2 関数がロックされるのかについては、アセンブリ言語のソースコードを理解していないと理解できないでしょう。 

終わり

  この章の内容は比較的長いですが、一般的に、コード例を示しながらアセンブリ言語について一般的に説明されています。アセンブリ言語と CPU 上で実行されるローカル コードの間には 1 対 1 の対応があるため、アセンブリ言語を理解することは、プログラムの実行フローを理解するのに非常に役立ちます。次の一節は著者の原文の翻訳です。

    アセンブリ言語の経験がないプログラマーは、車の運転方法だけを知っていて、車の構造を理解していないドライバーと同じです。このようなドライバーの場合、車が故障したり挙動がおかしくなった場合、自分では原因を見つけることができません。車の構造を理解していないと、運転中にガソリンを無駄に消費してしまう可能性があります。この場合、彼はプロのドライバーとしての資格はありません。対照的に、アセンブリ言語の経験のあるプログラマーは、コンピューターとプログラムの仕組みを理解しているドライバーと同等であり、問​​題を自分で解決できるだけでなく、運転中の燃料を節約することもできます。

    とても鮮やかな感じがします。この章では、プログラムがどのように実行されるのかについてもさらに深く理解することができました。この本の読書メモは以上です。最も重要な 5 つの章を紹介しました。多くのことを学びました。プログラムの基礎となる原則についての理解が深まりました。書籍『How Programs Run』の読書メモが完成しました!将来的には、基本的なコンピューターの本も追加する可能性があります。

    また、レッドチームターゲティングの分析やペネトレーションテストに関する技術共有なども更新していきますので、読者の皆様のご支援を心より願っております。 

おすすめ

転載: blog.csdn.net/Bossfrank/article/details/133495954
おすすめ