I'm wondering how to write a program in Z80 assembler that discovers its own memory location.

I thought that maybe I could somehow load the program counter PC into, for example, BC.

Is there a way of doing this?

I'm messing with a single board Z80 system with 2 KiB ROM, 2 KiB RAM and that's about it. No operating system, just a very simple monitor program in the ROM that lets me inspect memory, load data to memory and connect a terminal to the board via a serial port. The assembler I'm using is z80asm on Linux.

  • 9
    I don't know the z80 specifically, but I would call a routine, in this routine get the value in the stack to get return address, do a few adjustments & return value in a register: done. – Jean-François Fabre Feb 10 '20 at 21:56
  • 3
    This could be platform dependent.... – Rui F Ribeiro Feb 10 '20 at 22:29
  • 2
    Is it a program you write yourself? In that case you will know the address on most systems, since Z80 don:t have any automatic system for relocatable code. There is a small chance the system will relocate loaded programs, but that was not common on Z80 systems – UncleBod Feb 11 '20 at 11:58
  • 4
    Thanks. Considering the environment, I'm now really curious for the reason you need to determinate the loading address at runtime? After all, it's already defined during assembly time, so all calculation can be done during compile, not wasting any instructions at runtime. – Raffzahn Feb 11 '20 at 13:35
  • 2
    Depends on the environment. E. g. in CP/M it's easy: the address is 100h. The same goes for MS DOS .com programs. – Radovan Garabík Feb 11 '20 at 13:56

12 Answers 12


If there are two consecutive bytes of RAM one can write at a known address, one could store the byte values E1h, E9h [POP HL / JP (HL)] at that address and then CALL it to place the address following the call into HL. Alternatively, if those byte values appear at some known address in ROM one could simply call that address likewise. There isn't any way to find out the address of executing code without being able to CALL some function at a known address.

  • 1
    While the mnemonic is JP (HL) in reality it jumps to HL and not (HL). – Rui F Ribeiro Feb 10 '20 at 23:45
  • The problem here is that CALL is not relative. You have to call ROM, and search if you are lucky enough to have those random bytes there (it is not in the code in the ZX Spectrum 48K ROM) . Also if you are messing with unallocated stack, you do not want to be caught in the middle of an interrupt. You either have to do a HALT before the call, or a DI. – Rui F Ribeiro Feb 11 '20 at 10:40
  • 4
    @RuiFRibeiro: If there are three consecutive bytes of RAM available at a known address, and the stack has been set up, setting up and calling those bytes of RAM is more practical than hoping a function as described exists in ROM. – supercat Feb 11 '20 at 15:51
  • Now consider you're running on a ZX80 or ZX81. While read and write accesses are fine with the RAM at its designated addresses, you can't execute code there as long as bit 6 of the opcode is 0. Luckily both opcodes have bit 6 set. Other systems might work differently. – the busybee Feb 12 '20 at 12:57
  • 2
    Your code could include such a sequence so if not found in ROM the search will wrap back to 0 eventually hitting your code (assumes no special memory locations) – Thorbjørn Ravn Andersen Jun 4 '20 at 11:54

In general:

  1. Disable interrupts (and hope that NMI will not occur)

  2. Perform "CALL" to any RET with known address (e.g. CALL $007C at ZX Spectrum, feel free to consult your system ROM listing, look for any $C9)

  3. Lower stack one address (it should be the return address of the previous CALL): DEC SP, DEC SP

  4. Get an address to a BC pair: POP BC


General answer that works for all processors:

  • call a subroutine
  • in this subroutine, get the value stored in the stack pointer (read (SP) as a 16 bit word on a Z80. 32 bit processors would require that your read a 32 bit word). This tells the return address, which is the address of the instruction coming after the subroutine call.
  • now return this value in a register: you now have the value of the current program counter in this register. By doing some additions/subtractions, you can locate any program locations close to that point (be careful of assembler optimizations that may change the size of the instructions if you do that)

I'm far from being a z80 specialist (actually this code could be completely wrong), but something like:

   call  subr
   ; here af register contains the current PC location
   ; subtract 3 to get address before the "call" if it's of any use

    ld   af,(sp)

On a 68000 processor (I know it's not asked but there's more chance that the code is correct...) adapting Ross suggestion to avoid returning from a routine. Just pop the value from the stack to get PC and "forget" the subroutine call.

   bsr.w  subr  ; not possible to use .b with exact next address
   move.l   (A7)+,D0   ; pops the stack, get 32 bit value

