News:

Printed Amstrad Addict magazine announced, check it out here!

Main Menu
avatar_zhulien

Can you code a Z80 program with zero absolute addresses?

Started by zhulien, 15:42, 26 August 21

Previous topic - Next topic

0 Members and 1 Guest are viewing this topic.

zhulien

here is a challenge for you... Can you code a Z80 program with zero absolute addresses?


roudoudou

My pronouns are RASM and ACE

zhulien

other than something simple like an ret or a maths operation followed by ret?

Animalgril987

Certainly. Some game loaders do it, so that their code is relocatable (because HIMEM) will be different for the different CPCs.

BSC

** My SID player/tracker AYAY Kaeppttn! on github **  Some CPC music and experiments ** Other music ** More music on scenestream (former nectarine) ** Some shaders ** Some Soundtrakker tunes ** Some tunes in Javascript

My hardware: ** Schneider CPC 464 with colour screen, 64k extension, 3" and 5,25 drives and more ** Amstrad CPC 6128 with M4 board, GreaseWeazle.

norecess464

If my memory is still good, I think DAMS assembler (ADAM in UK: https://www.cpc-power.com/index.php?page=detail&num=4248) is entirely relocatable. It's considered as a big program for the Amstrad CPC.
My personal website: https://norecess.cpcscene.net
My current project is Sonic GX, a remake of Sonic the Hedgehog for the awesome Amstrad GX-4000 game console!

Zik


zhulien

how does DAMS achieve this?


The way i thought of doing it, perhaps is too complicated an approach, was to have a GUID in the code, search all memory for the GUID, from that GUID calculate where it was found and we can now calculate where we are actually executing then to update jump tables based on an offset (where we assembled the jump table from vs where we are currently executing) and that all calls are jumps via the recalculated jump tables. 


Is there a better way?

roudoudou


"easy" way: no call, no jump, no memory variable, only relative jumps
or use a relocation table, patch the code, run the code anywhere
My pronouns are RASM and ACE

m_dr_m

Yep, DAMS does it via a relocation table. No calls in such a big tool would be rather challenging (especially considering the small footprint of dams). Well, Orgams's z80 emulation for step by step debugging doesn't use calls, to be both fast and transparent.

andycadley


Traditionally with something like:

CALL pchl ; store PC in HL


....


pchl: POP HL
JP (HL)

eto


scruss

I'm told that Richard Russell's Z80 floating point library (used on the Z88 and later versions of BBC BASIC for Z80 machines) is relocatable, fully re-entrant and uses no storage outside the stack. I'm pretty sure it's using a jump block though, as using JR throughout would be complex.

andycadley

Quote from: eto on 19:28, 27 August 21
isn't pchl then absolute?
Well it depends on how strict you want to be about it. You could write the instructions to a fixed location (e.g. the screen or one of the RST points) first. Or pass dummy parameters from BASIC, writing the PCHL routine to (IX) via a series of direct instructions and then doing a CALL (IX) by the combination PUSH IX; RET


Got to be said though that writing fully relocatable code in Z80 is enormously hard work. The CPU just isn't really designed for it. You really want Register Indirect addressing on absolutely every type of instruction as well as something like LEA to calculate addresses.

GUNHED

Quote from: zhulien on 15:42, 26 August 21
here is a challenge for you... Can you code a Z80 program with zero absolute addresses?
Under FutureOS quite some apps start at &0000.  :)
http://futureos.de --> Get the revolutionary FutureOS (Update: 2023.11.30)
http://futureos.cpc-live.com/files/LambdaSpeak_RSX_by_TFM.zip --> Get the RSX-ROM for LambdaSpeak :-) (Updated: 2021.12.26)

eto

Quote from: andycadley on 21:05, 27 August 21Well it depends on how strict you want to be about it.

Well... I think if the question has at its core the restriction "zero absolute addresses". Once you soften this, you answer a totally different question.

zhulien


Since we cannot use an absolute address, we cannot know where our code or even labels are - we can search for our code. An example is here, we found our code id 'AGUID' (it could be a real guid if you want longer code). After having our one known found absolute address, we can now modify it to put a call to a nearby function that get's the PC. Having got the PC we can for example find the absolute address to a table of relative function offsets. Finally we then go to our actual code logic with IY having the absolute address of the table of relative function offsets. I have coded a relocatable call, but... of course in this example it breaks the rule to call it 3 times as it itself should not be an absolute address.


