自分でOSを1から作ってみる Advent Calendar 2024の4日目です。
CPUの主な構成要素
CPUは主に下の4つの部分から構成されています。
制御ユニット
制御ユニットは、CPUの命令実行プロセス全体を管理する役割を担っています。プログラムカウンタから次の命令を取り出し、命令をデコードして、それに従って適切な信号を他のユニットに送ります。制御ユニットは、以下の役割を通じてCPUの命令処理を円滑に進めます。
命令フェッチ
命令が格納されているメモリから次に実行する命令を取り出します。
デコード
命令を解釈し、実行に必要な操作を特定します。例えば、足し算命令ならALUを起動し、レジスタにある2つの値を加算するように指示します。
実行制御
ALUやレジスタに指示を出し、命令の実行を行います。また、ジャンプ命令や条件分岐命令などにより、プログラムの実行フローを制御します。
アセンブリの例として、JMP命令(ジャンプ命令)やCALL命令(サブルーチンの呼び出し)を解釈し、プログラムカウンタの値を変えて実行順序を変えることも、制御ユニットの働きです。
演算ユニット
演算ユニット(ALU)は、算術演算や論理演算を行う部分で、CPUの計算を担う装置です。ALUは以下のような操作を行います。
算術演算:ADD、SUB、MUL、DIVなどの算術演算を行い、加算・減算・乗算・除算などを実行します。
論理演算:AND、OR、XORなどのビット単位の論理演算を行います。
比較演算:CMP命令などを通じて、値を比較し、条件付きジャンプの判断材料を提供します。
ALUでの演算は、通常レジスタに格納されているデータに対して行われ、結果も再びレジスタに格納されます。
レジスタ
レジスタは、CPU内部にある高速のデータ保存装置です。レジスタは一時的にデータを保存し、ALUや制御ユニットと直接やり取りを行うため、メモリよりも非常に高速にデータを読み書きできます。CPUには複数のレジスタがあり、用途によって以下のように分類されます。
汎用レジスタ
データの計算や操作に使われる基本的なレジスタです。x86系ではAX、BX、CX、DXなどがこれです。
インデックスレジスタ
アドレス計算に使用されるレジスタで、例えばSIやDIがあり、配列操作や文字列操作に便利です。
フラグレジスタ
比較演算などの結果を保存するためのレジスタで、例えばゼロフラグ(演算結果がゼロなら1になる)やキャリーフラグ(桁あふれが発生した場合に1になる)などのフラグが含まれます。
プログラムカウンタ
次に実行する命令のアドレスを保持し、制御ユニットが使用します。
キャッシュメモリ
キャッシュメモリは、CPUとメインメモリ間で頻繁に使用されるデータや命令を一時的に保存する高速メモリです。キャッシュは、メモリアクセスを高速化し、CPUが高いパフォーマンスで動作できるようにサポートします。
キャッシュメモリには以下の階層があり、それぞれアクセス速度が異なります。
L1キャッシュ:CPUのコアごとに備わっている最も高速なキャッシュ。容量は少ないですが、アクセス速度が速いです。
L2キャッシュ:L1キャッシュよりもやや大容量で、コアごと、または複数コアで共有される場合もあります。
L3キャッシュ:CPU全体で共有され、L2キャッシュよりも大容量ですがアクセス速度はやや遅くなります。
キャッシュには「ヒット」と「ミス」があり、ヒットが発生するとキャッシュからデータを取得するため非常に高速です。ミスが発生した場合、データはメインメモリから取り出す必要があるため、やや遅くなります。
キャッシュはハードウェアによって自動的に管理されるため、アセンブリ命令で直接操作することはありませんが、アクセスパターンを意識することでキャッシュヒット率を高めることが可能です。
アセンブリ言語の命令の説明
前の項でCPUについて説明したので、アセンブリ言語の各命令について、それぞれCPU内部では何が行われているのか説明していこうと思います。アセンブリがわかる人は全く読む必要ないと思います。
MOV命令
MOV命令は、データを一つの場所(レジスタやメモリ)から別の場所にコピーするための命令です。
MOV AX, BX ; BXの内容をAXにコピー
例えばこの例では、BXレジスタの値がAXレジスタにコピーされ、CPU内で実行されるとき、レジスタ間でデータが転送されます。
ADD命令
ADD命令は、2つの値を加算してその結果をレジスタやメモリに格納します。
ADD AX, BX ; AXにBXの内容を加算し、結果をAXに格納
この例では、ALUがAXとBXの値を加算し、その結果をAXに格納します。
CPU内部での動作(ちょっと冗長な説明なので格納しました)
フェッチ: 命令を制御ユニットが取り出します。
デコード: 命令をデコードし、どのレジスタに加算するかを判断します。
実行: ALUが起動し、指定された2つの値を加算します。このとき、キャリーフラグも更新されます。
結果格納: 結果がレジスタやメモリに格納されます。
SUB命令
SUB命令は、2つの値を減算してその結果をレジスタやメモリに格納します。
SUB AX, 1 ; AXから1を引き、結果をAXに格納
この例では、AXの内容から1を引き、結果をAXに戻します。ALUが減算を行い、フラグレジスタに計算結果の状態が反映されます。
CPU内部での動作(ちょっと冗長な説明なので格納しました)
フェッチ: 制御ユニットが命令を取得します。
デコード: 命令を解釈し、どの値を引くかを判断します。
実行: ALUが呼び出され、減算を行い、結果をレジスタに格納します。ゼロフラグ、符号フラグ、キャリーフラグなどが設定されます。
結果格納: 結果が指定のレジスタに格納されます。
CMP命令
CMP命令は、2つの値を比較し、その結果をフラグレジスタに設定します。これは条件分岐の判断に使用される命令です。
CMP AX, BX ; AXとBXを比較
この例では、ALUがAXとBXを引き算し、ゼロフラグや符号フラグを更新します。ゼロフラグが1ならば2つの値が等しいことを示します。
CPU内部での動作(ちょっと冗長な説明なので格納しました)
フェッチ: 制御ユニットが命令を取得します。
デコード: 命令をデコードして、比較する2つの値を決定します。
実行: ALUが2つの値を引き算し、結果をフラグレジスタに反映します(実際には値を格納せず、フラグだけ更新されます)。
結果: 計算の結果、ゼロフラグや符号フラグが更新され、次の条件付き命令(JE、JGなど)で使用されます。
JMP命令
JMP命令は、プログラムカウンタ(PC)を指定したアドレスに設定し、プログラムの実行フローを変更します。
JMP 0x2000 ; アドレス0x2000にジャンプ
この命令が実行されると、次の命令がアドレス0x2000から読み込まれます。
CPU内部での動作(ちょっと冗長な説明なので格納しました)
フェッチ: 命令を取得し、デコードします。
デコード: 命令がジャンプ命令であることを認識し、ジャンプ先のアドレスを取得します。
実行: 制御ユニットがプログラムカウンタをジャンプ先のアドレスに設定し、次の命令からそのアドレスで実行を再開します。
CALL命令とRET命令
CALL命令はサブルーチン(関数)を呼び出す命令で、現在のプログラムカウンタの値をスタックに保存してからジャンプを行います。RET命令はスタックからプログラムカウンタを復元して、サブルーチンから復帰します。
CALL my_function ; サブルーチンmy_functionを呼び出し
...
my_function:
; サブルーチンの処理
RET ; 呼び出し元に戻る
この例では、CALL命令によりプログラムカウンタがサブルーチンのアドレスにジャンプし、RET命令により呼び出し元に戻ります。
CPU内部での動作(ちょっと冗長な説明なので格納しました)
CALL命令
フェッチとデコードで、サブルーチンアドレスを取得します。
スタック操作: 現在のプログラムカウンタの値をスタックにプッシュします。
ジャンプ: プログラムカウンタをサブルーチンのアドレスに設定します。
RET命令
スタック操作: スタックから戻りアドレスをプログラムカウンタにポップします。
実行再開: プログラムカウンタが復元されるので、呼び出し元に戻って実行が続行されます。
PUSHとPOP命令
PUSH命令はデータをスタックに保存し、POP命令はスタックからデータを取り出します。スタックはLIFO(後入れ先出し)構造を持ち、関数の引数やローカル変数の保存に使われます。
PUSH AX ; AXの内容をスタックにプッシュ
POP BX ; スタックから値をポップしてBXに格納
この例では、PUSHでAXの内容がスタックに格納され、POPでその内容がBXに読み込まれます。
CPU内部での動作(ちょっと冗長な説明なので格納しました)
PUSH
スタックポインタをデクリメントしてから、データをその位置に格納します。
POP
スタックからデータを取り出し、スタックポインタをインクリメントして元の位置に戻します。
これらの命令を通して、CPUは基本的な演算、データの転送、プログラムの流れの制御などを実行します。ちょっと説明が多くなってしまったので、明日はまた開発を再開したいと思います。