For the record, on a 680x0 it's even simpler with PC relative load effective address instructions:

   lea addr(pc),a0   ; label value is in address register
  • 2
    AF is the flags register and JR doesn't push a return value. You could simplify this with something like call next next: pop ix. Though CALL isn't relative. – user722 Feb 10 '20 at 22:21
  • 6
    To be picky: this answer and others tell how to get the address of a particular instruction, The "address of the program" is a little trickier, starting with defining what that really means. – another-dave Feb 11 '20 at 2:54
  • 2
    ...for all processors with a software-accessible call stack ;) There might be ones with a hidden call stack, accessible only by the call and return instructions. I think some tiny RAM-less AVR might have had something like this. – ilkkachu Feb 11 '20 at 10:55
  • 1
    @another-dave Sure, that needs to be defined before anything else. But when done, it's simply a subtraction of the offset from the instructions address. Ofc, this can only be valid when done in the same module (phase) as the defined start is :) So yes, there are many additional hurdles that could apply. For a definite answer more information is needed than given by the OP. – Raffzahn Feb 11 '20 at 13:20
  • 1
    I wouldn't load it into AF. AF is the flags register and that could be dangerous. – S.S. Anne Feb 11 '20 at 21:45

In general case, when the environment is unknown, the answer is simply "it can't".

However, for a known environment, it is almost always possible.

Since your environment is unknown, I'll assume it is ZX Spectrum. For other environments, find a suitable suggestion.

  1. If your program is called from BASIC, calling address is always in BC, as you wanted it (ZX Spectrum ROM specific).

  2. Call the routine POP HL:JP [HL] at the known address, as already answered by @supercat. There are no such bytes in the original ZX Spectrum ROM, however.

  3. Make your own POP HL:JP [HL] subroutine at the known free RAM address: LD HL,0xE9E1: LD [ADDR],HL:CALL ADDR. On ZX Spectrum, this could be video RAM or printer buffer (if run from BASIC).

  4. If you can guarantee there would be no interrupts (no NMI or you can disable INTs by DI), simply call to the RET 'subroutine', then recover return address like this: DEC SP:DEC SP:POP HL. Again you can either call a known ROM address or organize a byte at the known free RAM area.

  • JP (HL) is an unfortunate mnemonic, as it jumps to HL and not (HL). POP HL ; JP (HL) more than enough. – Rui F Ribeiro Feb 10 '20 at 23:53

If you are in the ZX Spectrum, BC is loaded with the calling address given to USR in BASIC. So at the beginning of a C/M program it is guaranteed you know the address.

Also, we do not be want/cannot be doing CALLs to our own code if we are trying to find where it was loaded. (for instance, for doing relocation/using relocation CALL tables as the GENS3 assembler did back in the day to be able to be loaded in ANY RAM position).

Also doing CALLs to ROM, does not guarantee you will have your address in the stack by the time you are trying to fetch it, as an interrupt can happen in the meanwhile, unless you got your interrupts disabled.

We can "cheat". For instance, with interrupts enabled, and knowing we only have interrupts 1/50 of a second, so we dont have to disable interrupts after the HALT for preserving the stack:

ORG 50000
LD  HL,0
LD  C,(HL)
LD  B,(HL)

After the halt, we know a interrupt has happened, and (SP-2) will still have the address of the instruction after the HALT - LD C,(HL)

So in the ZX Spectrum:

PRINT USR 50000 will return 50007.

PS. Following @Raffzahn tips for code optimization:

ORG 50000


returns 50005 (the address of SP,HL after HALT) -- e.g. BC is loaded with the address of the instruction after HALT.

PPS in a simple board, the same can be accomplished via:

  • 2
    That's a neat one. Ofc, quite dependent on the environment. I like it. (I smell some optimization in addition, like loading HL with -2 before ADD and doing a LD SP,HL, POP BC afterwards:) - SCNR) – Raffzahn Feb 11 '20 at 0:03
  • 1
    @Raffzahn Suggestions incorporated in the answer, thanks. – Rui F Ribeiro Feb 11 '20 at 10:28

I'm wondering how to write a program in assembler that discovers it's own memory location.

This depends much on the CPU used, as well as the OS/computer. Most important here if the code executed is relocated when loaded (*1) or not. The later is relevant for ROM code that may get inserted at different addresses.

