Un numero intero è un'astrazione che rappresenta valori interi, come 4, -2, 0 o 64532.
Per rappresentare un numero intero come sequenza di byte, si usa la notazione binaria. In questa notazione, ogni bit della sequenza rappresenta una diversa potenza di due, con il valore che aumenta man mano che l'indice del bit cresce da destra verso sinistra.
Se il numero può assumere solo valori non negativi, viene chiamato numero senza segno.
I numeri senza segno sono rappresentati direttamente come la somma delle potenze di due corrispondenti a tutti i bit impostati nella loro sequenza.
L'intervallo di numeri interi non negativi rappresentabili in un registro va da 0 (nessun bit impostato) a 2⁶⁴ - 1 (somma di tutti i 64 bit impostati).
Ampliare un numero senza segno a una dimensione maggiore si fa riempiendo tutti i bit superiori con 0, in modo che nessun nuovo bit contribuisca al valore.
Questa operazione si chiama estensione a zero.
L'istruzione movzx (z di zero) estende a zero un operando sorgente a 8 o 16 bit verso un operando di destinazione più grande.
Un operando sorgente a 32 bit viene sempre esteso a zero su tutti i 64 bit dell'operando di destinazione con un semplice mov.
Se un numero intero può assumere valori positivi o negativi, viene chiamato numero con segno.
Per rappresentare i numeri negativi, x86-64 usa la rappresentazione in complemento a due.
Nel complemento a due, anche i numeri con segno sono rappresentati come la somma delle potenze di due corrispondenti ai bit impostati. Tuttavia, se il bit più alto è impostato, viene sottratto invece che sommato agli altri.
Poiché questo bit corrisponde a un valore più alto della somma di tutti gli altri, in pratica questo significa che un numero con questo bit impostato è sempre negativo. Questo bit speciale si chiama bit di segno.
Ampliare un numero con segno a una dimensione maggiore significa riempire ogni nuovo bit superiore con una copia del bit di segno, in modo che il valore venga preservato. Questa operazione si chiama estensione del segno.
L'istruzione movsx (s di segno) estende il segno di un operando sorgente a 8 o 16 bit verso un operando di destinazione più grande.
Una variante di movsx chiamata movsxd fa lo stesso da un operando sorgente a 32 bit verso un operando di destinazione a 64 bit.
L'istruzione neg può essere usata per cambiare il segno di un numero.
In assembly, non c'è modo di capire se una sequenza di byte rappresenta un numero con segno o senza segno. Sta al programmatore dare un significato a quei byte.
L'uso dei commenti può essere di grande aiuto in questo compito.
In un concetto precedente, è stato detto che un numero costante, come 4 o -15, può essere usato come operando sorgente per molte istruzioni.
Questi numeri sono chiamati valori immediati.
Un valore immediato non è contenuto in un registro né in memoria: è codificato all'interno dell'istruzione stessa. Nella maggior parte delle istruzioni, lo spazio riservato a esso è largo solo 32 bit, indipendentemente da quanto è grande l'operando di destinazione.
Quando l'operando di destinazione è largo 64 bit, quei 32 bit vengono estesi nel segno per riempirlo. La metà superiore dell'operando viene riempita interamente con copie del bit più alto del valore immediato, quindi solo un numero nell'intervallo di un intero con segno a 32 bit può essere scritto in questo modo:
add rax, -1 ; the immediate is sign-extended, so all 64 bits of rax are affected
add rax, 2147483647 ; the largest immediate an instruction like this accepts
Un numero al di fuori di quell'intervallo non può essere usato come valore immediato.
L'eccezione a questa regola è mov, che può accettare un valore immediato completo a 64 bit quando l'operando di destinazione è un registro.
Se serve un valore immediato a 64 bit, usa prima mov per caricarlo in un registro, poi usa quel registro:
mov rax, 3435973837 ; this works, mov can take a 64-bit immediate
mov rdx, 18446744073709551615 ; the largest immediate mov accepts
sub rdx, rax
Nota che un valore immediato negativo e il numero senza segno con la stessa rappresentazione in bit sono equivalenti e vengono assemblati esattamente allo stesso valore:
mov rax, -1 ; rax = 18446744073709551615
mov rax, 18446744073709551615 ; rax = -1
L'addizione di due numeri può essere calcolata usando l'istruzione add.
C'è anche un'istruzione inc a un operando, che aggiunge 1 al valore nel suo operando:
inc rax ; rax = rax + 1
La somma di due numeri interi funziona allo stesso modo sia per i numeri senza segno sia per quelli con segno.
La sottrazione di due numeri interi si esegue con l'istruzione sub.
C'è anche un'istruzione dec a un operando, che sottrae 1 dal valore nel suo operando:
dec rax ; rax = rax - 1
Anche la sottrazione di due numeri interi funziona allo stesso modo sia per i numeri senza segno sia per quelli con segno.
In x86-64 ci sono due istruzioni diverse per eseguire la moltiplicazione tra due numeri.
Di norma, la moltiplicazione senza segno usa l'istruzione mul, mentre la moltiplicazione con segno usa imul.
L'istruzione mul assume la seguente forma a un operando, dove src è l'operando sorgente:
mul src
L'istruzione imul può assumere una forma a un operando, a due operandi o a tre operandi:
imul src
imul dest, src
imul dest, src1, src2
Due registri vengono usati implicitamente per eseguire una moltiplicazione nella forma a un operando: rax e rdx.
Se la moltiplicazione coinvolge due numeri a 64 bit, i 64 bit inferiori del risultato saranno in rax e i 64 bit superiori in rdx.
Di solito questo si indica con rdx:rax, per indicare che entrambi i registri vengono usati insieme:
mul rcx ; rax = lower 64 bits of rax * rcx
; rdx = upper 64 bits of rax * rcx
Lo stesso vale per altre dimensioni degli operandi.
Quindi, per esempio, se si moltiplicano due numeri a 32 bit, verranno usati eax e edx.
L'eccezione è la moltiplicazione tra due byte.
In questo caso, invece di dl:al, verrà usato ax.
La parte inferiore di ax (al) conterrà gli 8 bit inferiori del prodotto, mentre la parte superiore (ah) conterrà gli 8 bit superiori.
I registri usati implicitamente in una moltiplicazione, come rax e rdx, vengono sempre sovrascritti.
I valori in quei registri dovrebbero essere salvati prima dell'operazione se serviranno in seguito.
La forma a due operandi di imul ha un operando di destinazione esplicito e segue la sintassi usuale.
rdx non viene usato.
Invece, il risultato viene troncato per rientrare nell'operando di destinazione.
imul r8, r9 ; r8 = lower 64 bits of r8 * r9
La forma a tre operandi di imul ha due operandi sorgente, il secondo dei quali è sempre un valore immediato (un numero costante).
Entrambi gli operandi sorgente vengono moltiplicati e il risultato viene troncato e inserito nell'operando di destinazione:
imul r8, r9, 100 ; r8 = lower 64 bits of r9 * 100
Nota che l'operando di destinazione non viene usato nella moltiplicazione. Riceve solo il risultato.
Sia la moltiplicazione a due operandi sia quella a tre operandi troncano il risultato per farlo rientrare nella dimensione dell'operando di destinazione.
Una moltiplicazione a un operando conserva l'intero intervallo, ma di solito viene suddivisa in due registri, rdx e rax.
Quindi, a volte è utile ampliare gli operandi prima della moltiplicazione per fare spazio all'intero prodotto in un unico registro. Un operando senza segno viene esteso a zero, mentre uno con segno viene esteso nel segno:
movzx eax, di ; di and si hold unsigned 16-bit numbers
movzx ecx, si
mul ecx ; the 32-bit product fits in eax, and edx is cleared
Come nel caso della moltiplicazione, ci sono anche due istruzioni per eseguire la divisione tra due numeri.
La divisione senza segno usa l'istruzione div, mentre la divisione con segno usa idiv.
Entrambe le istruzioni funzionano con un solo operando:
div src
idiv src
La divisione a 16 bit, a 32 bit e a 64 bit usa rispettivamente dx:ax, edx:eax e rdx:rax come dividendo.
In questi casi, entrambi i registri agiscono insieme per creare un valore a 2N bit, dove N è la dimensione dell'operazione (16 bit, 32 bit o 64 bit).
Questo valore viene poi diviso per l'operando sorgente.
Il quoziente viene scritto in ax, eax o rax e il resto viene scritto in dx, edx o rdx, a seconda della dimensione dell'operazione.
La divisione tra byte è speciale: invece di usare dl:al, si usa ax.
Gli 8 bit inferiori di ax (al) conterranno il quoziente dell'operazione e gli 8 bit superiori (ah) conterranno il resto.
Nota che tutti i bit del dividendo dovrebbero essere impostati in modo appropriato prima della divisione.
Qualsiasi bit impostato in rdx (o in ah per la divisione a 8 bit) contribuisce al valore che viene diviso.
Nella divisione senza segno, quando il valore da dividere rientra nella metà inferiore, la metà superiore dovrebbe essere azzerata.
Va bene qualsiasi istruzione che azzeri quei bit.
Ad esempio, mov edx, 0 azzera i bit superiori nella divisione a 32 bit.
Nella divisione con segno, invece, il valore dovrebbe essere esteso nel segno.
Ci sono istruzioni che automatizzano questo processo: cbw, cwd, cdq e cqo.
La prima imposta i bit in ah in base al segno di al.
Le altre eseguono l'estensione del segno rispettivamente da ax a dx, da eax a edx e da rax a rdx.
I registri usati implicitamente in una divisione, come rax e rdx, vengono sempre sovrascritti.
I valori in quei registri dovrebbero essere salvati prima della divisione se serviranno in seguito.
Un negozio locale sta trasferendo il suo inventario in un magazzino più grande. Sei stato assunto per imballare e spostare tutto.
Avrai quattro attività, tutte legate alla gestione del trasporto.
Queste sono le istruzioni menzionate in questo concetto:
| Istruzione | Descrizione |
|---|---|
| add a, b | a = a + b |
| inc a | a = a + 1 |
| sub a, b | a = a - b |
| dec a | a = a - 1 |
| imul a | rdx:rax = a * rax (signed) |
| imul a, b | a = a * b (signed, truncated) |
| imul a, b, c | a = b * c (signed, truncated) |
| mul a | rdx:rax = a * rax (unsigned) |
| div a | rax = quotient, rdx = remainder of rdx:rax / a (unsigned) |
| idiv a | rax = quotient, rdx = remainder of rdx:rax / a (signed) |
| movzx a, b | a = b, adding 0 to the extra bits |
| movsx a, b | a = b, adding 1 to the extra bits if b < 0 or 0 otherwise |
Ricorda che puoi accedere allo stesso registro con dimensioni diverse cambiando il nome dell'operando.
Ad esempio: rax (64-bit), eax (32-bit), ax (16-bit), al (8-bit).
Puoi fare riferimento al concetto precedente per la tabella completa.
Gli articoli vengono imballati in scatole che devono essere etichettate con il loro peso. Non c'è una bilancia nei dintorni, ma per fortuna sai quanto pesa in media ogni articolo.
Per organizzare meglio le cose, una scatola contiene solo articoli di due prodotti diversi.
Definisci una funzione get_box_weight che restituisce il peso totale di una scatola, in g.
Questa funzione accetta come parametri, in questo ordine:
g
g
Tieni presente che una scatola vuota pesa 500 g.
Una costante WEIGHT_OF_EMPTY_BOX è definita all'inizio del file della soluzione.
Esempio:
get_box_weight(30, 40, 50, 20);
// => 2700
Tutti gli argomenti sono interi non negativi a 16 bit, e il valore restituito è un intero non negativo a 32 bit.
Le scatole vengono impilate e trasportate al nuovo magazzino con un camion. Tuttavia, lo spazio verticale nel camion è limitato.
Definisci una funzione max_number_of_boxes che restituisce quante scatole di una certa altezza possono essere impilate verticalmente (una sopra l'altra) nel camion.
Questa funzione accetta come parametro l'altezza della scatola, in cm.
Tieni presente che l'altezza interna del camion è 300 cm.
Una costante TRUCK_HEIGHT è definita all'inizio del file della soluzione.
Esempio:
max_number_of_boxes(30);
// => 10
L'argomento e il valore restituito sono interi non negativi a 8 bit.
L'altezza della scatola è sempre almeno 2, quindi il risultato rientra in 8 bit.
Nel nuovo magazzino c'è una lista di controllo con il numero di articoli non ancora contabilizzati per ogni prodotto. Per ogni nuova scatola spostata lì, devi calcolare il nuovo valore nella lista di controllo per ogni prodotto presente nella scatola.
Definisci una funzione items_to_be_moved che restituisce quanti articoli restano da spostare nel nuovo magazzino per un dato prodotto.
Questa funzione accetta come parametri, in questo ordine:
Esempio:
items_to_be_moved(76532, 120);
// => 76412
Gli argomenti sono interi non negativi a 32 bit. Il valore restituito è un intero a 32 bit. In caso di errore nel processo, è possibile che il risultato sia un numero negativo.
Il tuo compenso dipende da quante scatole sono state spostate e da quanti viaggi in camion sono stati necessari.
Per ogni scatola riceverai 5 dollari e per ogni viaggio riceverai 220 dollari.
Le costanti PAY_PER_BOX e PAY_PER_TRUCK_TRIP sono definite all'inizio del file della soluzione.
Tieni presente che potresti aver ricevuto in anticipo una parte di questo compenso per coprire i costi iniziali, e questo anticipo va sottratto dal compenso finale. Inoltre, alcuni prodotti non sono coperti da assicurazione e il tuo compenso sarà ridotto anche del valore degli articoli rotti o mancanti. Se non stai attento, potresti persino ritrovarti a dover dei soldi!
Questo significa che l'importo netto che ti spetta, o che devi, è:
net = boxes * PAY_PER_BOX + trips * PAY_PER_TRUCK_TRIP - up_front - broken_items * item_value
Questo compenso, o debito, sarà diviso in parti uguali tra te e un certo numero di lavoratori che hai assunto.
Il denaro rimanente, o il debito rimanente, è tuo.
Ad esempio, se l'importo netto è 100 da dividere tra 6 persone (tu e 5 lavoratori), tu ricevi 20 (100/(5 + 1) = 16 più i restanti 4).
Definisci una funzione calculate_payment che restituisce quanto devi essere pagato, o quanto devi pagare, alla fine.
Questa funzione accetta come parametri, in questo ordine:
Esempio:
calculate_payment(2000, 1000, 5, 21, 2, 1);
// => 2029
Il valore restituito è un intero a 64 bit.
Iscriviti a Exercism per imparare e padroneggiare x86-64 Assembly con 22 concetti130 esercizi e il mentoring di persone reali, tutto gratis.