4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Raspberry Pi4 ddコマンド以外でイメージ書き込み

Last updated at Posted at 2022-03-20

前文

きっかけは公式フォーラムの以下のトピ。
dd vs cp for flashing sd cards on the Linux command line. - Raspberry Pi Forums

Linux環境でイメージファイルの書き込みといえばddコマンドが定番だけど、馴染みが薄いしスイッチ類が分かりにくいしで使い勝手が悪い。cpコマンドでも代用できるので、そっちの方が良くね? という議論が交わされている。
実はcpコマンドの方が(僅かではあるが)速い、というのも興味深い。
そんな訳でddコマンド以外のイメージファイルの書き込み(操作)方法を考察検証してみた。

動作確認環境

Pi4B 4G、64 bit Raspberry Pi OS with desktop(bullseye)、USB-SSDブート。
イメージファイル書込み先は/dev/sdb。USB接続 microSDカードリーダ/ライタ。
microSDカードは Transcend の 32G。

検証に使用したイメージファイルは以下(LibreELEC-RPi4.arm-10.0.2.img.gz)、又はそれを解凍したファイル(LibreELEC-RPi4.arm-10.0.2.img)。
Raspberry Pi4用LibreELEC 10.0.2

イメージファイル書込み

まずddコマンドなら以下。

$ sudo dd if=./LibreELEC-RPi4.arm-10.0.2.img of=/dev/sdb bs=1M

次はcpコマンドで。確かに直観としてこちらの方が分かりやすい。

$ sudo cp ./LibreELEC-RPi4.arm-10.0.2.img /dev/sdb

冒頭で挙げたトピでは、cpコマンドでのイメージ書き込み例に対し、cpコマンドとは通常のファイルコピーで使用する物、イメージ書き込みなんて出来ないですよ、と脊髄反射している人が居た。
気持ちは分かるけど、これはコピー先がデバイスファイルという特殊ファイルの成せる技。
デバイスファイルは連動するストレージ、今回のケースなら/dev/sdbは microSDカードのイメージにダイレクトに紐づいている。ddコマンドを使用しなくても、このデバイスファイルを利用すれば、ストレージに対しハードウェアに近い低レベルな直接操作ができる、というのが基本的な考え方。
catコマンドなら以下。

$ cat ./LibreELEC-RPi4.arm-10.0.2.img | sudo tee /dev/sdb > /dev/null

うーん、一般ユーザのsudoなので微妙に複雑。sudo cat ~としても、リダイレクト先の/dev/sdb/にはsudoが作用しないので、sudo teeで橋渡ししている。しかしteeはファイルへの出力だけではなく標準出力(画面出力)も行ってしまい邪魔なのでそれは> /dev/nullで捨てている、という内容。回りくどいなぁ。
rootユーザなら単純に以下で良い。

# cat ./LibreELEC-RPi4.arm-10.0.2.img > /dev/sdb

圧縮ファイルをそのまま使用したい場合は以下。

$ gunzip -c ./LibreELEC-RPi4.arm-10.0.2.img.gz | sudo tee /dev/sdb > /dev/null

gunzip -cは解凍結果を標準出力に吐くのでそれをパイプで繋げて書き込み。
てか、ならzcatでも良くね、ということで以下でも同じ。

$ zcat ./LibreELEC-RPi4.arm-10.0.2.img.gz | sudo tee /dev/sdb > /dev/null

こちらも前述同様、rootユーザならストレートに以下。

# gunzip -c ./LibreELEC-RPi4.arm-10.0.2.img.gz > /dev/sdb
# zcat ./LibreELEC-RPi4.arm-10.0.2.img.gz > /dev/sdb

ベリファイ

イメージの書き込みでbalenaEtcherを使用している方は多いと思うけど、新品または新品に近い状態の microSDカードでもイメージ書き込み後のベリファイでエラーを吐くことがある。

(自分もそうだけど)英語で書かれた画面なんてよく分からないしでろくに確認もせずに閉じてしまい、見落としている人が多い予感。
因みにベリファイでエラーが報告されたら即ダメって話でもなくて、再度書き込めば次は正常終了だったりするので OK。つまりそれだけ不安定、信頼できないメディアってこと。

前項の、要領よくやればこんなに簡単なコマンドでイメージファイルの書き込みができますよ、で完結する話ではなく。本当に正しく書き込まれたのかこれ、という不安が必ずつきまとう。
という訳で次はベリファイを行うコマンドの考察。

diffコマンドはバイナリも比較できるので以下。しかし上手くいかない。

$ sudo diff --brief --report-identical-files ./LibreELEC-RPi4.arm-10.0.2.img /dev/sdb
ファイル ./LibreELEC-RPi4.arm-10.0.2.img と /dev/sdb は異なります

これはイメージファイルと microSDカード全体と比較してしまうから。そこでcmpコマンドで以下。

$ sudo cmp -b ./LibreELEC-RPi4.arm-10.0.2.img /dev/sdb
cmp: EOF on ./LibreELEC-RPi4.arm-10.0.2.img after byte 575668224, in line 541104