I think it is possible to continue with the route I have shown as an example, but instead of having a relocatable call, basically jump relative to the offsets in the table that is known. Yes, the code will be spaghetti code, and of course there is no recursion easily and no subroutines in the normal sense, the functions would essentially be using looked up JRs to make an equivalent to a call, and continue their flow with other looked up JRs rather than returning. There are definitely a lot better ways of doing things and even by having a single known service function such as the relocatable_call demonstrated, it could be easily possible to make everything else relocatable.


Any imrpovements?  Can we make a fully self-relocating code that runs from any memory location without a separate loader?



ORG #4000


JR FINDCODE


CODE_ID: TEXT "AGUID"
PCISHERE: JR GOTPC


GETPC: POP HL ; GET PC
PUSH HL
RET


GOTPC: LD BC, TAB_ADDRESSES - PCISHERE + 2
ADD HL, BC
PUSH HL
POP IY ; IY = ADDRESS OF TAB_ADDRESSES


JR ENTRY






; FIND SOME GUID
FINDCODE: LD BC, 0 ; WE WILL LATER OVERWRITE THE GUID TO BE A CALL
FINDLOOP: PUSH BC ; TO GETPC WHICH IS A KNOWN NUMBER OF BYTES FROM
POP HL ; THE CODE_ID
LD A, (HL)
CP 'A'
JR NZ, FINDNEXT


INC HL
LD A, (HL)
CP 'G'
JR NZ, FINDNEXT


INC HL
LD A, (HL)
CP 'U'
JR NZ, FINDNEXT


INC HL
LD A, (HL)
CP 'I'
JR NZ, FINDNEXT


INC HL
LD A, (HL)
CP 'D'
JR NZ, FINDNEXT
JR Z, FOUND


FINDNEXT: INC BC
JR FINDLOOP


; NOW WE KNOW THE ADDRESS OF THE CODE_ID
; LETS MODIFY IT TO BECOME A CALL TO GETPC
; BEFORE JUMPING TO IT
FOUND: PUSH BC ; BC = ADDRESS OF CODE_ID
PUSH BC
PUSH BC
POP HL ; HL = ADDRESS OF CODE_ID


; GETPC IS 7 BYTES BEYOND CODE_ID SO ADD 7 TO HL
LD BC, 7
ADD HL, BC


PUSH HL
POP DE ; DE = ADDRESS OF GETPC


POP HL ; HL = ADDRESS OF CODE_ID AGAIN


LD A, #CD ; A BECOMES CALL
LD (HL), A
INC HL


LD A, E ; G BECOMES PART OF GETPC ADDRESS
LD (HL), A
INC HL


LD A, D ; U BECOMES PART OF GETPC ADDRESS
LD (HL), A
INC HL


LD A, 0 ; I BECOMES NOP
LD (HL), A
INC HL


LD A, 0 ; D BECOMES NOP
LD (HL), A


POP BC ; BC = GETPC


JR CODE_ID ; JUMP TO CODE ID, WHICH THEN CALLS GETPC AND JUMPS TO
; GOTPC


TAB_ADDRESSES:
DW FUNC0-TAB_ADDRESSES, FUNC1-TAB_ADDRESSES, FUNC2-TAB_ADDRESSES


ENTRY: LD A,0 ; ENTRY POINT TO OUR APPLICATION
CALL RELOCATABLE_CALL ; RELOCATABLE CALL TO FUNCTION 0
HALT ; SHOULD HAVE A 255 IN A HERE


FUNC0: LD A,1 ; RELOCATABLE CALL TO FUNCTION 1
CALL RELOCATABLE_CALL ;
RET


FUNC1: LD A,2 ; RELOCATABLE CALL TO FUNCTION 2
CALL RELOCATABLE_CALL ;
RET


FUNC2: LD A,255 ; WE JUST WANT TO RETURN A RESULT OF 255
RET


RELOCATABLE_CALL:
; PUT CODE TO PRESERVE REGISTERS HERE
LD H, 0
LD L, A
ADD HL, HL ; GET THE ENTRY NUMBER


PUSH IY
POP BC ; BC = TABLE


ADD HL, BC ; HL = FUNCTION OFFSET TO READ


LD C, (HL) ; BC = 2 BYTE RELATIVE ADDRESS FROM THE TABLE
INC HL
LD B, (HL)


PUSH IY
POP HL
ADD HL, BC ; ADD THE TABLE TO RELATIVE ADDRESS TO GET AN ABSOLUTE ADDRESS


