Very interesting discussion! And very cool approaches!!!
The few times I've needed to do something like this, I just did something very simple that worked fine for me:
1) I fix the RAM address where I will want to copy the routine for execution
2) compile the routine in question separately, and place an "org" statement with the RAM address at the top
3) That's it, whenever you want to call that routine, you can decompress/copy it to RAM, and just call it normally
Since you know in advance where the code will be when executed, JP/JR is not an issue at all.
It gets bit interesting if the routine in question needs to call code from the rest of the program. But a simple solution for that is to include the .sym file resulting from compiling your main code (which defines all the labels), and that's it.
I agree, this discussion is really interesting, and I would like to use some of these techniques with the glass-assembler
Here's an example of relocatable code using a fixup (relocation) table. It needs a known address which has RAM and two bytes free. I've used 0FB36h (SAVSP, used only for the PLAY instruction) for this purpose.
I found this code a bit hard to follow (pardon my ignorance ), would you mind letting me know how this we can use this snippet? Like, what are TwoBytesFree really used for - I can't see it is used down in the code? Is the code itself going to be relocated? -How will the "jp LoopPrint"-command work in that case?
I found this code a bit hard to follow (pardon my ignorance ), would you mind letting me know how this we can use this snippet? Like, what are TwoBytesFree really used for - I can't see it is used down in the code? Is the code itself going to be relocated? -How will the "jp LoopPrint"-command work in that case?
The code, as written, works in any address as long as it is RAM (because it modifies some bytes). It is not being moved anywhere by this routine, just patched.
TwoBytesFree is an address where the code stores the following instructions (each one byte long):
pop hl jp (hl)
Calling TwoBytesFree after storing these two opcodes has the effect of retrieving the return address of the call into HL. That's because the CALL instruction stores the return address (the address of the instruction immediately following the call) on the stack, therefore the POP HL instruction will retrieve it and the JP (HL) instruction will jump to it, letting the code continue as if you executed RET, except with the return address in HL. It is used to let the code know the current address where it's running. (Note that JP (HL) is a misnomer; the instruction actually jumps to the value of HL, not to the address stored where HL points to, that is, if it was more consistently named, it should be named JP HL instead).
Now that the code knows where it is located, all it has to do is patch every absolute address it uses, to make it work with its current address. For that, it needs to know where the absolute addresses are, which you tell it through the FixupTable table, and add to each the difference between the address where it is located now and where it was located when it was assembled.
The address used by JP LoopPrint has an entry in the fixup table, therefore it will be patched to point to the new address where the correct loop point is. The same goes for LD HL,TextOfs. If the relocation was not working, the text printed could be anything. If you see the text "It worked!" it means that the relocation worked.
You can add a bin header so you can load it from disk, load it in any reasonable address (e.g. anything between 8200h and 0DE00h I guess, but don't use ORG because the whole point of the code is to not need it to start at its final destination), and use the openMSX debuger to place a breakpoint at the load address, then trace the execution step by step to see how it works. To add the bin header you can use this program if you have Python: https://notabug.org/pgimeno/vdptest/src/master/raw2bin.py
One flaw is that the code, as written, doesn't keep track of whether it is already patched, therefore you need to be careful that the relocation routine only runs once, otherwise the second time you run it you would be breaking it. But you want to avoid that in any case anyway, because executing it every time you call the code is wasteful.
On a different matter, I can't edit any longer, but these two lines:
Ofs: ld bc,-Ofs ; subtract real address
could be shortened to:
ld bc,-$ ; subtract real address
Also, if the address operand of that instruction itself is added to the fixup table, the routine can probably execute more times without risk.
Anyway, after rereading the request by friguron, I've realized that what he's after is something that I've needed as well, and that Glass implements. He wants to assemble part of his code at a fixed destination address, but with the code originally residing in the asm file.
Few assemblers make a distinction between the destination where the opcodes are generated, and the counter that keeps track of the addresses. Fortunately, the ORG instruction in Glass does not change the destination of the opcodes, making it suitable for friguron's purposes.
Here's how I've implemented it:
RoutineDest equ 0C000h org 0A000h ld hl,RoutineStart ld de,RoutineDest ld bc,RoutineEnd-RoutineDest ldir jp Continue RoutineStart equ $ org RoutineDest Print: ld a,(hl) or a ret z rst 18h inc hl ; we use jp instead of jr artificially here, for testing jp Print RoutineEnd equ $ org RoutineEnd-RoutineDest+RoutineStart Continue: ld hl,Text call Print ret Text db "Everything went well", 0
The main code is assembled at address 0A000h; the Print subroutine is designed to work in 0C000h. It is first moved there and then called. The JP in the routine's loop jumps to the correct address (0C000h).
Pasmo also changes the destination address, so the above code will not work with Pasmo.
To better see the difference, consider this file:
org 100h rst 0 org 200h rst 0
Pasmo will generate a 257 byte file with the first RST 0 in the first byte, and the second at offset 256 in the file, with zeros in between, while Glass will generate a 2 byte file with two RST 0 instructions in a row.
This is both good and bad. Glass allows doing what friguron wanted, but it makes it harder to place certain code at a known address.
(Note that JP (HL) is a misnomer; the instruction actually jumps to the value of HL, not to the address stored where HL points to, that is, if it was more consistently named, it should be named JP HL instead).
Actually in Glass you can write jp hl
, with or without parentheses . I generally stick to standard and portable syntax even if it is irregular (e.g. no
sub a,b
shenanigans supported), however I made an exception for this one, where the Z80 syntax is just incorrect and confusing.
Few assemblers make a distinction between the destination where the opcodes are generated, and the counter that keeps track of the addresses. Fortunately, the ORG instruction in Glass does not change the destination of the opcodes, making it suitable for friguron's purposes.
Y’know, this is actually what triggered me to develop Glass. I initially used Pasmo but once I found it has this (in my eyes) strange implementation of org
, I first submitted a patch for Pasmo but got not response on it, so I made my own assembler. All assemblers I’ve used in the past (Gen80, Compass) have this implementation of org
(maybe WBASS didn’t), and I don’t find the alternative way useful at all, it’s impossible to create MegaROMs with that or indeed to relocate code like this. I certainly hope most assemblers don’t implement org
the Pasmo way, at least I think the Z80 ones generally don’t.
To pad up to a certain address, you write ds 200H - $
.
I can see uses for both approaches. Let's assume you have planned your program with a specific memory layout in mind at fixed addresses; for example, header and basic initialization at 4000h, interrupt service routine at 4040h, interrupt vector table at 4100h, code from 4201h to 4FFFh, game map at 5000h, etc. In Glass you can't use ds with a negative value, which means you're forced to have the program in strict increasing memory address order. With Pasmo's approach, you can place stuff at these locations without worrying about placing them in the correct order. You can write either this:
org 100h rst 0 org 200h rst 38h
or this:
org 200h rst 38h org 100h rst 0
with the same result.
And on the other hand, in Pasmo it's not impossible to do what friguron wanted, but you need a different approach and the help of the build system:
print.asm:
org 0C000h Print: ld a,(hl) or a ret z rst 18h inc hl ; we use jp instead of jr artificially here, for testing jp Print
main.asm:
RoutineDest equ 0C000h include "print.sym" org 0A000h ld hl,RoutineStart ld de,RoutineDest ld bc,RoutineEnd-RoutineStart ldir jp Continue RoutineStart: incbin "print.bin" RoutineEnd equ $ Continue: ld hl,Text call Print ret Text db "Everything went well", 0
Makefile:
main.bin: main.asm print.bin print.sym pasmo main.asm main.bin print.bin print.sym: print.asm pasmo print.asm print.bin print.sym
To wrap up, I do believe that they are separate actions, and that ideally, assemblers should provide directives for both, and probably neither of them should be called ORG.
Thank you for the elaboration. Very clear now! And the distinction between Pasmo and Glass with regards to padding is really good to know!
As for the jp (hl) - thanks! -So far I have strayed away from this command as it never seemed
to fit to my code. I have needed "JP HL", but never "JP (HL)". After testing your code in the debugger I obviously concluded with your statements as well. So, yeah, wierd, counter-intuitive and misleading from Zilog.
I came to this thread again (after some days) and the amount of info is so useful, I'm not sure if should even refactor my rellocatable routines to adapt them to all this new asm tricks : )
Pgimeno got what I said in his 13:24h message... I'll try something as soon as I have the time. I'm happy Glass will be able to deal with my needs and I won't need to change it for a different assembler. Let's see if my routines can become universal (for my projects) and can be included with an "include" directive and being deployed freely onto RAM on different addresses, depending on each project needs.
Hi guys...
This weekend I've finally been able to mess around with my Z80 .asm routines (which eventually I might include inside some mem-managing routines library for my own internal MSX needs). Whatever, the thing is I've come up with a mix of many ideas, some from Grauw and some other from PGimeno. I'm putting them here and everyone might use them as a further reference besides the already suggested solutions (I've simplified my code in some sections for clarity)
This should be the main core of my first "truly rellocatable" routine:
... ... here should come the original org 04000H rom definition area ... ... ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; ;; a = desired slotid ;; h = page ;; ;; this is just a simplified example ENASLT_ENH: ; still in the "04000H area" org ENASLT_ENH_RAM ;; Scope change -> RAM ;; This OGR constant (ENASLT_ENH_RAM) should be an EQU defined one, probably ;; somewhere else. The whole rellocation of the routine will use just this ;; unique base value and nothing else (Example: ENASLT_ENH_RAM: equ 0F000H ) ;; this will be now the high RAM area definition, so Glass will take it for ;; referencing the code and data that will come next ENASLT_ENH_RAM_START: ;; Besides, we'll create some "var zone" space "inside" the routine, this ;; is nice because we don't need to define some EQU vars anywhere else. ;; Everything gets defined next to the actual code (!!), making the whole ;; routine and its optional RAM working space easily rellocatable. jr ENASLT_ENH_start_code ENASLT_ENH_vars: ENASLT_ENH_var_original_pslot: nop ENASLT_ENH_var_y: nop ENASLT_ENH_var_z: nop ENASLT_ENH_var_w: nop ENASLT_ENH_start_code: ; save pslot ; in a,(0A8H) ld (ENASLT_ENH_var_original_pslot),a ; ; ; BLABLABLA. ASM code. ; ; END_ENASLT_ENH_RAM: org ENASLT_ENH + (END_ENASLT_ENH_RAM - ENASLT_ENH_RAM_START) ;; we "return" to the "rom" area code definition (the above calculation ;; should yield something close to the original org definition, before ;; the ENASLT_ENH_RAM scope change, that is around 04000H )
One of the main (unexepected and very welcome) advantages of this method is that JR and JP work transparently inside the routine, that is, even long jumps will be working inside the new "RAM defined" scope (that's AMAZING and one of the main wishes I had days ago)
The routine (and probably some of the tricks to make it rellocatable) might change in the future, but for my immediate (and middle future) needs, I think I got what I wanted and needed when I asked my original question, thanks guys
NOTE: Even though (as Grauw said) probably this way of working with .asm files and code/org definition is not Glass' exclusive, or new, it's definitely non-newbie stuff, specially "playing around" with ORG in such a creative way, and specially when not all assemblers seem to deal with ORG directive in the same way. I'm an asm newbie and I would have never come up with all that on my own.
Whatever, I've managed to fit Glass to my needs and that should be enough. Or well, we could put it differently: Grauw's managed to meet everyone needs creating Glass
As I said before, thanks again!! and happy coding!
I'm running into a small issue. The line number displayed when reporting an error is off by 1. In about every editor out there, the first line of the file is line 1, but if there's an error in the first line, Glass reports it as 0.