di  -  giovedì 13 settembre 2012

Torniamo a parlare del fattore “legacy” delle CPU x86, con l’ultimo aspetto che era rimasto in sospeso e che rimane da sviscerare: la segmentazione.

Per inquadrare bene il peso di questa caratteristica è fondamentale analizzare in che modo vengono utilizzati i segmenti nel codice x86 e x64, e l’impatto che possono avere mediamente durante l’esecuzione, sia a livello di specifiche istruzioni o parti di esse che li possono coinvolgere, sia a livello di implementazione nella pipeline d’esecuzione del processore.

Un’introduzione al concetto di segmento si trova in un articolo che è stato scritto in passato, che potete trovare in questa pagina.

L’implementazione dei segmenti fa uso di 4 appositi registi a 16 bit (divenuti 6 a partire dall’80386, a cui sono stati aggiunti altri due segmenti per i dati, FS e GS), che vengono utilizzati dal processore in base al contesto d’esecuzione.

CS viene utilizzato sempre ogni volta che viene eseguito il fetch dei byte dell’istruzione che dev’essere eseguita. SS è impostato di default per tutte le istruzioni di push o che accedono ai dati tramite i registri SP e BP. Per l’accesso a dati che non ricadono nel caso precedente, si fa uso di DS. Infine per le istruzioni di stringa che hanno un operando di destinazione, viene utilizzato ES per accedere al dato.

Qualunque istruzione venga eseguita, almeno CS viene coinvolto per recuperare i byte dell’istruzione tramite l’offset fornito dal registro IP (Instruction Pointer; il PC di altri microprocessori). Inoltre si possono impiegare fino ad altri due segmenti per l’accesso in memoria, a seconda del tipo di istruzione da eseguire. Ciò significa che la segmentazione è sempre operativa.

Nel caso dell’8086, e in generale quando parliamo di esecuzione in modalità reale e virtuale 8086, la segmentazione si riconduce semplicemente all’aggiunta di un valore chiamato base (del segmento) a un offset, col meccanismo che è stato illustrato nel primo link fornito, per superare il limite dei 64KB di memoria altrimenti indirizzabili coi soli 16 bit forniti dai registri.

Con l’80386 i registri sono diventati a 32 bit, per cui anche in queste modalità è possibile indirizzare fino a 4GB di memoria fisica. In ogni caso la segmentazione è e rimane sempre attiva, quindi all‘offset a 32 bit fornito dal registro viene sommato ugualmente l’indirizzo base del segmento associato al tipo di dato a cui si accede.

Si tratta di un costo fisso addizionale rispetto ad altri processori privi del concetto di segmentazione (per lo meno per come l’ha concepito e realizzato Intel), che per accedere ai dati hanno bisogno esclusivamente dell’offset, il quale alla fine coincide con l’indirizzo vero e proprio a cui si vuol accedere.

Quindi qualunque accesso alla memoria vede coinvolta almeno un’ALU per il calcolo del puntatore al primo byte, che può essere molto semplice nel caso del fetch dell’istruzione (si devono sommare soltanto due valori: base del segmento e offset), o molto più complicata per indirizzamenti più complessi verso la memoria (base del segmento + registro base + registro indice moltiplicato per un fattore di scala 1/2/4/8 + offset).

Tolto il primo caso, che rimane una “tassa fissa” da pagare al legacy di x86, il secondo è di carattere più generale e la segmentazione non rappresenta un costo addizionale che pesa, in quanto sommare 3 o 4 valori per un’ALU ha lo stesso costo. In buona sostanza, il costo della somma della base del segmento risulta “assorbito” dall’intrinseca complessità dell’ALU.

Si potrebbe puntare il dito contro la complessità della modalità d’indirizzamento che x86 mette a disposizione (a partire dall’80386, però; in precedenza era assente lo scaling del registro indice), ma anche alcuni processori RISC la mettono a disposizione, perché si tratta di un pattern d’accesso ai dati abbastanza comune (sebbene non sia il più frequente, come vedremo in una nuova serie di futuri articoli); l’esempio più noto è l’accesso a un vettore i cui elementi occupano più di un byte.

Preso atto che la segmentazione pervade l’implementazione dell’architettura x86 (compresa x64, che non l’ha eliminata del tutto dalla modalità d’esecuzione a 64 bit), vediamo anche quale impatto ha sull’ISA.

In termini di registri, come detto, ne servono 4 a 16 bit per i vecchi 8086, 80186, e 80286, e 6 in tutti i successori. Non si tratta, quindi, di una richiesta esagerata, a maggior ragione da quando le microarchitetture hanno cominciato a richiedere centinaia di migliaia di transistor per la loro realizzazione.

