はじめに
本記事はVB.NETの初歩的な記法だけを使って、簡単な機械語で動く仮想CPUの実装法を解説します(※CPUにもいろいろありますが、この記事ではIntel社が製造しているCPUを対象とします)。その過程を通じて、初心者でもバイナリプログラミングが楽しめることと、バイナリプログラミングの魅力を伝えたいと思っています。
前回 CPUの基礎動作を実装したので、今回は機械語の命令を実装していきます。
これまでの連載
- VB.NETで仮想CPUを作ろう
- VB.NETで仮想CPUを作ろう (2) - レジスタの実装
- VB.NETで仮想CPUを作ろう (3) - 仮想CPUのGUI化
- VB.NETで仮想CPUを作ろう (4) - テストドライバの改良
- VB.NETで仮想CPUを作ろう (5) - CPUの基礎動作の実装
下準備
前回の実装を拡張していくので、あらかじめ前回までの部分の実装を済ませておいてください。後は専門用語とCPU構造の確認のため、第1回で用意した用意した3つのIntel社のマニュアルをすぐ読める状態にしてください。
それに加えて、中巻 B: 命令セット・リファレンス N-ZのA-6とA-7ページを印刷しておくことをお勧めします。絶対に必要なことではありませんが、この表をあらかじめ印刷しておき、その紙を参照しながら実装作業をすると効率がよくなります。
MOV命令実装の大まかな流れ
基本的に、機械語命令の動きをシミュレートするには、前段階としてCPUの基礎動作である、命令読み込み・命令解析・実行を実装する必要があります。
ですが、前回 命令読み込み・命令解析・実行をしっかり実装したので、今回を行う作業はそれほど大変ではありません。
バイナリから命令を判別するためのSearchOpeCodeMap
メソッドと、実行部分を実装したExecuteCommand
メソッドに少しの変更を加えて、最後にMOV命令そのものを実装するためにMov
メソッドを追加するだけです。
SearchOpeCodeMapメソッドの変更点
このメソッドにMOV命令に関する処理を付け加える場所は、コメントの「行と列から命令を割り出す」の真下です。分かりやすいように前回の時点でのSearchOpeCodeMap
メソッドにコメントを追加して掲載します。
'オペコードマップを検索し、 '実行する命令と対象となるレジスタを探索する。 Private Shared Function SearchOpeCodeMap(ByVal binary As Byte) _ As OpeCode Dim result As OpeCode = New OpeCode() '指定されている命令を特定するために行と列を導出 Dim row As Byte = CByte((binary And &HF0) >> 4) Dim col As Byte = CByte(binary And &HF) '行と列から命令を割り出す 'ここにMOV命令に関する処理を追加 '結果を返す Return result End Function
ここに一度自分でプログラムを追加してみましょう。
ヒントはIA-32 インテル アーキテクチャー・ソフトウェア・デベロッパーズ・マニュアル、中巻 B: 命令セット・リファレンス N-ZのA-6とA-7ページです。自分でこの表からMOV命令を探し、その動作をプログラムで行わせるにはどうしたらいいのか一度考えてみるとよいでしょう。
筆者の回答例を提示するので、確認してください。
'行と列から命令を割り出す Select Case row Case 11 result.Name = CommandName.Mov Select Case col Case 0 result.Destination = RegisterName.AL result.BitCount = 8 Case 1 result.Destination = RegisterName.CL result.BitCount = 8 Case 2 result.Destination = RegisterName.DL result.BitCount = 8 Case 3 result.Destination = RegisterName.BL result.BitCount = 8 Case 4 result.Destination = RegisterName.AH result.BitCount = 8 result.IsHi = True Case 5 result.Destination = RegisterName.CH result.BitCount = 8 result.IsHi = True Case 6 result.Destination = RegisterName.DH result.BitCount = 8 result.IsHi = True Case 7 result.Destination = RegisterName.BH result.BitCount = 8 result.IsHi = True Case 8 result.Destination = RegisterName.EAX result.BitCount = 32 Case 9 result.Destination = RegisterName.ECX result.BitCount = 32 Case 10 result.Destination = RegisterName.EDX result.BitCount = 32 Case 11 result.Destination = RegisterName.EBX result.BitCount = 32 Case Else Throw New ArgumentException("行" & row _ & "列" & col & "には対応しておりません。") End Select Case Else Throw New ArgumentException("行" & row _ & "列" & col & "には対応しておりません。") End Select
正解を見て、意外と簡単だと思った人が多いでしょう。それでは解説します。
この部分でするべきことは、行と列の2つのニブルからMOV命令がどのレジスタを対象にしているのかと、そのレジスタが何ビットなのかをresult
変数に設定するだけです。
具体的には、IA-32 インテル アーキテクチャー・ソフトウェア・デベロッパーズ・マニュアル、中巻 B: 命令セット・リファレンス N-ZのA-6とA-7ページを見て、そこからMOV命令を探してください。11行目(16新表記でB)にMOV命令があります。これをプログラムで表現すると、次のようになります。
Select Case row Case 11 result.Name = CommandName.Mov
続いて、11行目の列から必要なレジスタ、AL・BL・CL・DL・AH・BH・CH・DHを探してください。A-6ページの0~7列で見つかるはずです。これをプログラムで表現すると、次のようになります。
Select Case col Case 0 result.Destination = RegisterName.AL result.BitCount = 8
次はA-7ページの11行目(B)を見てください。eAX・eCX・eDX・eBXとなっており、EAXなどの見慣れたレジスタが見当たりません。これはどういうことでしょうか?
それは、Intel社のCPUが以前16ビットだったことが原因です。16ビットの頃はAX・CX・DX・BXとなっていました。しかし、現在主流の32ビットCPUを販売する頃に、過去との互換性をもたせつつ機械語を設計し直す必要が出てきたため、Intelは、AX・CX・DX・BXの場所を16ビットCPUの場合はそのまま、32ビットCPUの場合はEAX・ECX・EDX・EBXと、CPUのビット数に合わせて変化させることを決定しました。
こうすることで、今までの16ビットCPU用機械語はそのまま動きますし、32ビットCPU用機械語は基本的に32ビットを扱うので両方の要望を満たせます。
今回は32ビットCPUを扱うので、eAXなどのレジスタは32ビットレジスタとみなしてください。そうするとプログラムは次のようになります。
Select Case col Case 8 result.Destination = RegisterName.EAX result.BitCount = 32 Case 9 result.Destination = RegisterName.ECX result.BitCount = 32 Case 10 result.Destination = RegisterName.EDX result.BitCount = 32 Case 11 result.Destination = RegisterName.EBX result.BitCount = 32
気になる人も多いと思うので、ここではBitCount
だけ説明しておきます。このプロパティはレジスタのビット数のことで、いろいろな場面で重要となるのでこの情報を保存しています。
これでMOV命令の情報をすべて設定できるので、次は実行部分であるExecuteCommand
メソッドにプログラムを追加します。