kosarev / z80

Fast and flexible Z80/i8080 emulator with C++ and Python APIs
MIT License
63 stars 10 forks source link

odd behavior (via Python API) #49

Closed jmdyck closed 1 year ago

jmdyck commented 2 years ago

Here's the script I ran:

import z80

m = z80.Z80Machine()

image = b'\x00\xdd\x21\xf5\x04\x00'
m.set_memory_block(0, image)

for i in range(5):
    print('-' * 35)
    print(f"m.pc = {m.pc:04X}")

    (instr_asm, instr_nbytes) = m._disasm(image[m.pc: m.pc+4])
    addr_of_next_instr = m.pc + instr_nbytes
    instr_byte_string = ' '.join(
        f"{byte:02X}"
        for byte in image[m.pc: addr_of_next_instr]
    )
    print(f"instr is next {instr_nbytes} bytes (so following instr starts at {addr_of_next_instr:04X})")
    print(f"{instr_byte_string:12} = {instr_asm}")
    print('')

    m.ticks_to_stop = 1
    m.run()

And here's the output I got:

-----------------------------------
m.pc = 0000
instr is next 1 bytes (so following instr starts at 0001)
00           = nop

-----------------------------------
m.pc = 0001
instr is next 4 bytes (so following instr starts at 0005)
DD 21 F5 04  = ld Pix, W0x04f5

-----------------------------------
m.pc = 0002
instr is next 3 bytes (so following instr starts at 0005)
21 F5 04     = ld Phl, W0x04f5

-----------------------------------
m.pc = 0005
instr is next 1 bytes (so following instr starts at 0006)
00           = nop

-----------------------------------
m.pc = 0006
instr is next 1 bytes (so following instr starts at 0007)
             = nop

So: (1) At the line DD 21 F5 04 = ld Pix, W0x04f5, I would have expected the disasm string to be ld ix, 0x04f5. Instead, the format characters P and W are included along with their expansions. Looking at the code in on_format_char, I can't even figure out how this is possible. (2) Given that the instruction at address 1 is 4 bytes long, I would have expected the next value of m.pc to be 5. Instead, it's only 2. (But the one after that is 5, so the machine isn't just stepping through the image one byte at a time.) (3) Am I mis-using the Python API? I couldn't find any documentation.

(I fetched the z80 code today, so it's presumably up-to-date.)

kosarev commented 2 years ago

Hi @jmdyck, sorry for the delay. To answer your questions:

(1) The disassembling function you call is an internal one (note the leading underscore). c28c9687 has intentionally overridden on_format_char() to prepend the format character to help the disassembler (_disasm.py) figure the types of operands. The disassembler itself is still in development, but should be capable enough for what your script does. The just-submitted 96c8ced exposes the _Z80InstrBuilder class, which we can use for that and which is supposed to become part of the public z80 interface:

import z80

m = z80.Z80Machine()
b = z80._Z80InstrBuilder()

image = b'\x00\xdd\x21\xf5\x04\x00'
m.set_memory_block(0, image)

# for i in range(5):
while m.pc < len(image):
    print('-' * 35)
    print(f"m.pc = {m.pc:04X}")

    # (instr_asm, instr_nbytes) = m._disasm(image[m.pc: m.pc+4])
    instr = b.build_instr(m.pc, image[m.pc:m.pc+4])
    instr_asm = str(instr)
    instr_nbytes = instr.size

    addr_of_next_instr = m.pc + instr_nbytes
    instr_byte_string = ' '.join(
        f"{byte:02X}"
        for byte in image[m.pc: addr_of_next_instr]
    )
    print(f"instr is next {instr_nbytes} bytes (so following instr starts at {addr_of_next_instr:04X})")
    print(f"{instr_byte_string:12} = {instr_asm}")
    print('')

    m.ticks_to_stop = 1
    m.run()

(2) This is as expected. Every time the emulator runs into an IX/IY prefix, the instruction decoder changes its internal state respectively so it knows next time there's going be a prefixed instruction and then the emulator returns control. So the prefix bytes are treated very much like separate instructions. This is necessary, because otherwise the emulator would just never return control once it's given an image completely filled with prefix bytes. So if you want to step by full instructions, the instruction-running loop should be something like the following (requires 5e921cf):

    while True:
        m.ticks_to_stop = 1
        m.run()

        # Only stop at the end of a full instruction.
        if m.index_rp_kind is z80.HL:
            break

(3) No documentation for the disasm and actually any APIs yet, sorry. So far the focus was on figuring out the right interfaces, and this is still in progress, so I guess just asking is the way to go for now. (And I aim to respond sooner.)