L’Assembler per PowerPC

Oggi la programmazione in assembler non ha più quel fascino di una volta. Il linguaggio più diffuso è senza dubbio ormai il C. Utilizzare un linguaggio d’alto livello come il C fornisce dei vantaggi, per esempio in termini di portabilità. Può capitare, a volte, di dover scrivere porzioni di codice in assembler per interfacciare la propria applicazione verso dispositivi fisici o, anche, per aumentare le prestazioni del nostro sistema in termini di velocità e occupazione di memoria. Oltre a dare un’introduzione all’assembler daremo ampi stralci al suo modello di programmazione.

Introduzione

La specifica architetturale del PowerPc è stata rilasciata nel 1993 a 64-bit con un subset a 32-bit. La maggior parte dei PowerPC disponibili, ad eccezione di alcuni modelli, sono basati a 32-bit. Il  processore occupa un rilevante segmento di mercato grazie alle sue buone prestazioni, bassi consumi e dissipazioni. Esistono proposte basate sul core PowerPc con delle periferiche integrate: ethernet controller o I/O dedicati. Il PowerPc ha 32 registri a 32 o 64 bit (GPR) e vari altri registri dedicati: PC (program counter), LR (link register) o CR (condition register), uno dei registri più interessanti è il PVR (PowerPc Version Register). Questo registro è utile quando vogliamo conoscere la versione e il modello del processore in uso, di conseguenza è possibile definire diverse opzioni di inizializzazione del nostro ambiente di esecuzione. La tabella 1 mostra un esempio del contenuto di un PVR.

TABELLA 2 – CONTENUTO DI UN PVR

TABELLA 1 – CONTENUTO DI UN PVR

L’architettura originale del PowerPc definisce una famiglia di processori che offrono diverse prestazioni differenti sia in termini di costo che di possibilità tecniche. L’architettura è definita in quattro libri che definiscono quattro differenti livelli di architettura. Dalle famiglie 600/700 che sono state pensate e realizzate per applicazioni generali e ad ampio spettro, a quelle della famiglia 400 rivolta verso il mercato embedded.

PowerPC  EABI (Embedded Application Binary Interface)

L’interfaccia EABI è stata sviluppata dai produttori per rispondere alle esigenze d’intero perabilitià  per  le  applicazioni embedded basate su PowerPc. Questa interfaccia è un’ottimizzazione della interfaccia SVR4 Application Binary Interface. L’obiettivo dell’ottimizzazione nel mondo embedded è quello di minimizzare l’uso della memoria e, nello stesso tempo, mantenere le prestazioni. Una Application Binary Interface è un insieme di convenzioni per permettere al software l’interoperabilità del codice. Conseguentemente,  il PowerPc non ha un hardware stack pointer. In questo modo, chi sviluppa  il codice deve definire  il ruolo di ogni GPR (General Purpose Register). Questo comporta anche un allineamento con i tools utilizzati per lo sviluppo ed il testing del codice: compilatori e debugger. La differenza tra ABI ed EABI sta nella dif fer ente  risposta  al  segmento  di mercato. L‘interfaccia EABI si rivolge al mondo embedded ed è un’estensione dello standard Unix Sistema V Release 4 (SVR4) ABI per PowerPc. La tabella 2 mostra i  GPR utilizzati secondo la convenzione EABI.

TABELLA 1 – POWERPC EABI REGISTERS

TABELLA 2 – POWERPC EABI REGISTERS

I  simboli _SDA_BASE e _SDA2_BASE sono definiti a link time e questi specificano la zona della small data areas. Un programma deve caricare questi valori nei registri, rispettivamente, GPR13 e GPR2 prima  di  utilizzare i  dati  del programma. Prima di eseguire  il programma, la parte di startup del codice deve anche definire lo stack pointer in GPR1. Questo puntatore deve avere un allineamento a 8 byte. Questo lavoro viene svolto, di solito, con la routine _eabi(), l’utilizzatore non chiama direttamente questa funzione, ma il  compilatore deve inserire una chiamata alla funzione prima di eseguire la funzione main(). La maggioranza degli ambienti forniscono una funzione _eabi() automaticamente collegata, nel processo di linking, con il programma utente.

