Le procedure Assebly

Le procedure Assebly

Cosa sono e come si usano le procedure in assembly

L’uso delle macro di codice semplifica  la scrittura dei programmi assembly. L’uso delle macro comporta però l’espansione del codice sorgente e del codice eseguibile. Ciò significa anche maggior memoria principale utilizzata.

Differenza tra procedure e macro

Una soluzione a questo problema è l’uso di procedure (subroutines) che solo apparentemente svolgono un compito analogo alla macro.
Una procedura è ancora un blocco di codice con un nome simbolico, ma stavolta il nome della procedura non è un simbolo ma l’indirizzo della sua prima istruzione in memoria. Le procedure, infatti, sono allocate in memoria, in uno spazio privato, e devono essere chiamate a runtime dal codice del programma (o da altre procedure).
Ciò significa che il blocco di codice di una procedura non viene ripetuto nel sorgente ad ogni occorrenza del suo nome, ma solo usato dal chiamante: il blocco di codice di una procedura è unico e allocato in memoria, cioè le procedure operano a runtime. Inoltre, operando a runtime, possono crearsi veri e propri ambienti autonomi di  elaborazione, es. mediante la creazione, sempre a runtime, di zone di memoria private, dette variabili locali.
L’unico effetto collaterale di una procedura, rispetto alle macro di codice, è una maggior lentezza nell’esecuzione, dato che il codice chiamante deve preparare la memoria di solito lo stack) per avviare la procedura, e la procedura, a sua volta, deve ripristinare la memoria al suo termine e prima di ritornare al chiamante. Queste operazioni sono dette meccanismo di chiamata e ritorno della procedura.

Sintassi:

Le procedure vanno definite con una sintassi molto simile a quella delle macro di codice, anche se la collocazione delle procedure deve essere posta necessariamente nell’area codice, prima del programma principale, o dopo.

nome PROC

(codice)

ENDP

Scopo: Definisce il blocco di (codice) che sarà chiamato tramite l’istruzione CALL nome. Al termine bisogna ridare il controllo al chiamante con l’istruzione RET

Esempio:
ACAPO PROC
mov ah,2
mov dl,13
int 21h
mov dl,10
int 21h
ret
ENDP

Chiamata di una procedura

Una procedura deve essere chiamata dal codice del programma e quindi deve ritornare al chiamante per consentirgli il regolare flusso di esecuzione.
Istruzione CALL
Sintassi: CALL target
Scopo: L’istruzione CALL esegue le seguenti operazioni:
1) salva nello stack l’indirizzo di ritorno;
2) trasferisce il controllo all’operando target tramite un salto incondizionato.
L’indirizzo di ritorno è l’indirizzo dell’istruzione successiva a quella di CALL.
Preleva una word dallo stack, dall’indirizzo contenuto in SP, e la deposita in destinazione, quindi incrementa
SP di due unità.
Esempio:
CALL  ACAPO
la CALL ACAPO può essere vista come l’unione delle due istruzioni
PUSH IP
JMP ACAPO,
ricordando che il nome di una procedura è il suo indirizzo in memoria.

Istruzione RET

Sintassi: RET

Scopo: L’istruzione RET assume che l’indirizzo di ritorno si trovi attualmente in cima allo stack. Essa esegue le seguenti operazioni:

1) preleva dallo stack dell’indirizzo di ritorno
2) salto all’indirizzo di ritorno.

Esempio: RET

Nota: La RET, che va sempre posta come ultima istruzione di un blocco di procedura, esegue, praticamente, le seguenti istruzioni:

POP indirizzo ritorno
JMP indirizzo ritorno,
oppure, con una sola istruzione logica: POP IP

In entrambi i casi, se la procedura è di tipo FAR – cioè si trova in un segmento di codice differente da quello del chiamante, sia CALL che RET, invece di salvare/rileggere solo la parte offset dell’indirizzo del program counter (indirizzo di ritorno su due byte), salvano e rileggono sia la parte seg che la parte offset dell’indirizzo (indirizzo di ritorno su quattro byte) in modo del tutto trasparente al programmatore.

Preservare i registri

L’utilizzo delle procedure comporta un effetto collaterale abbastanza grave, detto interferenza:
i registri usati dalla procedura sovrascrivono il contenuto precedentemente salvato in quei registri dal chiamante, con l’effetto che al ritorno della procedura il chiamante non ritrova più i valori precedentemente salvati nei registri. Per evitare l’interferenza, la procedura deve preservare i registri in ingresso, ovvero salvare il contenuto dei registri che essa stessa userà al suo interno, salvandoli ordinatamente sullo stack, per poi ripristinarli ordinatamente appena prima di ritornare il controllo al chiamante (appena prima dell’istruzione RET). La preservazione dei registri può essere effettuata puntualmente, salvando sullo stack solo i registri usati dalla procedura, o in modo completo sfruttando due apposite istruzioni x-86, PUSHA e POPA che, rispettivamente, salvano sullo stack e riprendono dallo stack tutti i registri (ma solo per l’x-86 a partire dall’80186, con l’esclusione, quindi, dell’8086/88).

