Precedente Principale Sommario Informazioni Redazione Browser Successivo

[Rubrica Programmazione] Coordinamento di Francesco Sileno

ASM: Macro e Procedure

Questo mese riprendiamo il programmino dell'ultima volta, e lo modifichiamo per poter introdurre la programmazione tramite macro e procedure, come era facilmente intuibile dal titolo dell'articolo. Le differenze tra macro e procedura, concettualmente, sono irrisorie: entrambi i metodi ci consentono di scrivere una volta sola del codice che deve essere eseguito più volte. Praticamente sono tante...

di Francesco Sileno

Le Macro

Tanto per non annoiarvi, vediamo subito come appare il nostro programma ridotto a macro.
    .MODEL SMALL
    .STACK 2048

stampa MACRO string
    mov     ah,9
    mov     dx,OFFSET string
    int     21h
ENDM

    .DATA
domanda db 'Ciao, quale sarebbe il tuo nome?',0Dh,0Ah,'> $'
nome    db 255, 0, 254 DUP (00)
ricamo1 db 0Dh,0Ah,'Così ti chiami $'
ricamo2 db '... bah, pensavo meglio.',0Dh,0Ah,'$'

    .CODE
    ASSUME CS:@CODE,DS:@DATA

    MOV     AX,@DATA
    MOV     DS,AX

    stampa  domanda

    mov     ax,0A00h
    mov     dx,OFFSET nome
    int     21h

    xor     bx,bx
    mov     bl,[nome+1]
    mov     [nome+bx+2],'$'

    stampa  ricamo1

    stampa  [nome+2]

    stampa  ricamo2

FINE:
    MOV     AX,4C00H
    INT     21H

    END
Quella che appare in alto, prima della definizione dei segmenti, è una dichiarazione di macro. La sua sintassi è:
Nome_Macro MACRO Par1, Par2, ...
        [codice]
ENDM
e può essere fatta ovunque nel sorgente. Meglio però farla in modo che preceda l'effettiva chiamata, altrimenti si potrebbero generare dei problemi di referenziamento che dovrete risolvere con compilazioni a passate multiple. Per richiamarla è sufficiente:
Nome_Macro P1, P2, ...
Il controllo sui parametri viene effettuato in fase di compilazione, in questo esempio effettuare una chiamata a
STAMPA AX
DOVREBBE causare un errore, in quanto OFFSET AX al massimo potrebbe resituire le coordinate dei transistor che formano il latch che rappresenta il registro AX nella cpu.
In realtà ho appena scoperto che il TASM traduce OFFSET AX in AX. Curioso esempio di intelligenza artificiale, pur restando una chiamata priva di senso.

Quello che otteniamo nell'eseguibile non è una chiamata ad una procedura... in realtà il compilatore sostituisce ad ogni chiamata alla macro il codice per essa dichiarato, facendosi carico di ricopiare il codice ogni volta.
Per capire meglio, facciamo generare al compilatore un code listing del sorgente, ovvero un file di testo dove ci mostra il modo in cui traduce le istruzioni, le direttive, e i riferimenti per il linker. Ogni compilatore ha la sua sintassi, per il TASM usate il parametro /L.

      1	0000				 .MODEL	SMALL
      2	0000				 .STACK	2048
      3
      4				     stampa MACRO string
      5					 mov	 ah,9
      6					 mov	 dx,OFFSET string
      7					 int	 21h
      8				     ENDM
      9
     10	0000				 .DATA
     11	0000  43 69 61 6F 2C 20	71+  domanda db	'Ciao, quale sarebbe +
     12       75 61 6C 65 20 73 61+  il tuo nome?',0Dh,0Ah,'> $'
     13	      72 65 62 62 65 20	69+
     14	      6C 20 74 75 6F 20	6E+
     15	      6F 6D 65 3F 0D 0A	3E+
     16	      20 24
     17	0025  FF 00 FE*(00)	     nome    db	255, 0,	254 DUP	(00)
     18 0125  0D 0A 43 6F 73 8D 20+  ricamo1 db 0Dh,0Ah,'Così ti +
     19	      74 69 20 63 68 69	61+  chiami $'
     20	      6D 69 20 24
     21	0137  2E 2E 2E 20 62 61	68+  ricamo2 db	'... bah, pensavo    +
     22	      2C 20 70 65 6E 73	61+  meglio.',0Dh,0Ah,'$'
     23	      76 6F 20 6D 65 67	6C+
     24	      69 6F 2E 0D 0A 24
     25
     26	0152				 .CODE
     27					 ASSUME	CS:@CODE,DS:@DATA
     28
     29	0000  B8 0000s			 MOV	 AX,@DATA
     30	0003  8E D8			 MOV	 DS,AX
     31
     32					 stampa	 domanda