-bスイッチを指定しているので差異があればその位置と双方の値が表示される。それが表示されず且つcmp: EOF on ~でファイルの終端に達したことを示しているということは、イメージファイルの範囲内で相違点はなかったということ。
因みに異なる箇所がある場合の出力例は以下。

- /dev/sdb 異なります: バイト 4458241、行 655   0 ^@ 345 M-e

圧縮ファイルでの比較は以下。

$ gunzip -c ./LibreELEC-RPi4.arm-10.0.2.img.gz | sudo cmp -b - /dev/sdb
$ sudo zcmp -b ./LibreELEC-RPi4.arm-10.0.2.img.gz /dev/sdb

書き戻し(バックアップ)

ついでにバックアップコマンドの考察。
冒頭のイメージファイルから microSDカードへの書き込みの逆なので以下。以降出力ファイル名は<デバイスファイル名>.imgとする。

$ sudo cp /dev/sdb ./sdb.img
$ sudo cat /dev/sdb > ./sdb.img

但しこの場合、microSDカード全体のバックアップとなる。従って、前述のdiffコマンドでの比較はこのバックアップファイルの場合なら同一と判定される。

$ sudo diff --brief --report-identical-files ./sdb.img /dev/sdb
ファイル ./sdb.img と /dev/sdb は同一です

全体バックアップならこれで良いけど、バックアップ時の容量節約を考慮しパーティションサイズを敢えて縮小している場合はそのサイズでファイルに書き出したい。
今回、LibreELECイメージファイルを書き込んだ直後のsdbのパーティション構成は以下。

$ sudo fdisk -l /dev/sdb
Disk /dev/sdb: 28.33 GiB, 30416044032 bytes, 59406336 sectors
Disk model: STORAGE DEVICE
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x8d4aa938

Device     Boot   Start     End Sectors  Size Id Type
/dev/sdb1  *       8192 1056767 1048576  512M  c W95 FAT32 (LBA)
/dev/sdb2       1056768 1122303   65536   32M 83 Linux

GPartedで確認した画面が以下。少なッ。

簡単な図に起こすと以下。
パーティション構成_2.png
4M + 512M + 32M = 548M しか使用していない。極端な例だけど、この範囲で取得してみる。

上記の通り、sdbの最終セクタは 1122303 なので、先頭から1122304セクタコピーすれば良いことになる。
しかし手動でサイズを求めバックアップ時のパラメータとして設定するのは間違いの元なのでまずコマンドで求める方法を確定する。上記の通り、欲しい値はsudo fdisk -l /dev/sdbの出力結果の最後の行の 3列目で更にそれを +1 した値。
更にデバイス名sdbも変数化すると以下。

$ src='sdb'
$ sudo fdisk /dev/$src -l | tail -n 1 | awk '{$3=($3+1);print $3}'
1122304

期待した結果が得られた。これをddコマンドにパラメータとして組み込むと以下。

$ src='sdb'
$ sudo dd if=/dev/$src of=./$src.img bs=512 count=`sudo fdisk /dev/$src -l | tail -n 1 | awk '{$3=$3+1;print $3}'`
1122304+0 レコード入力
1122304+0 レコード出力
574619648 bytes (575 MB, 548 MiB) copied, 12.8095 s, 44.9 MB/s

一応ベリファイ。大丈夫そう。

$ sudo cmp -b ./$src.img /dev/$src
cmp: EOF on ./sdb.img after byte 574619648, in line 541104

しかし今回のテーマはddコマンド以外で行う、なので以下。

$ src='sdb'
$ sudo head -c `sudo fdisk /dev/$src -l | tail -n 1 | awk '{$3=($3+1)*512;print $3}'` /dev/$src > ./$src.img

headコマンドは-cスイッチで取得したい先頭バイト数を指定できる。つまりここでは単位がバイト数なので*512としている。(1セクタ = 512バイト)
そしてベリファイ。先ほどと同じ結果。

$ sudo cmp -b ./$src.img /dev/$src
cmp: EOF on ./sdb.img after byte 574619648, in line 541104

書き戻し(バックアップ)其ノ二

パーティション単位でのバックアップなら以下。

$ sudo cp /dev/sdb1 ./sdb1.img
$ sudo cp /dev/sdb2 ./sdb2.img

これならサイズを意識する必要がない。しかし MBR を含む先頭の領域となると、こういった方法では対応できない。サイズ指定が必要。うーむ。サイズを意識しないで抽象的にバックアップできることが究極の理想だけど無理っぽい。

この先頭の領域はsdb0.imgとし、パーティション単位で取得する方向でまとめるなら以下。

$ src='sdb'
$ sudo head -c `sudo fdisk -l /dev/$src | grep "${src}1" | awk '{print $3*512}'` /dev/$src > ./$src0.img
$ sudo cp /dev/${src}1 ./$src1.img
$ sudo cp /dev/${src}2 ./$src2.img

ベリファイは以下。sdb0.img、sdb1.img、sdb2.img をcatで連結し/dev/sdbと比較。

