仕様2と3の実装
機械語駆動式関数電卓で一番難しいのがこの2つの仕様です。始めに読みやすいように使用を再掲します。
仕様2 「指定する値と保存場所にレジスタを指定できるようにする」
仕様3 「レジスタの他に自由な値を指定できるようにする」
この2つの仕様を実現するためには、各機械語命令のソースにレジスタを指定できるようにしなければなりません。そうしないと例外が発生して正常に実行できません。
ではどうすればいいのかと言うと、連載を通じて行ってきたように機械語を実装すれば良いのです。この時、Intelマニュアルのオペコードマップに載っていないときもあります。その場合は、NASMやMASMなどのアセンブラ言語のオプションを使って機械語を見るか、IA -32 インテルR アーキテクチャ・ソフトウェア・デベロッパーズ・マニュアル、中巻:命令セット・リファレンス・マニュアルの命令の説明を参照します。
実際にやってみます。読者の皆さんも私と一緒にやってみてください。ひとまずMOV命令でディティネーションとソースがレジスタの場合を調べます。インテルマニュアル中間のMOV命令の説明を見ると、「89 /r MOV r/m32,r32 r32 をr/m32 に転送する」(インテルマニュアルから抜粋)となっています。この記述により、MOVでソースにレジスタを指定する方法は、89+ModR/Mであることが分かります。この知識を元に、GetBinaryメソッドと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)
'行と列から命令を割り出す
result.NextInfo = InfoType.Value
result.State = OpeCodeState.Fixed
Select Case row
Case 8
Select Case col
Case 9
result.Name = CommandName.Mov
result.NextInfo = InfoType.Register
result.BitCount = 32
Case Else
Throw New ArgumentException("行" & row & "列" & col & "には対応しておりません。")
End Select
Case Else
Throw New ArgumentException("行" & row & "列" & col & "には対応しておりません。")
End Select
'命令長設定
result.Length = GetCommandLength(result.Name)
'結果を返す
Return result
End Function
'指定された命令と対象レジスタから指定するべきバイナリを導出します
Public Shared Function GetBinary(ByVal cmd As CommandName, ByVal reg As RegisterName) As Byte
Dim result As Byte = 0
'オペコードマップの行と列の値を決定
Select Case cmd
Case CommandName.Mov
result = 11 << 4
Select Case reg
Case RegisterName.EAX, RegisterName.AX
result += 8
Case RegisterName.EBX, RegisterName.BX
result += 11
Case RegisterName.ECX, RegisterName.CX
result += 9
Case RegisterName.EDX, RegisterName.DX
result += 10
Case RegisterName.AH
result += 4
Case RegisterName.AL
result += 0
Case RegisterName.BH
result += 7
Case RegisterName.BL
result += 3
Case RegisterName.CH
result += 5
Case RegisterName.CL
result += 1
Case RegisterName.DH
result += 6
Case RegisterName.DL
result += 2
Case RegisterName.ModRMTo32Bit
result = 8 << 4
result += 1
Case Else
Throw New ArgumentException("命令" & cmd.ToString() & "は" & _
reg.ToString() & "レジスタをサポートしておりません。")
End Select
End Select
'結果を返す
Return result
End Function
'レジスタを表す列挙体
Public Enum RegisterName
EAX
EBX
ECX
EDX
AX
BX
CX
DX
AH
AL
BH
BL
CH
CL
DH
DL
IMM '即値
ModRMTo32Bit '32ビットのレジスタをModR/Mで決定する
End Enum
Public Enum InfoType
Value
Register
RegisterAndImm
End Enum
両メソッドは基本的に、Select Case部分の項目を増やしただけで行っていることは今までと同じです。しかし、今までにない「ソースとしてレジスタを指定する」方式に対応するために、RegisterNameとInfoTypeに新しい値を追加しました。この新しい情報は解析処理を行うAnalyzeBinaryメソッドで使用します。このメソッドはかなり修正が必要なので、修正部分を掲載した後、詳しく説明します。
'読み込まれたバイナリ値を解析して実行する命令を特定します。 Public Sub AnalyzeBinary() Dim binary As Byte While binarys.Count <> 0 '指定されている命令とレジスタを決定 binary = binarys.Dequeue() Dim ope As OpeCode = SearchOpeCodeMap(binary) '値を導出 Dim modrm As ModRM If ope.Length > 1 Then 'オペコードの次にある情報に適した処理を行う Select Case ope.NextInfo Case InfoType.Register 'ModR/Mバイトが続いている場合 modrm = New ModRM(Me.binarys.Dequeue()) 'ディスティネーションとソースを決定する If ope.Name = CommandName.MulOrDiv Then 'MUL命令もしくはDIV命令の場合 Dim reg As RegisterName If ope.BitCount = 32 Then reg = GetRegister32Name(modrm.RMValue) ope.Value = New Register(Me.GetRegister32Value(reg)) ElseIf ope.BitCount = 8 Then reg = GetRegister8Name(modrm.RMValue) ope.Value = New Register(Me.GetRegisterByteValue(reg)) End If ope.Source = reg 'MUL命令なのかDIV命令なのかを判別する Select Case modrm.RegOrOpeValue Case 4 ope.Name = CommandName.Mul Case 5 ope.Name = CommandName.Div Case Else Throw New ArgumentOutOfRangeException( _ "ModR/MのRegOrOpeValueフィールドには" & _ modrm.RegOrOpeValue & "を指定しないでください") End Select Else 'ディスティネーションで指定されたレジスタを取得 If ope.BitCount = 32 Then ope.Destination = GetRegister32Name(modrm.RMValue) ElseIf ope.BitCount = 8 Then ope.Destination = GetRegister8Name(modrm.RMValue) End If 'ソースで指定されたレジスタから値を取得 Dim reg As RegisterName If ope.BitCount = 32 Then reg = GetRegister32Name(modrm.RegOrOpeValue) ope.Value = New Register(Me.GetRegister32Value(reg)) ElseIf ope.BitCount = 8 Then reg = GetRegister8Name(modrm.RegOrOpeValue) ope.Value = New Register(Me.GetRegisterByteValue(reg)) End If ope.Source = reg End If '今後の拡張のために確定したことを記録する ope.State = OpeCodeState.Fixed End Select End If '実行する命令を記憶 cmds.Enqueue(ope) End While End Sub
少々長いですが、やっていること自体は簡単です。レジスタ部分がModR/Mに指定されている状態なので、ModR/Mのバイトを取得し、R/Mフィールドからディスティネーションとなるレジスタ、レジスタ/オペコード・フィールドからソースとなるレジスタを取り出しているだけです。
一つ注意が必要なのは、ソースからさらにレジスタの値を取得する必要があることです。この処理をしておくことによりソースを意識せずに済むので、機械語命令様メソッド(MovやAdd)の実装が簡単になります。これで、一先ずMOV命令を指定できるようになりました。
同様の手順をADDとSUBに行うことで、ソースにレジスタを指定することができます。しかし、MUL命令とDIV命令はできません。この2つの命令は、普段はEAXに固定されていますので、それを変更するとなると、ModR/Mと即値の両方が必要となります。それで、次の修正を行われなければなりません。
'読み込まれたバイナリ値を解析して実行する命令を特定します。 Public Sub AnalyzeBinary() Dim binary As Byte While binarys.Count <> 0 '指定されている命令とレジスタを決定 binary = binarys.Dequeue() Dim ope As OpeCode = SearchOpeCodeMap(binary) '値を導出 Dim modrm As ModRM If ope.Length > 1 Then 'オペコードの次にある情報に適した処理を行う Select Case ope.NextInfo Case InfoType.RegisterAndImm 'ModR/Mと即値が続いている場合 'ModR/Mのディスティネーションで指定されたレジスタを取得 modrm = New ModRM(Me.binarys.Dequeue()) If ope.BitCount = 32 Then ope.Destination = GetRegister32Name(modrm.RMValue) ElseIf ope.BitCount = 8 Then ope.Destination = GetRegister8Name(modrm.RMValue) End If 'ソースとなる即値をバイナリから取得 ope.Source = RegisterName.IMM If ope.BitCount = 32 Then Dim values(3) As Byte For i As Integer = 0 To 3 values(i) = Me.binarys.Dequeue() Next 'Intelはリトルエンディアンなので値を逆にする values = GetLittleEndianValue(values) '取得したバイト配列からダブルワードの値を構築 Dim value As UInteger = ConvertValues(values) ope.Value = New Register(value) Else ope.Value = New Register(binarys.Dequeue(), ope.IsHi) End If 'Add命令なのかSub命令なのかを判別する Select Case modrm.RegOrOpeValue Case 0 ope.Name = CommandName.Add Case 5 ope.Name = CommandName.Subtract Case Else Throw New ArgumentOutOfRangeException( _ "ModR/MのRegOrOpeValueフィールドには" & _ modrm.RegOrOpeValue & "を指定しないでください") End Select End Select End If '実行する命令を記憶 cmds.Enqueue(ope) End While End Sub
一見難しく見えますが、やっていることはModR/Mと即値のバイトを順番に取得しているだけです。今まで解析した命令事の応用と言えるでしょう。すなわち、InfoType.Valueでの処理とInfoType.Registerでの処理を足しただけなのです。
プログラミングはこのように同じことの組み合わせが多々ありますので、初心者の方もとにかくプログラミングを多くすることにより、自然と大きなソフトが実装できるようになっていきます。これで命令を指定できるようになりました。次は命令を指定するCommandクラスの修正を行います。このクラスはToBinaryメソッドを修正することによりソースにレジスタを指定できるようになります。
'バイナリ表現でこのオブジェクトのインスタンスを返します Public Function ToBinary() As Byte() '準備 Dim binary(4) As Byte 'コマンドとディスティネーションをバイナリ化 If Me.m_cmd = CommandName.Mul OrElse Me.m_cmd = CommandName.Div OrElse _ Me.m_cmd = CommandName.Inc OrElse Me.m_cmd = CommandName.Dec OrElse _ Me.m_source = RegisterName.IMM Then binary(0) = IntelCpu.GetBinary(Me.m_cmd, Me.m_destination) Else binary(0) = IntelCpu.GetBinary(Me.m_cmd, RegisterName.ModRMTo32Bit) End If 'ソース部分をバイナリ化 Dim length = IntelCpu.GetCommandLength(Me.m_cmd) If length > 1 Then If Me.m_source = RegisterName.IMM Then '即値の位置を修正するための変数 Dim addIndex As Integer = 1 'ModR/Mが先に必要な場合は追加する If IntelCpu.IsModRM(Me.m_cmd, Me.m_destination) = True Then ReDim Preserve binary(5) addIndex = 2 binary(1) = IntelCpu.GetModRMBinary(Me.m_cmd, Me.m_destination) End If '即値をバイナリ化する Dim values As Byte() = IntelCpu.DivisionValue(Value) values = IntelCpu.GetLittleEndianValue(values) For i As Integer = 0 To 3 binary(i + addIndex) = values(i) Next Else 'レジスタが指定されているのでModR/Mのバイナリを取得する ReDim Preserve binary(1) If Me.m_cmd = CommandName.Mul OrElse Me.m_cmd = CommandName.Div Then binary(1) = IntelCpu.GetModRMBinary(Me.m_cmd, Me.m_source) ElseIf Me.m_source = RegisterName.IMM Then binary(1) = IntelCpu.GetModRMBinary(Me.m_cmd, Me.m_destination) Else binary(1) = IntelCpu.GetModRMBinary(Me.m_cmd, Me.m_destination, Me.m_source) End If End If Else ReDim Preserve binary(0) End If '最終結果を返す Return binary End Function 'ModR/Mが必要な命令かを判定します Public Shared Function IsModRM(ByVal cmd As CommandName, ByVal reg As RegisterName) '即値なので必要なし If reg = RegisterName.IMM Then Return False End If '既に必要だと明示されているのでTrue If reg = RegisterName.ModRMTo32Bit Then Return True End If '命令名とレジスタ名から必要性を判定 Dim result As Boolean = False Select Case cmd Case CommandName.Add If Not (reg = RegisterName.EAX OrElse reg = RegisterName.AL) Then result = True End If Case CommandName.Subtract If Not (reg = RegisterName.EAX OrElse reg = RegisterName.AL) Then result = True End If End Select Return result End Function 'GetBinaryに関連するIntelCpu側の修正 'ModR/Mのバイナリを取得します Public Shared Function GetModRMBinary(ByVal cmd As CommandName, ByVal reg As RegisterName) As Byte Dim result As ModRM = New ModRM(0) result.ModValue = 3 Select Case cmd Case CommandName.Add result.RegOrOpeValue = 0 Case CommandName.Subtract result.RegOrOpeValue = 5 Case CommandName.Mul result.RegOrOpeValue = 4 Case CommandName.Div result.RegOrOpeValue = 5 Case Else Throw New ArgumentOutOfRangeException(cmd.ToString() & "はサポートしていません。") End Select Dim bit As Byte = GetRegisterBit(reg) If bit = 8 Then result.RMValue = GetRegisterByteModRMValue(reg) ElseIf bit = 32 Then result.RMValue = GetRegister32ModRMValue(reg) Else Throw New ArgumentOutOfRangeException(cmd.ToString.ToUpper() & _ "では16ビットのレジスタを指定できません。") End If Return result.Binary End Function 'ModR/Mのバイナリを取得します Public Shared Function GetModRMBinary(ByVal cmd As CommandName, _ ByVal destination As RegisterName, ByVal source As RegisterName) As Byte '初期化処理 Dim result As ModRM = New ModRM(0) result.ModValue = 3 'エラー判定 If cmd = CommandName.Mul OrElse cmd = CommandName.Div OrElse _ cmd = CommandName.Inc OrElse cmd = CommandName.Dec Then Throw New ArgumentOutOfRangeException(cmd.ToString() & "はサポートしていません。") End If 'ディスティネーションのバイナリを取得 Dim bit As Byte = GetRegisterBit(destination) If bit = 8 Then result.RMValue = GetRegisterByteModRMValue(destination) ElseIf bit = 32 Then result.RMValue = GetRegister32ModRMValue(destination) Else Throw New ArgumentOutOfRangeException("ディスティネーションに16ビットのレジスタを指定できません。") End If 'ソースのバイナリを取得 bit = GetRegisterBit(source) If bit = 8 Then result.RegOrOpeValue = GetRegisterByteModRMValue(source) ElseIf bit = 32 Then result.RegOrOpeValue = GetRegister32ModRMValue(source) Else Throw New ArgumentOutOfRangeException("ソースに16ビットのレジスタを指定できません。") End If '結果を返す Return result.Binary End Function
今回のポイントとToBinaryの「ソース部分をバイナリ化」部分です。先ほど説明したように、ソースにレジスタが指定されている場合と、MUL命令に対する特別な配慮が必要な点に注意してプログラムを読んでください。そうすれば自ずと意味が分かると思います。
なお、IsModRMメソッドは、機械語に関する詳細な知識がなくてもIntelCpuオブジェクトが扱えるようにするためのものです。もしこのメソッドがなければ、各命令を考慮して一々ModR/Mの必要性について考えなくてはなりません。それでは非常に不便なのでこの様なメソッドを追加しておくとよいでしょう。
あと、GetModRMBinaryメソッドも修正されています。その理由は、ソースにレジスタを指定できるようにし、IntelCpuオブジェクトを扱いやすくするための配慮です。以上ですべての仕様の実装が終わりました。しかし、一つ不便なことがありますので追加しておきました。