1    33	0005  B4 09			 mov	 ah,9
1    34	0007  BA 0000r			 mov	 dx,OFFSET domanda
1    35	000A  CD 21			 int	 21h
     36
     37	000C  B8 0A00			 mov	 ax,0A00h
     38	000F  BA 0025r			 mov	 dx,OFFSET nome
     39	0012  CD 21			 int	 21h
     40
     41	0014  33 DB			 xor	 bx,bx
     42	0016  8A 1E 0026r		 mov	 bl,[nome+1]
     43	001A  C6 87 0027r 24		 mov	 [nome+bx+2],'$'
     44
     45					 stampa	 ricamo1
1    46	001F  B4 09			 mov	 ah,9
1    47	0021  BA 0125r			 mov	 dx,OFFSET ricamo1
1    48	0024  CD 21			 int	 21h
     49
     50					 stampa	 [nome+2]
1    51	0026  B4 09			 mov	 ah,9
1    52	0028  BA 0027r			 mov	 dx,OFFSET [nome+2]
1    53	002B  CD 21			 int	 21h
     54
     55					 stampa	 ricamo2
1    56	002D  B4 09			 mov	 ah,9
1    57	002F  BA 0137r			 mov	 dx,OFFSET ricamo2
1    58	0032  CD 21			 int	 21h
     59
     60	0034			     FINE:
     61	0034  B8 4C00			 MOV	 AX,4C00H
     62	0037  CD 21			 INT	 21H
     63
     64					 END
              ^^^^^^^^^^
              In questa colonna
              potete vedere il
              codice macchina delle
              istruzioni tradotte
Vedete? La dove noi avevamo scritto STAMPA, il compilatore ha sostituito il codice che costituisce la macro.

Notate che anche i parametri vengono sostituiti, ovvero: ad ogni parametro formale dichiarato, viene automaticamente sostituito il parametro effettivo della chiamata. Ed ovviamente se a questo punto si verifica un incongruenza tra tipi di dato esce fuori l'errore.

A causa del procedimento di copiatura ( macro espansione ), si deve far attenzione all'uso di etichette all'interno delle macro. Vediamo questo esempio:

    .MODEL SMALL
    .STACK 2048

Prova   MACRO
    jmp salto
    prova_word dw 0
salto:
ENDM

    .DATA

    .CODE
    ASSUME CS:@CODE,DS:@DATA

    MOV     AX,@DATA
    MOV     DS,AX

    Prova
    Prova

FINE:
    MOV     AX,4C00H
    INT     21H

    END

Il suo listing:
      1 0000                             .MODEL SMALL
      2	0000				 .STACK	2048
      3					 LOCALS
      4
      5				     Prova   MACRO
      6					 jmp salto
      7					 prova_word dw 0
      8				     salto:
      9				     ENDM
     10
     11
     12	0000				 .DATA
     13
     14	0000				 .CODE
     15					 ASSUME	CS:@CODE,DS:@DATA
     16
     17	0000  B8 0000s			 MOV	 AX,@DATA
     18	0003  8E D8			 MOV	 DS,AX
     19
     20					 Prova
1    21	0005  EB 06			 jmp salto
1    22	0007  0000			 prova_word dw 0
**Error** esmac.asm(21) PROVA(2) Symbol already defined elsewhere:   +
PROVA_WORD
1    23	0009			     salto:
**Error** esmac.asm(21) PROVA(3) Symbol already defined elsewhere:   +
SALTO
     24					 Prova
1    25	0009  EB 02			 jmp salto
1    26	000B  0000			 prova_word dw 0
**Error** esmac.asm(22) PROVA(2) Symbol already defined elsewhere:   +
PROVA_WORD
1    27	000D			     salto:
**Error** esmac.asm(22) PROVA(3) Symbol already defined elsewhere:   +
SALTO
     28
     29	000D			     FINE:
     30	000D  B8 4C00			 MOV	 AX,4C00H
     31	0010  CD 21			 INT	 21H
     32
     33					 END
Poichè la copiatura avviene in modo esatto, nessuno dice al compilatore che l'etichetta di una macro viene duplicata se la macro è chiamata più di una volta.
Per ovviare, il metodo più semplice consiste nell'aggiungere la direttiva LOCALS ( nessun punto iniziale! ) all'inizio del sorgente, e dichiarare le etichette in una macro nel formato @@Label, sia per le etichette di salto, che per le variabili.
    .MODEL SMALL
    .STACK 2048
    LOCALS

