Ogni bit di un intero può essere usato per memorizzare un valore binario. Poiché molte situazioni coinvolgono informazioni binarie, come vero o falso, inclusione o esclusione, acceso o spento, la rappresentazione binaria di un intero a N bit fornisce un modo compatto per codificare lo stato binario di N elementi. Questo rende essenziale, in assembly, la capacità di manipolare bit e byte. Il set di istruzioni x86-64 offre un'ampia varietà di istruzioni di manipolazione bit a bit.
Queste istruzioni operano su singoli bit di un operando.
Prendono tutti due operandi: il secondo indica l'indice del bit su cui si opera nel primo operando. Tutti copiano il bit selezionato nel flag di carry (CF).
| Nome | Descrizione |
|---|---|
bt |
copia il bit in CF senza modificare alcun operando |
bts |
copia il bit in CF e lo imposta nell'operando di destinazione |
btr |
copia il bit in CF e lo azzera nell'operando di destinazione |
btc |
copia il bit in CF e lo complementa (lo inverte) nell'operando di destinazione |
Le operazioni bit a bit vengono eseguite su tutti i bit di un operando.
Hanno tutte un'istruzione con lo stesso nome dell'operazione bit a bit eseguita:
| Nome | Descrizione |
|---|---|
and |
1 se entrambi i bit sono 1 |
or |
1 se almeno uno dei bit è 1 |
xor |
1 se i bit sono diversi |
not |
1 se il bit era 0; 0 se il bit era 1 |
La maggior parte di esse prende due operandi, esegue un'operazione bit a bit su entrambi e memorizza il risultato nell'operando di destinazione.
L'eccezione è not, che prende un solo operando di destinazione.
Quando interpretiamo uno e zero rispettivamente come inclusione ed esclusione, un intero viene chiamato maschera di bit (o semplicemente maschera).
Una maschera di bit «filtra» gli elementi perché uno zero nel bit i-esimo esclude l'elemento i-esimo, mentre un uno lo include.
Usiamo comunemente anche una maschera di bit per includere certi bit di un intero ed escluderne altri.
Ad esempio, sia A un intero la cui rappresentazione binaria è:
| indice | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
|---|---|---|---|---|---|---|---|---|
| bit | 1 | 0 | 0 | 1 | 0 | 1 | 0 | 1 |
Sia inoltre M un intero la cui rappresentazione binaria è:
| indice | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
|---|---|---|---|---|---|---|---|---|
| bit | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 1 |
Entrambi sono interi a 8 bit.
In questo caso, possiamo dire che M seleziona i bit 0, 2 e 3 di A ed esclude gli altri.
Le istruzioni bit a bit discusse prima sono utili per manipolare interi con le maschere. Ad esempio:
A che non sono selezionati da M, esegui l'AND bit a bit: A AND M.A selezionati da M, esegui l'OR bit a bit: A OR M.L'istruzione test esegue un AND bit a bit tra entrambi gli operandi e imposta i flag in base al risultato.
Se A è il primo operando e B il secondo:
| flag | impostato quando |
|---|---|
CF |
sempre azzerato |
ZF |
A AND B == 0 |
SF |
il bit di segno di A AND B è impostato |
OF |
sempre azzerato |
Questa istruzione prende due operandi e aggiorna i flag, ma non modifica i suoi operandi.
Queste istruzioni spostano i bit nell'operando di destinazione di un numero di posizioni specificato dal secondo operando.
Il secondo operando deve essere un numero costante (un immediate) o il registro cl (gli 8 bit più bassi di rcx).
| Nome | Descrizione |
|---|---|
shl/sal
|
Sposta i bit a sinistra |
shr/sar
|
Sposta i bit a destra |
Nota che il conteggio nel secondo operando viene mascherato a 5 bit, o 6 bit con un operando di destinazione a 64 bit.
Ogni bit successivo viene di fatto ignorato.
Ciò significa che lo shift massimo è 31, o 63 con un operando a 64 bit.
Sia shl che sal eseguono esattamente la stessa operazione: uno è un alias dell'altro.
Ogni volta che viene eseguito uno shift a sinistra, i bit più vicini alla fine della sequenza rispetto alla lunghezza dello shift vengono prima spostati in CF e poi scartati.
D'altra parte, all'inizio viene aggiunto un numero di nuovi bit azzerati pari alla lunghezza dello shift.
Poiché ogni bit in un intero rappresenta una potenza di 2, uno shift a sinistra di n posizioni ha l'effetto di moltiplicare l'intero per 2ⁿ.
Ci sono due istruzioni per spostare i bit a destra: shr e sar.
Ogni volta che viene usata una delle due istruzioni, i bit più vicini all'inizio della sequenza rispetto alla lunghezza dello shift vengono prima spostati in CF e poi scartati.
D'altra parte, alla fine viene aggiunto un numero di nuovi bit pari alla lunghezza dello shift.
La differenza tra loro è che shr sposta bit 0 all'estremità sinistra, mentre sar sposta 1 se il bit più significativo era impostato e 0 altrimenti.
Ciò significa che sar preserva il segno nello shift di un intero con segno.
Poiché ogni bit in un intero rappresenta una potenza di 2, uno shift a destra di n posizioni usando shr ha l'effetto di eseguire una divisione senza segno per 2ⁿ.
Analogamente, uno shift a destra di n posizioni usando sar ha l'effetto di eseguire una divisione con segno per 2ⁿ.
Queste istruzioni spostano i bit nell'operando di destinazione di un numero di posizioni specificato dal secondo operando.
Il secondo operando deve essere un numero costante (un immediate) o il registro cl (gli 8 bit più bassi di rcx).
La differenza tra una rotazione e uno shift è che una rotazione non scarta né aggiunge bit. I bit che verrebbero scartati da uno shift vengono invece spostati all'estremità opposta. Quindi tutti i bit rimangono, cambiano tutti posto.
| Nome | Descrizione |
|---|---|
rol |
Ruota i bit a sinistra |
ror |
Ruota i bit a destra |
Nota che il conteggio nel secondo operando viene mascherato a 5 bit, o 6 bit con un operando di destinazione a 64 bit.
Ogni bit successivo viene di fatto ignorato.
Ciò significa che la rotazione massima è 31, o 63 con un operando a 64 bit.
Ci sono altre utili istruzioni di manipolazione dei bit:
| Nome | Descrizione |
|---|---|
popcnt |
Conta il numero di bit impostati |
bsr |
Ottiene l'indice del bit impostato più significativo. Se nessun bit è impostato, il risultato non è definito |
bsf |
Ottiene l'indice del bit impostato meno significativo. Se nessun bit è impostato, il risultato non è definito |
Queste istruzioni funzionano tutte con due operandi a 16 bit, 32 bit o 64 bit.
Non possono essere usate con operandi a 8 bit.
Il tuo amico ti ha appena inviato un messaggio con un segreto importante. Per non rendere facile agli altri leggerlo, il messaggio è stato cifrato eseguendo una serie di manipolazioni sui bit. Dovrai scrivere i metodi che aiutano a decifrare il messaggio.
Queste sono le istruzioni sui singoli bit menzionate in questo concetto:
| Nome | Descrizione |
|---|---|
| bt | copia il bit in CF senza modificare alcun operando |
| bts | copia il bit in CF e lo imposta nell'operando di destinazione |
| btr | copia il bit in CF e lo azzera nell'operando di destinazione |
| btc | copia il bit in CF e lo complementa (inverte) nell'operando di destinazione |
Queste sono le istruzioni bit a bit menzionate in questo concetto:
| Nome | Descrizione |
|---|---|
| and | 1 se entrambi i bit sono 1 |
| or | 1 se almeno uno dei bit è 1 |
| xor | 1 se i bit sono diversi |
| not | 1 se il bit era 0; 0 se il bit era 1 |
Queste sono le istruzioni di scorrimento menzionate in questo concetto:
| Nome | Descrizione |
|---|---|
| shl/sal | Sposta i bit a sinistra |
| shr/sar | Sposta i bit a destra |
Queste sono le istruzioni di rotazione menzionate in questo concetto:
| Nome | Descrizione |
|---|---|
| rol | Ruota i bit a sinistra |
| ror | Ruota i bit a destra |
Queste sono le istruzioni varie menzionate in questo concetto:
| Nome | Descrizione |
|---|---|
| popcnt | Conta il numero di bit impostati |
| bsr | Ottiene l'indice del bit impostato più significativo. Se nessun bit è impostato, il risultato è indefinito |
| bsf | Ottiene l'indice del bit impostato meno significativo. Se nessun bit è impostato, il risultato è indefinito |
Il messaggio è codificato in un intero a 16 bit. Tuttavia, tra questi, gli 8 bit più alti non fanno in realtà parte del messaggio, ma sono una maschera che deve essere usata nella decifratura.
Implementa la funzione extract_higher_bits che prende un intero a 16 bit e restituisce i suoi 8 bit più alti.
extract_higher_bits(0b1010010011000101)
// => 0b10100100
Saper estrarre la maschera non basta, devi anche isolare il messaggio.
Implementa la funzione extract_lower_bits che prende un intero a 16 bit e restituisce i suoi 8 bit più bassi.
extract_lower_bits(0b1010010011000101);
// => 0b11000101
Alcuni bit sono impostati sia nel messaggio che nella maschera. Questa è un'informazione molto importante che verrà usata più avanti.
Implementa extract_redundant_bits che prende un intero a 16 bit, che codifica sia il messaggio che una maschera, e restituisce un intero a 8 bit con solo i bit ridondanti impostati.
Un bit nel numero restituito dovrebbe essere impostato a 1 dove è 1 sia nel messaggio che nella maschera.
Tutti gli altri bit dovrebbero essere azzerati.
extract_redundant_bits(0b1010010011000101);
// => 0b10000100
Poi, ci sono alcuni bit che devono essere impostati a 1 nel messaggio, in base alla maschera.
Implementa la funzione set_message_bits che prende un intero a 16 bit, che codifica sia il messaggio che una maschera, e restituisce il risultato dell'impostazione a 1 dei bit nel messaggio.
Un bit del messaggio dovrebbe essere impostato a 1 dove il bit nella maschera è 1.
Tutti gli altri bit dovrebbero essere lasciati invariati, in modo che restino impostati se erano già impostati, e azzerati se erano già azzerati.
set_message_bits(0b1010010011000101);
// => 0b11100101
C'è un pezzo del puzzle non esplicito nel messaggio: il numero a 16 bit 0b1011001100111100.
Questo numero è la tua chiave privata condivisa e dovresti usarla per aiutarti a decifrare il messaggio.
Per farlo, devi prima ruotare i bit della tua chiave privata verso sinistra di un certo numero di posizioni. Il numero di posizioni è uguale al numero di bit ridondanti impostati sia nel messaggio che nella maschera.
Implementa la funzione rotate_private_key che prende un intero a 16 bit, che codifica sia il messaggio che una maschera, e restituisce il risultato della rotazione della tua chiave privata.
Questo risultato è un intero a 16 bit.
rotate_private_key(0b1010010011000101);
// => 0b1100110011110010
NASM (The Netwide Assembler, l'assembler usato da questo track) supporta le costanti in formato binario con il prefisso 0b.
Supporta anche l'uso di un underscore (_) come separatore in una costante, per leggibilità:
PRIVATE_KEY equ 0b1011_0011_0011_1100
Per poter essere usata nella decifratura, la tua chiave privata deve essere formattata per isolare i bit rilevanti.
Per formattare completamente una chiave privata, devi:
Un bit invertito è 1 se era 0 e 0 se era 1.
Implementa la funzione format_private_key che prende un intero a 16 bit, che codifica sia il messaggio che una maschera, e restituisce una chiave privata a 8 bit completamente formattata.
format_private_key(0b1010010011000101);
// => 0b11000001
Una volta che hai il messaggio con tutti i bit rilevanti impostati e la chiave privata formattata, è il momento di unirli per ottenere il messaggio risultante.
Il messaggio risultante è un intero a 16 bit, di cui:
Implementa la funzione decrypt_message che prende un intero a 16 bit che codifica sia il messaggio che una maschera, e restituisce un intero a 16 bit con il messaggio completamente decifrato.
Questa funzione dovrebbe usare la chiave privata formattata che generi con format_private_key e anche il messaggio con tutti i bit rilevanti impostati con set_message_bits.
decrypt_message(0b1010010011000101);
// => 0b1100000111100101
Iscriviti a Exercism per imparare e padroneggiare x86-64 Assembly con 22 concetti130 esercizi e il mentoring di persone reali, tutto gratis.