DJGPP V2 Quick Asm Programming Guide

 

Indirizzo e-mail dell'autore: avly@castle.net
HomePage dell'autore: http://www.castle.net/~avly/djgpp.html
Traduzione: MrCODE (mrcode@tiscalinet.it)
HomePage Traduttore: http://web.tiscalinet.it/mrcode

Questo tutorial non cercherà di insegnare come programmare in codice x86, ma tenterà di mostrare come fare codice asm sia inline che external per DJGPP. Si assumerà una certa familiarità con l'asm "standard" Intel, come quello usato ad esempio nel TASM, MASM etc.

Sintassi assembly x86 AT&T

Il DJGPP usa la sintassi AT&T, che differisce leggermente dal formato regolare INTEL a cui siete forse abituati. Le differenze principali sono:

Ecco alcuni esempi.Gli equivalenti in stile Intel (se presenti) sono riportati a fianco:

movw %bx, %ax   // mov ax, bx
xorl %eax, %eax   // xor eax, eax
movw $1, %ax        // mov ax,1
movb X, %ah         // mov ah, byte ptr X
movw X, %ax         // mov ax, word ptr X
movl X, %eax        // mov eax, X

Molti opcodes sono identici sia nel formato AT&T che Intel , eccetto per questi:

movsSD             // movsx
movzSD             // movz

dove S e D sono il suffisso di dimensione del source e del destination , rispettivamente:

movswl %ax, %ecx    // movsx ecx, ax
cbtw                // cbw
cwtl                // cwde 
cwtd               // cwd 
cltd                // cdq
lcall $S,$O      // call far S:O
ljmp $S,$O      // jump far S:O
lret $V            // ret far V
I prefissi degli operandi non dovrebbero essere scritti sulla stessa linea su cui l'istruzione agirà, Per esempio rep e stosd dovrebbero 
due istruzioni separate. Anche i riferimenti alla memoria sono leggermente diversi. L'usale riferimento alla memoria in formato 
Intel del tipo:

section:[base + index*scale + disp]

in DJGPP diventa:

section:disp(base, index, scale)

Ecco alcuni esempi:

movl 4(%ebp), %eax                 // mov eax, [ebp+4])
addl (%eax,%eax,4), %ecx           // add ecx, [eax + eax*4])
movb $4, %fs:(%eax)                // mov fs:eax, 4)
movl _array(,%eax,4), %eax         // mov eax, [4*eax + array])
movw _array(%ebx,%eax,4), %cx      // mov cx, [ebx + 4*eax + array])
Le istruzioni di salto usano sempre il displacement più piccolo.Le seguenti istruzioni lavorano sempre con displacement di un byte: 
jcxz, jecxz, loop, loopz, loope, loopnz e loopne. Come suggerito nella documentazione online, un salto  jcxz foo  può essere 
espanso per lavorare così:

	jcxz cx_zero
	jmp cx_nonzero
cx_zero: 
	jmp foo
cx_nonzero:

La documentazione avvisa anche di usare cautela con le istruzioni mul e imul. L' espansione delle istruzioni di moltiplicazione è fatta usando un operando. Per esempio imul $ebx, $ebx non metteranno il risultato in edx:eax. Usare la forma singola dell'operando  imul %ebx per ottenere il risultato espanso.

Inline Asm

Inizierò per primo con l'assembler inline perchè sembra essere una delle domande fatte più di frequente. 
Questa è la sintassi di base, come descritta nel manuale in linea:

__asm__(asm statements : outputs : inputs : registers-modified);

I quattro campi sono:

un semplice esempio:

      __asm__("
pushl %eax\n
movl $1, %eax\n 
popl %eax"
);

Non c'è necessità di usare gli altri tre campi, fin quando non si voglia specificare delle variabili in input o output da passare alla procedura inline oppure si sporchi accidentalmente qualche registro.

Ecco il caso in cui ci siano delle variabili in input .

int i = 0;
 __asm__("
pushl %%eax\n
movl %0, %%eax\n
addl $1, %%eax\n
movl %%eax, %0\n
popl %%eax"
:
: "g" (i)
);    // incrementa la varibile i