Prova   MACRO
    jmp @@salto
    @@prova_word dw 0
@@salto:
ENDM
    ...
In questo modo i riferimenti vengono risolti durante l'espansione della macro, usando delle etichette incrementali ed univoche.

Le Procedure

Questa invece è la versione dello stesso programma, che invece della macro usa una procedura:
    .MODEL SMALL
    .STACK 2048
    PAGE ,70

    .DATA
domanda db 'Ciao, quale sarebbe il tuo nome?',0Dh,0Ah,'> $'
nome    db 255, 0, 254 DUP (00)
ricamo1 db 0Dh,0Ah,'Così ti chiami $'
ricamo2 db '... bah, pensavo meglio.',0Dh,0Ah,'$'

    .CODE
    ASSUME CS:@CODE,DS:@DATA

    MOV     AX,@DATA
    MOV     DS,AX

    mov     dx,offset domanda
    push    dx
    call    stampa

    mov     ax,0A00h
    mov     dx,OFFSET nome
    int     21h

    xor     bx,bx
    mov     bl,[nome+1]
    mov     [nome+bx+2],'$'

    mov     dx,offset ricamo1
    push    dx
    call    stampa

    mov     dx,offset nome+2
    push    dx
    call    stampa

    mov     dx,offset ricamo2
    push    dx
    call    stampa

FINE:
    MOV     AX,4C00H
    INT     21H

stampa PROC NEAR
    pop     ax
    pop     dx  ; puntatore a stringa
    push    ax
    mov     ah,9
    int     21h
    ret
stampa ENDP

    END
Ora la procedura deve essere scritta all'interno del segmento codice, e si deve fare attenzione alla posizione in cui la si mette. Poiché in questo caso non si tratta si una dichiarazione, ma della scrittura di codice 'definitivo', il cui indirizzo di ingresso sarà rappresentato dal nome stesso della procedura.
Se l' avessi definita all'inizio del segmento codice, in fase di esecuzione il suo corpo sarebbe stato eseguito lo stesso all'avvio, nonostante non ci fosse stata alcuna chiamata! Forse un estratto di ipotetico listing del nostro programma con la procedura definita all'inizio del segmento codice vi farà capire meglio:
     ...
     19
     20	0152				 .CODE
     21
     22	0000			     stampa PROC NEAR
     23	0000  58			 pop	 ax
     24	0001  5A			 pop	 dx  ; puntatore a   +
     25				     stringa
     26	0002  50			 push	 ax
     27	0003  B4 09			 mov	 ah,9
     28	0005  CD 21			 int	 21h
     29	0007  C3			 ret
     30	0008			     stampa ENDP
     31
     32					 ASSUME	CS:@CODE,DS:@DATA
     33
     34	0008  B8 0000s			 MOV	 AX,@DATA
     35	000B  8E D8			 MOV	 DS,AX
     36
     37	000D  BA 0000r			 mov	 dx,offset domanda
     38	0010  52			 push	 dx
     39	0011  E8 FFEC			 call	 stampa
     ...
Avete capito? Il codice è rimasto la dove era scritto, e nessuno dice al dos che all'inizio c'è una procedura... a meno di non usare un JMP che scavalca tutte le procedure per arrivare direttamente all'inizio del programma vero e proprio.

    [...]
    .CODE
    ASSUME CS:@CODE,DS:@DATA

    jmp Start_Code

    Proc1   PROC NEAR
        [...]
    Proc1   ENP

    Proc2   PROC NEAR
        [...]
    Proc2   ENP

Start_Code:
    MOV     AX,@DATA
    MOV     DS,AX
    [...]
Dunque, la dichiarazione ha questa forma:
Nome_Proc PROC [NEAR| FAR]
        [codice]
	ret
Nome_Proc ENDP
NEAR o FAR indicano se procedura è contenuta nello stesso segmento del codice, o in un altro. Per ora noi tratteremo solo procedure NEAR.

Quando la cpu trova una CALL, salva sullo stack il registro IP, poi lo imposta con l'indirizzo della procedura chiamata. All'interno della procedura, l'istruzione RET effettua un ripristino di IP, prelevandolo dallo stack, facendo quindi tornare l'esecuzione all'istruzione immediatamente seguente la CALL.
Per questo è importante che al termine della procedura, prima di RET, lo stack si trovi nelle esatte condizioni in cui era appena entrati, e per questo è ancora più importante che alla fine di una procedura vi sia un RET... il compilatore non controlla che ci sia, e se non c'è la cpu continua a macinare come istruzioni ciò che segue... pensate se subito dopo avete dichiarato una procedura che procede alla formattazione della traccia 0 del vostro HD...