$ src='sdb'
$ sudo cat ./${src}0.img ./${src}1.img ./${src}2.img | sudo cmp -b /dev/$src -
cmp: EOF on - after byte 574619648, in line 541104

コマンドまとめ

イメージファイル書込みはベリファイを含めて以下。

$ src='./LibreELEC-RPi4.arm-10.0.2.img.gz'
$ dst='sdb'
$ zcat $src | sudo tee /dev/$dst > /dev/null
$ sudo zcmp -b $src /dev/$dst
cmp: EOF on - after byte 575668224, in line 541104

バックアップは以下。

$ src='sdb'
$ sudo head -c `sudo fdisk /dev/$src -l | tail -n 1 | awk '{$3=($3+1)*512;print $3}'` /dev/$src > ./$src.img
$ sudo cmp -b ./$src.img /dev/$src
cmp: EOF on ./sdb.img after byte 574619648, in line 541104

最終確認

最初のイメージファイルと最終的に microSDカードから吸い出したイメージファイルを比較してみる。結果は以下。

$ src='sdb'
$ zcmp -b ./LibreELEC-RPi4.arm-10.0.2.img.gz ./$src.img
cmp: EOF on ./sdb.img after byte 574619648, in line 541104

sdb.imgはパーティションサイズできっちり切り出しているので最初のイメージファイルより微妙にサイズが小さい。
一応、最初のイメージファイルの 574619648(22400000h) 以降を確認すると以下。

$ zcat ./LibreELEC-RPi4.arm-10.0.2.img.gz | od -j 574619648 -Ax -tx1z -
22400000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  >................<
*
22500000

00で埋まっているだけの余白なので問題ないでしょう。sdb上も以下の内容。(100000h = 1048576)

$ sudo od -j 574619648 -Ax -tx1z -N 1048576 /dev/sdb
22400000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  >................<
*
22500000

注意点

実はベリファイで少々躓いた。結果が異なる場合があり当初は原因が分からず悩まされた。全体的には同じだが、一部(1バイト)だけ異なるようなケース。
原因はマウント状態やdirty bitだった。

まずdirty bit。これはストレージがmount(マウント)されると設定され、umount(アンマウント)されるとクリアされるという仕様らしい。
もし新規でmountしたストレージのdirty bitが既に設定済だった場合は、前回明示的にumountされずに取り外されたか(リムーバブルストレージの場合)、電源が落ちたか、といった正規の手続きを踏んでいなかったことを示す。
その場合、本来ならストレージに書き込まれる筈だったメモリ上のデータがフラッシュされていない(書き込まれていない)、不整合が発生している可能性がある、ということになる。という仕様のもの。

問題はこのdirty bitが microSDカードに設定されている場合、イメージファイルを書き込んでもリセットされず残るという。
結果ベリファイをかけるとdirty bitで差異が発生してしまう。
異なる値が、イメージファイル上では0、microSDカード上では1だった場合はまずこれが原因。
その場合はfsckコマンドで状態を確認。nスイッチは検査だけで修復は行わない、vは詳細表示スイッチ。実行例は以下。(正常時)

$ sudo fsck -nv /dev/sdb1; echo $?
fsck from util-linux 2.36.1
fsck.fat 4.2 (2021-01-31)
Checking we can access the last sector of the filesystem
Boot sector contents:
System ID "MTOO4026"
Media byte 0xf0 (5.25" or 3.5" HD floppy)
       512 bytes per logical sector
      8192 bytes per cluster
         1 reserved sector
First FAT starts at byte 512 (sector 1)
         2 FATs, 16 bit entries
    131072 bytes per FAT (= 256 sectors)
Root directory starts at byte 262656 (sector 513)
       512 root directory entries
Data area starts at byte 279040 (sector 545)
     65501 data clusters (536584192 bytes)
8192 sectors/track, 4 heads
         0 hidden sectors
   1048576 sectors total
Checking for unused clusters.
/dev/sdb1: 268 files, 18214/65501 clusters
0
$ sudo fsck -nv /dev/sdb2; echo $?
fsck from util-linux 2.36.1
e2fsck 1.46.2 (28-Feb-2021)
STORAGE: clean, 12/8192 files, 6938/32768 blocks
0

echo $?で明示的に戻り値も表示。0が返ってきているので正常終了。
もしdirty bitが設定されている場合はこれで表示され確認できる。その場合は改めてfsckをかけ、dirty bitをクリアすること。
また、mountされている状態でバックアップを取るとベリファイで異なる結果となり無駄に悩むことになるので、umountしてから行うこと。

$ sudo umount /dev/sdb1
$ sudo umount /dev/sdb2

余談

dd (UNIX) - Wikipedia

dataset definitionの略であるが、IBMのメインフレームのJob Control Language(ジョブ制御言語、JCL)の「DD文」(DD statement)に由来するため、引数の構文が、Unixの一般的なコマンドの引数のそれとは激しく異なっている(datasetというのはメインフレーム用語)。

なるほどねぇ。ddコマンドの引数構文の異質さ気持ち悪さはこれが原因なのか。

4
2
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?