Engineering Note

プログラミングなどの技術的なメモ

Python x86エミュレータの作成(ModR/M)

cpu

本記事はPythonで簡単なx86エミュレータを作成します。

前回ではorg疑似命令によるプログラムの配置場所を指定するプログラムを作成しました。

今回はModR/Mを実装し、オペランドを柔軟に指定する方法について学んでいきます。

 

 

ModR/Mとは

前回では、org疑似命令によるプログラムの配置場所を指定する方法について学びました。

 

 

なお、前回まででは指定したレジスタに即値をコピーするような単純なことしかできませんでしたが、今回のModR/Mを実装することで、[esp-16]のようにオペランドを柔軟に指定することが可能となります。

 

ModR/Mは以下のような1バイトで表現されます。

 

fig1. ModR/M

上記のMod(上位2ビット)とR/M(下位3ビット)でアドレッシングモードを選びます。

また真ん中の3ビットはREGと呼ばれ、オペコードを拡張するために使われたり、8つある汎用レジスタから1つのレジスタを指定したりするために使用されます。

 

なお、使用するアセンブリ言語プログラムは、参考書籍と同じものを使用します。

 

;modrm-test.asm
BITS 32
    org 0x7c00
    sub esp, 16
    mov ebp, esp
    mov eax, 2
    mov dword [ebp+4], 5
    add dword [ebp+4], eax
    mov esi, [ebp+4]
    inc dword [ebp+4]
    mov edi, [ebp+4]
    jmp 0

 

Pythonによるスクリプトの作成

それでは、PythonでModR/Mに対応させ、併せて算術命令も実装していきます。

 

# emulator.py
class ModRM:
    def __init__(self):
        self.modrm = {
            "mod"       :0x00,
            "opecode"   :0x00,
            "reg_index" :0x00,
            "rm"        :0x00,
            "sib"       :0x00,
            "disp8"     :0x00,
            "disp32"    :0x00
        }

class Emulator:
    def __init__(self):
        self.register_name = ["EAX", "ECX", "EDX", "EBX", "ESP", "EBP", "ESI", "EDI"]
        self.registers = {
            "EAX": 0x00,
            "ECX": 0x00,
            "EDX": 0x00,
            "EBX": 0x00,
            "ESP": 0x00,
            "EBP": 0x00,
            "ESI": 0x00,
            "EDI": 0x00
            }
        self.eflags = None
        self.memory = None
        self.eip = None
        self.instructions = [None for i in range(256)]

    def init_instructions(self):
        self.instructions[0x01] = self.add_rm32_r32
        self.instructions[0x83] = self.code_83
        self.instructions[0x89] = self.mov_rm32_r32
        self.instructions[0x8b] = self.mov_r32_rm32
        for i in range(8):
            self.instructions[0xb8 + i] = self.mov_r32_imm32
        self.instructions[0xeb] = self.short_jump
        self.instructions[0xe9] = self.near_jump
        self.instructions[0xc7] = self.mov_rm32_imm32
        self.instructions[0xff] = self.code_ff

    def create_emu(self, size, eip, esp):
        self.eip = eip
        self.registers["ESP"] = esp
        self.memory = [0x00 for _ in range(size)]

    def dump_registers(self):
        for i in range(len(self.registers)):
            name = self.register_name[i]
            print("{} = 0x{:08x}".format(name, self.registers[name]))
        print("EIP = 0x{:08x}".format(self.eip))

    def mov_r32_imm32(self):
        reg = self.get_code8(0) - 0xb8
        value = self.get_code32(1)
        reg_name = self.register_name[reg]
        self.registers[reg_name] = value
        self.eip += 5
        if self.eip >= 0x100000000:
            self.eip ^= 0x100000000

    def short_jump(self):
        diff = self.get_sign_code8(1)
        if diff & 0x80:
            diff -= 0x100
        self.eip += (diff + 2)

    def get_code8(self, index):
        code = self.memory[self.eip + index]
        if not type(code) == int:
            code = int.from_bytes(code, 'little')
        return code

    def get_sign_code8(self, index):
        code =  self.memory[self.eip + index]
        code = int.from_bytes(code, 'little')
        return code & 0xff

    def get_code32(self, index):
        ret = 0x00
        for i in range(4):
            ret |= self.get_code8(index + i) << (i * 8)
        return ret

    def get_sign_code32(self, index):
        return  self.get_code32(index)

    def near_jump(self):
        diff = self.get_sign_code32(1)
        if diff & 0x80000000:
            diff -= 0x100000000
        self.eip += (diff + 5)

    def parse_modrm(self):
        m = ModRM()
        code = self.get_code8(0)
        m.modrm["mod"] = ((code & 0xc0) >> 6)
        m.modrm["opecode"] = m.modrm["reg_index"] = ((code & 0x38) >> 3)
        m.modrm["rm"] = code & 0x07

        self.eip += 1
        if (m.modrm["mod"] != 3 and m.modrm["rm"] == 4):
            m.modrm["sib"] = self.get_code8(0)
            eip += 1
        if (m.modrm["mod"] == 0 and m.modrm["rm"] == 5) or m.modrm["mod"] == 2:
            m.modrm["disp32"] = self.get_sign_code32(0)
            m.modrm["disp8"] = m.modrm["disp32"] & 0xff
            eip += 4
        elif m.modrm["mod"] == 1:
            m.modrm["disp8"] = m.modrm["disp32"] = self.get_sign_code8(0)
            self.eip += 1

        return m

    def mov_rm32_imm32(self):
        self.eip += 1
        m = self.parse_modrm()
        value = self.get_code32(0)
        self.eip += 4
        self.set_rm32(m, value)

    def set_rm32(self, m, value):
        if m.modrm["mod"] == 3:
            self.set_register32(m.modrm["rm"], value)
        else:
            address = self.calc_memory_address(m)
            self.set_memory32(address, value)

    def set_memory8(self, address, value):
        self.memory[address] = value & 0xff

    def set_memory32(self, address, value):
        for i in range(4):
            self.set_memory8(address+i, value >> (i*8))

    def calc_memory_address(self, m):
        if m.modrm["mod"] == 0:
            if m.modrm["rm"] == 4:
                print("not implemented ModRM mod = 0, rm = 4")
                sys.exit(0)
            elif m.modrm["rm"] == 5:
                return m.modrm["disp32"]
            else:
                return self.get_register32(m.modrm["rm"])
        elif m.modrm["mod"] == 1:
            if m.modrm["rm"] == 4:
                print("not implemented ModRM mod = 1, rm = 4")
                sys.exit(0)
            else:
                return self.get_register32(m.modrm["rm"]) + m.modrm["disp8"]
        elif m.modrm["mod"] == 2:
            if m.modrm["rm"] == 4:
                print("not implemented ModRM mod = 2, rm = 4")
                sys.exit(0)
            else:
                return self.get_register32(m.modrm["rm"]) + m.modrm["disp32"]
        else:
            print("not implemented ModRM mod = 3")
            sys.exit(0)

    def mov_rm32_r32(self):
        self.eip += 1
        m = self.parse_modrm()
        r32 = self.get_r32(m)
        self.set_rm32(m, r32)

    def mov_r32_rm32(self):
        self.eip += 1
        m = self.parse_modrm()
        rm32 = self.get_rm32(m)
        self.set_r32(m, rm32)

    def get_rm32(self, m):
        if m.modrm["mod"] == 3:
            return self.get_register32(m.modrm["rm"])
        else:
            address = self.calc_memory_address(m)
            return self.get_memory32(address)

    def get_memory8(self, address):
        return self.memory[address]

    def get_memory32(self, address):
        ret = 0
        for i in range(4):
            mem = self.get_memory8(address + i)
            if not type(mem) == int:
                mem = ord(mem)
            ret |= mem << (8*i)
        return ret

    def set_r32(self, m, value):
        self.set_register32(m.modrm["reg_index"], value)

    def get_r32(self, m):
        return self.get_register32(m.modrm["reg_index"])

    def add_rm32_r32(self):
        self.eip += 1
        m = self.parse_modrm()
        r32 = self.get_r32(m)
        rm32 = self.get_rm32(m)
        self.set_rm32(m, rm32 + r32)

    def sub_rm32_imm8(self, m):
        rm32 = self.get_rm32(m)
        imm8 = self.get_sign_code8(0)
        self.eip += 1
        self.set_rm32(m, rm32 - imm8)
        # print("0x{:08x}".format(self.registers["ESP"]))

    def code_83(self):
        self.eip += 1
        m = self.parse_modrm()
        if m.modrm["opecode"] == 5:
            self.sub_rm32_imm8(m)
        else:
            print("not implemented: 83 /{}".format(m.modrm["opecode"]))
            sys.exit(1)

    def inc_rm32(self, m):
        value = self.get_rm32(m)
        self.set_rm32(m, value + 1)

    def code_ff(self):
        self.eip += 1
        m = self.parse_modrm()

        if m.modrm["opecode"] == 0:
            self.inc_rm32(m)
        else:
            print("not implemented: FF /{}".format(m.modrm["opecode"]))
            sys.exit(1)

    def get_register32(self, index):
        reg = self.register_name[index]
        return self.registers[reg]

    def set_register32(self, index, value):
        reg = self.register_name[index]
        self.registers[reg] = value

