Interfacciare l'Assembly con il Turbo Pascal


Questo tutorial è fuori luogo ma siccome il mio amico Takeshi è più di una volta che mi chiede alcune cose su questo argomento ho deciso di anticipare e di dedicargli questo breve tutorial. Quindi Takeshi è tutto tuo !!!

Come saprete scrivere programmi stand alone in assembly non è una cosa molto comoda visto anche che oggi esistono dei compilatori che riescono ad ottimizzare il codice in modo sorprendente, quindi invece di scrivere il programma interamente in assembly si preferisce spesso utilizzare un linguaggio ad alto livello che chiama alcune routine critiche scritte in assembly.
Per fare questo col Pascal il vostro programma deve contenere la direttiva $L e la dichiarazione delle procedure e funzioni EXTERNAL, a sua volta il programma assembly deve contenere le direttive PUBLIC e EXTRN.
La direttiva {$L FILEASM.OBJ} indica al programma Pascal di cercare il file FILEASM.OBJ che come avrete già capito deve essere compilato.
Ogni procedura o funzione Assembly che volete che venga vista dal programma Pascal deve essere dichiarata PUBLIC e deve avere un corrispondente sempre nel programma Pascal.
La sintassi di una procedura EXTERNAL in Pascal è simile ad una dichiarazione di una procedura FORWARD e deve stare al livello più esterno del programma o della unit (non può cioè essere dichiarata all'interno di altre procedure):

          procedure AsmProc(a:integer;b:real); external;
Questa dichiarazione corrisponderà al seguente frammento di codice Assembly:
    
          CODE    SEGMENT BYTE PUBLIC
          AsmProc PROC NEAR
                  PUBLIC AsmProc
                  ...
                  ...
                  ...
         AsmProc  ENDP
         CODE     ENDS 

Solo le label dichiarate con la direttiva PUBLIC nel programma Assembly sono visibili dal Pascal ed esse devono essere dichiarate nel CODE SEGMENT in quanto il Turbo Pascal non permette definizioni PUBLIC nel DATA SEGMENT.

Un modulo Assembly può accedere ad ogni funzione , procedura, variabile o costante solo se dichiarata nel livello più esterno del programma Pascal (quello delle variabili globali).
Supponiamo di aver dichiarato le seguenti variabili in Pascal:

	var
         a:byte;
	 b:word;
         d:integer;
	 c:shortint;
	 e:real;
	 l:pointer;
tutte queste variabili sono accessibili dal programma Assembly usando la direttiva EXTRN in questo modo:
    
	EXTRN A:BYTE
	EXTRN B:WORD
	EXTRN C:BYTE
 	EXTRN D:WORD 
	EXTRN E:FWORD   
	EXTRN L:DWORD
per quanto riguarda le procedure e le funzioni facciamo riferimento al seguente esempio:
{Codice Turbo Pascal}
unit Esempio1;
 
interface
 procedure Proc1;
 procedure Proc2;

implementation
 var
  A:word;

 procedure AsmProc; near; external;
  {$L ASMPROC.OBJ}

 procedure Proc2;
  begin
   writeln('Siamo in Proc2');
  end;
 
 procedure Proc3; near;
  begin
   writeln('Siamo in Proc3');
  end;
 
 procedure Proc4; far;
  begin
   writeln('Siamo in Proc4');
  end;

 procedure Proc1;
  begin
   writeln('Siamo in Proc1');
   A:=10;
   writeln('Prima di AsmProc A vale ',A);
   AsmProc;
   writeln('Dopo AsmProc A vale ',A);
  end;
end.

;Codice  Assembly

DATA    SEGMENT WORD PUBLIC
        ASSUME DS:DATA
        EXTRN  A:WORD
DATA    ENDS

CODE    SEGMENT BYTE PUBLIC
        ASSUME CS:CODE
        EXTRN  Proc2:FAR
        EXTRN  Proc3:NEAR
        EXTRN  Proc4:FAR
        
AsmProc PROC NEAR
        PUBLIC AsmProc
        CALL   FAR PTR Proc2
        CALL   Proc3
        CALL   FAR PTR Proc4
        mov    cx,ds:A		;Queste 3 instruzioni sottraggono 2 da A
        sub    cx,2    
        mov    ds:A,cx
        ret
AsmProc ENDP
CODE    ENDS
        END

Il programma principale sar...:
 program Prova;
  uses Esempio1;
 begin
  proc1;
 end.
Provate a compilare il tutto e ad eseguire il programma, non spenderò ulterior tempo basta vedere l'output del programma per capire cosa succede.

Vediamo ora un altro aspetto molto importante : il passaggio dei parametri. Il Turbo Pascal passa i parametri alle funzioni ed alle procedure salvandoli nello stack nell'ordine in cui li incontra nella dichiarazione, vedremo di seguito come questi vengono salvati:

- PARAMETRI PASSATI PER VALORE -
I parametri passati per valore sono quelli che non possono essere modificati dalla funzione o procedura che li usa, questi vengono trattati in modo diverso a seconda del tipo:

- Tipi scalari (Boolean, Char, Shortint,Byte, Integer, Word, Longint,Subrange     
  type ed Enumerated types); dipende dalla dimesione della variabile.
  Se la variabile occupa 1 byte viene salvata nello stack come se fosse di 2    
  byte ma la parte più significativa è ignorata.
  Se la variabile occupa 2 byte viene pushata nello stack cosi com'è.
  Se la variabile occupa 4 byte occuper... due posizioni nello stack: prima 
  vengono salvati i 2byte più significativi poi gli altri 2

- Real
  Sono salvati nello stack e occupano 3 posizioni (6byte) prima viene salvata 
  la parte più significativa.

- Pointer
  Vengono salvati direttamente nello stack come puntatori far (la prima word
  contiene il segmento la seconda l'offset).Il programma assembly può usare
  le istruzioni LDS o LES per recuperare il valore del puntatore.

- String
  Le stringhe a causa della loro lunghezza non vengono salvate nello stack,
  in esso si salva il puntatore alla stringa. E' quindi dovere della procedura
  chiamata non modificare la stringa referenziata dal puntatore, essa deve
  copiarsi la stringa e lavorare sulla copia.

- Record e Array
  Se la loro dimensione è di 1, 2 o 4 byte vengono salvati nello stack come i
  tipi scalari, se invece la loro dimensione è 3, 5 o maggiore nello stack si
  salva il puntatore come nel caso di stringhe.

- Set
  Vengono trattati come le stringhe, si salva un puntatore che punta ad una
  rappresentazione a 32 bit del set; il primo bit (LSB) corrisponde al primo
  elemento del set.
- PARAMETRI PASSATI PER INDIRIZZO -
Tutti i parametri passati per indirizzo (con il var nelle intestazioni delle procedure) passano un puntatore di tipo far che punta alla loro attuale locazione di memoria.


Il Turbo Pascal si aspetta che tutti i parametri nello stack siano rimossi prima di uscire dal sottoprogramma.
Ci sono due modi per sistemare lo stack. Potete utilizzare l'istruzione RETn dove n è il numero di byte dei parametri che sono stati salvati nello stack, oppure si può salvare l'indirizzo di ritorno ed estrarre i parametri uno a uno.

Vediamo ora come si accede ai parametri passati dal Turbo Pascal al programma Assembly.
Quando la routine riceve il controllo in cima allo stack c'è l'indirizzo di ritorno (2 o 4 byte dipende se la routine è near o far) e sotto i parametri che gli sono stati passati.
Ci sono 3 tecniche per accedere a questi parametri :

 - Usare il registro BP per accedere allo stack
 - Usare un altro registro (base o indice) per prelevare i parametri
 - Estrarre l'indirizzo di ritorno e poi i parametri
Le prime due sono più complicate e le vediamo dopo, la terza prevede di salvare l'indirizzo di ritorno in un posto sicuro e poi di mettere i parametri in registri; quest'ultima tecnica funziona bene se la routine in questione non richiede spazio per le variabili locali.
La prima tecnica prevede l'uso del Base Pointer come indice sullo stack nel seguente modo:
        CODE    SEGMENT
                ASSUME cs:CODE
        MiaProc PROC FAR             ;procedure MiaProc(i,j:integer); 
external;
                PUBLIC MiaProc
        j       EQU WORD PTR [bp+6]  ;j è sopra il BP salvato e l'ndirizzo
                                     ;di ritorno
        i       EQU WORD PTR [bp+8]  ;i è appena sopra j
                push    bp           ;salva il valore di BP
                mov     bp,sp        ;fa puntare BP allo stack
                mov     ax,i         ;in questo modo accedo alle varibili 
                ...
                ...
Quando si usa il BP per accedere ai parametri il Turbo Assembler prevede un metodo alternativo per calcolare gli offset delle variabili utilizzando la direttiva ARG.
Questa va usata all'interno di una PROC e determina automaticamente l'offset dei parametri relativo a BP. Inoltre calcola anche la dimensione totale dei parametri per poterla usare con la RET.
Siccome i simboli creati con ARG sono locali alla procedura non c'è bisogno di usare nomi unici in procedure diverse.
Qui di seguito vi riscrivo l'esempio di poco fa usando la direttiva ARG:
        CODE    SEGMENT
                ASSUME cs:CODE
        MiaProc PROC FAR             ;procedure MiaProc(i,j:integer);external;
                PUBLIC MiaProc
                ARG j:WORD, i:WORD = RetBytes
                push    bp
                mov     bp,sp
                mov     ax,i
                ...
                ...
L'istruzione ARG j:WORD, i:WORD = RetBytes uguaglia automaticamente i a [WORD PTR BP + 6] e j a [WORD PTR BP + 8].
La variabile RetBytes contiene il numero di byte occupati di parametri (in questo caso 4), cos? la procedura può finire con RET RetBytes.
Tutte queste variabili esistono solo all'interno della procedura.
Inoltre la direttiva ARG tiene conto del fatto che la procedura sia FAR o NEAR.
Una cosa a cui si deve fare attenzione è che usando questa direttiva i parametri sono estratti in ordine inverso a come sono dichiarati nella procedura Pascal.
Un'altra precauzione da considerare è che il Turbo Pascal salva le variabili che occupano un solo byte in una posizione dello stack (2byte) ed è compito del programmatore tener conto di questo; ad esempio supponiamo di avere:
                function MiaFunc(i,j:char):string; external;
La direttiva ARG deve essere qualcosa del tipo:
                ARG j:BYTE:2, i:BYTE:2 =RetBytes RETURNS result:DWORD
Il :2 dopo gli argomenti è necessario per dire al Turbo Assembler che ogni carattere viene salvato come un array di 2 byte (a noi interessa solo il byte meno significativo); la parola RETURNS specifica la variabile di uscita che nel caso di stringhe come abbiamo visto è il puntatore ad essa.
Nota: la dimensione della stringa non ha alcune effetto sulla variabile RetBytes.

Un'altra semplificazione messa a disposizione dal Turbo Assembler è la direttiva .MODEL che permette di specificare oltre il modello di memoria da usare il linguaggio da supportare. Sempre sull'esempio di prima:

                        .MODEL large, PASCAL
                        .CODE
                MiaProc PROC FAR i:WORD, j:WORD
                        PUBLIC MiaProc
                        mov     ax,i
                        ...
                        ...
Notate che non si devono specificare i parametri in ordine inverso e che alcune altre cose non servono (push bp, mov bp,sp) inoltre viene settato il ritorno al chiamante con POP BP e RETn.

Il secondo modo per accedere ai parametri (si era detto) era quello di usare un altro Base Register (BX) o un Index Register (SI o DI).
Ricordate pero che il segmento di default per questi registri è CS non SS quindi si deve fare attenzione :

                CODE    SEGMENT
                        ASSUME cs:CODE
                MiaProc PROC FAR
                        PUBLIC MiaProc
                j       EQU WORD PTR ss:[bx+4]
                i       EQU WORD PTR ss:[bx+6]
                        mov     bx,sp
                        mov     ax,i
                        ...
                        ...
Qui sopra è mostrato come si può utilizzare il registro BX. Siccome non si deve salvare BP questo modo è più efficiente.

Ora che abbaiamo visto come il Turbo Pascal passa i parametri ad una routine Assembly , vediamo come sempre il Turbo Pascal salva i parametri di uscita ad una funzione.
Come per i parametri d'ingresso dipende dal tipo:

- Tipi scalari
  Se la dimensione del dato è 1 byte lo mette in AL
  Se la dimensione è 2 byte lo mette in AX
  Se la dimensione è 4 byte lo mette in DX:AX (con la parte più significativa
  in DX)
- Real
  I 6 byte del numero reale vengono salvati in 3 registri DX BX AX con la 
parte
  più significativa in DX e quella meno significativa in AX.
- String
  Le stringhe vengono salvate in un'area temporanea allocata dal Pascal prima
  della chiamata; un puntatore di tipo FAR a quest'area viene salvato nello
  stack prima del primo parametro.(Nota: questo puntatore non fa parte della
  lista dei parametri).
- Pointer
  I puntatori sono salvati in DX:AX (segment:offset)
L'ultimo argomento che vorrei trattare riguarda l'allocazione di memoria (sia statica che volatile) per le nostre routine Assembly.
Il Turbo Pascal permette all'Assembler di riservare spazio per le variabili statiche nel DATA SEGMENT, per allocare lo spazio si deve usare la solita direttiva DB o DW e cos? via :
                DATA    SEGMENT PUBLIC
                MiaInt  DW  ?
                MioChar DB  ?
                ...
                ...
                DATA    ENDS
Tutto questo con 2 restrizioni : primo queste variabili sono private (non possono essere visibili dal programma Pascal), secondo non possono essere pre-inizializzate (quindi MiaInt DW 45 non causa un errore ma MiaInt non verr. inizializzata a 45 quando eseguirò il programma).
Potete comunque aggirare l'ostacolo usando la direttiva EXTRN e dichiarando le variabili nel programma Pascal.
Per quanto riguarda la memoria volatile essa viene allocata sullo stack per tutta la durata della chiamata.
Per capire come funziona non c'è niente di meglio che un bell'esempio in cui si alloca spazio per due interi a e b:
                CODE    SEGMENT
                        ASSUME cs:CODE
                MiaProc PROC FAR
                        PUBLIC MiaProc
                        LOCAL  a:WORD, b:WORD = LocalSpace
                                                ;a va in [bp-2] e b in [bp-4]
                i       EQU WORD PTR [bp+6]     ;i sta  sopra BP e l'indirizzo
                                                ;di ritorno
                        push    bp              ;salvo BP
                        mov     bp,sp           
                        sub     sp,LocalSpace   ;crea lo spazio per le 2 word
                        mov     ax,45
                        mov     a,ax            ;a:=45
                        xor     ax,ax           ;a:=0
                        mov     b,ax            ;b:=0
                        ...
                        ...                     ;fate quello che volete fare!!
                        ...
                        mov     sp,bp           ;ripristina l'originale SP
                        pop     bp              ;recupera BP
                        ret     2               ;estrae i 2 byte di i
                MiaProc ENDP
                CODE    ENDS
                        END
L'istruzione LOCAL a:WORD, b:WORD = LocalSpace riserva [BP-2] per a e [BP-4] per b e assegna a LocalSpace il valore 4 che altro non è che la dimensione dell'area per le variabili locali della procedura.

Bene Takeshi spero di essere stato sufficientemente chiaro e di averti dato qualche utile tips per Interfacciare i tuoi programmi(ni) in grafica 3D con efficienti routine in assembly in modo da far ruotare quel maledetto cubo a velocità supersoniche anche su un 286... :-)))


Assembly Page di Antonio
<< Indice >>