Passaggio di parametri

Le procedure diventano realmente fondamentali quando permettono il passaggio dei parametri, ovvero possono svolgere il proprio compito sulla base di valori che il chiamante decide a runtime.
Il passaggio dei parametri tramite registri è molto veloce e semplice, ma ha molte limitazioni, prima di tutto la quantità dei registri disponibili.
Le procedure, per linguaggi ad alto e a basso livello come l’assembly, usano in realtà lo stack per passare i parametri e, quando serve, per ritornarli al chiamante.
L’idea è semplice: il chiamante, prima di chiamare la procedura con la consueta istruzione CALL, deposita sullo stack i valori che intende passare alla procedura. La procedura, prima di iniziare il suo compito, preleva dallo stack i parametri e li usa al suo interno.
Per ritornare valori dalla procedura al chiamante, si usa lo stesso meccanismo.
In questo caso il passaggio di parametri si dice tramite lo stack.
Il passaggio di parametri tramite lo stack deve tener presente che, sullo stack, come ultimo valore, verrà sempre posto l’indirizzo di ritorno della procedura– ad opera dell’istruzione CALL. Pertanto la procedura dovrà prelevare i parametri senza eliminare dalla cima dello stack l’indirizzo di ritorno, che dovrà essere usato dall’istruzione RET per ritornare correttamente al chiamante.
Esistono varie tecniche per passare i parametri sullo stack. Le più diffuse prendono il nome di cdecl (usata dal linguaggio C e derivati) e stdcall (usata dal linguaggio Pascal e dalle API di alcuni Sistemi Operativi).
Vedremo un passaggio di parametri alle procedure abbastanza simile allo stile del C o cdecl, che usa il registro BP (Base Pointer) per prelevare i dati sullo stack senza modificare il registro SP (Stack Pointer). Si ricorda che il registro BP ha la proprietà di indirizzare in memoria, cioè di contenere indirizzi di memoria.
1. Prima di tutto il chiamante deve porre nello stack i parametri richiesti dalla procedura. L’operazione si effettua con la consueta istruzione PUSH, ripetuta tante volte quanti sono i parametri da passare.

passaggio-di-parametri-nello-stack-v10-5-728 passaggio-di-parametri-nello-stack-v10-7-728
Inizio Stack vuoto  Inseriamo due parametri  nello stack PUSH NUM1 PUSH NUM2

2. Quindi si effettua la chiamata normalmente, con l’istruzione CALL. Essa immetterà sulla cima dello stack, come di consueto, l’indirizzo di ritorno.
 

passaggio-di-parametri-nello-stack-v10-9-728
Call somma salva l’IP nello Stack

3. La procedura, a sua volta, deve immediatamente salvare sullo stack il registro BP, dato che verrà usato e sovrascritto per prelevare i parametri.
 

passaggio-di-parametri-nello-stack-v10-11-728
PUSH BP        
MOV BP,SP

4. Quindi il registro BP deve essere impostato con il valore dello Stack pointer SP, mediante una istruzione MOV: in questo modo BP punta alla cima dello stack.
 

passaggio-di-parametri-nello-stack-v10-13-728
MOV BP,SP

5. Ora i parametri possono essere prelevati uno a uno tramite BP, avendo cura di ricordare che il primo parametro è profondo 4 byte nello stack: infatti i primi due byte in cima alla pila riportano il valore di BP (appena memorizzato), e i successivi due byte riportano il valore dell’indirizzo di ritorno. Ogni parametro si scosta di due byte, pertanto a BP+4 corrisponde il valore del primo parametro, a BP+6 il valore del secondo parametro, a BP+8 il valore del terzo parametro, e così via.

passaggio-di-parametri-nello-stack-v10-13-728 (1)
MOV AX,[BP+4]       
MOV BX,[BP+6]    

6. Ora può essere scritto il codice della procedura, comprese le eventuali istruzioni per preservare i registri.
7. Infine, appena prima dell’istruzione RET, va ripristinato il registro BP, che se tutto è stato svolto correttamente, si trova attualmente in  cima allo stack. Una volta prelevato il valore originale di BP, l’indirizzo di ritorno è disponibile in cima alla pila per l’istruzione RET.
8. Il chiamante, quando riprende il controllo, si ritrova i parametri ancora sullo stack, per cui deve ripristinare lo stato dello stack  deallocandoli, cioè facendo tornare lo Stack Pointer SP al valore originario. Ciò è semplice, tramite una istruzione ADD: si  aggiungono allo Stack Pointer tante ‘doppiette’ quanti sono i parametri (es., per 3 parametri: ADD SP,6). Una delle maggiori differenze tra la tecnica cdecl e stdcall consiste nel fatto che cdecl impone che sia il chiamante a deallocare i parametri dallo stack,
mentre in stdcall è la procedura a farlo.
 

Leave a Reply


Latest Posts

AAA