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

 

Loading