技術屋卵の足跡

いつか振り返ったときにどれだけ這いずり回ったかを確認したい

ddでサイズの違うNTFSパーティションをクローンして失敗した話

256GB SSD x2から1TB NVME SSD x1に変更したときの失敗談。

初めの状態

最初にPCを組んだ時に手元にWindowsがなかった関係で、とりあえずWindows Insider Previewでも入れてみるかとなったのがすべての始まり。 そこに後で購入したWindowsをインストールしたら、次のような地獄のディスクマップになっていた。

SSD 1SSD 2RecoveryRecoveryEFIWindows10 Insider PreviewWindows10

Windowsをインストールしたときに新しいSSD挿すから全部良しなにやってくれとインストーラーにぶん投げていたので、こんなことになっていたことに今更気が付いた。

問題点

やったこと

Gentooのインストールメディアは有事に備えて常に準備しているので、こいつでddを使った。

arch wikiをベースに

dd if=/dev/sda of=/dev/nvme0n1 bs=64K conv=noerror,sync status=progress

といった感じで、SSD1の内容をNVME SSDにクローンして partedでリカバリーを削除、Insiderのパーティションを1TBまで拡張して

dd if=/dev/sdb1 of=/dev/nvme0n1p3 bs=64K conv=noerror,sync status=progress

ddでWindowsパーティションのクローンを行った。 このままだと256GBのパーティションの情報のままなので、partedでパーティションを拡張してディスク周りは完了。

パーティション周りを更新したので、最後にWindowsのインストールメディアからbootrecなりbcdeditEFIのブートに関する情報を更新した。

結果

1TBが256GB程度と認識される。

  • Windowsのディスクマネージャーやpartedで見てみると1TBのパーティションと認識
  • mountしてdfWindowsエクスプローラーから見ると256GBしか使えない模様
  • ディスクマネージャー側では正しく認識しているので、再度拡張しようとしてもうまくいかない

直し方

結局はフォーマットしなおして、ファイルベースのコピーをしたので、ここからは参考にならない情報であることを先に宣言します

ソフトウェアによって出ている情報が異なるので、ドライブ内部でデータの不整合が生じているっぽいと考え、GPTテーブル(ディスク単位での情報)とNTFSのboot sector(パーティション単位での情報)の内容を比較してみた。

HxDなどのバイナリエディタでディスクを開いてみる。

f:id:pfpfdev:20200316125411p:plain
GPTテーブル上のパーティション情報

Wikipediaの情報をもとにこれを解読すると、LBAは[0xFF, 0x67, 0x70, 0x74](リトルエンディアン)を指しているので、おおよそ1TBの容量を持っている。

しかしながらNTFSのboot sectorでは、ここの情報を基に解読するとTotal Sectorsが[0xFF, 0xB7, 0xF1, 0x1B]となっており、256GB程度の容量となっていた。 Checksumは0埋めされていたので、ここのTotal Sectorsを正しく書き換えればうまくいきそうという指針が立つ。

GPTテーブルから計算したTotal Sectorsは0x745d27ffであるので、これをリトルエンディアンで書き込めばよさげである。

とここまで計算したが、Windows側からだと起動ドライブに対するRawな書き込みができず、Gentooのインストールメディアにはいい感じのディスクエディタが入っていなかったので、結局はmkfsrsyncでファイルベースのコピーをして直った。

まとめ

ddは同じサイズのパーティションに対して行うのが安全。

jupyterの布教

Sector数とかバイト数とか1TBオーダーだと平気でint64の範囲を超えてくるので、Excelなどの普通の電卓だと計算できなくなる。 pythonならデフォルトで多倍長整数だし、10進数と16進数の変換がとっても楽なので、こういう計算がしやすい。

jupyter notebookだと、インラインで計算しつつメモもとれるので、こんな感じの計算にピッタリだった。

参考程度に今回使ったjupyter notebookのmarkdown形式のexportを以下に載せてみる。


# 今回使用するクラス


class Sector:
    # addr:リトルエンディアンのHexArray
    def __init__(self, addr):
        self.addr = 0
        for i, h in enumerate(addr):
            self.addr += 256**(i)*h

    # バイナリエディタのジャンプ機能で必要
    def offset(self):
        return hex(self.addr*0x200)


class Partition:
    def __init__(self, cur, next):
        self.current = cur
        self.next = next
# GPTテーブルについて
# 値が正しく計算できているか検証をした
Primary = Partition(Sector([0x1]), Sector([0xAF, 0x6D, 0x70, 0x74]))

# 計算結果のoffsetにジャンプしてGPTセカンダリテーブルがあったら正しく計算できている
print(Primary.next.offset())
0xe8e0db5e00
# GPTテーブルから読み取ったData partitionの情報
Data = Partition(Sector([0x00, 0x40, 0x13]), Sector([0xFF, 0x67, 0x70, 0x74]))

print(Data.current.offset())
# 容量
print((Data.next.addr - Data.current.addr)*0x200/2**30, "GiB")
0x26800000
930.9111323356628 GiB
# もともとのTotal Sectors
original = Sector([0xff, 0xb7, 0xf1, 0x1b])
# 容量を計算してみる
print(original.addr*0x200/2**30, "GiB")
# GPT側から計算したセクタの数
print(hex(Data.next.addr - Data.current.addr))
# よって書き換えるべきセクタ数は
print("ff 27 5d 74 00 00 00 00")
223.55371046066284 GiB
0x745d27ff
ff 27 5d 74 00 00 00 00