L’interfaccia EABI è stata sviluppata dai produttori per rispondere alle esigenze d’interoperabilitià per le applicazioni embedded basate su PowerPc. Questa interfaccia è un’ottimizzazione della interfaccia SVR4 Application Binary Interface. L’obiettivo dell’ottimizzazione nel mondo embedded è quello di minimizzare l’uso della memoria e, nello stesso tempo, mantenere le prestazioni.

Quando usare l’assembler

Non sempre è utile adoperare linguaggi ad alto livello. Può capitare, infatti, di interagire con i registri  della CPU. Per esempio, il linguaggio  C consente di applicare le operazioni di decremento e incremento sui registri, ma se esiste la necessità di memorizzare da qualche parte un registro particolare, come lo stack pointer, l’operazione non è più consona ad un linguaggio come il C. Infatti, quando si assegna lo stack ad un registro, di solito si utilizza l’assembler in quanto risulta più consono per queste operazioni. Questa è una delle ragioni per cui la sezione di startup di un’applicazione è, di solito, scritta in assembler. Inoltre, può anche capitare di gestire le eccezioni, in questo caso occorre mettere da parte lo stato dei registri per poi, al termine della gestione dell’eccezione, di riprendere  il loro valore allo stato precedente dell’eccezione stessa. La scrittura in assembler di porzioni del nostro programma può essere necessario per diverse ragioni, per esempio se vogliamo aumentare il livello d’ottimizzazione del nostro lavoro può essere necessario ricorrere a questa scelta, ma, in ogni caso, questa opzione è la nostra ultima possibilità da ponderare attentamente. In ogni modo è opportuno tenere presente:

Profile

Fate il profile del vostro codice prima di iniziare qualsiasi lavoro di ottimizzazione. Cercate di ottenere, da questa attività, lo stato di codifica della vostra realizzazione con i vari colli di bottiglia. Solo al termine di questo lavoro, e individuati  i criteri oggettivi su sui intervenire, si inizia il lavoro di ottimizzazione del codice. In questo caso a volte può essere necessario addirittura riscrivere porzioni di codice direttamente in assembler.

Ottimizzazione degli algoritmi

In questo contesto, a volte può essere necessario scegliere diverse strutture dati da implementare in assembler. Oppure, quando utilizziamo strutture iterative su una lista circolare può essere utile scegliere tabelle hash o alberi binari.In ogni caso, quando esiste questa esigenza è opportuno tenere presente queste considerazioni:

1) il codice scritto in C è estremamente portabile ed è compreso da un ampio spettro di programmatori, viceversa, il codice scritto in assembler è difficile da comprendere e non è per nulla portabile;

2) il codice C è, relativamente, molto più facile da debuggare;

3) di contro, il codice assembler ci permette di realizzare funzioni dove il requisito  prestazionale gioca un peso rilevante. Il compilatore  della nostra cross-factory è lo strumento migliore per ricavare indicazioni utili della stesura del nostro codice. Prima di iniziare a riscrivere le porzioni di codice incriminato, proviamo a utilizzare lo switch –O3 e le direttive __inline__. Infatti, il compilatore conosce perfettamente il flusso dello scheduling dell’architettura hardware disponibile e di conseguenza può regolare perfettamente l’attività della pipeline. Per osservare come il nostro cross compilatore genera codice assembler a volte risulta utile selezionare gli appositi switch in fase di compilazione. Per esempio, utilizzando il compilatore gcc con l’opzione:
gcc –O3 –S file_name.c

si genera un file assembler secondo il formato previsto dall’assemblatore gas (GNU assembler).  Il file generato ha una estensione nome_file.s. Il listato 1 fornisce un breve esempio di traslazione da un file C a uno assembler listato 2.

