Track
/
x86-64 Assembly
x86-64 Assembly
/
Esercizi
/
Gestione dell'inventario
Gestione dell'inventario

Gestione dell'inventario

Esercizio di apprendimento

Introduzione

Numeri interi

Notazione binaria

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.

Numeri senza segno

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.

Numeri con segno

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.

Caution

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.

Valori immediati

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

Somma

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.

Sottrazione

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.

Moltiplicazione

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
Moltiplicazione a un operando

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.

Caution

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.

Moltiplicazione a due operandi

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
Moltiplicazione a tre operandi

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.

Gestire l'overflow

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

Divisione

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.

Caution

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.

Istruzioni

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.

Note

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
Note

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.

1. Calcola il peso di ogni scatola

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:

  • Il numero di articoli del primo prodotto nella scatola
  • Il peso di ogni articolo del primo prodotto, in g
  • Il numero di articoli del secondo prodotto nella scatola
  • Il peso di ogni articolo del secondo prodotto, in 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.

2. Calcola quante scatole entrano nel camion

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.

3. Verifica che tutti i prodotti siano stati contabilizzati

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:

  • Il numero di articoli non ancora contabilizzati per un prodotto
  • Il numero di articoli del prodotto contenuti in una scatola

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.

4. Calcola il pagamento

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:

  • Quanto hai ricevuto in anticipo, come intero non negativo a 64 bit
  • Il numero totale di scatole spostate, come intero non negativo a 32 bit
  • Il numero di viaggi in camion effettuati, come intero non negativo a 32 bit
  • Il numero di articoli rotti o mancanti, come intero non negativo a 32 bit
  • Il valore di ogni articolo perso, come intero non negativo a 64 bit
  • Il numero di lavoratori con cui dividere il compenso o il debito, come intero positivo a 8 bit

Esempio:

calculate_payment(2000, 1000, 5, 21, 2, 1);
// => 2029

Il valore restituito è un intero a 64 bit.

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

Vuoi iniziare Gestione dell'inventario?

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