mem_size = 1024 * 1024

emu = Emulator()
emu.create_emu(mem_size, 0x7c00, 0x7c00)
binary = open('modrm-test.bin', 'rb')
offset = 0x7c00
while True:
    b = binary.read(1)
    if b == b'':
        break
    emu.memory[offset] = b
    offset += 1
binary.close()

emu.init_instructions()
while emu.eip < mem_size:
    code = emu.get_code8(0)
    print("EIP = 0x{:02x}, Code = 0x{:02x}".format(emu.eip, code))
    if emu.instructions[code] == None:
        print("\n\nNot Implemented: 0x{:02x}".format(code))
        break
    emu.instructions[code]()
    if emu.eip == 0x00:
        print("\n\nend of program.\n\n")
        break

emu.dump_registers()

 

動作確認

それでは、上記で作成したスクリプトを実行してみます。

なお、事前にアセンブリ言語のプログラムはbinファイルとしてビルドしておきます。

 

 > python emulator.py
 EIP = 0x7c00, Code = 0x83
 EIP = 0x7c03, Code = 0x89
 EIP = 0x7c05, Code = 0xb8
 EIP = 0x7c0a, Code = 0xc7
 EIP = 0x7c11, Code = 0x01
 EIP = 0x7c14, Code = 0x8b
 EIP = 0x7c17, Code = 0xff
 EIP = 0x7c1a, Code = 0x8b
 EIP = 0x7c1d, Code = 0xe9


 end of program.


 EAX = 0x00000002
 ECX = 0x00000000
 EDX = 0x00000000
 EBX = 0x00000000
 ESP = 0x00007bf0
 EBP = 0x00007bf0
 ESI = 0x00000007
 EDI = 0x00000008
 EIP = 0x00000000

 

ModR/Mによる柔軟なオペランド指定に加え、算術命令も実行できたことが確認できました。

 

参考書籍

自作エミュレータで学ぶx86アーキテクチャ-コンピュータが動く仕組みを徹底理解!