; PUT CODE TO RESTORE REGISTERS HERE


PUSH HL ; CALL THE ADDRESS
RET





Animalgril987

#17
Why not:
  POP HL
  PUSH HL    ; HL now has copy of return address
  DEC HL       ; HL points to high byte of called address
  LD D, (HL)
  DEC HL      ;  HL now points to high byte of called address
  LD E, (HL)


and DE holds the entry address of your routine.

roudoudou

Quote from: Animalgril987 on 13:12, 28 August 21
Why not:
  POP HL
  PUSH HL    ; HL now has copy of return address
  DEC HL       ; HL points to high byte of called address
  LD D, (HL)
  DEC HL      ;  HL now points to high byte of called address
  LD E, (HL)


and DE holds the entry address of your routine.
you assume there is a CALL before your routine but AMSDOS has his own ways  ;D

My pronouns are RASM and ACE

Animalgril987

Quote from: roudoudou on 14:31, 28 August 21Quote from: Animalgril987 on Today at 13:12:53
Why not:
  POP HL
  PUSH HL    ; HL now has copy of return address
  DEC HL       ; HL points to high byte of called address
  LD D, (HL)
  DEC HL      ;  HL now points to high byte of called address
  LD E, (HL)


 and DE holds the entry address of your routine.
you assume there is a CALL before your routine but AMSDOS has his own ways
Very true.
Also a typo: 3rd comment should say LOW byte, not high.

andycadley

Quote from: roudoudou on 14:31, 28 August 21
you assume there is a CALL before your routine but AMSDOS has his own ways  ;D
Yes, this sort of approach will fail if you've been called via an indirection block of some kind, as the stacked return address will actually be the location of the indirection rather than the code it eventually jumps to.

andycadley

Quote from: eto on 11:03, 28 August 21
Well... I think if the question has at its core the restriction "zero absolute addresses". Once you soften this, you answer a totally different question.
Well it depends on whether you interpret that as strictly no absolute addressing or more as a requirement that the code automatically relocates itself (either initially or on every call).


Writing truly address independent code on the Z80 (i.e. where you can't just inline modify an absolute addressed instruction) is particularly difficult. Not impossible, but you definitely have to jump through some hoops to accomplish it.


Cheating slightly and using the fact that passing parameters from BASIC will reserve some scratch memory pointed to by IX isn't really using absolute addressing but it certainly feels like skirting around the limitations a bit.

zhulien

To write a relocator, I would do the following:


- assemble the application, eg: at 0000
- assemble the application again at a different address which changes the high and low byte of each call, eg: 0101
- write a file of addresses for use with a loader


Then write the loader, allocate the application ram, allocate the address file (if not processed as it is read), change each address then free up the address file in ram.


If a program can just run from where ever it is loaded, or self-relocate (that bit would at least need to run where ever it is loaded), then that has a useful purpose.


Are there any better ways?  If crunched of course the process would be a bit different.  Are there any crunchers/relocators that do so 'in place'?  eg: FFFCCC (F = free ram, C = crunched app) --> UUUUUF (U = unchrunched app)


Does anyone not use a mass storage device on CPC these days that makes crunching even necessary?  I think the Z80 cycles and complexity are not worth it given storage for CPC is now plentiful... how to fill up so many gigabytes of data with a Z80 anyway?

FloppySoftware

#23
I use CP/M PRL files to achieve this. They have a relocatable bitmap based on page offset.


You can generate PRL files with DR LINK, ZASM or my PRL program. There are other tools to do this.


You can write a PRL loader, I did it for my SamaruX project.
floppysoftware.es < NEW URL!!!
cpm-connections.blogspot.com.es

krusty_benediction

Never tested, but winape assembler proposes these directives:
relocate_end    Mark the end of a relocatable section of code.
relocate_start    Mark the start of a relocatable section of code.
relocate_table [byte|word] [base_address]    Generate a relocation table. By default a table of word sized offsets is generated, override this by specifying byte for small code sections. The base_address specifies the relative origin for the values in the table.

and these variables

relocate_count    The number of entries in the relocation table.
relocate_size    The size of the relocation table, assuming the table is using word entries.
I guess other assemblers have such functionalities too.I would say they generate a table of addresses you have to patch by adding them the loading address of the program.
No need to do the things by yourself
 

Powered by SMFPacks Menu Editor Mod