← Back to Courses
module
32

Appendix II - Assemblers are not all the same

Sooner or later you will find Einstein code somewhere else: in a book, a magazine listing, a forum post, or somebody's old disk. You will paste it in, and some of it will not assemble.

Usually that does not mean the code is wrong. It means it was written for a different assembler.

What varies, and what does not

Assembly language splits into two halves, and only one of them is standard.

The instructions are the processor's. LD, JP, JR, DEC, ADD, RET and the rest are Zilog's names for what the Z80 can do. Every Z80 assembler ever written uses them the same way, because they describe the chip. Code you find will have the same instructions doing the same things.

Everything else is invented by whoever wrote the assembler. How you write a number. What the directives for placing data are called. The rules for labels. How you define a constant. None of that is the processor's business, so every assembler made its own choices, and they disagree.

Which means translating code you find is usually mechanical rather than difficult. Keep the instructions. Adjust the scaffolding around them.

What this course's assembler accepts

Every one of these means 255:

        LD A,255
        LD A,0FFh
        LD A,$FF
        LD A,0xFF
        LD A,11111111b
        LD A,%11111111

Placing data:

Directive What it does
DEFB, or DB One or more bytes
DEFW, or DW A 16-bit value, low byte first
DEFS n, or DS n Reserve n bytes
DEFM "text" Text as bytes

Text works with single or double quotes: DEFB "hi", DEFB 'hi' and DEFM "hi" all give the same two bytes.

Naming a constant needs a colon, which many assemblers do not want:

LIVES:  EQU 3
        LD A,LIVES

Comments run from a semicolon to the end of the line. Labels need a colon, are case-sensitive, and have no length limit - the full rules are in S4.

The one that will catch you out

Some assemblers write hexadecimal with a hash: #FF. This one does not, and it does not tell you so. The line assembles, reports no error, and produces something else entirely:

        LD A,1          ; assembles to 3E 01 - correct
        LD A,#01        ; assembles to 4A - not a load of A at all

4A is a completely unrelated instruction, and you get the same wrong byte whatever value you put after the hash.

Worse, a stray hash anywhere after the operand does it too:

        LD A,1 #note    ; assembles to 4A
        LD A,1 ;#note   ; assembles to 3E 01 - fine, it is inside a comment

So: if you paste in code that writes hex as #FF, change it to 0FFh. And if a program is behaving in a way that seems impossible, check for a stray hash. Use semicolons for comments and you are safe - a hash inside a semicolon comment is harmless.

And one more, which is worse

Most Z80 assemblers let you put two instructions on one line, separated by a colon:

        LD A,255 : OUT (8),A

Ours does not, and does not say so. It emits a single byte - 4A, which is LD C,D and has nothing to do with either instruction - in place of the four bytes you wrote. The same wrong byte appears whatever the operands, and the assembly reports success.

        LD A,255                ; on two lines: 3E FF D3 08, correct
        OUT (8),A

        LD A,255 : OUT (8),A    ; on one line:  4A

A backslash separator does the same thing.

This is the most likely of all these differences to bite you, because the colon form is common and reads perfectly naturally. A program using it throughout will assemble cleanly and do nothing at all - which is exactly what happens to one well-known Einstein tile-graphics listing: it produces a blank screen and no error.

Put one instruction per line. If you paste in code that uses separators, split every such line before you try to run it.

These two - #FF for hex, and the colon separator - are the only silent differences known. Everything else on this page fails loudly, which is much kinder.

Instructions that are not instructions

The differences above are all about notation - how you write a number, what a directive is called. There is a nastier category: some assemblers accept things that are not Z80 instructions at all, and quietly produce working code for them.

The one you will actually meet is this:

        LD HL,DE        ; copy DE into HL

There is no such Z80 instruction. The processor has no 16-bit register-to-register load - not LD HL,DE, not LD DE,HL, not any combination. But some assemblers accept it as a convenience and emit the two 8-bit loads for you:

        LD H,D
        LD L,E

Which means Einstein listings in circulation contain LD HL,DE, and on the assembler they were written for it genuinely worked. It is not a typo and whoever wrote it was not confused. Our assembler simply does not offer the shorthand.

When you meet it, you have two replacements:

        EX DE,HL        ; one byte - but it swaps rather than copies
        LD H,D          ; two bytes - a true copy, DE left alone
        LD L,E

EX DE,HL is usually what you want, and is cheaper than the shorthand it replaces.

This category is worse than the notation differences because the code looks right. #FF at least looks unusual enough to check. LD HL,DE looks like ordinary assembly, sits in a listing that clearly once worked, and fails with an error blaming something else entirely:

error: unable to resolve reference: DE

The assembler has read DE as the name of a label you never defined. If you ever see unable to resolve reference naming a register, this is what has happened.

A checklist for code that will not assemble

Work down it before assuming the code is broken:

  1. Hex written as #FF. Change to 0FFh. Remember this one fails silently, so check it even when the code assembles.
  2. A directive under another name. DB, DW and DS all work here, as do the DEF- forms. Something like .byte or BYTE does not.
  3. EQU with no colon. Add one.
  4. Two instructions on one line, separated by a colon or backslash. Split them. This one fails silently - check for it even when the code assembles.
  5. A 16-bit register-to-register load, usually LD HL,DE. Not a real instruction; some assemblers accept it anyway. Replace with EX DE,HL if a swap will do, or LD H,D / LD L,E for a true copy. Watch for the misleading error - it blames the register as an undefined label.
  6. Label names. No length limit here, so nothing to fix - but code written for an assembler that did limit them often uses cryptic short names, and you are free to rename them to something readable.
  7. Capitals in label names. They matter here. If a program jumps to LOOP but defines loop:, that is your error.
  8. Something genuinely unsupported. Macros, conditional assembly and include files vary most of all between assemblers, and are the most likely things to need real rewriting rather than a substitution.

Why this is worth knowing about at all

The Einstein was never a big enough machine to settle on one toolchain. People used what they had - the assembler that came with the machine, one typed in from a magazine, a cross-assembler on something else entirely. So the code that survives is written in several dialects, and none of them is the "real" one.

That is worth knowing when you read something that does not work. The listing is not necessarily wrong, and neither are you. It was just written for a different tool.

Get the Newsletter

New guides, disk images and community finds, roughly once a quarter. No spam, we promise, this isn't Tatung's marketing department.
Your subscription could not be saved. Please try again.
Your subscription has been successful.

Newsletter

Subscribe to our newsletter and stay updated.