仕様5・6・7の実装例
仕様5 「四則演算(加算、減算、乗算、除算)機能をつける」
仕様6 「インクリメント、デクリメント、Moveが行えるようにする」
仕様7 「指定した機械語が確認できるようにする」
仕様5と6は、これまでの回でやってきたことで、仮想CPUへ機械語を命令しその結果を受け取ることにより実現できます。
この仕様については、IntelCpuオブジェクトの動きを管理するオブジェクトを作る方法などがありますが、今回筆者は生産効率を考えて、発行した命令を表すCommandオブジェクトを実装する方法を選びました。その理由は、他の仕様も視野に入れて検討した結果、集中管理するのはコストがかかるので、命令そのものをオブジェクト捉えて自身を管理させたらいいと考えたからです。
筆者が考えたCommandオブジェクトは、命令を表すオブジェクトであり、自身だけを把握しています。文章だけでは分かり辛いと思いますのでコードを提示した後詳しい解説を行います。
Imports System.Text Imports VirtualCPU '機械語命令を表すオブジェクト Public Class Command Private m_cpu As IntelCpu Private builder As StringBuilder 'このオブジェクトを文字列として表す時に使用します '命令の名前 Private m_cmd As CommandName Public Property Name() As CommandName Get Return Me.m_cmd End Get Set(ByVal value As CommandName) Me.m_cmd = value End Set End Property '処理対象とするレジスタ Private m_destination As RegisterName Public Property Destination() As RegisterName Get Return Me.m_destination End Get Set(ByVal value As RegisterName) Me.m_destination = value End Set End Property 'ソースオペランド Private m_source As RegisterName Public Property Source() As RegisterName Get Return Me.m_source End Get Set(ByVal value As RegisterName) Me.m_source = value End Set End Property '指定する値 Private m_value As UInteger Public Property Value() As UInteger Get Select Case Me.m_source Case RegisterName.IMM Return Me.m_value End Select End Get Set(ByVal value As UInteger) Select Case Me.m_source Case RegisterName.IMM Me.m_value = value End Select End Set End Property 'コンストラクタ共通の処理 Private Sub New(ByRef cpu As IntelCpu, ByVal name As CommandName, ByVal destination As RegisterName) Me.builder = New StringBuilder() Me.m_cpu = cpu Me.m_cmd = Name Me.m_destination = Destination End Sub 'ソースにレジスタを指定してインスタンスを生成する Sub New(ByRef cpu As IntelCpu, ByVal name As CommandName, ByVal destination As RegisterName, ByVal source As RegisterName) Me.New(cpu, name, destination) Me.m_source = source End Sub 'ソースに即値を指定してインスタンスを生成する Sub New(ByRef cpu As IntelCpu, ByVal name As CommandName, ByVal destination As RegisterName, ByVal source As UInteger) Me.New(cpu, Name, destination) Me.m_value = source Me.m_source = RegisterName.IMM End Sub '指定された命令を表す文字列を返します Public Overrides Function ToString() As String '初期化 builder.Remove(0, builder.Length) 'オペコード ディスティネーション ソース の形式で文字列を返します。 'ただし、即値が指定された場合はソースは値、それ以外はレジスタの名前とします。 'また、INC命令のようにソースがない場合は表示しません。 If Me.m_cmd = CommandName.Subtract Then 'SUB命令の場合は直接指定する builder.Append("SUB") Else builder.Append(Me.m_cmd.ToString().ToUpper()) End If Dim length As Integer = IntelCpu.GetCommandLength(Me.m_cmd) builder.Append(" ") builder.Append(Me.m_destination.ToString()) If length > 1 Then builder.Append(" ") If Me.m_source = RegisterName.IMM Then builder.Append(Me.m_value.ToString()) Else builder.Append(Me.m_source.ToString()) End If End If '作成した文字列を返す Return Me.builder.ToString() End Function 'バイナリ表現でこのオブジェクトのインスタンスを返します Public Function ToBinary() As Byte() '準備 Dim binary(4) As Byte 'コマンドとディスティネーションをバイナリ化 binary(0) = IntelCpu.GetBinary(Me.m_cmd, Me.m_destination) 'ソース部分をバイナリ化 Dim length = IntelCpu.GetCommandLength(Me.m_cmd) If length > 1 Then If Me.m_source = RegisterName.IMM Then '即値をバイナリ化する Dim values As Byte() = IntelCpu.DivisionValue(Value) values = IntelCpu.GetLittleEndianValue(values) For i As Integer = 0 To 3 binary(i) = values(i) Next Else 'レジスタが指定されているのでModR/Mのバイナリを取得する ReDim Preserve binary(1) binary(1) = IntelCpu.GetModRMBinary(Me.m_cmd, Me.m_source) End If Else ReDim Preserve binary(0) End If '最終結果を返す Return binary End Function End Class
少し長いコードですが、実装そのものはさほど難しくありません。初心者の方も安心して読み進めてください。
まずプロパティについてですが、機械語命令が持つ情報である、命令の名前、ディスティネーション、ソース(レジスタの場合使用)、ソースの即値です。各情報については、これまでの連載で説明したので特に違和感を感じないと思います。
ToStringメソッドとToBinaryメソッドについては説明が必要だと思いますので詳細に説明します。
ToStringメソッドはCommandListのItemsに追加したときに分かりやすく表示するために作りました。すなわち、このメソッドの内容が、命令の一覧が表示される場所に表示されるのです。例えば、「MOV EAX, 0」のようになります。 このメソッドで行っていることは、先ほどの例のような文字列を構築しているだけです。もし分からない場合は、MSDNなどでStringBuilderクラスについて調べてみてください。このメソッドで行っていることがよく分かります。
ToBinaryメソッドはIntelCpuクラスを使用して、指定された機械語命令をバイナリ形式に変換しています。今までの連載を通じて作成してきたTestDriverプロジェクトの「機械語実行に関連するメソッド」と同じ処理を行っています。実はこのメソッドは後で処理を追加していきますので、その時改めて説明します。
これで準備ができたので、MOVなどのボタンが押された時の処理を実装します。これらの命令系ボタンは個別に実装しても良いのですが、それでは効率が悪いので同じメソッドを共有することにします。コードは次のようになります。
'機械語駆動式関数電卓を正常に動作させるための下準備を行います Private Sub Calculator_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load '各種変数の初期化を行う cpu = New IntelCpu() Me.InitRegisters() 'コントロールの準備 RegisterCombo.SelectedIndex = 0 ValueCombo.SelectedIndex = 0 NumberButtons = New List(Of Button)() NumberButtons.AddRange(New Button() {NumberButton0, NumberButton1, _ NumberButton2, NumberButton3, NumberButton4, NumberButton5, _ NumberButton6, NumberButton7, NumberButton8, _ NumberButton9, NumberButtonA, NumberButtonB, _ NumberButtonC, NumberButtonD, NumberButtonE, NumberButtonF}) '10進数を標準にする DecimalOption_CheckedChanged(Nothing, EventArgs.Empty) '各命令系ボタンにコマンド名を設定する Me.AddButton.Tag = CommandName.Add Me.IncButton.Tag = CommandName.Inc Me.SubButton.Tag = CommandName.Subtract Me.DecButton.Tag = CommandName.Dec Me.MulButton.Tag = CommandName.Mul Me.DivButton.Tag = CommandName.Div 'イベントの設定 AddHandler cpu.RegisterValueChanged, AddressOf RegisterValueChanged End Sub '各機械語命令に関するボタンが押された時のイベントです Private Sub CommandButtons_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles IncButton.Click, SubButton.Click, MulButton.Click, MovButton.Click, DivButton.Click, DecButton.Click, AddButton.Click '押されたボタンに対応する機械語命令を導出 Dim name As CommandName = CType(CType(sender, Control).Tag, CommandName) 'エラーチェック If Me.ValueCombo.SelectedIndex = 0 And IntelCpu.IsImm(name) = False Then MessageBox.Show(name.ToString().ToUpper() & "命令は即値を指定できません。", "操作エラー", _ MessageBoxButtons.OK, MessageBoxIcon.Error) Exit Sub End If 'ディティネーション・レジスタを決める Dim reg As RegisterName = CType(Me.RegisterCombo.SelectedIndex, RegisterName) '命令を生成する Dim cmd As Command If ValueCombo.SelectedIndex = 0 Then 'ソースとして即値が指定された場合 cmd = New Command(Me.cpu, name, reg, Me.inputValue) Else 'ソースとしてレジスタが指定された場合 '※レジスタは0は即値なので1を引いている cmd = New Command(Me.cpu, name, reg, CType(Me.ValueCombo.SelectedIndex - 1, RegisterName)) End If '命令を実行する If Me.StepExeMenu.Checked = True Then '最終命令を即実行 Me.cpu.ReadBinary(cmd.ToBinary()) Me.cpu.AnalyzeBinary() Try Me.cpu.ExecuteCommand() Catch zero As DivideByZeroException MessageBox.Show("0で除算してしまいました", "演算エラー", _ MessageBoxButtons.OK, MessageBoxIcon.Error) Exit Sub End Try End If '命令を記録/表示する(仕様7はこれでOK) Me.CommandList.Items.Add(cmd) End Sub 'IntelCpuクラスの新メソッド '即値が指定が可能なのか判定します Public Shared Function IsImm(ByVal cmd As CommandName) As Boolean If cmd = CommandName.Mul OrElse cmd = CommandName.Div Then Return False End If Return True End Function
では上記実装の説明を行います。
まず各命令ボタンで同じメソッドを使用するために、ButtonクラスのTagプロパティを有効活用しています。このアプリケーションが起動したとき(Loadイベント)の処理として、各ボタンのTagプロパティに対応する命令を割り当てておきます。
次に、クリックイベントの冒頭で、Tagプロパティから対応する命令の情報を取り出すことにより、何のボタンがクリックされたのか分かる仕組みにしています。
このようなTagプロパティを使った処理は、業務アプリではよくあることなので、ぜひ覚えて置いてください。役に立つはずです。
これでどの命令が指定されたのか判別できるようになったので、次にエラーチェックを行います。これが必要な理由は、INCとDEC命令では1で固定なので即値が指定できないからです。ですから、操作している人が混乱しないようにエラーメッセージを出すようにしました。
その次に、ディスティネーションアドレス(処理対象となるレジスタ)を決めてから、実行命令に関する情報を整理しています。この処理については先ほど説明したCommandクラスに処理を任せているだけです。
ここまでくれば、後は命令を実行してその命令を記録するだけです。この2つの処理は簡単ですが、例外に気をつけてください。DIV命令でソースで0が指定されたときDivideByZeroException例外が発生します。この例外を捕捉しておかないと、アプリケーションが異常な状態になりますので、必ずチェックしてください。
以上で仕様5、6、7の実装は終わりです。次は仕様8の実装を解説します。