Inoltre non è necessario porre particolare attenzione per questa tipologia di registri, in quanto le poche istruzioni preposte alla loro gestione o utilizzo provvedono esclusivamente a leggerne o scriverne il contenuto. Quindi niente operazioni aritmetiche, logiche, o di altro tipo, ma soltanto load o store, dunque molto semplici da gestire (quando tratteremo i selettori il discorso cambierà).

Esistono, quindi, delle specifiche istruzioni di MOV che consentono di leggere un valore a 16 bit dalla memoria e caricarlo nel registro di segmento specificato; viceversa, è possibile scriverne il valore in una locazione di memoria a 16 bit. Inoltre, similmente agli altri registri, si può eseguire il PUSH o il POP di un segmento nello/dallo stack.

Le speciali istruzioni LDS e LES provvedono, invece, a caricare in un colpo solo il segmento DS o ES e un registro general purpose, specificando una locazione di memoria (che in gergo viene chiamata “puntatore far“; “lontano”) dalla quale attingere i rispettivi valori disposti sequenzialmente. L’80386 ha introdotto LFS e LGS per fare lo stesso coi nuovi segmenti FS e GS.

Istruzioni come queste erano molto diffuse coi vecchi processori a 16 bit, in quanto consentivano di superare il limite dei 64KB di memoria indirizzabili facendo uso di soli 16 bit di offset, come già detto. Un qualunque segmento dati mette a disposizione fino a 64KB di memoria, ma potendo usare più segmenti dati si poteva quindi coprire tutto lo spazio d’indirizzamento.

Un accorto uso dei registri di segmento dati consente di ridurre il costo dei continui caricamenti di nuovi segmenti, ma è chiaro che, dovendo operare con molte strutture dati, quest’operazione era, purtroppo, molto frequente e penalizzante in termini prestazionali e di spazio occupato (le istruzioni di caricamento dei segmenti hanno un costo e occupano spazio).

A ciò si aggiunge il fatto che normalmente il processore utilizza il segmento DS per accedere ai dati, per cui se si voleva far uso di un altro segmento (ad esempio ES, ma anche CS nel caso di variabili memorizzate direttamente nel segmento di norma riservato al codice), si doveva ricorrere ai famigerati prefissi di segmento.

Per i programmatori assembly si tratta di etichette (ad esempio “ES:”) da specificare come prefisso dell’operando di memoria, che istruiscono il processore a non utilizzare DS, ma il segmento desiderato, per accedere a quella locazione di memoria.

Tutto ciò si traduce in costi maggiori sia in termini d’esecuzione (non per le microarchitetture più moderne) che di spazio (si tratta di un byte in più che “cambia al volo” il segmento di default). Inoltre l’uso dei prefissi comporta una maggior complessità del decoder delle istruzioni, come abbiamo visto in un paio di articoli (qui e qui).

Le incarnazioni a 32 bit di quest’architettura hanno consentito di eliminare del tutto questo meccanismo, in quanto gli offset per indirizzare la memoria sono passati da 16 a 32 bit, e quindi un singolo segmento dati consente, da solo, di indirizzare ben 4GB.

In linea teorica è sempre possibile continuare a operare coi segmenti esattamente come coi vecchi processori, e quindi indirizzare 4GB di memoria per ognuno di essi, ma in pratica tutti i sistemi operativi moderni mettono a disposizione quello che viene chiamato “modello flat“, quindi tutti i segmenti puntano sostanzialmente alla stessa area di memoria (di 4GB, appunto), e il solo offset discrimina l’accesso a un dato (o istruzione).

Ciò ha permesso di semplificare notevolmente l’implementazione e la gestione dei processi/thread, anche e soprattutto per i programmatori, che non devono gestire puntatori “far” a 48 bit (16 per il segmento, e 32 per l’offset), ma che si “accontentano” di puntatori “near” (vicini) a 32 bit.

Com’è facile intuire, con un modello flat i segmenti vengono caricati una sola volta, alla creazione del processo, e poi “dimenticati” sia dal s.o. che dalle applicazioni. Quindi le uniche e poche istruzioni di manipolazione dei segmenti si trovano all’interno del kernel; per il resto la CPU non ne incontrerà durante il resto dell’esecuzione del codice.

Un discorso simile ai dati valeva anche per il codice. Essendo presente un solo segmento per il codice, CS (quello correntemente in uso), in passato il processore era limitato a indirizzare 64KB di codice.

Per superare questo limite era possibile anche qui far uso dei puntatori “far“, tramite apposite istruzioni di salto (CALL, JMPfar“) e di ritorno (RETfar“) che consentivano di caricare un nuovo segmento di codice assieme a un nuovo offset (dalla memoria o come valore immediato per quelle di salto; dallo stack per quella di ritorno) dal quale cominciare a prelevare i byte della prossima istruzione da eseguire.

