Salta al contenuto principale

dd: copiare dati a livello di blocchi

Inviato da tuxsa il

Clonare un disco, scrivere un'immagine su una chiavetta, salvare i primi byte di un disco, creare un file da 1 GB in un attimo: sono lavori diversi, ma a basso livello sono tutti la stessa operazione, cioè copiare byte da un posto a un altro. Per farlo, da decenni, si usa dd. È potente e, come mkfs, non fa nessuna domanda prima di sovrascrivere.

Cos'è

dd fa parte di GNU coreutils, quindi è presente in qualunque sistema Linux. Il nome, secondo la tradizione, viene da un vecchio comando dei mainframe IBM (convert and copy) e, per via della sua sintassi insolita e dei danni che può fare, gira anche il soprannome scherzoso di disk destroyer: non usa le solite opzioni con il trattino, ma coppie nome=valore, come if= e of=. Lavora su file, ma anche su dispositivi, perché in Linux un disco è un file speciale in /dev.

Cosa fa

Legge i dati da un'origine (if=, input file) e li scrive su una destinazione (of=, output file), un blocco alla volta. Senza if= legge dallo standard input, senza of= scrive sullo standard output, per cui si può usare in pipeline. Alla fine stampa un riepilogo di quanto ha copiato. Non sa nulla di file system: copia byte, non file.

Come si usa

$ dd if=ORIGINE of=DESTINAZIONE [bs=DIMENSIONE] [count=N] [opzioni]

Negli esempi lavoro su file normali e su immagini di disco: sono innocui e riproducono il comportamento di un dispositivo vero. Dove compare un nome come /dev/sdb è solo un esempio e lo dico esplicitamente. I messaggi qui sotto vengono da dd 9.4 in inglese; con un sistema in italiano le righe di riepilogo potrebbero comparire tradotte.

Esempio 1: creare un file di una dimensione precisa

$ dd if=/dev/zero of=vuoto.img bs=1M count=10
10+0 records in
10+0 records out
10485760 bytes (10 MB, 10 MiB) copied, 0.00630007 s, 1.7 GB/s

/dev/zero produce un flusso infinito di byte a zero. Con bs=1M (dimensione del blocco) e count=10 (numero di blocchi) il file finale è di 10 MiB, come conferma l'ultima riga. Le righe "records in/out" riportano i blocchi letti e scritti: 10+0 significa 10 blocchi interi e 0 parziali. Velocità e tempi, naturalmente, cambiano da macchina a macchina.

Esempio 2: copiare un file e verificarlo

$ dd if=src.bin of=copia.bin
5+1 records in
5+1 records out
3000 bytes (3.0 kB, 2.9 KiB) copied, 4.2404e-05 s, 70.7 MB/s
$ cmp src.bin copia.bin && echo identici
identici

Senza bs la dimensione del blocco è di 512 byte: un file di 3000 byte dà quindi 5 blocchi interi più uno parziale, ed è il significato di 5+1. Con bs=1000 avrei avuto 3+0. cmp confronta i due file byte per byte e non stampa nulla se sono uguali: qui lo uso per verificare la copia.

Esempio 3: saltare byte in lettura o in scrittura (skip e seek)

$ printf 'ABCDEFGHIJ' > t.txt
$ dd if=t.txt of=s.txt bs=1 skip=3 count=4 status=none; cat s.txt
DEFG
$ printf 'ABCDEFGHIJ' > u.txt
$ printf 'xyz' | dd of=u.txt bs=1 seek=2 conv=notrunc status=none
$ cat u.txt
ABxyzFGHIJ

skip salta blocchi in ingresso, seek li salta in uscita: la differenza è fondamentale e facilissima da confondere. Il primo comando legge 4 byte a partire dal quarto; il secondo sovrascrive tre byte al terzo posto. conv=notrunc impedisce a dd di troncare il file di destinazione. Senza, in un file da 10 byte la scrittura di xyz lo avrebbe ridotto a 3 byte: l'ho provato sia senza seek, sia con seek=2 su un file nuovo, dove il risultato erano 5 byte (due byte nulli seguiti da xyz).

Esempio 4: clonare un disco o una partizione, anche compressi

