
本記事はPythonで簡単なx86エミュレータを作成します。
主にCPUが動作する仕組みを学ぶことを目的とし、オペコードはmov命令とjmp命令のみで、難しいところはすっ飛ばしてとりあえず動くものにしました。
アセンブリ言語プログラムの作成
今回のプログラムでは、mov命令とjmp命令のみでCPUが動作する雰囲気を味わってみます。
;test.asm
BITS 32
start:
mov eax, 41
mov ebx, 42
mov ecx, 43
mov edx, 44
jmp short start
EAX、EBX、ECX、EDXレジスタにそれぞれ、41、42、43、44の即値をmov命令でコピーし、最後にjmp命令で先頭の0x00番地へジャンプして終了させます。
Pythonによるスクリプトの作成
それでは、PythonでCPUをエミュレートするスクリプトを作成します。
# emulator.py
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):
for i in range(8):
self.instructions[0xb8 + i] = self.mov_r32_imm32
self.instructions[0xeb] = self.short_jump
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 = 0
for i in range(4):
ret |= self.get_code8(index + i) << (i * 8)
return ret
mem_size = 1024 * 1024
emu = Emulator()
emu.create_emu(mem_size, 0x00, 0x7c00)
binary = open('test.bin', 'rb')
offset = 0x00
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()
各レジスタにはレジスタ番号というものが振られていて、mov命令に関しては「0xB8 + レジスタ番号」となり、オペコードがどのレジスタにコピーするかを内包しています。
そのため、「オペコード - 0xB8」でどのレジスタにコピーをするかを特定します。
jmp命令に関しては、現在の位置を起点とし、オペランドから符号付き整数値を1バイト読み取りeipに加算します。
この時、2の補数表現を用いて前に127、後ろに128の範囲でジャンプすることができます。
また、instructionsに各オペコードに対応した関数を登録しておき、eipから読み取ったオペコードを実行します。
なお、mov命令およびjmp命令以外のオペコードが見つかった場合は、「Not Implemented」を表示させ終了します。
動作確認
それでは、上記で作成したスクリプトを実行してみます。
なお、事前にアセンブリ言語のプログラムはbinファイルとしてビルドしておきます。
> python emulator.py EIP = 0, Code = 0xb8 EIP = 5, Code = 0xbb EIP = 10, Code = 0xb9 EIP = 15, Code = 0xba EIP = 20, Code = 0xeb end of program. EAX = 0x00000029 ECX = 0x0000002b EDX = 0x0000002c EBX = 0x0000002a ESP = 0x00007c00 EBP = 0x00000000 ESI = 0x00000000 EDI = 0x00000000 EIP = 0x00000000
実行ファイルを読み取るごとに、EIPとオペコードを表示し、最終的に現在のレジスタの状態をダンプし、問題なく各レジスタに即値がコピーされたことが確認できました。