Ovviamente il prezzo da pagare era una penalizzazione in esecuzione, dovendo caricare un nuovo segmento, oltre che memorizzare nello stack il vecchio valore, per cui la tendenza era quella accorpare il più possibile il codice in appositi segmenti, in modo da minimizzarne l’impatto.

Anche qui, col modello “flat” tutto ciò non è necessario, in quanto il codice risulta distribuito nei 4GB di memoria indirizzabile, e quindi si fa sempre uso di un solo segmento già caricato e di puntatori “near” ove necessario.

Gli unici casi in cui vengono ancora utilizzati i segmenti anche nel modello “flat” riguardano gli interrupt e le eccezioni. Quindi le istruzioni INT, INTO (non presente in x64) e IRET caricano sempre un segmento (o selettore) e un offset, e lo stesso avviene quando si solleva un’eccezione (che va a pescare i valori da apposite entry nella interrupt table).

Si tratta di scenari non eliminabili (d’altra parte l’esecuzione deve passare al codice del kernel o del device, che si trovano in appositi segmenti), e comunque non frequenti (il processore generalmente non passa il tempo a servire richieste di interruzione o a elaborare eccezioni).

Non abbiamo ancora parlato dei selettori, che riprenderemo col prossimo articolo di questa serie, che hanno rappresentato una notevole e più interessante innovazione per alcune caratteristiche che mettono a disposizione e il diverso comportamento del processore (che “fa altro” rispetto a quanto visto finora), e ci si potrebbe chiedere perché dedicare tanto tempo agli obsoleti segmenti.

Il motivo è che, con x64, il futuro s’è, in parte, ricongiunto col passato. In modalità a 64 bit, infatti, i meccanismi messi in piedi coi selettori sono stati eliminati; il processore opera in una modalità sostanzialmente simile a quella reale di 8086 & compagnia, ignorando persino la base dei segmenti quando indirizza la memoria, e facendo uso del solo offset (che, a questo punto, coincide col puntatore).

In realtà la base del segmento continua a essere aggiunta all’offset, ma il valore rimane sempre zero. Le uniche eccezioni sono rappresentate dai segmenti FS e GS (quando usati con gli appositi prefissi), nel qual caso la rispettiva base viene correttamente considerata dal processore per il calcolo dell’indirizzo, come per i vecchi processori.

Ciò si è reso necessario, nonché molto comodo, per indirizzare le variabili locali di un thread oppure per alcune strutture del kernel, che quindi possono essere velocemente raggiunte tramite FS e GS.

Concludendo, abbiamo visto che l’impatto dei segmenti nell’architettura x86 non è poi così pesante in termini di risorse impiegate e anche durante l’esecuzione (il costo fisso rimane il calcolo dell’indirizzo dell’istruzione, ma è una semplice somma di due valori), specialmente lavorando in modalità a 32 bit “flat” (che è quella usata ovunque ormai) che consente di ignorarne l’esistenza.

9 Commenti »

