Track
/
x86-64 Assembly
x86-64 Assembly
/
Esercizi
/
Tavolozza colori
Tavolozza colori

Tavolozza colori

Esercizio di apprendimento

Introduzione

La memoria

La memoria viene di solito mappata per un programma dal sistema operativo (OS) secondo una disposizione generale:

indirizzo regione di memoria
alta stack
...
heap
segmento di lettura e scrittura
segmento di codice/sola lettura
bassa riservata

La memoria a segmenti è organizzata in sezioni, con permessi diversi.

Le funzioni che abbiamo definito finora erano tutte nella sezione .text. Questa sezione contiene dati eseguibili in sola lettura. Altre sezioni servono a dichiarare variabili di dati, che possono essere in sola lettura o di lettura e scrittura, ma non sono eseguibili.

Sezione .data

I dati inizializzati si dichiarano nella sezione .data.

In NASM (The Netwide Assembler, l'assembler usato da questo track), una variabile inizializzata ha un nome, una direttiva che ne indica la dimensione dei dati e un elenco di valori separati da virgola. Ognuno di questi è separato dagli altri da uno spazio, e l'etichetta può facoltativamente essere seguita da un :.

Le direttive principali e le relative dimensioni dei dati sono:

direttiva dimensione
db 1 byte
dw 2 byte
dd 4 byte
dq 8 byte

Ad esempio, questo dichiara una variabile di un byte chiamata space con il valore 10:

section .data
    space db 10

Le variabili dichiarate in section .data sono mutabili, cioè sono di lettura e scrittura. Hanno anche una durata di memorizzazione statica, il che significa che esistono per tutta la durata di esecuzione del programma.

Sezione .rodata

La sezione .rodata è simile a section .data. Entrambe le sezioni contengono dati inizializzati, che si dichiarano allo stesso modo e hanno la stessa durata di memorizzazione.

La differenza principale tra le due è che i dati in section .rodata sono immutabili, cioè in sola lettura.

Note

Le costanti definite con equ sono diverse da quelle definite in section .rodata.

Una costante definita con equ non occupa spazio in memoria e viene sostituita direttamente con il suo valore dall'assembler. È infatti un segnaposto per quel valore.

Al contrario, le costanti definite in section .rodata sono effettivamente memorizzate in memoria, e hanno un indirizzo.

Accesso ai dati

Etichette e indirezione

I dati dichiarati devono avere un nome associato. Questo nome si chiama etichetta.

Un'etichetta è un simbolo che codifica l'indirizzo specifico dei dati in memoria. Gli indirizzi in x86-64 sono valori a 64 bit.

In NASM, provare ad accedere ai dati direttamente con la loro etichetta non restituisce la memoria allocata, ma il suo indirizzo:

section .data
    example dq 27 ; this declares a 8-byte variable initialized with 27

section .text
fn:
    mov rax, example ; this stores the address of the declared variable in rax, not its contents
    ...

Per accedere al contenuto di un indirizzo di memoria, è necessario dereferenziarlo. Questa operazione si chiama indirezione.

In NASM, si fa con []:

section .data
    example dq -27 ; this declares a 8-byte variable initialized with -27

section .text
fn:
    mov rax, [example] ; this dereferences example and access the value stored in memory (-27)
    ...

Tuttavia, in alcune situazioni può esserci ambiguità riguardo alla dimensione della memoria dereferenziata. In quei casi, bisogna usare un prefisso che specifichi questa dimensione.

Questi sono i prefissi più importanti e le loro dimensioni in un tipico programma x86-64:

prefisso dimensione
byte 1 byte
word 2 byte
dword 4 byte
qword 8 byte

Lo stesso caricamento si può scrivere con la dimensione indicata esplicitamente:

    mov rax, qword [example] ; same dereference, size stated explicitly

È buona pratica usare sempre un prefisso quando si dereferenzia la memoria.

Scrittura in memoria

La scrittura in memoria avviene allo stesso modo, dereferenziando un indirizzo:

section .data
    example1 db 10            ; example1 is a 1-byte memory location initialized with value 10
    example2 dq -456          ; example2 is a 8-byte memory location initialized with value -456
    example3 dd 54            ; example3 is a 4-byte memory location initialized with value 54

section .text
fn:
    mov byte [example1], 20   ; example1 now has value 20
    mov qword [example2], rdx ; example2 now has value equal to the contents in rdx
    mov dword [example3], eax ; example3 now has value equal to the contents in eax

Nota che puoi usare operandi in memoria nella maggior parte delle istruzioni senza prima caricarne il contenuto in un registro. Tuttavia, di solito non è possibile usarli sia nell'operando sorgente sia in quello di destinazione, ma solo in uno dei due:

section .data
    example4 dw 4
    example5 dq -8
    example6 dd 15

section .text
fn:
    add word [example4], 5     ; example4 is now a 2-byte memory location with the value 4 + 5 = 9
    imul rax, qword [example5] ; rax = rax * (-8)
    ; this is not possible -> sub dword [example6], dword [example6]
L'istruzione LEA

Sebbene si possa usare una mov per memorizzare l'indirizzo di una variabile in un registro, esiste un'istruzione con questo scopo specifico: lea.

Questa istruzione usa un operando in forma di memoria, ma non legge la memoria. Calcola invece l'espressione dell'indirizzo effettivo e scrive il risultato nell'operando di destinazione:

lea rax, [example] ; this stores the address of 'example' in rax

È più idiomatico usare lea per calcolare e memorizzare indirizzi di memoria nei registri.

Indirizzamento relativo

Quando si accede a locazioni di memoria, il comportamento predefinito in NASM è generare indirizzi assoluti, cioè indirizzi di memoria fissi.

Per motivi di sicurezza, gli eseguibili sono spesso compilati come PIE (Position Independent Executable, eseguibile indipendente dalla posizione), dove le regioni di memoria vengono collocate in posizioni casuali. In un PIE, l'indirizzo finale di una variabile non è noto al momento del collegamento. Il codice calcola invece gli indirizzi come offset dal valore in un registro speciale chiamato rip, che punta alla prossima istruzione da eseguire.

Questo si chiama di solito indirizzamento relativo a RIP.

In NASM puoi richiedere l'accesso relativo a RIP con l'operatore rel:

mov rax, qword [rel variable]

L'indirizzamento relativo può anche diventare l'impostazione predefinita per un file sorgente con default rel all'inizio.

Tutti gli esercizi di questo track sono compilati e collegati come PIE, quindi rel va usato per generare indirizzi relativi.

Visibilità

Le etichette (funzioni e dati) definite in una qualsiasi sezione (ad esempio .text, .data, .rodata) sono visibili all'interno dello stesso file sorgente. Se dichiarate global, sono visibili anche agli altri file sorgente.

Viceversa, le etichette definite in altri file sorgente sono visibili al file sorgente corrente se dichiarate extern. In questo caso, nell'assembly non c'è alcuna indicazione della dimensione dei dati: questa deve essere nota in anticipo.

default rel

section .data

global number1 ; 'number1' is a variable visible to other source files
number1 db 200

extern number2 ; 'number2' is a variable visible to the current source file, but defined in another

section .text

extern sum ; sum is a function visible to the current source file, but defined in another

fn:
    mov dil, byte [number1]
    mov sil, byte [number2]
    call sum
    ...

Istruzioni

Il tuo amico José è un insegnante in una scuola della zona. Gli è venuta un'idea per degli esperimenti divertenti, per mostrare come si possono combinare i colori per ottenerne di diversi.

Ha chiesto il tuo aiuto per quegli esperimenti.

Note

In questo esercizio un colore è rappresentato da un numero a 32 bit (4 byte), che codifica il suo valore RGB.

Un valore RGB è formato da 3 canali, Rosso, Verde e Blu, ognuno dei quali occupa 8 bit (1 byte). Il quarto byte è di solito riservato al canale Alpha, ma in questo esercizio il suo valore sarà vuoto (0).

1. Ottieni il valore RGB di un colore

I valori di ogni colore sono già memorizzati in una tabella, definita in un altro file sorgente. Un colore è identificato da un indirizzo univoco in questa tabella.

Definisci una funzione get_color_value che restituisce il valore a 32 bit di un colore. Questa funzione prende come parametro un indirizzo valido di questo colore nella tabella dei colori.

get_color_value(black)
// => 0

Suggerimento: 32 bit equivalgono a 4 byte.

2. Aggiungi un colore di base

Per mescolare colori diversi, José fisserà prima un colore di base e cambierà solo il colore secondario che viene combinato con esso.

Definisci una funzione add_base_color che salva il valore a 32 bit di un colore nella variabile base_color, così da poterlo usare in seguito. Questa funzione non restituisce alcun valore e prende come parametro l'indirizzo del colore nella tabella dei colori.

La variabile base_color la definisci tu e deve essere accessibile da altri file sorgente.

Non ci sarà mai più di 1 colore di base alla volta. Se si aggiunge un nuovo colore di base, quello vecchio viene scartato.

Per impostazione predefinita, all'avvio del programma, base_color va inizializzata con il valore a 32 bit del bianco, che è 0xFFFFFF00.

Suggerimento: NASM accetta numeri definiti in esadecimale usando 0x all'inizio, come in 0xFFFFFF00.

3. Definisci le costanti per i colori primari

José prevede di fare molte combinazioni usando i colori primari, quindi vuole averli separati per accedervi rapidamente. Dato che usa RGB per rappresentare i colori, i colori primari sono:

  • RED, con il valore 0xFF000000.
  • GREEN, con il valore 0x00FF0000.
  • BLUE, con il valore 0x0000FF00.

Definisci una costante per ciascuno di questi colori. Queste costanti devono essere accessibili da altri file sorgente.

4. Combina i colori

I colori vanno combinati secondo una combining_function definita in un altro file sorgente. Questa funzione prende come parametri i valori a 32 bit di base_color e di un colore secondario da mescolare con esso. Restituisce il valore a 32 bit del colore combinato.

Definisci una funzione make_color_combination che combina due colori e salva il risultato in memoria. Questa funzione non restituisce alcun valore e prende come parametri, in quest'ordine:

  • L'indirizzo in cui va memorizzato il valore a 32 bit del colore combinato.
  • L'indirizzo di un colore secondario nella tabella dei colori, da combinare con il colore primario.
Caution

Tieni presente che combining_function potrebbe modificare i valori nei registri che stai usando. Assicurati di salvare in memoria ogni variabile che ti serve prima di chiamare la funzione.

Modifica tramite GitHub Il link si apre in una nuova finestra o scheda
x86-64 Assembly Exercism

Vuoi iniziare Tavolozza colori?

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