Passing parameters
One of the phases of invoking a subroutine, in assembler language, is the passing of parameters.
There are four common methods for passing parameters:
- through the machine registers;
- storage of the values in a predetermined memory area;
- via stack;
- writing data to the memory area immediately following the subroutine call.
Of these four modes, the Hi-tech C compiler uses the latter.
First method
Nel metodo di passaggio dati tramite registri, l’Accumulatore è ovviamente il registro migliore per passare un parametro a 8 bit.
Il registro doppio HL invece può essere utile per passare un indirizzo (16 bit).
Anche il registro doppio DE potrebbe passare un indirizzo ed usare EX DE, HL per scambiare gli indirizzi.
Il registro doppio BC è adatto invece per passare valori di conteggio.
I registri indice IX e IY sono ovviamente i registri più adatti per memorizzare indirizzi base a strutture dati dove gli elementi
sono recuperabili tramite offset. Quest’ultimo metodo è usato generalmente nelle routine di interruzione e per il salvataggio
e recupero dei registri macchina.
Second method
In the method of passing data through registers, the Accumulator is obviously the best register to pass an 8-bit parameter.
The double register HL, on the other hand, can be useful for passing an address (16 bits).
Double register DE could also pass an address and use EX DE, HL to swap addresses.
The double register BC, on the other hand, is suitable for passing count values.
Index registers IX and IY are obviously the most suitable registers for storing base addresses in data structures where the elements
can be recovered by offsets. The latter method is generally used in abort routines and for saving
and retrieving machine logs.
Third method
The execution of the CALL subr instruction requires the addition of the return address (address of the calling program immediately
following the 3 bytes of the CALL instruction) in the base of the stack. Recall that the stack extends from the memory address
prefixed with LD SP, ind16 decreasing, that is, from high memory addresses downwards. An address occupies 2 bytes so
in the stack the parameters are in the memory cells from base + 2 upwards.
The invoked subroutine copies the address of the stack pointer into a memory area with LD (addr), SP or in the double register HL:
LD HL, 0
ADD HL, SP
If HL is to be used in the subroutine for other needs, then register IX can be used
LD IX, 0
ADD IX, SP
The calling program puts the parameters on the stack and allocates space for the results before invoking the subroutine. Let’s see an example. We pass an 8-bit parameter and a 16-bit parameter to a subroutine, from this we want to obtain an 8-bit result. The first step is to prepare the place for the 8 result bits in the stack:
LD HL, -1 | ; one byte for the return parameter |
ADD HL, SP | ; copy stack pointer address in HL |
LD SP, HL | ; updates the stack pointer with the new value |
Let’s see what happens in a fragment of memory:
D001 | … | |
D000 | … | |
CFFF | … | |
CFFE | … | ← stack pointer after LD SP, HL |
CFFD | return parameter | ← stack pointer before ADD SP, HL |
Now let’s put in the stack the parameters to send, one of 8 bits and one of 16 bits:
LD A, (parametro8) | ; 1 byte parameter to sent |
PUSH AF | ; but in the stack it occupies 2 bytes (AF) |
INC SP | ; retrieves the place of an unnecessary byte of register F |
; in HL the 2-byte parameter | |
PUSH HL | ; put into stack (HL) 2-byte |
CALL subroutine |
The stack pointer increment after PUSH AF is necessary because we have to pass an 8-bit parameter while PUSH AF would take up 16 bits.
The second INC SP is instead used to point to the address D002 after the PUSH HL (otherwise the stack pointer would point to the address D001).
This is the situation of the memory area after the previous operations:
D003 | … | |
D002 | Return address from LSB subroutine | |
D001 | Return address from MSB subroutine | |
D000 | Input parameter L | |
CFFF | Input parameter H | |
CFFE | Input parameter A | |
CFFD | Return parameter | |
CFFC | … |
At this point the stack points to address D002 where the return address following the subroutine call resides.
To point to the data area you need to add 1 + 1 byte (return address) to the stack pointer:
LD HL, 2 | ; data area offset |
ADD HL, SP | ; calculate the new address |
LD SP, HL | ; updates the stack pointer with the new value |
The stack now points to the address D000. The subroutine can now fetch the parameters it needs:
LD E, (HL) | ; first parameter |
DEC HL | |
LD D, (HL) | ; second parameter |
DEC HL | |
LD A, (HL) | ; third parameter |
EX DE, HL | ; swaps the pointer in DE and in HL the first two parameters |
In registers HL and A we have the passed parameters. After the calculations we put the 8-bit result on the stack:
EX DE,HL | ; address retrieval from DE |
DEC HL | ; HL points to CFFD, the address of the return parameter |
LD (HL), A | ; copy the result to the stack |
We adjust the stack pointer to the right value to get the return address.
The current stack value is CFFD, so 5 places lower than necessary (we used 1 + 1 + 1 byte for the parameters and 2 bytes for the return address):
LD HL, 5 | |
ADD HL, SP | |
LD SP, HL |
The stack pointer now points to the right position to return to the calling program via RET.
The calling program must now collect the result from the stack which after returning from the subroutine points to the address D003. The return parameter is 5 bytes below (remember? 1 + 1 + 1 + 2 bytes):
LD HL, 5 | |
ADD HL, SP | |
LD SP, HL | |
POP AF |
In register A we have the result.
Fourth method
The passage of parameters in the memory area immediately following the invocation of subroutines, as we have already said, is the one used by the Hitech C compiler. When a subroutine is invoked, the return address resides at the bottom of the stack. Therefore by executing POP IX the index register points to the memory area where are the parameters to pass. Let’s see an example:
02F2 | CALL subr1 | ; Call subroutine1 |
02F5 | Parm1 | ; First 8-bit parameter |
02F6 | Parm2 | ; Second parameter of 16 bits |
02F7 | ||
02F8 | Parm3 | ; Third parameter 16 bit |
02F9 |
At address 02FA there is the continuation of the calling program. Suppose Subr1 is at 0300:
0300 Subr1: | POP IX | ; IX = 02F5 |
0301 | LD A,(IX+1) | ; A = parm2 LSB |
0305 | LD E,A | |
0306 | LD A,(IX+2) | ; A = parm2 MSB |
030A | LD D,A | |
030B | LD A,(IX+3) | ; A = parm3 LSB |
030F | LD L,A | |
0310 | LD A,(IX+4) | ; A = parm3 MSB |
0313 | LD H,A | |
0314 | LD A,(IX+0) | ; A = parm1 |
The POP IX instruction loads into the index register IX the return address to the program placed by CALL Subr1 on the stack. In IX we therefore have an address that does not point to an instruction but to the data block to be passed to the Subr1 subroutine. By adding the appropriate offset to IX, we can load the values to be passed into the registers. After a series of calculations, the Subr1 subroutine returns the result by putting it at the address pointed to by IX plus the appropriate offset. Recall that IX still bets 02F5, so we can deposit the return value easily. Now let’s see how to return to the address immediately following the invocation of Subr1:
0320 | LD BC, 5 | ; 5 bytes of parameters (2 + 2 + 1) |
0323 | ADD IX, BC | ; 02F5 + 5 = 02FA |
0325 | PUSH IX | ; IX = 02FA |
0327 | RET | ; back to 02FA |
Recall that IX contains the address of the first parameter, therefore the return address from the subroutine is given by the sum of the latter and the number of bytes occupied by the parameters. In this case IX is equal to 02F5, the bytes used are 5 so the return address is 02FA then PUSH IX puts the correct address on the stack for the return. A simple RET takes us back to the main program flow.
The “clean” organization of the parameters pays for itself in terms of machine time, the LD A instruction (IX + offset) costs 5 machine cycles. This explains why the code generated by compiling with Hi-tech C is not fast in execution compared to that of other C compilers.
We have seen that to pass parameters to a function written in assembler Z80, Hi-tech C uses a procedure that at first sight may seem complex but very efficient.
When an assembler function is invoked by the C language program, the arguments are pushed onto the stack in reverse order, that is, the first argument is in the lowest memory address. The same happens for a C language routine invoked by an assembler program.
In the case of a single floating point parameter, the structure is the modified IEEE754P one:
sign | 1 bit |
exponent | 7 bit |
mantissa | 24 bit normalized |
The number is stored with the sign in the bit7 of the highest byte and the mantissa in the lowest byte.
To read the parameter we need to use the updated IX pointer with the stack pointer plus some offset.
The offset, with respect to the stack pointer, to be adopted is 6 due to 2 bytes for the return address, 2 bytes for IX and another 2 bytes for the first PUSH that saves the IX index:
psect text, global global _programma, prg1, prg2 arg equ 6 ; offset to first argument _programma: push iy ; procedure reading input parameters push ix ld ix,0 add ix,sp ; IX points to first argument ld a, 00h ; ld (flagz), a ; flagz contains 0 ; copy first argument in HL DE ld e,(ix+arg+0) ; low byte mantissa ld d,(ix+arg+1) ; middle byte mantissa ld l,(ix+arg+2) ; high byte mantissa ld h,(ix+arg+3) ; sign + exponent .... ld sp,ix ; procedure of exit pop ix pop iy ret
The return of the processing result is in HL DE.
In the case of word type, the value returns in HL; a long word in HL DE with the top in HL. The integers are in L and the sign in H.
The character pointers occupy 2 bytes and are loaded in HL, for example we see a module receiving a character pointer where it writes ‘0’, ‘1’, the string terminator ‘\ 0’ and returns the pointer to the received vector in HL:
psect text, global global _stringa, prg1, prg2 arg equ 6 ; offset to first argument push iy ; procedure reading input parameters push ix ld ix,0 add ix,sp ; pointer to arguments ; in HL the address to the pointer ld l,(ix+arg+0) ; high byte of the address ld h,(ix+arg+1) ; low byte of the address ld (hl), 48 ; '0' inc hl ld (hl), 49 ; '1' inc hl ld (hl), 0 ; string terminator dec hl ; many dec hl for each element of the vector excluding the terminator dec hl ; HL points to the first element of the vector ld sp,ix ; then exit pop ix pop iy ret
Now let’s see an example in Z80 assembler language that takes up the FMOD library function published in LIBV.
In this module you can see in practice how parameters are passed between the various modules, how to invoke existing library modules and how to receive data and return the result. The compilation is done with the command ZAS FMOD.Z80, the generated obj file is added to the library with LIBR R LIBV.LIB FMOD.OBJ
; double fmod(double z, double x) ; ritorna il resto della divisione di z per x, entrambi floating point ; ; fmod(z,x) = z - x * floor(z/x) ; se x>0 allora (0 <= fmod(z,x) <= x) ; se x<=0 allora (-x <= fmod(z,x) <= 0) ; ; Formato di un numero floating point ; ------------ ; * segno * 1 bit ; *-----------* ; * esponente * 7 bit ; *-----------* ; * mantissa * 24 bit normalizzato ; ------------- ; Il numero e' memorizzato con il segno nel byte piu' alto e ; la mantissa nel byte piu' basso psect text, global global _fmod, _floor, flsub, flmul, fldiv, fladd arg equ 6 ; offeset primo argomento _fmod: ex (sp), iy ; salva IY per l'indirizzo di ritorno push ix ld ix, 0 add ix, sp ; in IX il puntatore ai parametri ld l,(ix+arg+4) ; salva x in HL DE ld h,(ix+arg+5) ; segno ed esponente in H ld e,(ix+arg+6) ; in L e DE la mantissa ld d,(ix+arg+7) ; push hl ; salva x nello stack push de ; ld l,(ix+arg+0) ; salva z in HL DE ld h,(ix+arg+1) ; segno ed esponente in H ld e,(ix+arg+2) ; in L e DE la mantissa ld d,(ix+arg+3) ; ; abbiamo x (4 byte) nello stack ; z in HL DE ; Divisione float di libreria. Il numero in HL DE e' diviso ; con il numero che risiede nello stack sotto all'indirizzo ; di ritorno. ; Lo stack e' pulito e il risultato torna in HL DE. ; Vedere il sorgente round in float.as ; ( z / x ) call fldiv ; divide HL DE per ; HL DE salvato nello stack ; risultato in HL DE exx ; aggiusta lo stack pop de pop hl exx ; Arrotonda il numero in HL DE all'unita' superiore ; con lo shift di bit ; floor.c e' in libf.lib ; floor( z / x ) call _floor ; Moltiplicazione floating point. Il numero in HL DE e' ; moltiplicato con il numero che risiede nello stack sotto ; all'indirizzo di ritorno. ; Lo stack e' pulito e il risultato torna in HL DE. ; Vedere il sorgente in flmul in float.as ; x * floor( z / x ) push hl ; salva floor(z/x) push de ; nello stack ; recupera x ld l,(ix+arg+4) ; salva x in HL DE ld h,(ix+arg+5) ; segno ed esponente in H ld e,(ix+arg+6) ; in L e DE la mantissa ld d,(ix+arg+7) ; call flmul ; moltiplica HL DE per ; HL DE salvato nello stack ; risultato in HL DE exx ; aggiusta lo stack pop de pop hl exx ; Sottrazione floating point. Il valore nello stack ; e' sottratto da HL DE: z - x = z + (-x) ; Vedere il sorgente in flsub in float.as ; z - x * floor( z / x ) call flsub ; sottrae da HL DE il valore ; HL DE salvato nello stack ; risultato in HL DE ld sp,ix pop iy ret