L'assembly scritto a mano è pieno di ripetizioni. La stessa breve sequenza di istruzioni compare ancora e ancora, con poche variazioni, e ci sono poche astrazioni per ridurre il codice ripetitivo. Non solo: programmare in assembly richiede di gestire valori senza una semantica evidente. Non ci sono parametri con nome e la maggior parte dei dettagli della macchina sottostante deve essere gestita manualmente dal programmatore.
NASM, l'assembler usato dal track, offre alcuni strumenti che possono aiutare il programmatore con entrambi i problemi. Questi strumenti vengono eseguiti al momento dell'assemblaggio, prima che qualsiasi istruzione raggiunga il processore. Guidano l'assembler su come riscrivere il testo sorgente prima ancora che venga tradotto in codice macchina.
Gli strumenti più importanti tra questi sono le macro, che possono essere viste come una ricetta per l'assembler. Vengono espanse nel punto in cui sono usate, come se il codice fosse stato scritto lì a mano.
Le macro possono accettare un certo numero di token letterali come parametri, il che le rende riutilizzabili in contesti diversi.
Tuttavia, non sono funzioni: non c'è call, né ret, né trasferimento del controllo a un altro punto del codice.
La definizione più semplice assegna un nome a un valore o a una breve espressione con %define:
%define WIDTH 8
%define BUFFER_SIZE (WIDTH * 16)
Ogni occorrenza successiva di WIDTH viene sostituita con 8 prima dell'assemblaggio.
La sostituzione è testuale, quindi il nome viene sostituito ovunque compaia, sia in un operando sia all'interno di un'altra definizione.
Per le costanti numeriche, preferisci equ invece di %define.
Una macro a riga singola dichiarata con %define viene espansa solo quando viene usata, e può essere riassegnata o annullata.
Una costante equ, invece, viene valutata nel punto di definizione e non viene mai modificata.
Un %define può anche accettare parametri con nome, comportandosi come una piccola funzione inline:
%define double(x) ((x) + (x))
mov eax, double(WIDTH) ; assembled as mov eax, ((8) + (8))
Racchiudi tra parentesi sia l'intero corpo sia ogni parametro.
Con %define square(x) x*x, la chiamata square(1+1) si espande in 1+1*1+1, che è 3.
Scritto come %define square(x) ((x)*(x)), la stessa chiamata si espande in ((1+1)*(1+1)), che è 4.
I nomi definiti con %define distinguono maiuscole e minuscole.
La variante %idefine rende il nome insensibile alle maiuscole e minuscole, se lo si desidera.
Le macro a riga singola sono utili per dare un significato a varie parti del codice. Se usate con attenzione, migliorano la leggibilità e riducono la probabilità di errori di battitura. Per esempio, un registro che ha uno scopo dedicato all'interno di una funzione può essere definito in base al suo scopo:
%define COUNTER rcx
...
.loop:
...
dec COUNTER
jnz .loop
...
Un'intera sequenza di codice, con un numero qualsiasi di righe, viene catturata con %macro e chiusa da %endmacro.
Un numero dopo il nome indica quanti argomenti accetta la macro, che possono essere zero.
Quando vengono passati degli argomenti, sono separati da una virgola (,):
%macro example_macro 0
...
%endmacro
Gli argomenti nelle macro multiriga non hanno un nome.
Sono indicati come %1, %2 e così via, nell'ordine in cui vengono passati:
; Multiplies two values, returning the result in the first argument
%macro mult_two 2 ; %1 = accumulator, %2 = value
imul %1, %2
%endmacro
mult_two eax, ecx ; multiplies eax and ecx and returns the result into eax
mult_two r8, r9 ; multiplies r8 and r9 and returns the result into r8
Nota che gli argomenti devono essere una sintassi valida per le operazioni eseguite nel corpo della macro.
Qui mult_two eax, ecx e mult_two r8, r9 vengono entrambi assemblati, ma mult_two eax, cl no, dato che imul eax, cl non è definito.
Una macro che definisce un'etichetta ordinaria si rompe nel momento in cui viene usata due volte, perché anche l'etichetta verrebbe definita due volte.
Per usare etichette all'interno di una macro che può essere chiamata più di una volta, è necessario anteporre a tali etichette %%.
Questo indica all'assembler di assegnare a questa etichetta un nome diverso e univoco a ogni espansione:
; clobbers rcx
%macro clear_n 2 ; %1 = base address, %2 = count of dwords to clear
xor ecx, ecx
%%loop:
mov dword [%1 + rcx*4], 0
inc ecx
cmp ecx, %2 ; %2 is compared with ecx: it is an immediate or a 32-bit operand
jb %%loop
%endmacro
clear_n rdi, 4 ; gets its own %%loop
clear_n rsi, 8 ; a different %%loop, no clash
È possibile ripetere un blocco di codice di una riga usando times.
Questo è particolarmente utile quando si dichiarano valori ripetuti in memoria:
section .data
negative_block: times 16 db -1
; this assembles to 16 db -1 in sequence, i.e., an array of 16 bytes, all equal to -1
Per la ripetizione di codice multiriga, possiamo usare invece %rep:
%rep 2 ; number of repetitions: 2
add rcx, 32
mov dword [rsi + rcx], 0
%endrep
Nota che il numero di ripetizioni è specificato subito dopo %rep e c'è un %endrep per chiudere il blocco ripetuto.
È comune usare un contatore al momento dell'assemblaggio all'interno di un blocco %rep, proprio come un ciclo a runtime usa un contatore.
Mentre una costante intera al momento dell'assemblaggio può essere dichiarata con equ, per le variabili intere al momento dell'assemblaggio usiamo invece %assign.
A differenza di %define, %assign viene valutato immediatamente come numero e può essere riassegnato:
xor eax, eax
%assign i 0
%rep 4
add eax, i ; add 0, then 1, then 2, then 3
%assign i i + 1
%endrep
Dopo che il preprocessore è stato eseguito, il ciclo non c'è più e rimangono quattro istruzioni add, con gli immediati 0, 1, 2 e 3 incorporati.
Questo è lo srotolamento del ciclo fatto dall'assembler: nessun registro contatore e nessun salto, solo codice lineare.
Ci sono vari costrutti condizionali disponibili per il preprocessore. Selezionano tra diversi percorsi, espandendo il loro corpo sul posto, ma solo quando la loro condizione è vera.
%if ... %elif ... %else ... %endif selezionano un ramo da un test numerico:
%if WIDTH > 4
mov eax, 2
%elif WIDTH > 2
mov eax, 1
%else
mov eax, 0
%endif
Spesso le macro vengono tenute in un file a parte e richiamate in ogni sorgente che ne ha bisogno con %include:
%include "patterns.mac"
Il file indicato viene inserito letteralmente in quel punto, prima dell'assemblaggio, esattamente come se il suo contenuto fosse stato digitato lì. Questo mantiene le definizioni in un unico posto e permette a diversi file sorgente di condividerle.
Se il file di macro può essere assemblato come standalone, dovrebbe avere un'estensione .asm.
Nota che le macro non hanno linkage.
Anche se il file di macro viene assemblato, le sue macro sono visibili solo a un altro file che lo %include.
Altrimenti, un file usato solo con %include può avere qualsiasi estensione.
L'uso di .asm o .mac è una convenzione comune.
Mantieni la libreria di macro su cui si basa il motore di composizione di una rivista. Il motore stesso, cioè le funzioni che impaginano ogni pagina, è fisso e non sei tu a scriverlo. Quello che scrivi è il meccanismo in fase di assemblaggio su cui è costruito.
Questo esercizio non richiede di scrivere alcuna funzione, solo macro. Queste macro vengono poi incluse e chiamate in funzioni già predefinite in un altro file.
Hai quattro compiti.
Ogni macro viene chiamata con il nome esatto e l'ordine degli argomenti indicati dal suo compito, quindi scrivile esattamente così. Altrimenti il file non si assemblerà.
Il formato della rivista è un insieme di numeri fissi, e il resto dell'impaginazione vi fa riferimento solo per nome. Dovresti dare a ciascuno un nome leggibile in fase di assemblaggio:
COLUMN_COUNT vale 4
COLUMN_STRIDE vale 18
SPACE vale 0x20, il byte vuotoRULE vale 0x3D, il byte =
RULE_WIDTH vale 4
PLAIN vale 0x2E, il byte .
RULED vale 0x2D, il byte -
BOXED vale 0x23, il byte #
Il motore riporta questi valori nell'ordine indicato sopra.
out = {4, 18, 32, 61, 4, 46, 45, 35} // the eight named values, in order
fill_run
Il motore azzera una bozza e traccia le righe della testata imprimendo una sequenza di un singolo byte. Racchiudi questo passaggio in una macro su più righe che il motore possa chiamare ovunque serva riempire una sequenza.
Per prima cosa, definisci un nome leggibile COUNTER per il registro rcx.
Poi definisci la macro fill_run, che deve riempire un array con N copie di un byte.
Accetta tre argomenti in questo ordine:
fill_run rdi, rax, SPACE
fill_run rdx, RULE_WIDTH, RULE
La macro fill_run può sovrascrivere liberamente COUNTER, ma nessun altro registro.
Nota che il numero di byte può essere passato in un registro generico a 64 bit, come rax o rsi, oppure come valore immediato, come 10 o 4.
Puoi considerare che questo numero non è mai negativo.
Questa macro viene chiamata tre volte, in due funzioni diverse.
length = 8
page = {32, 32, 32, 32, 32, 32, 32, 32}
width = 8
page = {61, 61, 61, 61, 32, 32, 32, 32} // '====' then spaces
lay_columns
La griglia è fissa per il formato, quindi gli inizi delle colonne possono essere calcolati in fase di assemblaggio.
Definisci la macro lay_columns, che scrive COLUMN_COUNT offset, ognuno un intero a 64 bit.
Accetta due argomenti in questo ordine:
lay_columns rsi, rax
Il numero di offset da scrivere all'indirizzo di destinazione è fisso e uguale a COLUMN_COUNT.
Ogni offset è un valore a 64 bit calcolato con offset[i] = margin + i * COLUMN_STRIDE, dove i è l'indice dell'offset a partire da 0, e margin è il secondo argomento.
Questa macro può sovrascrivere i suoi due argomenti registro, ma nessun altro. Viene chiamata una volta.
margin = 3
out = {3, 21, 39, 57}
set_header
Ognuna delle tre sezioni fisse ha uno stile di intestazione fissato dal formato, quindi il byte è noto in fase di assemblaggio.
Definisci set_header, che accetta due argomenti in questo ordine:
0 è plain, 1 è ruled e 2 è boxed.L'indirizzo di destinazione è l'espressione di un indirizzo di memoria effettivo.
Prende un registro di base che può essere sommato a un registro indice scalato di 1, 2, 4 o 8, più un valore immediato:
set_header rdx+8*rcx+2, 1
Seleziona il byte di intestazione (PLAIN, RULED o BOXED) con l'assemblaggio condizionale (%if / %elif / %else) invece di un salto a runtime.
Questa macro non deve sovrascrivere alcun registro. Viene chiamata tre volte nella stessa funzione.
pitch = 4
out = {46, 0, 0, 0, 45, 0, 0, 0, 35} // '.', '-', '#'
Iscriviti a Exercism per imparare e padroneggiare x86-64 Assembly con 22 concetti130 esercizi e il mentoring di persone reali, tutto gratis.