Include <stdio.h>
Void main (void)
{printf(“CIAO”);
}
Listato 1
.data # inizia la sezione dati
Messaggio:
       .string “CIAO\n”
       Len =.-msg
.text  # termina la sezione dati e inizia la sezione codice
_start:
       li 0,4 # syscall (sys_write)
       li 3,1 # first argument: file descriptor (stdout)
              # second argument: pointer to message to write
       lis 4,string@ha # load top 16 bits of &msg
       addi 4,4,string@ # load bottom 16 bits
       li 5,len # third argument: message length
       sc     # chiama una system call, in questo modo
              # accediamo al kernel and exit
       li 0,1 # il numero della syscall (sys_exit)
       li 3,1 # primo argomento (exit code)
       sc # chiama il kernel
Listato 2

L’assembler del PowerPC richiede un registro di destinazione per tutti le operazioni basate su registro (si ricorda che questo microprocessore è basato su architettura RISC). Il registro di destinazione è sempre il  primo della lista degli argomenti espressa nella linea assembler. Con il PowerPc  sotto Linux, le chiamate di sistema (system call) sono fatte utilizzando l’istruzione syscall (sc) con il numero della chiamata nel registro gpr0 e gli argomenti in gpr3. La chiamata di sistema, l’ordine degli argomenti e il numero degli stessi possono essere differenti a seconda del sistema operativo ospite (per esempio NetBSD piuttosto che Mac OS). Questa diversità è una delle ragioni per cui i programmatori tipicamente utilizzando le chiamate attraverso la libreria libc, infatti, questa libreria gestisce gli specifici dettagli del sistema operativo. I registri  del PowerPc hanno un numero non un nome. In questo modo, il registro 3 è identificato come gpr3, mentre il registro 3 in virgola mobile come fgpr3. Vediamo delle considerazioni su alcune particolari istruzioni:

ISTRUZIONI IMMEDIATE

Con l’opcode li (load immediate)  si vuole intendere che la costante conosciuta al momento della compilazione è posta in un registro come espresso dall’opcode. addi è l’istruzione addizione con operando immediato, addi 3,3,1 il registro 3 è incrementato di 1 e messo in 3. Viceversa, con add 3,3,1 invece si incrementa  il registro 3 con il contenuto del registro 1 e si pone il risultato in 3. Da questo si deduce che se le istruzioni finiscono con la lettera i, allora queste sono istruzioni immediate.

Mnemonici

li  non è una istruzione, ma è un m-nemonico. Non è una istruzione assembler, ma, per poter essere accettato dal microprocessore, dovrà essere tradotto in una istruzione assembler al momento della generazione del codice oggetto. Utilizzando  il comando GNU objdump –d si ottiene la stampa, a video o su file, dei mnemonici del PowerPc in luogo delle istruzioni presenti nel file originale.

Puntatori

Vediamo dal listato 1 che il messaggio  da visualizzare  è indirizzato dall’etichetta string. Il PowerPc utilizza istruzioni a 32 bit di lunghezza fissa (ia32 utilizza istruzioni variabili), di conseguenza le istruzioni che utilizzano istruzioni a 32 bit sono posizionati su interi a 32. Questo intero è diviso in una serie di campi con differenti significati: opcode (6 bits), registro sorgente (5 bits), registro destinazione (5 bits) e valore immediato (16 bits). Il numero dei bit di ciascun campo variano dal tipo di istruzioni, ma la cosa importante da sapere è che questi bit occupano spazio nell’istruzione. Con l’istruzione addi, per esempio, i primi tre campi sono inseriti direttamente nell’istruzione, mentre i rimanenti 16 bits del valore immediato sono semplicemente aggiunti. La conseguenza di questo è che possono essere utilizzati solo 16 bits per indicare valori immediati, cioè non è possibile caricare un valore di 32 bit in un qualsiasi registro. Per fare questo è necessario utilizzare due istruzioni, nel primo è caricato la prima parte dei 16 bits e nel secondo la rimanente parte. Dal punto di vista implementativo è necessario utilizzare la notazione:

@ha      per high
@l        per low

