News:

The Savage///Circuits website has been upgraded to a more efficient theme.

Main Menu

Random Z80 Hand Disassembly

Started by Chris Savage, Nov 25, 2025, 02:02 PM

Previous topic - Next topic

Chris Savage

Downloaded a random EPROM from a device into the XGPro software and realized immediately it was Z80 code. How? Well, having worked with the Z80 for so many years you start to recognize that the first several bytes of almost every Z80 program start with very specific bytes. This is mostly to show @JKnightandKARR how I decode Z80 assembly code.

You cannot view this attachment.

Here is a screenshot of the buffer window. As you can see, the first byte is $31, which is the Z80 opcode for:

LD SP,<address>, where <address> is the start address of the stack pointer (SP). So, in this case, that would be:

LD SP, $A000

The next byte is $F3, which is the Z80 opcode for:

DI, which disables interrupts.

The next byte is $C3, which is the Z80 opcode for:

JP <address>, where <address> is the the value to be loaded into the program counter (jumped to). The next two bytes are the address, so:

JP $0100

You can see that the program jumps to $0100. That's because $0038 through $0066+ are interrupt vector addresses. At $0100 we see a byte value of CD, which is the Z80 opcode for CALL <address>, with the following two bytes being $19 and $01, the subroutine would be located at $0119.

Next we have $CD $8E $01, which is CALL $018E.

Continuing, this is what we have:

$0100    CALL $0119
$0102    CALL $018E
$0106    CALL $0326
$0109    CALL $OEF6
$010C    LD A,($8E40)
$010F    OR A
$0110    JP NZ,$0106

I will eventually disassemble the entire thing, but thus far we can see that the code does what almost every Z80 program does, which is set the stack pointer, disable interrupts, then start setting up hardware, which is most likely what the first few CALLs are doing. $0110 could be considered a "LOOP" for all intents and purposes, because it unconditionally jumps back to $0106, which would be the "DO" of the loop. In between it's making a couple of calls, the loading the A register with the contents of memory address $8E40, then activating the flags, until it gets a $00 in the A register, at which point the loop exits and the code continues.

It will be interesting to see exactly what is happening here. More to come.

                    Bringing concepts to life through engineering.

Chris Savage

Okay, I have attached the original binary file, and a disassembled version using DZ80 Version 2.0. For those who are curious. I have no idea what the code is for as it is from an obsolete industrial control unit. But you might find something interesting.

NOTE: Disassembled code often contains data blocks, which the disassembler will try to turn into op-codes. So you need to be able to tell what is happening as you go to make use of the code. I may come back later and point out any useful blocks I find. Much of what I followed seems to be initializing external hardware for which the disassembled source has no references for.

                    Bringing concepts to life through engineering.

Chris Savage

Since this code is from a piece of hardware for which we know NOTHING of the I/O map, we can assume IN / OUT instructions are talking to various external hardware, however, other blocks of code could be doing any number of things. Have a look at the following block of code.

0122 d30c      out    (0ch),a
0124 d34c      out    (4ch),a
0126 d35c      out    (5ch),a
0128 d36c      out    (6ch),a
012a 2f        cpl   
012b d31c      out    (1ch),a
012d d3f1      out    (0f1h),a
012f 3ec0      ld      a,0c0h
0131 d3f2      out    (0f2h),a
0133 210080    ld      hl,8000h
0136 01501e    ld      bc,1e50h
0139 af        xor    a
013a 77        ld      (hl),a
013b 23        inc    hl
013c 0b        dec    bc
013d 79        ld      a,c
013e b0        or      b
013f c23901    jp      nz,0139h
0142 3e38      ld      a,38h
0144 32048d    ld      (8d04h),a
0147 3e37      ld      a,37h
0149 32058d    ld      (8d05h),a
014c cd373e    call    3e37h
014f 21378d    ld      hl,8d37h
0152 060e      ld      b,0eh
0154 3eff      ld      a,0ffh
0156 77        ld      (hl),a
0157 23        inc    hl
0158 10fc      djnz    0156h
015a 21248d    ld      hl,8d24h
015d 0605      ld      b,05h
015f 3ef0      ld      a,0f0h
0161 77        ld      (hl),a
0162 23        inc    hl
0163 10fc      djnz    0161h
0165 212c8d    ld      hl,8d2ch
0168 0605      ld      b,05h
016a 3ef0      ld      a,0f0h
016c 77        ld      (hl),a
016d 23        inc    hl
016e 10fc      djnz    016ch
0170 213f8e    ld      hl,8e3fh
0173 3601      ld      (hl),01h
0175 213e8e    ld      hl,8e3eh
0178 3603      ld      (hl),03h
017a 2100c0    ld      hl,0c000h
017d 010020    ld      bc,2000h
0180 af        xor    a
0181 77        ld      (hl),a
0182 23        inc    hl
0183 0b        dec    bc
0184 79        ld      a,c
0185 b0        or      b
0186 c28001    jp      nz,0180h
0189 3eff      ld      a,0ffh
018b ed47      ld      i,a
018d c9        ret   
018e db3c      in      a,(3ch)
0190 e61f      and    1fh
0192 47        ld      b,a
0193 cdce01    call    01ceh
0196 ed56      im      1
0198 fb        ei     

This section is interesting to me because it seems that after sending the value of the A register to several outputs, the code is then, ostensibly, filling 7,760 bytes of RAM, starting at $8000 with $00, clearing it. But the block of code that does this has me baffled. Perhaps @granz or @MicroNut could offer some thoughts here. My OCD is causing me to get hung up here.

0133 210080    ld      hl,8000h
0136 01501e    ld      bc,1e50h
0139 af        xor     a
013a 77        ld      (hl),a
013b 23        inc     hl
013c 0b        dec     bc
013d 79        ld      a,c
013e b0        or      b
013f c23901    jp      nz,0139h

In this section, the HL pair is being loaded with $8000, which is most of my designs is the bottom of RAM. The BC pair is being loaded with $1E50 (7760 in decimal). Next there is an XOR A, which would have the effect of setting the A register to $00. Next we're loading the memory address at HL with $00, incrementing the HL registers, decrementing the BC registers, and then, here's where it gets weird in my mind...

XOR A is a faster way of doing a "LD A, $00", so that makes sense. Normally, the next instruction would be testing to see if the BC register has reach zero and to loop if not. I know that the "LD A, C" and "OR B" are used to detect when the BC registers reach zero, but I can't remember why it does it this way. Obviously, it keeps going until BC is zero, clearing those bytes.

                    Bringing concepts to life through engineering.