$ sudo dd if=/dev/sdb of=disco-clone.img bs=1M status=progress
$ dd if=d.img bs=1M status=none | gzip -c > d.img.gz
$ gunzip -c d.img.gz | dd of=rip.img bs=1M status=none
$ cmp d.img rip.img && echo ripristino-ok
ripristino-ok

La prima riga è illustrativa (/dev/sdb è un nome d'esempio): crea un'immagine completa di un disco, da fare a disco smontato. Le altre tre le ho provate su un'immagine ext4 da 32 MB creata con mkfs.ext4: la compressione con gzip ha ridotto il file a circa 33 KB (un file system quasi vuoto è quasi tutto zeri) e la decompressione ha prodotto una copia identica. Anche un clone diretto con dd ha dato lo stesso sha256sum dell'originale. Con ssh la stessa pipeline può mandare l'immagine a un altro computer, ma non l'ho provata qui.

Esempio 5: cancellare la firma di un file system o salvare l'MBR

$ dd if=/dev/zero of=clone.img bs=1M count=1 conv=notrunc status=none
$ blkid clone.img
$ dd if=disco.img of=mbr.bak bs=512 count=1 status=none
$ od -An -tx1 -j510 -N2 mbr.bak
 55 aa

Azzerare il primo megabyte cancella la firma del file system e della tabella delle partizioni: dopo, blkid non stampa più nulla (codice 2), come nel mio test. È un modo rapido per "svuotare" un disco prima di riusarlo, ma si ottiene lo stesso con wipefs -a, più mirato. Il secondo gruppo copia il primo settore di un'immagine con tabella MBR: 512 byte, di cui gli ultimi due (55 aa) sono la firma di avvio. Dopo aver azzerato quel settore, fdisk -l non vedeva più la tabella; dopo aver rimesso mbr.bak con dd la partizione disco.img1 è tornata al suo posto. Con bs=446 count=1 si copia solo il codice di avvio, senza la tabella delle partizioni.

Esempio 6: leggere da una pipe (iflag=fullblock)

$ yes | dd of=y.bin bs=1M count=1 iflag=fullblock status=none
$ ls -l y.bin | awk '{print $5}'
1048576
$ yes | dd of=y2.bin bs=1M count=1 2>&1 | head -1
0+1 records in
$ ls -l y2.bin | awk '{print $5}'
8192

Quando dd legge da una pipe, ogni lettura restituisce solo ciò che c'è: qui 8192 byte invece del megabyte richiesto, e il file finale è molto più piccolo del previsto (0+1 indica un blocco solo, parziale). Con iflag=fullblock dd insiste finché il blocco non è pieno. È un classico tranello di count=: se la sorgente è una pipe, aggiungi sempre iflag=fullblock.

Esempio 7: vedere l'avanzamento

$ dd if=/dev/zero of=/dev/null bs=1 count=2500000 status=progress
2500000 bytes (2.5 MB, 2.4 MiB) copied, 0.688775 s, 3.6 MB/s
$ pkill -USR1 -x dd

status=progress mostra una riga che si aggiorna sul posto, con byte copiati, tempo e velocità; su copie veloci compare solo il riepilogo finale. Il segnale USR1 (con pkill, o kill -USR1 PID) chiede a un dd già in esecuzione di stampare lo stato: nel mio test, con una copia lenta, ha risposto con le righe "records in/out" e i byte copiati fino a quel momento. Utile se hai dimenticato status=progress a copia già partita.

Esempio 8: file "sparsi"

$ dd if=/dev/zero of=sp.img bs=1 count=0 seek=1G status=none
$ ls -l sp.img | awk '{print $5}'
1073741824
$ du -h sp.img | cut -f1
0

Con count=0 e seek=1G dd crea un file che sembra da 1 GiB ma non occupa spazio su disco: ls -l mostra la dimensione apparente, du quella reale (0). È l'idea dei file sparsi, utile per dischi di macchine virtuali. Anche conv=sparse evita di scrivere i blocchi di zeri: copiando 64 MiB di zeri, il file risultante era da 64 MiB per ls e da 0 per du. Su alcuni file system questa ottimizzazione può non essere disponibile.

Esempio 9: uso negli script

#!/bin/bash
[ $# -eq 2 ] || { echo "uso: $0 origine destinazione" >&2; exit 1; }
SRC="$1"
DST="$2"
[ -r "$SRC" ] || { echo "non leggibile: $SRC" >&2; exit 1; }
[ -e "$DST" ] && { echo "esiste già: $DST" >&2; exit 2; }
dd if="$SRC" of="$DST" bs=1M conv=fsync status=none || exit 3
cmp "$SRC" "$DST" && echo "copia verificata: $DST"

Lo script clona un file o un dispositivo, si rifiuta di sovrascrivere una destinazione che esiste già (è la protezione che a dd manca) e verifica la copia con cmp. conv=fsync forza la scrittura fisica prima di terminare. Sulle immagini ha dato "copia verificata" (codice 0), "esiste già" (codice 2) al secondo lancio, "non leggibile" per un file inesistente e il messaggio d'uso senza argomenti (codice 1). Per usarlo su un dispositivo reale serve sudo e un file di destinazione nuovo, non un disco intero esistente.

Opzioni utili

  • if=FILE e of=FILE: origine e destinazione (stdin e stdout se assenti).
  • bs=DIMENSIONE: dimensione del blocco (1M, 4k...). Il valore predefinito, 512 byte, è basso: per i dischi si usano blocchi più grandi.
  • count=N: copia solo N blocchi. skip=N: salta N blocchi in ingresso. seek=N: salta N blocchi in uscita.
  • status=none|noxfer|progress: controlla i messaggi (assenti, solo riepilogo, avanzamento continuo).
  • conv=notrunc: non tronca il file di uscita. conv=fsync: forza la scrittura su disco alla fine. conv=sparse: salta gli zeri. conv=ucase, lcase: converte in maiuscolo o minuscolo.
  • conv=sync: riempie con zeri i blocchi parziali (nel mio test un file da 700 byte è diventato da 1024). conv=noerror: continua dopo un errore di lettura.
  • iflag=fullblock: legge blocchi interi anche da una pipe.

Attenzione a

  • of= è la parte pericolosa: se sbagli il dispositivo di destinazione lo sovrascrivi senza avvisi. Prima di premere Invio controlla il nome con lsblk; con le lettere dei dischi, un errore (sda al posto di sdb) è irreparabile.
  • Origine e destinazione invertite: è l'errore più frequente. Rileggi sempre il comando da sinistra a destra: if è ciò che leggi, of ciò che scrivi.
  • Dispositivi smontati: clonare o scrivere su un dispositivo montato dà copie incoerenti. Smonta con umount prima.
  • Disco di destinazione più piccolo: la copia si ferma con un errore, lasciando un'immagine tronca. Controlla le dimensioni con lsblk prima di cominciare.
  • conv=notrunc dimenticato: quando scrivi dentro un file già esistente, senza questa opzione il file viene troncato.
  • Pipe e count: con sorgenti diverse da un file usa iflag=fullblock, altrimenti ottieni copie più corte del previsto (esempio 6).
  • Dischi con errori: conv=noerror,sync permette di proseguire oltre i settori illeggibili e di riempirli di zeri, mantenendo gli offset. L'ho descritto ma non provato, perché il sandbox non ha settori difettosi. Per recuperare dati da dischi danneggiati esistono strumenti più adatti, come ddrescue.
  • Tempo e blocchi piccoli: con bs molto piccolo la copia di un disco grande diventa lentissima. Per i dischi usa blocchi da 1M o più.
  • Copie "sparse" e verifica: controlla sempre con cmp o sha256sum che la copia sia identica, soprattutto prima di cancellare l'originale.

Riepilogo

ComandoCosa fa
dd if=A of=BCopia A su B, blocco per blocco
dd if=/dev/zero of=F bs=1M count=10Crea un file da 10 MiB di zeri
dd if=A of=B bs=1M status=progressCopia mostrando l'avanzamento
dd if=A bs=1 skip=3 count=4Legge 4 byte a partire dal quarto
dd of=F bs=1 seek=2 conv=notruncScrive dentro un file senza troncarlo
dd if=DISCO bs=512 count=1 of=mbr.bakSalva il primo settore (MBR)
dd if=/dev/zero of=DEV bs=1M count=1Azzera il primo megabyte
cmd | dd of=F iflag=fullblockScrive l'output di una pipe in blocchi interi
pkill -USR1 -x ddChiede a dd di stampare lo stato