Guida Il Linguaggio Macchina di x86 e x86-64 (Parte 1)

DispatchCode

Moderatore
24 Maggio 2016
499
14
375
224
Il Linguaggio Macchina di x86 e x86-64 (Parte 1)

Ho scritto e riscritto parzialmente questo articolo non so più quante volte, ogni qualvolta lo rileggevo, mi rendevo conto di aver trattato troppo alcuni argomenti e di aver così snaturato l'articolo stesso, l'oggetto di tutto: il linguaggio macchina di queste CPU.

Nell'articolo su 8086 risultava semplice un sunto minimale sulla CPU, senza andar troppo fuori dal seminato. In questo caso è differente. Ho deciso quindi di tralasciare gli aspetti relativi a queste due architetture e limitarmi a riportare quelli che sono i registri utilizzati, così come i set di istruzioni per giungere poi a ciò che è l'oggetto dell'articolo: la lunghezza delle istruzioni e i campi che le compongono, così come la codifica di questi registri in "linguaggio macchina".

Ho deciso di dividerlo in due parti, dove la prima riguarderà solamente i registri presenti in queste architetture e nei vari set mentre nella seconda parte si andrà a fondo, guardando il formato delle istruzioni e il codice macchina stesso (un pò come ho fatto con 8086).
Questa prima parte sarà povera di immagini e piuttosto schematica.


1    Preambolo

Alcuni concetti non saranno del tutto nuovi, si tratterà infatti di un'estensione di vecchi concetti, quelli relativi a 8086. Chi si sta avventurando nella lettura di questo articolo potrebbe trovare più confortevole un passaggio al precedente, Il linguaggio macchina di 8086.
Non è obbligatorio ai fini della comprensione, ma alleggerisce un minimo il carico di informazioni da elaborare.

2    Registri a 32-bit

La famiglia x86 racchiude i processori a 32bit. Tutti i registri di 8086 sono ancora presenti e sono stati estesi a 32bit.

reg_x86_gpr.png

Figura 1

La figura 1 dovrebbe essere esplicativa: in sostanza i registri di segmento sono rimasti quelli di prima ed hanno la medesima dimensione. I registri generali sono stati estesi, così come è stato esteso il registro IP, chiamato ora EIP, e il registro dei FLAGS, chiamato ora EFLAGS.

I registri CR, che sono registri di controllo; uno dei più noti è probabilmente CR3 in quanto contiene l'indirizzo fisico delle tabelle delle pagine quando si fa uso di memoria virtuale.
Riporto la descrizione dei registri tratta dall'articolo Memoria Virtuale: x64 Virtual Address Translation:

Riporto di seguito i signficati, solo a titolo informativo:
CR0: ogni bit ha un determinato significato, ed alcuni possono essere letti e scritti. Le info vanno dal tipo di cache utilizzato per un tipo di pagina, alla validità, all'accesso etc.
CR1: è riservato, la CPU genera una Invalid Opcode Exception se si tenta di accedervi;
CR2: riporta l'indirizzo che ha causato il Page Fault (#PF, per abbreviare);
CR3: indirizzo fisico della prima tabella delle pagine della gerarchia;
CR4: altre informazioni sullo stato di alcuni flags, sempre ampio 32bit;
CR5 e CR7: come CR1;