I/O drivers (slot PROM) of Apple II expansion cards are a great example here. Each slot has an assigned address range for a driver ROM as well as I/O registers. To work accordingly the ROM code had to find it's own address first. Since a ROM isn't relocated it couldn't simply call an address within. Instead a 'well known RTS' (Return instruction) in Monitor ROM was called. After returning, the return address could be peeked from stack, much like this:

STACK    .EQ   $100

         JSR   KNOWNRTS
         LDY   STACK,X     ;PCH
         LDA   STACK-1,X   ;PCL

This is complete independent form any code relocation, but only works if there is a known Return instruction. Under some OS this may as well be possible, like the MS-DOS PSP containing a RETF at location 52h (*2).

Another way is to call some (informative) OS function and later on examine the stack.

Now, if the code is relocated when loaded, a Subroutine Call can be used to emulate pushing the address:

         JSR   NEXT
         PLA   ;PCL

Of course if relocation is done by anything but the most simple relocator, it will as well adjust address constants, so it simply can be defined as memory word:


I thought that maybe I could somehow load the program counter into for example BC.

This sounds as if you imply an 8080 type CPU. The principles are the same and the examples may look like this:

Using a known return:

KNOWNRET EQU   ???      ;Known Return, depends on OS/machine

         DEC   SP
         DEC   SP
         POP   BC

Code relocated when loaded (really short now):

         CALL  NEXT
NEXT:    POP   BC       ;POP B in 8080 syntax

Now BC contains the address of NEXT.

Or even shorter by letting Assembler/linker do the work:

THIS     LD    BC,THIS  ;LXI B,THIS in 8080 syntax

Bottom line, it all comes down to the environment and how much can be used.

P.S.: For all of these examples disabling interrupts may be needed, again depending on CPU, OS and environment.

*1 - That is, the loader patches all absolute references in a piece of code according to the location the code is loaded.

*2 - $FF58 is part of the Restore subroutine of the Apple II Monitor ROM and used wherever a Return or null-function is needed. For example does BASIC initialize the '&' extension vector ($3F5) with a jump to $FF58.

*3 - Then again in x86 everything is segment based and the actual segments can be read out anyway.

  • I wonder why Woz didn't put an easier-to-use function in ROM that would example the high byte of the calling address, place the low three bits in the Y register, shift those left four bits, and place that value in the X register? Since many I/O cards only have 256 bytes of ROM, being able to save a few bytes on the "identify slot" routine would be very useful. That would have been enough extra space, for example, to allow the Disk II ROM to have shut off the drive if there's no disk in it. – supercat Feb 10 '20 at 23:59
  • Problem is that CALL is not relative. You load the program in another random address, and it breaks. Think relocatable code like GENS3 in the ZX Spectrum where you could load it anywhere in RAM. Ah, and for messing with the stack you have to disable interrupts. In my example I do not, because I am doing it after a HALT and know I have lots of T states to spare. – Rui F Ribeiro Feb 11 '20 at 10:32
  • @RuiFRibeiro True. I guess I need to clarify the reloacation reference a bit more (done) - it's about code being relocated when loaded, not relocatable code. The interrupt part is only relevant for systems that work with interrupts. Your guess is as good as mine, as the OP didn'T specify any particular CPU, computer or OS. – Raffzahn Feb 11 '20 at 12:48
  • 1
    @RuiFRibeiro :) Well, glad I added 8080/Z80 versions as well. Main point for my answer was to show as well a real world scenario where such an operation would make sense - after all, code running at unknown address was extreme uncommon during 8 bit time (unless using a 6809 that is). – Raffzahn Feb 11 '20 at 13:40
  • 1
    ok. Would it be too confusing to put z80 in the question title at this stage? – rgh Feb 11 '20 at 13:40

The Z-80 has no direct way to read the program counter contents. However, there are a few techniques that can work with minimal assumptions. If your code is loaded into RAM it can use self-modification to find itself:

        ld      hl,0
        ld      a,(hl)
look:   ld      (hl),a
        dec     hl
        ld      a,(hl)
        ld      (hl),0
loc:    jr      look
        ld      (hl),a      ; repair, HL = loc + 1

When this completes HL will point to loc + 1. The program marches down from the top of memory temporarily setting each memory location to 0. When it gets to loc + 1 the jr there will be changed to go to the next instruction. At which point the jr is fixed and the program now knows where it is in memory.

If your program is in a ROM at an unknown location you could do something more complicated in terms of checksumming memory in order to identify your code. Or look for some random "signature" bytes. Or if you know there are two bytes of RAM at a certain memory location you can do this:

        ld      de,($3c00)
        ld      hl,$e9e1
        ld      ($3c00),hl  ; write $e1 $e9 which is POP HL; JP (HL)
        call    $3c00
loc:    ld      ($3c00),de  ; HL = loc

If you only know one byte of RAM, this will do:

find:   ld      hl,mem
        ld      a,(hl)
        ld      (hl),$c9    ; "RET"
        call    mem
loc:    ld      (hl),a      ; restore
        dec     sp
        dec     sp
        pop     hl          ; now HL==loc

Though it does have to be careful that the RAM modified does not happen to be one of the two bytes of the stack used. There are ways around this but for most systems you're highly likely to be able to pick a RAM location that does not conflict.

The DI instruction stops most interrupts which would change the stack address if one occurs after the ret is executed. However, the non-maskable interrupt (NMI) can't be stopped. One workaround is to validate the address loaded into HL say by ensuring it points to a ld (hl),a instruction. Add this code:

        ld      a,(hl)
        cp      $77     ; LD (HL),A opcode
        jr      nz,find

Obviously that could be fooled by an unlucky data being placed on the stack. Further checks can be made to reduce the chances of such a coincidence to practical certainty.

Or you could make the almost perfectly safe assumption that an NMI will merely change the top of the stack to an address within your program. In that case you only need to search back for the ld (hl),a instruction to lock down the position:

        ld      hl,mem
        ld      a,(hl)
        ld      (hl),$c9    ; "RET"
        call    mem
loc:    ld      (hl),a      ; restore
        ld      hl,0
        add     hl,sp
        dec     hl
        ld      a,(hl)
        dec     hl
        ld      l,(hl)
        ld      h,a
look:   ld      a,(hl)
        cp      $77         ; LD (HL),A opcode
        dec     hl
        jr      nz,look
                                ; now HL==loc - 1

These solutions may not be entirely practical but they do point out how to work with minimal system knowledge.

  • 1
    That last one only works if you can be sure there are no interrupts between the ret and pop hl. – Toby Speight Feb 12 '20 at 9:41
  • The DI is there to stop interrupts. NMI can still get through, though, that's true. I'll edit in some possible workarounds. – George Phillips Feb 12 '20 at 17:49

The Nascom II system ROM NAS-SYS 3 reserved one of the RST instructions (RST 10h) for emulating a relative call. On that computer, obviously

RCAL $+2

would do the trick for getting the current address (+2) into HL. The 1k debugger extension ROM used this and other tricks in order to be fully relocatable. Of course, this is not a generally useful answer since it is system dependent. But it was a nice byte-saving feature which NAS-SYS itself used quite a bit.


One method that hasn't been mentioned is writing to a safe memory buffer with the pop hl, jp (hl) call. For example if I know there is space at $4000 (ZX Spectrum screen memory) but I don't where my program has been loaded I can:

ld hl,$e9e1    ; pop hl jp (hl) combination
ld ($4000),hl
call $4000

; hl will now be the loaded address of this next instruction
  • Is that any different from my Feb 10 '20 answer? – supercat Jan 9 at 16:40

Platform independent:

  • scan the memory for a RET opcode.
  • call that memory location
  • adjust the stack pointer
  • pop from the stack to get the address of your call
  • 3
    This requires of course the scan hitting a stable location. 'cause some other process might change it between finding the RET and using it. This includes ports as well. – Raffzahn Feb 12 '20 at 11:38
  • @Raffzahn di/ei before and after will go a long way towards stability (reliable enough in anything retro-related not nuclear-mission critical, if you make sure not to scan memory mapped ports). – Radovan Garabík Feb 12 '20 at 12:03
  • @RadovanGarabík Sure, it helps a bit, except if it's exactly that, a memory mapped a port or counter or whatsoever delivering some changing value. 'Process' is not restricted to a CPU process (of the same CPU). Excluding them invalidates the claim of being platform independent, doesn't it? – Raffzahn Feb 12 '20 at 12:17
  • @Raffzahn well, sure - though I'd argue that memory mapped I/O on Z80 is a nonstandard (though not that uncommon) way of doing I/O (as opposed to, say, 6502) – Radovan Garabík Feb 12 '20 at 13:28
  • @RadovanGarabík Not as rare as one might think. Already the TRS-80 had ports (and the keyboard(!)) memory mapped. Similar some CPC (+/GSX) as well as MSX machines - to name a few very common machines. Claiming platform independence doesn't work even when restricting to Z80 only machines. – Raffzahn Feb 12 '20 at 13:50

