16

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.

Raffzahn
  • 222,541
  • 22
  • 631
  • 918
twisted
  • 263
  • 2
  • 5
  • 10
    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
  • It might be really helpful to add the CPU used, as well as what computer and OS it is about - maybe as well mentioning the Assembler/Linker in use - as all of that may influence what is the right answer. – Raffzahn Feb 11 '20 at 13:17
  • 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
  • Now the environment is known, so the simplest way is to put POP HL:JP [HL] into the ROM at the known address, for exampe at 0x7FE (last two bytes). – lvd Feb 11 '20 at 14:38
  • 1
    If CP/M is still available, you should consider using it. It's spectacularly simple - programs load at 0x100 and run from there. Back in the day, I wrote real time embedded software for Z-80s, but we did all our dev/debug work in CP/M. When it was time to burn an EPROM, we'd just burn a JMP to 0x100 at address zero and go. The release builds could also run from the CP/M command line - though they'd blow away the "OS" in the process. We'd get it back with a reset and a reload. – Flydog57 Feb 12 '20 at 06:49
  • Slight nit-pick: assembly language is the uncompiled code using mnemonics. It gets complied into machine code which is then executable by the CPU. Depending on what you mean by "program" the answer might be "it can't". However, you probably want some instructions that after executing leave the address in a specific register. – CJ Dennis Feb 12 '20 at 20:13
  • 1
    Looking at question, answers and the questioners comments to answers I find this question even more confusing. It is still unclear why you want the program to guesstimate the adress. In this type of system the starting address need to be known before the program is run, otherwise you can't start the program. A Z80 processor will not relocate the code by itself, neither will a simple monitor program. – UncleBod Feb 13 '20 at 06:01

12 Answers12

22

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.

supercat
  • 35,993
  • 3
  • 63
  • 159
  • 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
  • @RuiFRibeiro It depends if you think of HL as the register itself, or the value contained in the HL register. – CJ Dennis Feb 12 '20 at 20:25
  • @thebusybee The ZX8[01] forces bit 6 of the fetched data to be 1 for instruction fetches from RAM? What a strange design. – user253751 Feb 13 '20 at 18:39
  • 1
    @user253751: The ZX80 and ZX81 abuse the CPU as a device to generate consecutive memory fetches at four-cycle intervals during the displayed part of each scan line. This is done by making the processor see any byte that's fetched as a NOP until it detects an end-of-line marker, while external hardware sees the fetched bytes and outputs them to the display. – supercat Feb 13 '20 at 18:45
  • @supercat How does this prevent you running opcodes from RAM with bit 6 set? Does bit 6 unset trigger the end-of-line circuit? – user253751 Feb 14 '20 at 12:01
  • @user253751: By my understanding (I've never really used or programmed the ZX81 myself) the ZX81 blanks the opcodes seen by the ZX80, so any opcode run from the internal RAM which does not have bit 6 set will read as a no-op. – supercat Feb 14 '20 at 14:58
  • @supercat I would've assumed it only did that when drawing the screen. That would make it extremely difficult to write machine code programs, otherwise. – user253751 Feb 14 '20 at 15:12
  • @user253751: I don't know that the ZX80 allows "normal" machine code to be run from the internal RAM. The machine was designed to be as simple as possible, and anyone wanting to do anything non-trivial with it would have a 16K memory expansion unit, so an inability to run "normal" code from the internal RAM might not have been seen as a problem. On the other hand, I vaguely recall a chess program that fit in 1K, but maybe I'm misremembering the target platform. – supercat Feb 14 '20 at 15:39
  • I've used this method for my situation although I recognize, as others have pointed out, that it may not work if an interrupt intervenes. – twisted Feb 22 '20 at 13:09
  • 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 04 '20 at 11:54
  • Is there not a call indirect opcode? – Joshua Jan 08 '21 at 17:10
  • @Joshua: There are nine call instructions, and eight RST instructions which could be employed for similar purposes if one could alter RAM at the target addresses. None of these instructions perform an indirect call. – supercat Jan 09 '21 at 16:39
  • I'm guessing the bit 6 must be set restriction only applies when a ZX80/81 is running in SLOW mode when it's frequently drawing the screen. I assume that when in FAST mode, the TV isn't being drawn and the CPU instructions are fetched from memory as normal. – Chris Walsh Mar 15 '21 at 23:23
11

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

Martin Maly
  • 5,535
  • 18
  • 45
9

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
   ...

subr:
    ld   af,(sp)
    ret

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
subr:
   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
addr:
Jean-François Fabre
  • 10,805
  • 1
  • 35
  • 62
  • 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. –  Feb 10 '20 at 22:21
  • I see, no need to return from the subroutine, I get it.. please feel free to edit the z80 code of the answer above. I can use a good z80 lesson – Jean-François Fabre Feb 10 '20 at 22:22
  • Don't really know Z80 well myself, I had to Google about the JR instruction. –  Feb 10 '20 at 22:26
  • I only know z80 through disassembling arcade games. Never coded anything with that processor myself... – Jean-François Fabre Feb 10 '20 at 22:27
  • Only works for processors with a subroutine call stack :) – RETRAC Feb 11 '20 at 00:27
  • If there's no call stack then it's probably even easier, The return address will get stored somewhere -- register, first word of subroutine, … :-) – dave Feb 11 '20 at 02:49
  • 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. – dave Feb 11 '20 at 02:54
  • for 68k it is as simple as lea (pc),a0 – lvd Feb 11 '20 at 07:26
  • yeah simple pc relative lea does the job. – Jean-François Fabre Feb 11 '20 at 07:35
  • 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
  • @ilkkachu Hihi. Yes, that would add another level of fun :) – Raffzahn Feb 11 '20 at 13:21
  • @ilkkachu I can comfirm the PIC12F508 (page 27) has an inaccessible stack. But we're talking about a machine with 25 bytes of RAM here. The hidden stack adds another 3 whole bytes! (2 x 12 bits) – user253751 Feb 11 '20 at 15:58
  • 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
  • 1
    If something like call next followed by next: pop hl works, then so does next: ld hl, next. In either case, this is only valid if the address of the code is known at compile time. – Misha Lavrov Feb 12 '20 at 19:36
  • Unfortunately ld af,(sp) makes no sense for a Z80. 1. AF is the (8-bit) Accumulator A and (8-bit) Flag F registers, never treated as a 16-bit value except when PUSHing, POPping, or EXchanging. You would want to use one of BC, DE, or HL. 2. You can't get (SP) except by POPping which also adds 2 to SP. So you'd want to POP BC, POP DE, or POP HL. – CJ Dennis Feb 12 '20 at 20:23
  • okay, maybe someone wants to [edit] the answer so the z80 code is correct? – Jean-François Fabre Feb 14 '20 at 02:54
