SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

DeveloperZine(デベロッパージン)- エンジニアの意思決定を支える技術情報メディア ProductZine

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

VB.NETで学ぶ機械語の基礎

VB.NETで仮想CPUを作ろう(14)
- 機械語駆動式 関数電卓を作ろう! 解答編(後半)

VB.NETで学ぶ機械語の基礎 第14回

ダウンロード VirtualCPU14.zip (475.5 KB)

仕様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メソッドを修正します。

MOV命令に関する修正部分の抜粋
    'オペコードマップを検索し、実行する命令と対象となるレジスタを探索する。
    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メソッドで使用します。このメソッドはかなり修正が必要なので、修正部分を掲載した後、詳しく説明します。

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と即値の両方が必要となります。それで、次の修正を行われなければなりません。

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.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メソッドを修正することによりソースにレジスタを指定できるようになります。

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オブジェクトを扱いやすくするための配慮です。以上ですべての仕様の実装が終わりました。しかし、一つ不便なことがありますので追加しておきました。

次のページ
終了時の問題

この記事は参考になりましたか?

VB.NETで学ぶ機械語の基礎連載記事一覧

もっと読む

この記事の著者

インドリ(インドリ)

分析・設計・実装なんでもありのフリーエンジニア。ブログ「無差別に技術をついばむ鳥(http://indori.blog32.fc2.com/)」の作者です。アドバイザーをしたり、システム開発したり、情報処理技術を研究したりと色々しています。座右の銘は温故知新で、新旧関係なく必要だと考えたものは全て学...

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

CodeZine(コードジン)
https://codezine.jp/article/detail/3425 2010/04/27 12:04

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー