dd

Copy blocks, with control over size and position

Updated 2026-09-11

dd copies bytes from one place to another in fixed-size blocks, and its interest is the control: where in the input to start, where in the output to write, how much, and whether to truncate what was there. cp can do none of that, which is why dd survives despite a syntax that belongs to a different decade.

The syntax is its own: if= and of= rather than positional arguments, no dashes, and no spaces around the equals. Everything unrecognised is a silent no-op, so dd if=disk.img of=/dev/sdb bs 1M runs with the default 512-byte block and finishes an hour later than expected.

The of= argument is the one that destroys things. dd writes wherever it is pointed, to a device as readily as to a file, and gets /dev/sdb and /dev/sdc confused about as often as anyone does. Run lsblk or blkid on the target immediately before, in the same terminal, so what you read is what you type.

Block size is a throughput setting and nothing else: bs=1M moves the same bytes as the default bs=512 in a fraction of the calls. Where bs does change the result is with count, skip and seek, which are all counted in blocks rather than bytes, so count=5 means five of whatever bs says.

By default dd truncates the output file. conv=notrunc is what lets it write into the middle of one, and leaving it out is how a patch of a few bytes turns into a file three bytes long.

Sample files used on this page

Every example below was run against these files. Recreate them to follow along.

two disk images data.img holds a 32M ext4 labelled tipsdata, and blank.img is 16M of zeros. Examples that make a file of their own write it into the page's working directory, which is emptied before the next one runs. The same pair backs mkfs, mount, blkid and fsck.

total 20912
-rw-r--r-- 1 root root 16777216 Jun 15 10:00 blank.img
-rw-r--r-- 1 root root 33554432 Jun 15 10:00 data.img
21 outputs, collapsed by default

Making a file of a given size

bs is the block size and count is how many blocks, so the size is the two multiplied. status=none silences the summary, which is worth having in a script and worth leaving on when you want to see what happened.

Make an 8M file of zeros

dd if=/dev/zero of=disk.img bs=1M count=8

The summary goes to standard error, which is why it appears even when the output is redirected. records in and records out count blocks rather than bytes, and a mismatch between the two means something was short-read.

Show output

Your output will differ: the elapsed time and the transfer rate are this machine's

8+0 records in
8+0 records out
8388608 bytes (8.4 MB, 8.0 MiB) copied, 0.002727 s, 3.1 GB/s

Make the same file without the commentary

dd if=/dev/zero of=disk.img bs=1M count=8 status=none
stat -c "%s bytes" disk.img

status=none drops the summary entirely. The alternative, status=progress, replaces it with a running count that updates in place, which is what you want on a copy that takes minutes.

Show output
8388608 bytes

Use a smaller block size

dd if=/dev/zero of=disk.img bs=512 count=16 status=none
stat -c "%s bytes" disk.img

Sixteen blocks of 512 bytes rather than eight of a megabyte. count is always in blocks, so changing bs and leaving count alone changes the size of the result.

Show output
8192 bytes

Fill a file with random bytes

dd if=/dev/urandom of=r.bin bs=1k count=4 status=none
stat -c "%s bytes" r.bin

/dev/urandom is the source for a file that has to be unguessable rather than merely present. It is slower than /dev/zero by orders of magnitude, which shows up on anything larger than this.

Show output
4096 bytes

Make a large file that occupies nothing

dd if=/dev/zero of=sparse.img bs=1M count=0 seek=64 status=none
echo "apparent: $(stat -c %s sparse.img)"
echo "allocated: $(du -h sparse.img | cut -f1)"

count=0 writes no data and seek=64 moves the offset first, so the file ends 64M in with nothing behind it. du reports what is allocated and stat reports the length, and a sparse file is where the two disagree.

Show output
apparent: 67108864
allocated: 0

Flush to the device before reporting success

dd if=/dev/zero of=disk.img bs=1M count=4 conv=fsync

Without conv=fsync the copy is finished when the kernel has the data, not when the disk does, so the rate dd reports is partly a measure of how much memory was free. Add it before drawing conclusions from the number, and before unplugging anything.

Show output

Your output will differ: the elapsed time and the transfer rate are this machine's

4+0 records in
4+0 records out
4194304 bytes (4.2 MB, 4.0 MiB) copied, 0.00781867 s, 536 MB/s

Position within the file

skip moves the read forward in the input, seek moves the write forward in the output, and both count in blocks of bs. Setting bs=1 makes them byte offsets, at the cost of a system call per byte.

Copy a file whole

printf 'hello world\n' > src.txt
dd if=src.txt of=dst.txt status=none
cat dst.txt

With no bs, count, skip or seek this is a slow cp with a stranger syntax. Everything below is what makes it worth using instead.

Show output
hello world

Take the first few bytes

printf '0123456789' > src.txt
dd if=src.txt of=part.txt bs=1 count=5 status=none
cat part.txt; echo

bs=1 count=5 is five bytes from the start. head -c 5 does the same thing faster, and dd is only the better tool here once skip or seek is involved.

Show output
01234

Start partway into the input

printf '0123456789' > src.txt
dd if=src.txt of=part.txt bs=1 skip=5 status=none
cat part.txt; echo

skip=5 discards the first five bytes and copies the rest. This is how a partition is pulled out of a whole-disk image, with skip set to where the partition starts.

Show output
56789

Take a slice from the middle

printf '0123456789' > src.txt
dd if=src.txt bs=1 skip=3 count=4 status=none
echo

skip and count together, and no of= at all, so the result goes to standard output where a pipe can take it. Four bytes starting at the fourth.

Show output
3456

Take a slice out of a disk image

dd if=/srv/images/data.img of=part.img bs=1M skip=1 count=2 status=none
stat -c "%s bytes" part.img

A two-megabyte slice, starting one megabyte in. The same arithmetic pulls a partition out of a whole-disk image, once fdisk -l has said where that partition starts and how long it is.

Show output
2097152 bytes

Overwrite bytes in place

printf 'AAAA' > f.txt
printf 'BB' | dd of=f.txt bs=1 seek=1 conv=notrunc status=none
echo "result: $(cat f.txt)"

seek=1 starts writing one byte in and conv=notrunc keeps what follows. The input comes from a pipe, so there is no if=.

Show output
result: ABBA

Watch what happens without conv=notrunc

printf 'AAAA' > f.txt
printf 'BB' | dd of=f.txt bs=1 seek=1 status=none
echo "result: $(cat f.txt)"
echo "size: $(stat -c %s f.txt)"

The same command without conv=notrunc: dd truncates the output file first, so the tail is gone and four bytes have become three. There is no warning and no way back.

Show output
result: ABB
size: 3

Reading from a pipe

A read from a pipe returns what is available rather than what was asked for, and dd counts a short read as a whole block. This is the trap that makes a piped dd quietly copy a fraction of what it was told to.

Watch a piped read come up short

head -c 3000000 /dev/zero | dd of=short.img bs=1M count=2 2>&1 >/dev/null | grep records

0+2 means zero full blocks and two partial ones: dd asked for a megabyte twice, got whatever the pipe had ready, and counted each as one of its two blocks. The file it wrote is a fraction of two megabytes and a different fraction on every run.

Show output
0+2 records in
0+2 records out

Make it wait for whole blocks

head -c 3000000 /dev/zero | dd of=full.img bs=1M count=2 iflag=fullblock 2>&1 >/dev/null | grep records
stat -c "%s bytes" full.img

iflag=fullblock keeps reading until the block is full or the input ends, and 2+0 says both blocks were whole. Give it to any dd whose input is a pipe, a socket or a slow device.

Show output
2+0 records in
2+0 records out
2097152 bytes

Pad a short block instead

printf 'abc' | dd of=padded.bin bs=8 count=1 conv=sync status=none
stat -c "%s bytes" padded.bin
od -c padded.bin | head -1

conv=sync pads each short block out to bs with NUL bytes rather than writing it short. That is what a fixed-record format wants and what a file copy never does.

Show output
8 bytes
0000000   a   b   c  \0  \0  \0  \0  \0

Whole devices and images

The job dd is still kept around for. An image of a device is a byte-for-byte copy including the filesystem's own structures, so it restores to a working disk, and so pointing it at the wrong one leaves nothing to recover.

Copy an image byte for byte

dd if=/srv/images/data.img of=copy.img bs=1M status=none
cmp /srv/images/data.img copy.img && echo identical

cmp afterwards rather than trust: a copy that reported success and differs is the case worth catching, and it costs one command. bs=1M is what keeps a copy of any size from taking all day.

Show output
identical

Make an image and put a filesystem on it

dd if=/dev/zero of=disk.img bs=1M count=8 status=none
loop=$(losetup --find --show disk.img)
mkfs.ext4 -q -L made "$loop"
mount "$loop" /mnt/backup
df -h /mnt/backup | tail -1

The whole sequence: dd makes the space, mkfs makes the filesystem, mount attaches it. 6.5M usable out of 8M, because the journal and the inode tables were spent first.

Show output

Your output will differ: the loop device number depends on what else on this machine has one attached

/dev/loop0      6.5M   46K  5.9M   1% /mnt/backup

Keep the holes when copying a sparse image

dd if=/dev/zero of=big.img bs=1M count=8 status=none
dd if=big.img of=copy.img bs=1M conv=sparse status=none
echo "apparent: $(stat -c %s copy.img)"
echo "allocated: $(du -h copy.img | cut -f1)"

conv=sparse skips writing a block that is all zeros, so the copy takes no space at all while reporting the same length. Wrong for a device, where the blocks that go unwritten keep whatever was on them.

Show output
apparent: 8388608
allocated: 0

Read a device without writing anything

dd if=/srv/images/data.img of=/dev/null bs=1M status=none
echo "read ok: $?"

Writing to /dev/null reads the whole device and discards it, which is a way to find out whether every block is readable. A failing disk reports an I/O error part way through rather than at the end.

Show output
read ok: 0

Send the summary somewhere separate

dd if=/dev/zero bs=1M count=4 2>&1 >/dev/null | tail -1

dd writes its data to standard output and its summary to standard error, so the two can be separated. Here the data is discarded and the summary kept, which is the form for timing a read without storing it.

Show output

Your output will differ: the elapsed time and the transfer rate are this machine's

4194304 bytes (4.2 MB, 4.0 MiB) copied, 0.000507625 s, 8.3 GB/s