7

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.

lvd
  • 10,382
  • 24
  • 62
7

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
ADD HL,SP
DEC HL
DEC HL
HALT
LD  C,(HL)
INC HL
LD  B,(HL)
RET

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
LD    HL,$FFFE
ADD   HL,SP
HALT
LD    SP,HL
POP   BC
RET 

PRINT USR 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:

ORG   XXXX
LD    HL,$FFFE
ADD   HL,SP
DI
CALL  ADDRESS_IN_MONITOR_ROM_OF_A_RET_OPCODE
LD    SP,HL
POP   BC
EI
RET 
Rui F Ribeiro
  • 2,727
  • 13
  • 24
  • 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 00:03
  • 1
    @Raffzahn Suggestions incorporated in the answer, thanks. – Rui F Ribeiro Feb 11 '20 at 10:28
5

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
KNOWNRTS .EQ   $FF58   ;KNOWN RTS INSTRUCTION (*1)

         JSR   KNOWNRTS
         TSX
         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
NEXT:    PLY   ;PCH
         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:

THIS     DW    THIS

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

         CALL  KNOWNRET
         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.

Raffzahn
  • 222,541
  • 22
  • 631
  • 918
  • 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
  • @Raffzahn The OP edited it into Z80 now. :( – Rui F Ribeiro Feb 11 '20 at 13:32
  • question was tagged z80 from the outset – twisted Feb 11 '20 at 13:36
  • @rgh Tagging is only a weak hint, as it's meant for search, not so much to define the question. A question should include as much relevant information as necessary. – Raffzahn Feb 11 '20 at 13:37
  • 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? – twisted Feb 11 '20 at 13:40
  • Is there a call indirect opcode? If so, push a RET instruction, disable interrupts, and call it. – Joshua Jan 08 '21 at 17:16
4

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:

        di
        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:

        di
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:

        di
        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.

George Phillips
  • 7,676
  • 36
  • 32
3

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
POP HL

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.

  • I believe the RM 380Z had a similar feature, although I forget which RST they used. – Neil Jan 06 '24 at 22:58
2

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:

programStart:
    ... lots of program code here ...

    LD HL, (dummycall+1)
    JR dontreally

dummycall:
    CALL programStart
dontreally:

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:

programStart:
    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.
Tommy
  • 36,843
  • 2
  • 124
  • 171
  • 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 04: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 07: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
2

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 ...

darooman
  • 21
  • 1
1

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
Janka
  • 2,162
  • 11
  • 12
  • 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
1

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.

With:

    ld hl,$

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

Or:

    dw  $

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

Expanding this leads to this additional simple solution:

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

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

the busybee
  • 1,054
  • 6
  • 15
  • 1
    This is the assembler figuring out the address. I'm wondering how a program can discover its own address at runtime. – twisted 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. – twisted Feb 12 '20 at 15:36
  • 1
    $ 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
  • Not much different from using any other label - except now it depends on the workings of an Assembler, as not all understand '$' as reference to the actual instruction location - and yes, without telling in the question, what the problem is to be solved, any or none of the solution shown is valid. – Raffzahn Feb 13 '20 at 07:22
  • @Raffzahn Right, that's why I wrote the introduing first paragraph. BTW, good morning to Bavaria. How may I contact you more privately? – the busybee Feb 13 '20 at 07:55
  • @thebusybee: Writing a program that can be loaded in RAM at any multiple-of-256 address isn't too hard if one knows either an area of ROM containing "POP HL / JP (HL)" or a fixed-address spot in RAM where one could put that. Assume the program for use at two addresses 256 bytes apart, build a list of the differences (ensuring they all differ by one, which they should), and then attach that list with a little code to patch all the differences. – supercat Feb 13 '20 at 17:04