← Back to Courses
module
14

Tables of addresses

Why this matters

S13's tables held values: five numbers, four letters, one byte each. Indexing worked because every element was the same size, so element n was n bytes along.

Now try it with these:

message1: DEFB "Ready",0
message2: DEFB "Set",0
message3: DEFB "Go!",0

Six bytes, four bytes, four bytes. Where does the second one start? You would have to count the first one to find out, and count both to find the third. There is no arithmetic that gets you there.

The way out is to stop storing the things in a table and store where they are instead. Addresses are all the same size - two bytes, always - so a table of addresses can be indexed even when the things they point at cannot.

An address stored as data

DEFW puts a 16-bit value in your program, and if you give it a label it puts that label's address:

data:   DEFB 42
ptr:    DEFW data       ; two bytes: the address of data

ptr does not contain 42. It contains the address where 42 lives. That is a pointer, and it takes two bytes because addresses are 16-bit - the low byte first, the little-endian order from S4.

If data happened to sit at C000, then ptr holds 00 C0.

Following it

To get from the pointer to the data, load the pointer into a register pair and then read through it:

        LD HL,(ptr)     ; HL = the address stored at ptr
        LD A,(HL)       ; A = 42

Two instructions. The first is the one from S10 that reads a 16-bit value out of memory - and what it reads happens to be an address, which is the whole trick. Following a pointer to the thing it points at is called dereferencing it.

The longer way, and when you need it

You will also see it done like this:

        LD HL,ptr       ; HL points AT the pointer
        LD E,(HL)       ; low byte of the address
        INC HL
        LD D,(HL)       ; high byte
        EX DE,HL        ; HL = the address
        LD A,(HL)       ; A = 42

Five instructions for the same result, and both give 42 - I checked.

So why would anyone write it? Because LD HL,(nn) needs the address written into the instruction. It can only load from a place you knew about when you assembled the program. The moment you calculate which pointer you want - which is the entire point of a table - you have the pointer's address in a register, and there is no instruction that loads HL from an address held in a register.

So: LD HL,(ptr) for a pointer you can name. The long form for one you had to work out. Which is the next section of this page.

EX DE,HL is doing real work there - it is the one-byte swap from S10, used because the two bytes had to be assembled in DE and the result is wanted in HL.

A table of addresses

table:  DEFW message1
        DEFW message2
        DEFW message3

Three entries, two bytes each. Entry n is at table + n*2.

Doubling a small index is one instruction, because adding a number to itself doubles it:

        ADD A,A         ; A = A * 2

So the full job - pick entry 1, follow it, print what it points at:

        ORG 256

        LD A,1          ; which message
        ADD A,A         ; times two - each entry is two bytes
        LD L,A
        LD H,0          ; HL = the offset
        LD DE,table
        ADD HL,DE       ; HL = the address OF the pointer

        LD E,(HL)       ; now follow it
        INC HL
        LD D,(HL)
        EX DE,HL        ; HL = the address of the message

        CALL puts
        RET

puts:   LD A,(HL)
        CP 0
        RET Z
        RST 8
        DEFB 158        ; ZOUTC
        INC HL
        JR puts

message1: DEFB "Ready",0
message2: DEFB "Set",0
message3: DEFB "Go!",0

table:  DEFW message1
        DEFW message2
        DEFW message3

What you should see

>Set

Change the LD A,1 to 0 or 2 and you get Ready or Go!. Nothing else in the program changes - and it would still be one line to change with thirty messages in the table.

The two halves, kept straight

There are two levels of indirection here and it is easy to lose track. Say them out loud as you read the code:

  1. HL = the address of the pointer - after the ADD HL,DE.
  2. HL = the address of the message - after the EX DE,HL.
  3. A = a character from the message - after the LD A,(HL).

Every pointer table in every program you write is those same three steps.

Where this goes

Once you can index a table of addresses, a lot of things become one lookup.

A message by number. Above. Error messages, prompts, level names.

A menu. The player presses 2; you index a table and print the third entry, with no comparisons at all.

Sprite frames. Animation frames are rarely the same size. A table of addresses handles them; a table of data cannot.

And routines, not just data. DEFW an address of code rather than data, and JP (HL) jumps to whatever HL holds. That turns a table of addresses into a table of behaviours - press a key, index a table, jump to the routine for it. That is how a game's control scheme is usually built, and it replaces the longest chain of comparisons you would otherwise write.

Change one thing

  • Change LD A,1 to LD A,2. Then to LD A,3, which is past the end of the table. What gets printed, and where did the address come from?
  • Delete the ADD A,A. Which message prints, and can you explain the result from the byte layout rather than by guessing?
  • Replace the four-instruction follow with LD HL,(table). Which message do you get, and why does it not respond to LD A,1 any more?
  • Swap the order of the three DEFW lines without touching anything else. What changes? What does that tell you about where the messages live?
  • Point two table entries at the same message. Is there any reason not to?

Exercises

14.1 - Follow one pointer. Store a byte, make a DEFW pointer to it, and load the byte into A through the pointer. Do it both ways - one instruction and five - and satisfy yourself they agree.

14.2 - Pick and print. Three messages, a table, and code that prints the second. Then make the index come from a byte in memory rather than from LD A, so that changing one stored value changes the output.

14.3 - Ask which. Read a key with S8's call 156, subtract '1' from it to turn 1, 2, 3 into 0, 1, 2, and print the matching message. That is a working menu in about fifteen instructions.

14.4 - Bounds. Extend 14.3 so that any key outside 1 to 3 prints nothing instead of following a pointer that is not there. Nothing in the machine will do this for you.

14.5 - Two levels deep. Make a table of addresses of tables, and follow it twice to reach a message. Say out loud what HL holds after each step - that is the only way to keep it straight.

14.6 - A table of routines. DEFW the addresses of three subroutines instead of three messages, and use JP (HL) to jump to the one you index. Each routine can just print a different letter. You have now written the dispatch mechanism that a game's input handling is built on.

When it goes wrong

What you see What it means
The wrong message, one entry out The index was not doubled, or was doubled twice. Each entry is two bytes.
Rubbish printed, then more rubbish The pointer was not followed - HL still held the address of the pointer, not the address it contained.
The right message but the program does not return The puts terminator. Check every message has its ,0.
A message you did not put in the table An index past the end. The bytes there are not a pointer, but the machine will follow them anyway.
Nothing at all EX DE,HL missing, so HL held something unrelated. Read HL in the register line and compare it with the listing.

Summary

  • A pointer is an address stored as data. DEFW label creates one, two bytes, low byte first.
  • LD HL,(ptr) follows a pointer whose location you can name. For one you calculated, read the two bytes with LD E,(HL) / INC HL / LD D,(HL) and EX DE,HL.
  • A table of addresses is indexed by base + n*2. ADD A,A doubles the index in one instruction.
  • Addresses are all the same size, which is why a table of pointers can index things - like strings - that are not.
  • DEFW an address of code and JP (HL) jumps to it, which turns the same pattern into a table of routines.

Next

S15. How the screen actually works. Everything you have put on screen so far went through a ROM routine. Next you find out what is behind it: a second chip, with its own memory that the Z80 cannot address, holding a picture it redraws every frame.

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.