Since we now know that the OP uses a DIY Z80 system and z80asm for generating its machine code, I feel the need to add this simple answer:

Use the special expression $:

The special value "$" is the address of the first byte of the current command.


    ld hl,$

you will get the address of the opcode of ld hl,imm16 in HL.


    dw  $

will place the address of the first byte of the stored address at this place.

Expanding this leads to this additional simple solution:

    ; any code you like
    ld hl,start
    ; ...
    dw start

Let the assembler do the work. Developers are lazy, aren't you?

  • This is the assembler figuring out the address. I'm wondering how a program can discover its own address at runtime. – rgh Feb 12 '20 at 14:15
  • 1
    Well, you got a lot of answers. But which problem would you like to solve? This is especially hard to guess from the system you describe. -- And, what do you mean by "address of the program"? – the busybee Feb 12 '20 at 15:09
  • Yes, going to try and actually implement one of those answers over the next couple of days. By 'address of the program', I just mean any part of it. Doesn't matter what since I know the bytes the assembler emits. – rgh Feb 12 '20 at 15:36
  • $ is not a runtime operation. Ĩt only gets the current address that is being compiled in. If the intention is writing relocatable code that can run on any address, obviously $ does not work. If on the other hand, you just want to know the fixed address where it is being assembled... – Rui F Ribeiro Feb 12 '20 at 15:52
  • 1
    @RuiFRibeiro It's really hard to write relocatable code for the Z80. E.g. each and every call has to be relocated. I don't think that such a small system as the OP describes will have such mechanics built in. That's why I (and others as well) asked for clarification of the background. As long as this question is not answered, we are all just guessing. – the busybee Feb 12 '20 at 16:22

To close the circle on some of the other answers:

Amongst other features, the Z80 supports CALL only to absolute addresses. So, either:

(i) you can't use CALL at all in your hypothetical system (or therefore any internal subroutines); or (ii) you can assume a runtime loader modified the target of your CALLs.

Therefore, the knee-jerk answer is don't even bother using the stack as others have suggested. Just directly read the argument of a CALL, assuming your runtime loader is smart enough to modify absolute loads. There's no PC-relative loading so that's likely. If so then:

    ... lots of program code here ...

    LD HL, (dummycall+1)
    JR dontreally

    CALL programStart

EDIT: or, as Raffzahn points out, if you're assuming a loader that has fixed your absolute addresses then there's no need for the artificial complexity:

    LD HL, programStart

If you can't assume that your absolute addresses have been fixed, but can assume a working stack, you can do an artificial CALL and pop as otherwise suggested.

If you've been asked to write code for a Z80 in which you may have been arbitrarily relocated, but your internal addresses haven't been adjusted then:

  • you can't use CALL; and
  • can't use any version of LD except immediate.

So you're in the realm of the vaguely ridiculous.

Even leaking the PC via MEMPTR requires you to be able to make assumptions about both the memory map (as MEMPTR leaks via BIT n, (HL) only, so you'll need to be able to do this with a suitable HL) and the IO map (as you'll need CPI and CPD to get the full contents of the leaked PC given that only bits 3 and 5 leak). So that's out.

So, in net:

  • either your linker will patch up your CALL addresses and the above snippet will work — and other than for academic purposes, it's difficult to imagine a scenario in which that won't be true;
  • or it won't in which case if you know nothing whatsoever about the rest of the memory map then I currently think the task is impossible;
  • but if you at least know two contiguous IO addresses and a single memory address where reading has no consequence, you can use a CPIR/CPDR that executes at least twice to load the PC into MEMPTR, then use BIT n, (HL), CPI and CPD very slowly to inspect it.
  • Metacommentary, that doesn't rise above a comment: I've pitched in late primarily because MEMPTR had otherwise gone unmentioned, and I think it adds something to the conversation. Even if not much. – Tommy Feb 13 '20 at 4:30
  • 1
    Isn't having the linker relocation a dummy call exactly the same as having it do a word ( DW label) or a loade (LD HL,label)? – Raffzahn Feb 13 '20 at 7:19
  • 1
    Ugh, yeah. But, my stupidity aside, is the rest of the reasoning at all helpful? I was definitely on the fence about answering at all this late in the game, with so much already having been covered, and am very happy to delete if duplicative. – Tommy Feb 13 '20 at 12:50
  • Naa. don't delete. While the shown methods have been stated before, you added some quite useful explanation. – Raffzahn Feb 13 '20 at 14:04

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.