Assemblatore in linea - Inline assembler

Nella programmazione di computer , un assemblatore in linea è una caratteristica di alcuni compilatori che consente di incorporare codice di basso livello scritto in linguaggio assembly all'interno di un programma, tra codice che altrimenti è stato compilato da un linguaggio di livello superiore come C o Ada .

Motivazione e alternative

L'incorporamento del codice in linguaggio assembly viene solitamente eseguito per uno di questi motivi:

  • Ottimizzazione : i programmatori possono utilizzare il codice in linguaggio assembly per implementare le parti più sensibili alle prestazioni degli algoritmi del loro programma , codice che tende a essere più efficiente di quello che potrebbe altrimenti essere generato dal compilatore.
  • Accesso alle istruzioni specifiche del processore : la maggior parte dei processori offre istruzioni speciali, come le istruzioni Confronta e Scambia e Prova e Imposta che possono essere utilizzate per costruire semafori o altre primitive di sincronizzazione e blocco. Quasi tutti i processori moderni hanno queste o istruzioni simili, poiché sono necessarie per implementare il multitasking . Esempi di istruzioni specializzate si trovano nei set di istruzioni SPARC VIS , Intel MMX e SSE e Motorola Altivec .
  • Accesso a convenzioni di chiamata speciali non ancora supportate dal compilatore.
  • Chiamate di sistema e interrupt: i linguaggi di alto livello raramente hanno una funzione diretta per effettuare chiamate di sistema arbitrarie, quindi viene utilizzato il codice assembly. Gli interrupt diretti sono forniti anche più raramente.
  • Per emettere direttive speciali per il linker o l'assemblatore, ad esempio per modificare sezioni, macro o creare alias di simboli.

D'altra parte, l'assemblatore in linea pone un problema diretto per il compilatore stesso in quanto complica l'analisi di ciò che viene fatto a ciascuna variabile, una parte fondamentale dell'allocazione dei registri. Ciò significa che le prestazioni potrebbero effettivamente diminuire. L'assemblatore in linea complica anche il futuro porting e la manutenzione di un programma.

Strutture alternative sono spesso fornite come un modo per semplificare il lavoro sia per il compilatore che per il programmatore. Funzioni intrinseche per istruzioni speciali sono fornite dalla maggior parte dei compilatori e wrapper di funzioni C per chiamate di sistema arbitrarie sono disponibili su ogni piattaforma Unix .

Sintassi

Negli standard linguistici

Lo standard ISO C++ e gli standard ISO C (allegato J) specificano una sintassi supportata in modo condizionale per l'assemblatore in linea:

Una dichiarazione asm ha la forma
  asm-declaration :
     asm ( string-literal ) ;
La dichiarazione asm è supportata in modo condizionale; il suo significato è definito dall'implementazione.

Questa definizione, tuttavia, è usata raramente nell'attuale C, poiché è contemporaneamente troppo liberale (nell'interpretazione) e troppo ristretta (nell'uso di una sola stringa letterale).

Nei compilatori reali

Nell'uso pratico, l'assemblaggio in linea che opera sui valori è raramente autonomo come codice fluttuante. Poiché il programmatore non può prevedere a quale registro è assegnata una variabile, i compilatori in genere forniscono un modo per sostituirli come estensione.

Esistono, in generale, due tipi di assembly inline supportati dai compilatori C/C++:

  • asm (o __asm__ ) in GCC . GCC utilizza un'estensione diretta delle regole ISO: il modello di codice assembly è scritto in stringhe, con input, output e registri clobbed specificati dopo le stringhe in due punti. Le variabili C vengono utilizzate direttamente mentre i nomi dei registri sono citati come stringhe letterali.
  • __asm in Microsoft Visual C++ (MSVC), compilatore Borland/Embarcadero C e discendenti. Questa sintassi non si basa affatto sulle regole ISO; i programmatori scrivono semplicemente ASM all'interno di un blocco senza la necessità di conformarsi alla sintassi del C. Le variabili sono disponibili come se fossero registri e sono consentite alcune espressioni C. Il compilatore ARM aveva una struttura simile.

Le due famiglie di estensioni rappresentano interpretazioni diverse della divisione del lavoro nell'elaborazione dell'assemblaggio in linea. Il modulo GCC conserva la sintassi complessiva del linguaggio e suddivide in compartimenti ciò che il compilatore deve sapere: cosa è necessario e cosa è cambiato. Non richiede esplicitamente al compilatore di comprendere i nomi delle istruzioni, poiché il compilatore è necessario solo per sostituire le sue assegnazioni di registro, più alcune operazioni di spostamento , per gestire i requisiti di input. Tuttavia, l'utente è incline a specificare i registri clobbed in modo errato. La forma MSVC di un linguaggio specifico del dominio incorporato fornisce facilità di scrittura, ma richiede che il compilatore stesso conosca i nomi del codice operativo e le loro proprietà di disturbo, richiedendo una maggiore attenzione nella manutenzione e nel porting. È ancora possibile controllare l'assemblaggio in stile GCC per errori clobber con la conoscenza del set di istruzioni.

GNAT (frontend del linguaggio Ada della suite GCC) e LLVM utilizza la sintassi GCC. Il linguaggio di programmazione D utilizza ufficialmente un DSL simile all'estensione MSVC per x86_64, ma l'LDC basato su LLVM fornisce anche la sintassi in stile GCC su ogni architettura. MSVC supporta solo l'assemblatore in linea su x86 a 32 bit.

Da allora il linguaggio Rust è migrato a una sintassi che astrae le opzioni di assemblaggio in linea oltre alla versione LLVM (stile GCC). Fornisce informazioni sufficienti per consentire la trasformazione del blocco in una funzione assemblata esternamente se il backend non è in grado di gestire l'assemblaggio incorporato.

Esempi

Una chiamata di sistema in GCC

In genere non è possibile chiamare direttamente un sistema operativo in un sistema che utilizza la memoria protetta. Il sistema operativo viene eseguito a un livello più privilegiato (modalità kernel) rispetto all'utente (modalità utente); un interrupt (software) viene utilizzato per effettuare richieste al sistema operativo. Questa è raramente una funzionalità in un linguaggio di livello superiore, quindi le funzioni wrapper per le chiamate di sistema vengono scritte utilizzando l'assemblatore in linea.

Il seguente esempio di codice C mostra un wrapper di chiamate di sistema x86 nella sintassi dell'assembler AT&T , utilizzando GNU Assembler . Tali chiamate vengono normalmente scritte con l'ausilio di macro; il codice completo è incluso per chiarezza. In questo caso particolare, il wrapper esegue una chiamata di sistema di un numero dato dal chiamante con tre operandi, restituendo il risultato.

Per ricapitolare, GCC supporta sia l' assemblaggio di base che quello esteso . Il primo passa semplicemente il testo alla lettera all'assemblatore, mentre il secondo esegue alcune sostituzioni per le posizioni dei registri.

extern int errno;

int syscall3(int num, int arg1, int arg2, int arg3)
{
  int res;
  __asm__ (
    "int $0x80"        /* make the request to the OS */
    : "=a" (res),      /* return result in eax ("a") */
      "+b" (arg1),     /* pass arg1 in ebx ("b") [as a "+" output because the syscall may change it] */
      "+c" (arg2),     /* pass arg2 in ecx ("c") [ditto] */
      "+d" (arg3)      /* pass arg3 in edx ("d") [ditto] */
    : "a"  (num)       /* pass system call number in eax ("a") */
    : "memory", "cc",  /* announce to the compiler that the memory and condition codes have been modified */
      "esi", "edi", "ebp"); /* these registers are clobbered [changed by the syscall] too */

  /* The operating system will return a negative value on error;
   * wrappers return -1 on error and set the errno global variable */
  if (-125 <= res && res < 0) {
    errno = -res;
    res   = -1;
  }
  return res;
}
#include <errno.h>

long int syscall3(unsigned int num, int arg1, int arg2, int arg3){

    int retA, retB;
    long int ret;
    asm (   "syscall\n\t" /* http://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64/ */
            :"=a"(retA),"=d"(retB) /* return call,see doc https://man7.org/linux/man-pages/man2/syscall.2.html */
            :"a"(num),"d"(arg3),"D"(arg1),"S"(arg2) /* input syscall cf above */
            :"cc","memory"
        );
    ret=retA; /* return value can be on 2 registers, plz complete */
    
    if (-125 <= ret && ret < 0) {
        errno = -ret;
        ret   = -1;
    }
    return ret;
}

Istruzioni specifiche del processore in Re

Questo esempio di linea di assemblaggio del linguaggio di programmazione D codice mostra che calcola la tangente di x con l' x86 's FPU ( x87 istruzioni).

// Compute the tangent of x
real tan(real x)
{
   asm
   {
       fld     x[EBP]                  ; // load x
       fxam                            ; // test for oddball values
       fstsw   AX                      ;
       sahf                            ;
       jc      trigerr                 ; // C0 = 1: x is NAN, infinity, or empty
                                         // 387's can handle denormals
SC18:  fptan                           ;
       fstp    ST(0)                   ; // dump X, which is always 1
       fstsw   AX                      ;
       sahf                            ; // if (!(fp_status & 0x20)) goto Lret
       jnp     Lret                    ; // C2 = 1: x is out of range, do argument reduction
       fldpi                           ; // load pi
       fxch                            ;
SC17:  fprem1                          ; // reminder (partial)
       fstsw   AX                      ;
       sahf                            ;
       jp      SC17                    ; // C2 = 1: partial reminder, need to loop 
       fstp    ST(1)                   ; // remove pi from stack
       jmp     SC18                    ;
   }
trigerr:
   return real.nan;
Lret:                                    // No need to manually return anything as the value is already on FP stack
   ;
}

Per i lettori che non hanno familiarità con la programmazione x87, l' idioma fstsw-sahf seguito dall'idioma di salto condizionato viene utilizzato per accedere ai bit C0 e C2 della parola di stato FPU x87. fstsw memorizza lo stato in un registro generico; sahf imposta il registro FLAGS sugli 8 bit più alti del registro; e il salto è usato per giudicare su qualunque bit di flag che corrisponda al bit di stato della FPU.

Riferimenti

link esterno