e i registri DR, per il debugging. Potete trovare maggiori info anche utilizzando Wikipedia oppure Wiki OSDev (le risorse saranno linkate al termine dell'articolo), senza perdervi nelle documentazioni di Intel.

3    Registri a 64-bit


reg_x64_gpr.png

Figura 2

Anche qui ci saranno alcune vecchie conoscenze risalenti a 8086. I registri sono poi stati estesi a 64bit e invece della E hanno come prefisso R (RAX, RBX,...). Non c'è modo di indirizzare direttamente la parte alta di RAX (o di uno di questi registri); la parte più bassa sarà sempre in EAX e così via.

Ma, un momento: dove sono finiti AH, CH, DH, BH? Esistono, ed hanno il medesimo significato di prima. Tuttavia hanno una limitazione importante: non si possono usare in presenza del REX prefix; lo vedremo in seguito più dettagliatamente, basti sapere che questo prefisso viene inserito quando vengono utilizati i nuovi registri che iniziano con la lettera R.

Sono poi stati aggiunti numerosi altri registri, come r8, r9 e a seguire gli altri. Anche qui è possibile accedere alla double word, word o byte, in questo caso usando uno dei suffissi (d, w, b).

4    Registri a 80-bit


Da i386 è stato introdotto un ulteriore chip, chiamato x87 Coprocessor, noto anche come FPU. E' l'unità che si occupa dei calcoli in virgola mobile, e che è presente ovviamente anche oggi nei moderni PC (ma integrata nella CPU).

Qui sono stati introdotti alcuni registri, 8 precisamente, da ST(0) a ST(7).

5    Registri MMX (SIMD)


Si tratta della prima generazione SIMD (Single Instruction, Multiple Data); sono stati introdotti registri e istruzioni che consentono di operare su dati a 64bit (dimensione dei registri MMX) e cosa più importante su "packet data"... 64-bit alla volta! Supponete di avere 4 word, per un totale quindi di 64-bit. Usando un registro MMX è possibile elaborare (moltiplicare, dividere etc etc) 4 word alla volta. In sostanza invece di fare 4 somme separate, se ne fa una unica.
Il concetto verrà ripreso più avanti, quindi chiariremo in seguito il loro utilizzo. I registri MMX (che sono 8, da MMX0 a MMX7) sono ormai obsoleti, e altri set di istruzioni hanno introdotto ulteriori registri (e istruzioni).

Faccio notare che sembra una cosa da poco forse, ma in applicazioni matematiche o che fanno uso di grafica, sono un passo avanti enorme. Si tratta di "parallelismo a livello di istruzione", e velocizza molto le operazioni. I software fanno ancora uso di set successivi a questo per applicazioni di questo tipo (oltre che ovviamente alla scheda video).

6    Registri XMM (SSE, Streaming SIMD Extensions)


I registri XMM sono a 128-bit. Si tratta di 8 registri in x86 da XMM0 a XMM7 ai quali se ne aggiungono altri 8 in x64, da XMM8 a XMM15.

Questo è un esempio di un codie che fa uso di SIMD per la somma di due vettori (compilabile con MSVC, GCC non è compatibile con questa sintassi, deve essere adattato):
C:
#include<stdio.h>

#define   LEN    12

int main() {
  int vect1[LEN] = {1,2,3,4,5,6,7,8,9,10,11,12};
  int vect2[LEN] = {1,2,3,4,5,6,7,8,9,10,11,12};

  int res_vect1[LEN];
  
  int i = 0;
  
  __asm
  {
    lea      eax, vect1
    lea      ebx, vect2
    xor      ecx, ecx

    _while:
    cmp      ecx, LEN * 4
    jge      _end
    
      movups   xmm0, [eax + ecx]
      movups   xmm1, [ebx + ecx]
    
      addps    xmm0, xmm1                
      movups   [res_vect1 + ecx], xmm0   
      
      add      ecx, 4
      jmp      _while
    
    _end:
  
  }

  printf("Sum:\n\tV = (");
  for(; i<LEN; i++) {
    printf("%d, ", *(res_vect1+i));
  }
  printf("\b\b)\n");

  return 0;
}

Vi lascio il link ad un articolo dove SIMD è stato utilizzato per velocizzare il calcolo dei frattali dell'insieme di Mandelbrot: https://nullprogram.com/blog/2015/07/10/

7    Registri YMM (AVX)

Questo set è conosciuto come AVX e vede l'introduzione di altre istruzioni e di nuovi registri che estendono i precedenti; i registri si chiamano YMM e sono a 256-bit. Come prima, sono 8 registri in x86 e 16 in x86-64.
Più tardi, come per SSE che vede più versioni, è arrivato AVX2, che aggiunge altre istruzioni.

Queste istruzioni le si riconosce subito, infatti iniziano tutte con una v.

8    Registri ZMM (AVX-512)

Questo è un nuovo set di istruzioni, il più recente, e si tratta di ben 32 registri da 512-bit e sono i registri ZMM.

9    Conclusione prima parte

Questa prima parte è conclusa, ed era più per fornire un'idea generale di registri e istruzioni presenti in queste architetture. La parte 2 entrerà nel vivo del codice macchina!
Spero di non aver fatto confusione, se notate qualcosa di non chiaro o altro, commentate qui sotto e fatemelo presente. Se avete domande, sentitevi liberi di chiedere.