Quindi, dal listato 1:

lis 4,string@ha # load top 16 bits of &msg addi 4,4,string@l # load bottom 16 bits

Alcune considerazioni a livello di registro

Vediamo a questo punto alcune considerazioni da tenere presenti per gestire correttamente  i registri del PowerPc. Iniziamo con il time base register per poi affrontare altri registri che sono specifici per determinate attività.

Time  Base

L’architettura PowerPc definisce un registro a 64-bit utilizzato come time base e tutte le implementazioni hanno un registro a 64 eccetto il 403GA che ne utilizza uno a 56 bit. Questo registro contiene un valore che è incrementato in base ad una frequenza selezionata dipendente dall’implementazione. La frequenza è incrementata, nella famiglia 600, ogni ¼ del bus clock rate. La famiglia 400 incrementa questo time base ogni execution unit clock o da un clock esterno fornito dall’implementatore. Le istruzioni per scrivere verso questo registro non dipendono dall’implementazione, in questo modo le scritture al registro con un 32 bit PowerPC dovrà lavorare correttamente anche su un 64 bit. Il modo di lettura di questo registro dipendono, invece, dall’implementazione. Per la famiglia 600, 401x2 e 405 il registro è letto mediante l’uso di diversi SPR (sono registri del PowerPc): questi registri sono utilizzati per scrivere i registri a 32 bit TBU e TBL. Il livello di privilegio definito come user consente la lettura utilizzando le istruzioni mftb per leggere il registro TBL e mftbu per TBU. Il livello supervisor  consente di scrivere i registri utilizzando mttbl e mttbu per scrivere, rispettivamente, nel registro TBL e TBU. L’architettura descritta in book E non dispone dell’istruzione mftb. Il core del modello 440 aderisce all’architettura proposta dal manuale e utilizza l’accesso del 403.

Altri specifici  registri

Il valore  del registro PVR è unico per ogni processore, ciascuna implementazione ha un proprio valore. Vediamo in questo breve elenco quali sono i registri  specifici della famiglia 600.

SPRG0-SPRG3

Solo i processori della famiglia 600 hanno questi registri. Il processore  SPRG0 è un registro riservato che contiene il puntatore allo stack pointer utilizzato dal primo livello del gestore delle eccezioni.  Il registro SPRG1 è utilizzato come registro di backup di un GPR ed è utilizzato come stack pointer per contenere altri registri mediante la copia del contenuto di SPRG0 verso il registro GPR, anche in questo caso SPRG1 è utilizzato al primo livello del gestore di eccezioni. La norma EABI specifica che il  registro GPR1 è utilizzato come stack pointer ed è sempre, in ogni caso, valido.

HID0

Questo registro è chiamato Hardware Implementation Dependent 0 ed è utilizzato per abilitare e determinare alcuni parametri del processore, quali il bus parity control, il power management,  il controllo della cache e altro. Il significato di questo registro cambia in base al processore selezionato.

HID1

Questo registro è utilizzato per configurare la PLL. L’accesso a questo registro è consentito utilizzando i registri mtspr e MFSPR.

EAR

Questo registro (External Access Register) è un registro opzionale nell’OEA del PowerPc. L’EAR è utilizzato per controllare gli accessi verso il mondo esterno.  I componenti della famiglia 400 non dispongono di un registro equivalente.

PIR

Anche questo è un registro opzionale ed è utilizzato dal sistema operativo per assegnare un identificatore al processore. Questo è anche utilizzato quando il processore comunica con il mondo degli I/O.

L’uso dello standard EABI porta indubbi vantaggi nelle applicazioni. Per prima cosa è possibile, in questo modo, integrare differenti componenti software in maniera indipendente dal compilatore utilizzato e svincolarsi dall'obbligo di supportare differenti toolchain.

 

 

2 Commenti

  1. Stefano Lovati Stefano Lovati 7 gennaio 2018
  2. Riccardo Ventrella Riccardo Ventrella 17 gennaio 2018

Scrivi un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *