Track
/
x86-64 Assembly
x86-64 Assembly
/
Esercizi
/
Impaginazione rivista
Impaginazione rivista

Impaginazione rivista

Esercizio di apprendimento

Introduzione

Macro

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.

Macro a riga singola

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.

Note

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))
Caution

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
    ...

Macro multiriga

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.

Etichette all'interno di una macro

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

Ripetizione al momento dell'assemblaggio

È 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.

Assemblaggio condizionale

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

Suddivisione in file

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.

Istruzioni

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.

Caution

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à.

1. Dai un nome alle costanti di formato

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 vuoto
  • RULE 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

2. Scrivi la macro 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:

  1. l'indirizzo di destinazione, passato in un registro generico a 64 bit.
  2. il numero di byte (N) da scrivere, passato in un registro generico a 64 bit o come valore immediato.
  3. il byte da scrivere, passato come valore immediato.
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

3. Scrivi la macro 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:

  1. l'indirizzo di destinazione, passato in un registro generico a 64 bit
  2. il margine sinistro, passato in un registro generico a 64 bit
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}

4. Scrivi la macro 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:

  1. l'indirizzo di destinazione.
  2. un id di stile letterale passato come valore immediato, dove 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} // '.', '-', '#'
Modifica tramite GitHub Il link si apre in una nuova finestra o scheda
x86-64 Assembly Exercism

Vuoi iniziare Impaginazione rivista?

Iscriviti a Exercism per imparare e padroneggiare x86-64 Assembly con 22 concetti130 esercizi e il mentoring di persone reali, tutto gratis.