I commenti inseriti dai lettori di AppuntiDigitali non sono oggetto di moderazione preventiva, ma solo di eventuale filtro antispam. Qualora si ravvisi un contenuto non consono (offensivo o diffamatorio) si prega di contattare l'amministrazione di Appunti Digitali all'indirizzo info@appuntidigitali.it, specificando quale sia il commento in oggetto.

  • # 1
    banryu
     scrive: 

    Complimenti Cesare per l’articolo (ovviamente anche per i precedenti di questa serie): sono una piacevole (e comprensibile) lettura anche per i profani come me.

  • # 2
    Cesare Di Mauro (Autore del post)
     scrive: 

    L’argomento è un po’ ostico, per cui i feedback sono molto importanti per avere il polso della situazione.

    Grazie. :)

  • # 3
    Sisko212
     scrive: 

    Bella serie di articoli, mi ha riportato un pò ai vecchi tempi, quando programmavo assembly all’ itis, sui pc 8086 (mi pare si faccia ancora oggi, nonostante tutto… e questo la dice lunga sullo stato di aggiornamento delle scuole italiane)…. mi ha stupito, ma neanche poi tanto, che ancora ci sia il problema dell’offset sugli attuali x86 a 32bit, anche se ovviamente, visto lo spazio di indirizzamento, risulta inutile/inutilizzato.
    Per quanto riguarda il feedback… bhe la vedo dura… nel senso che oggi come oggi, dubito troverai qualcuno che programmi ancora qualche cosa in assembly su x86, e men che meno a x64, salvo particolarissime esigenze.
    La capacità di calcolo di queste piattaforme è sovrabbondante per il 99,99% delle applicazioni standard, per cui tutti oggi usano linguaggi di qualche tipo.
    Il motivo per cui si usava l’assembly a suo tempo, come meglio di me saprai, era per spremere al massimo prestazioni e memoria, che all’epoca erano molto, ma molto più scarse di oggi… e questo senza tirare in ballo il fattore costi delle cpu.
    Mi sà che se oggi si vuole trovare qualcuno che programmi ancora assembly di qualche genere, bisogna rivolgersi ai microcontrollori, pic o atmel che siano… dove effettivamente ha ancora un certo senso per il tipo di compiti che devono svolgere.
    In fin dei conti, nei bei tempi andati, quando i s.o. (vedi dos) non rompevano le scatole con gli assegnamenti di risorse, e l’astrazione dell’hardware, spesso anche i pc venivano usati con qualche scheda i/o per fargli fare compiti che oggi facciamo con i normali microcontrollori no ?

  • # 4
    floriano
     scrive: 

    anche per i microcontrollori si è passati ai linguaggi di alto livello, per i pic in uso nei professionali si usa qualche versione del c o del basic, invece per gli arduino ci sono una marea di strumenti per lavorare in c…

    ovviamente tutto ciò poi dipende dalla buona volontà e dal tempo disponibile del professore di turno…

  • # 5
    Cesare Di Mauro (Autore del post)
     scrive: 

    Concordo. Ormai l’assembly si usa poco, e giustamente ci si rivolge a strumenti di più alto livello perché è più importante la produttività (e quindi il time-to-market).

    @Sisko212: so che se ne mastica poco di assembly, ormai, ma il feedback è importante per capire se c’è gente interessata all’argomento, o se questo è stato esposto in maniera troppo complicata, in modo da regolarmi per i prossimi (eventualmente decidendo di cambiare completamente e parlare d’altro ;).

    Perché il taglio degli articoli vuol essere divulgativo, oltre che far emergere alcune cose che magari non tutti conoscono (parlavi dell’offset prima, ma penso che ti riferissi alla base del segmento sommata sempre e comunque all’offset calcolato, anche quando ormai non servirebbe).

    Credo sia interessante anche riflettere su cosa comportino effettivamente determinate scelte. Oggi si continua a parlare di x86, di quanto sia brutta come ISA, che sembra portarsi dietro tutti i mali del mondo e i relativi costi.
    Ho pensato questa serie di articoli per parlare appositamente di quest’argomento, sviscerandolo e analizzandone gli aspetti fin al più basso livello, limitatamente alle mie capacità ed esperienza, in modo da fornire un quadro più chiaro e completo, e permettere ai lettori di dare il giusto peso al concetto di “legacy” che si porta dietro quest’architettura.

  • # 6
    Michele
     scrive: 

    Ho letto con molto interesse l’articolo e penso che parlare di cose un po’ più complesse della media non sia un male anzi è un occasione per imparare qualcosa di nuovo.
    Anche perchè di articoli “facili” è pieno internet.

    Grazie Cesare.

  • # 7
    Ciano
     scrive: 

    Concordo pienamente con Michele, seguo da anni alcuni siti di news e recensioni di tecnologia, e gli articoli sono sempre meno tecnici e li sto lentamente abbandonando.

  • # 8
    TheFoggy
     scrive: 

    Seguo sempre con attenzione i tuoi articoli, Cesare, e devo dire che riesci ad esprimere concetti difficili in modo davvero molto chiaro. Nonostante abbia fatto Ingegneria Informatica (e Perito Informatico, prima), alcuni dettagli non mi sono mai stati spiegati (il discorso “costo dell’operazione”, ad esempio).
    Trovare programmatori assembly è difficile, verissimo…anche perchè il numero di istruzioni è aumentato parecchio, dai tempi dell’8086!! (che rimane un ottima CPU su cui imparare…è impensabile spiegare l’assembly su processori più moderni, IMHO. La scuola da un’infarinatura, se poi la cosa dovesse servire più avanti, le basi ci saranno). E poi…l’assembly ha sempre il suo fascino! ;)

    Quoto, inoltre, Michele. Ok semplificare tutto..ma ogni tanto qualcosa di più tecnico non fa male…

  • # 9
    Cesare Di Mauro (Autore del post)
     scrive: 

    Intanto ringrazio ancora tutti.

    Posso dirti che, lavorando spesso in assembly nell’ultimo anno, m’è pure tornata la voglia di “sporcarmi le mani” coi dettagli di più basso livello. :)

Scrivi un commento!

Aggiungi il commento, oppure trackback dal tuo sito.

I commenti inseriti dai lettori di AppuntiDigitali non sono oggetto di moderazione preventiva, ma solo di eventuale filtro antispam. Qualora si ravvisi un contenuto non consono (offensivo o diffamatorio) si prega di contattare l'amministrazione di Appunti Digitali all'indirizzo info@appuntidigitali.it, specificando quale sia il commento in oggetto.