Per passare i parametri alle procedure non ä semplice come per le macro, ma ci sono un paio metodi:

  • usare le facilitazioni messe a disposizione dal compilatore, che permette di usare una tecnica simile a quella usata nelle macro. Ma io non conosco questi metodi, e quindi vi rimando al vostro manuale.
  • smanazzare lo stack, che fa sentire più potenti e produce spesso inchiodamenti spettacolari. E ovviamente questi metodi li conosco.
Dunque, usiamo lo stack, e tenete presente che anche usando le alternative del compilatore, alla fine tutto si traduce comunque in una manipolazione di stack. Nel nostro esempio, l'unico parametro di ingresso è costituito dal puntatore ad una stringa, una sola word. Quindi all'ingresso della procedura avremo la seguente situazione:
            |  Free   |
            +---------+
     N      |         | <- SS:SP
            +---------+
     N+2    |   IP    |
            +---------+
     N+4    | StrPtr  |
            +---------+
            |   ...   |
Ovviamente IP è l'ultima cosa immessa sullo stack, dopo i nostri parametri. Come facciamo ora a prelevare dallo stack i parametri che ci servono, senza compromettere le informazioni necessarie a tornare indietro?

Bene, esaminandolo ( lo stack, alzate lo sguardo! ), vi renderete conto che se si vogliono usare PUSH-POP, dovremo aver cura di salvare il vecchio IP da qualche parte, estrarre tutta la roba che ci serve ( e niente altro! ) , quindi rimettere il vecchio IP a posto. Insomma, qualcosa tipo:

stampa PROC NEAR
	pop ax		; vecchio IP
	pop bx		; puntatore a stringa
        push ax         ; ri-salva l'IP di ritorno
	mov StringPtr,bx
	...
	ret
stampa ENDP
Oppure usiamo BP ( non possiamo usare SP! ), lo inizializziamo allo stesso valore di SP, e lo usiamo come punto di riferimento:
stampa PROC NEAR
	push bp
	mov bp,sp
        mov dx,[bp+4]   ; automaticamente si usa il segmento SS
	...
	ret
stampa ENDP
In questo caso sarebbe preferibile che subito dopo la CALL via sia un numero di POP pari al numero di PUSH usate per immettere parametri sullo stack, altrimenti in poco tempo riempirete lo stack di immondizia inutile. Questo metodo può servire se si devono inviare dei valori di ritorno, in questo caso li si immette delle posizioni precedentemente occupate dai parametri di ingresso, senza effettuare troppi push e pop... decidete voi quale metodo vi ispira maggiormente!

Mischiamole!

Io sono molto pigro, a volte mi pesano le dita, e quando devo usare le procedure mi scoccia molto inserire ogni volta tutta la serie di push ed eventuali pop. Per ogni chiamata, devo rifare le stesse operazioni. "Toh!", mi son detto, "ma le macro esistono proprio per questo!".
Stampa MACRO Stringa
	push offset Stringa
	call P_Stampa
ENDM
	...

P_Stampa PROC NEAR
	...
        ret
P_Stampa ENDP
Adesso è molto più bello, no?

Si, ma insomma?

In conclusione, quale usare? Macro o procedure? Dipende. Riassumendo, le caratteristiche distintive sono:
Macro
il codice viene sostituito e ripetuto, occupa spazio.
Procedure
per la chiamata e il ritorno sono necessarie operazioni di manipolazione stack e registri. La gestione dei parametri necessita di operazioni su stack. Anche se minimo, vi è un rallentamento.
Ormai il rallentamento dovuto alle procedure è una cosa pió che trascurabile, ma in alcuni casi particolari ( vedi demo-coders o games writers ) diventa un interessante questione sapere quanti cicli di clock si risparmiano trasformando in macro.
Per gli esseri umani normali, fate un po' come vi viene meglio!

Ora vi lascio, devo preparami per la prossima bocciatura. Vedrò se per la prossima volta trovo qualcosa di sfizioso da fare...

                                                      esaminatamente,
                                                            Cthulhu


Copyright © 1996 Beta. Tutti i diritti riservati.
Precedente Principale Sommario Informazioni Redazione Browser Successivo