Niente Panico! Proverò a spiegarvi cosa ho combinato:La nostra variabili in input è i e la vogliamo incrementare di 1. Non abbiamo nessuna variabile in output e non modifichiamo alcun registro (salviamo eax e lo ripristiniamo all'uscita della routine), perciò il secondo campo e l'ultimo campo sono vuoti.

Visto che il campo input è specificato, dobbiamo lasciare i "due punti" blank per il campo output, ma niente per l'ultimo campo, visto che non è usato.Lasciare un newline o almeno uno spazio tra i "due punti" vuoti.

Analizziamo il campo input. Le direttive sono istruzioni chen oi diamo al compilatore su come manipolare le variabili che gli passiamo. Le direttive sono racchiuse tra apici. Quindi adesso...cosa significa "g" ? "g" lascia al compilatore la decisione di dove caricare il valore contenuto nella variabile i . In generale la maggiora parte delle variabili può rientrare nel caso  "g" , lasciando al compilatore la decisione di dove caricarle (gcc potrebbe addirittura ottimizzarle). Altre comuni forme sono "r" (carica la variabile dentro qualsiasi registro disponibile), "a" (ax/eax), "b" (bx/ebx), "c" (cx/ecx), "d" (dx/edx), "D" (di/edi), "S" (si/esi), etc.

Il solo input che abbiamo sarà referenziato come %0 all'interno del programma asm. Se abbiamo due variabili in input, allroa saranno indicate come %0 e %1, nell'ordine in cui appaiono listate nel campo di input (vedi l'esempio seguente). Per N variabili in input e nessun output, %0 fino a  %N-1 allora corrisponderà agli input, nell'ordine in cui sono listati.

Se nessun campo input, output, o registro modificato sono usati, i nomi dei registri all'interno del programma asm devono essere preceduti da due caratteri di percentuale (%) characters, invece di uno. Confrontate questo esempio con il primo, che non usa nessuno degli ultimi tre campi.

Facciamo il caso di due inputs e introduciamo "volatile":

      int i=0, j=1;
      __asm__ __volatile__("
	pushl %%eax\n
	movl %0, %%eax\n
	addl %1, %%eax\n
	movl %%eax, %0\n
	popl %%eax"
	:
	: "g" (i), "g" (j)
      );    // incrementa i di un valore j

Okay, questa volta abbiamo a che fare con due input. Nessun problema, dobbiamo solo ricordare che %0 corrisponde alla prima variabile (i in questo caso), e %1 to j, che è listato dopo i.
A
h già...cosa è esattamente questa cosa del volatile? Impedisce al compilatore di modificare le nostre istruzioni assembler (riordinando, cancellando, combinando, etc.), e le assemblerà così come sono (si, gcc altrimenti le ottimizzerebbe come meglio crede!). Suggerisco di usare volatile la maggior parte delle volte, e d'ora in poi lo useremo anche negli esempi.

Analizziamo il caso in cui ci sia anche un output field.

      int i=0;

      __asm__ __volatile__("
            pushl %%eax\n
            movl $1, %%eax\n
            movl %%eax, %0\n
            popl %%eax"
      : "=g" (i)
      );    // assegna 1 a i

TQuesto sembra esattamente uguale al caso precedente in cui avevamo una variabile in input; e effettivamente non è molto differente. Tutte le direttivedi output sono precedute da un segno di uguale (=). Anche esse usano come riferimento %0 fino a %N-1 all'interno delle istruzioni assembler, nell' ordine in cui sono state listate nell' output field. Vi potreste chiedere cosa accade se si usano entrambi i campi di input e di output, bene, il prossimo esempio vi mostrerà come usarli insieme.

      int i=0, j=1, k=0;

      __asm__ __volatile__("
            pushl %%eax\n
            movl %1, %%eax\n
            addl %2, %%eax\n
            movl %%eax, %0\n
            popl %%eax"
      : "=g" (k)
      : "g" (i), "g" (j)
      );    // k = i + j

Okay, l'unica parte poco chiara è la numerazione delle variabili all'interno delle istruzione assembler. Ora vi spiego:

Quando si usano entrambi i campi di input e output:

%0 ... %K sono gli outputs

%K+1 ... %N sono gli inputs

Nel nostro esempio, %0 si riferisce a k, %1 a i, e %2 a j. Semplice, no? :)

Non abbiamo ancora usato l'utlimo campo, il registers-modifiedl. Se abbiamo la necessità di usare qualche registro all'interno del nostro codice assembler, o lo salviamo e ripristiniamo esplicitamente con push e pop, oppure lo listiamo in questo campo, e lasciamo che sia il gcc a prendersi cura di lui.

Di seguito il precedente esempio senza il push&pop del registro eax.

      int i=0, j=1, k=0;

      __asm__ __volatile__("
            pushl %%eax\n
            movl %1, %%eax\n
            addl %2, %%eax\n
            movl %%eax, %0\n
            popl %%eax"
      : "=g" (k)
      : "g" (i), "g" (j)
      : "ax", "memory"
      );    // k = i + j

Informiamo ill gcc che stiamo usando il registro eax , inserendolo nel campo registers-modified e questi si prenderà carico di salvarlo e ripristinarlo, se si renderà necessario.Il nome del registro a 16-bit , copre i registri a 32-, 16- o 8-bit.

Se staimo anche ciacciando la memoria (writing to variables, etc.), è raccomandato specificare "memory" nel campo registers-modified. Questo significa che tutti gli esempi fatti fin qui dovrebbero avere "memory" impostato (eccetto il primissimo ovvio), ma ho scelto di non inserirlo fino ad ora , giusto per semplificare le cose.

labels locali all'interno del codice asm inline dovrebbero essere terminate con b o f, rispettivamente per riferimenti backward e forward.

Per esempio,

      __asm__ __volatile__("

            0:\n
                 ...
                 jmp 0b\n
                 ...
                 jmp 1f\n
                 ...
            1:\n

                 ...
      );

Qui c'è un esempio di codice C "mixato" a salti inline in assembler (thanks to Srikanth B.R per questo tip).

void MyFunction( int x, int y )

{ 
      __asm__( "Start:" );
      __asm__( ...do some comparison... ); 
      __asm__( "jl Label_1" ); 

      CallFunction( &x, &y );     
      __asm__("jmp Start"); 

Label_1: 
      return; 
}
External Asm

Blah... Okay, bene.Ecco una ricetta: prendete un pò del vostro codice C/C++ in files, e compilatelo con gcc -S file.c. Dopo ispezionate il file file.S. Il layout di base è;

      .file "myasm.S"
 
        .data
        somedata: .word 0
        ... 

        .text


   .globl __myasmfunc

        __myasmfunc:
        ...

        ret

Macros, macros! C'è un file header libc/asmdefs.h che ci torna comodo per scrivere external asm. Basta includerlo all'inizio del nostro codice asm e usare le macros in accordo. Per esmepio ecco il file myasm.S:

        #include <libc/asmdefs.h>

         .file "myasm.S"
         .data
         .align 2

        somedata: .word  0
        ... 

        .text
        .align 4

        FUNC(__MyExternalAsmFunc)
        ENTER

                       movl   ARG1, %eax
                       ...
                       jmp    mylabel
                       ...
               mylabel:
                       ...
        LEAVE

Questo potrebbe essere un'ottimo scheletro per il vostro codice assembler esterno

Altre risorse

La maniera migliore per impare queste cose è studiare il codice altrui. Troverete del codice inline nel file header sys/farptr.h. Se avete Linux (n.d.t. cosa aspettate?) potrete trovare file da studiare nell'albero dei sorgenti del kernel, in i386/ o simile. Controllate anche la directory djgpp2/ a x2ftp.oulu.fi, cercando librerie grafiche o per giochi che includano i sorgenti.

Se avete prodotto del codice assembler ed avete la necessità di convertirlo dalla sintassi Intel a quella AT&T, allora potete:

Cercare nel mail archives uno script sed che converte la sintassi Intel in quella AT&T.
Standard Disclaimer: All trademarks mentioned are owned by their respective companies. There are absolutely no 
guarantees, expressed or implied, on anything that you find in this document. I cannot be held responsible for anything 
that results from the use or misuse of this document. 

Torna alla main page! Torna alla pagina principale