【概要】GPTテーブルを読み取って、テキストで出力する Python プログラムを作りました。gdisk ではテキスト出力できない GPT ヘッダの位置を読み出す目的で作りましたが、GPT テーブル内に入っている他の情報もすべて出力します。このプログラムを使って、Windows でディスクのパーティションテーブルが正常に設定されているか、調査しました。
1. はじめに
クローンディスクを作っていて気になったのが、Windowsでは、GUIDパーティションテーブル (GUID Partition Talbe; GPT) の第2GPTヘッダを正しい位置、つまり、ディスクの末尾に配置しているか、という点です(関連記事は、ここ)。
Windowsで作ったディスクを gdisk で覗くと、次のエラーメッセージが出力されます。
The protective MBR's 0xEE partition is oversized! Auto-repairing.
gdisk のソースコードを読めばいいのかもしれませんが、この原因をよく理解していませんでした。パーティションサイズの認識に何らかの異常があるのですが、このエラー自体は深刻なものではなく、自動修復しても構わない程度のもののようです*1。
最初に疑ったのは、Windows。Windowsでは認識したことになっているディスクサイズが実際のディスクサイズと違うのではないか?その影響が、先のエラーメッセージとなって現れたと推測しました。このことを確認する一つの方法が、第2GPTヘッダの位置が適切な場所、つまり、ディスク末尾に置かれているか、ということです。
GPTテーブルを読み取るプログラムは、このような背景から生まれました。
なお、使用した言語は、Pythonです。Python 初心者なので、変なところがあると思います。ご指導・ご鞭撻のほどよろしくお願い致します。
2. Pythonプログラム
2.1 仕様
GPTヘッダの情報を全てテキストで出力します。入力は、dd ダンプしたハードディスクイメージか、gdisk で保存したGPTデータです。
gdisk の CUI で取り出せない主なデータは、第1と第2のGPTヘッダの位置情報とCRCの値、MBRぐらいです。それ以外の情報は、gdiskでもほぼすべてテキスト出力できます。
今回作った read-gpt-table.py では、gdisk でテキスト出力できない GPTヘッダの位置情報を含めて、すべてのデータを出力します。
目的や入出力が異なりますが、GPTテーブルを読み出すCのプログラムもあります。今回の目的には合致しませんでしたが、参考にさせて頂きました。 ● GUID Partition Table を読む - jou4のブログ |
2.2 コード
GPTテーブルを読み取るためのプログラム read-gpt-table.py を付録A につけておきます。使い方は、次の通りです。
$ read-gpt-table.py Usage: read-gpt-table.py file [option] Read GPT table from file. Option: --dd : file is a dd-dumped image. (default) --gdisk : file is a GPT table saved by gdisk. Caution: gdisk may repair GPT header automatically. To get GPT header as is, use dd-dumped image.
注にも書きましたが、gdiskでは、自動的にGPTヘッダを修復してしまうことがあるので、変更を加えないそのままのGPTヘッダを読み取りたい場合には、ddダンプしたファイルを使った方がよいです。
なお、動作確認は、ubuntu/lubuntu 18.04 の pyhonの3.6.6で行っています。
2.3 使い方
● ddダンプしたファイルの場合
$ dd if=/dev/sdX of=foo.img $ read-gpt-table.py foo.img --dd
● gdiskのGPTデータの場合
$ gdisk /dev/sdX ( command 'b' を実行し、foo.gpt に保存 ) $ read-gpt-table.py foo.gpt --gdisk
3. WindowsのGPTテーブルを調べた
3.1 gdiskで保存されるGPTテーブル
GPTテーブルのほとんどの情報は、gdiskのCUIから読み出すことができます。しかし、筆者が知りたかったGPTヘッダの位置情報については、CUIからは調べることはできません。
一方、gdiskのコマンド 'b' によって保存されたバイナリのファイルには、GPTテーブルのすべての情報が保存されますので *2、このバイナリファイルを解析することで、知りたい情報を得ることができます。
このデータファイルの構造は、下記の計35セクタ ( =17,920 バイト)です。
- [MBR(1), 第1GPTヘッダ(1), 第2GPTヘッダ(1), パーティションエントリー(32)]
ddダンプしたファイルでは、ファイルの先頭に [MBR, 第1GPTヘッダ, パーティションエントリー]の計34セクタが配置され、第2GPTヘッダやパーティションエントリーのコピーはディスク末尾に配置されますので、プログラミングの際には、少々、注意を払う必要があります。 |
但し、gdiskが保存するGPTデータは、メモリ上に展開されたパーティション情報です。gdiskのマニュアルには、バックアップコマンド b について以下の説明があります。
Save partition data to a backup file. You can back up your current in-memory partition table to a disk file using this option. The resulting file is a binary file consisting of the protective MBR, the main GPT header, the backup GPT header, and one copy of the partition table, in that order. Note that the backup is of the current in-memory data structures, so if you launch the program, make changes, and then use this option, the backup will reflect your changes.
この説明を読むと、前述の "Auto-repairing" が実施された場合にも、自動修復された状態のGPTテーブルがファイルに出力されそうです。
3.2 WindowsのGPTテーブルに異常なし
次の二つのディスクについて、GPTテーブルを調査しました。
- Windowsシステムが入った1TBのHDD
- Windowsシステムが入った500GBのSSD
プログラムの出力例を付録Bに示します。
HDDの場合でも、SSDの場合でも、ddダンプしたファイルから読み取ったGPTテーブルと、gdiskのGPTデータから読み取ったGPTテーブルとの間では、MBRの極一部を除き同一でした。
一番気になっていた第2GPTの位置ですが、ディスク末尾の最終セクタへのオフセット値を示していて、正常です。また、ddダンプしたディスクイメージの末尾の1セクタ(512バイト)から読み込んだ第2GPTテーブルにも異常は見られませんでした。
Windowsが作ったパーティションテーブルだからと言って、GPTについては、おかしなことをやっているわけではなさそうです。
但し、「GPTについては」です。
3.3 WindowsのMBRはいい加減
gdiskの結果とddの結果で異なる部分は、MBRの部分でした。これは、MBRの第1パーティションの全セクタ数で、例えば、SSDの場合には以下となります。
バイト数 | ddの値 (修復前) | gdiskの値 (修復後) | |
ブートフラグ | 1 | 0x 00 | ← |
最初のセクタ(CHS方式) | 3 | 0x 00 20 00 | ← |
パーティション識別子 | 1 | 0x EE (=GPT) | ← |
最後のセクタ(CHS方式) | 3 | 0x FF FF FF | ← |
最初のセクタ(LBA方式) | 4 | 0x 00 00 00 01 | ← |
パーティションの全セクタ数 | 4 | 0x FF FF FF FF | 0x 3A 38 60 2F (=976,773,167) |
Windowsの場合、パーティションの最初のセクタ数(LBA)は正しいようですが、パーティションの全セクタ数は実際の数字を反映せず、0xFFFFFFFFが設定されています。
gdiskは、パーティションサイズが0xFFFFFFFFというのは大きすぎる (The protective MBR's 0xEE partition is oversized!) と判定し、自動修復(Auto-repairing) して、GPTが管理するセクタ数(=ディスクの全セクタ数-1)をMBRの第1パーティションの全セクタ数として設定しているようです。HDDの場合も同様に修正されています。
Windowsとしては、パーティションサイズが-1 (0xFFFFFFFF) という設定は、サイズ不明ということなのでしょう。パーティションサイズを取得できないわけではないので、手抜き実装としか思えません。 |
CHS方式のセクタ数も不正確と思いますが、gdisk は修復していません。CHS方式でのディスク管理は廃れた方式なので、gdisk も相手にしなかったということでしょうね。
やはり、Windows。MBR については、いい加減な実装をしていました。
4. 結論
Windowsでは、第2GPTヘッダーが正しく配置されないという疑念がありましたが、特に問題なさそうです。
但し、WindowsのMBRのパーティションサイズは正しく入力されていません。gdiskでは、Windowsが設定した不適切なのMBRの値を自動的に正しい値に設定し直しているようです。
モヤモヤしていたことなので、疑念が晴れてスッキリしました。また、Pythonの勉強にもなりました。
(2018/9/24)
関連記事
- Dynabook AZ77のSSDへの換装 (5) gpartedでパーティションをいろいろリサイズした - 虎之助の徒然記
- クローンディスクの作り方 (1) Linuxの場合 - 虎之助の徒然記
- 【Python】numpy 整数の演算結果が float64 に。摩訶不思議なデータ型変換ルール。 - 虎之助の徒然記
付録A:GPTテーブルを読み取るPythonプログラム
ソースコードとサンプルデータは、ここ(Googleドライブ)に置いておきます。
#!/usr/bin/python3 # # read-gpt-table.py # # Read and print GPT partition table from a file saved with dd or gdisk. # # Copyright (C) 2018 Toranosuke Tenyu # # ### structure of GPT table in dd-dumped image ### # LBA0 is MBR # LBA1 is 1st GPT Header # LBA2-33 is partition entries (128 entries) # last LBA is 2nd GPT Header (the end of disk) # # see: https://en.wikipedia.org/wiki/GUID_Partition_Table ### structure of GPT table in GPT data saved with gdisk ### # # LBA0 is MBR # LBA1 is 1st GPT Header # LBA2 is 2nd GPT Header # LBA3-LBA34 is partition entries (128 entries) # # The size of GPT data by gdisk is 35 sectors (17920 bytes) # The size of LBA is 1 sector. import sys import numpy as np ### gpt header dtype (512 byte) gpt_dtype = np.dtype([ ('signature', 'S8'), ('revision', '>u4'), ('header_size', '<u4'), ('crc32_1', '<u4'), ('reserved_1', '<u4'), ('loc_1st_gpt', '<u8'), ('loc_2nd_gpt', '<u8'), ('first_usable_sector','<u8'), ('last_usable_sector', '<u8'), ('disk_guid', 'u1',16), ('starting_lba', '<u8'), ('num_of_entry', '<u4'), ('size_of_entry', '<u4'), ('crc32_2', '<u4'), ('reserved_2', 'u1',420) ]) ### read GPT Header def read_gpt(filename, offset=512): # open gpt file fp = open(filename,'rb') fp.seek(offset) # read GPT Header from file gpt = np.fromfile(fp,dtype=gpt_dtype,count=1) # close file fp.close() return gpt ### print GPT Header ### def print_gpt(gpt): print('signature : %s' %gpt['signature'].tostring().decode('ascii')) print('revision : 0x%08x'%gpt['revision'][0]) print('header size : %d'%gpt['header_size']) print('crc32(1) : %d'%gpt['crc32_1']) print('reserved(2) : %d'%gpt['reserved_1']) print('1st GPT location : %d'%gpt['loc_1st_gpt']) print('2nd GPT location : %d'%gpt['loc_2nd_gpt']) print('first usable sector : %d'%gpt['first_usable_sector']) print('last usable sector : %d'%gpt['last_usable_sector']) print('disk_guid : %s'%guid_toString(gpt['disk_guid'][0])) print('starting LBA : %d'%gpt['starting_lba']) print('number of entry : %d'%gpt['num_of_entry']) print('size of entry : %d'%gpt['size_of_entry']) print('crc32(2) : %d'%gpt['crc32_2']) # Convert binary GUID to string def guid_toString(guid): # print(guid) guid_ascii ='' # data1 (ulong) guid_ascii += f'%02X'%guid[3] guid_ascii += f'%02X'%guid[2] guid_ascii += f'%02X'%guid[1] guid_ascii += f'%02X'%guid[0] guid_ascii += '-' # data2 (ushort) guid_ascii += f'%02X'%guid[5] guid_ascii += f'%02X'%guid[4] guid_ascii += '-' # data3 (ushort) guid_ascii += f'%02X'%guid[7] guid_ascii += f'%02X'%guid[6] guid_ascii += '-' # data4 (uchar[8]) guid_ascii += f'%02X'%guid[8] guid_ascii += f'%02X'%guid[9] guid_ascii += '-' guid_ascii += f'%02X'%guid[10] guid_ascii += f'%02X'%guid[11] guid_ascii += f'%02X'%guid[12] guid_ascii += f'%02X'%guid[13] guid_ascii += f'%02X'%guid[14] guid_ascii += f'%02X'%guid[15] return guid_ascii ### partition entry (128 byte/entry) ### entry_dtype = np.dtype([ ('type_guid', 'u1',16), ('unique_guid', 'u1',16), ('first_lba', '<u8'), ('last_lba' , '<u8'), ('attribute_flag', '<u8'), ('partition_name', '<u2',36) ]) def read_entry(filename,offset=1024,npart=-1): # open file fp = open(filename,'rb') fp.seek(offset) # read partition entry from file entry = np.fromfile(fp,dtype=entry_dtype,count=npart) # close file fp.close() return entry def print_entry(entry): count = 0 for item in entry: if item['first_lba'] != 0 : print('partition type guid :', guid_toString(item['type_guid'])) print('unique partition guid :', guid_toString(item['unique_guid'])) print('first_lba :', item['first_lba']) print('last_lba :', item['last_lba']) print('attribute flags : 0x%016X'%item['attribute_flag']) print('partition name : \'{}\''.format(item['partition_name'].tostring().decode('utf-16-le').strip('\0'))) print('') count += 1 print('Number of valid entries = %d\n'%count) ### MBR ### def read_mbr(filename,offset=0): fp = open(filename,'rb') fp.seek(offset) mbr = np.fromfile(fp,dtype='<u1',count=512) fp.close() return mbr def print_mbr(mbr): count=1 for item in mbr : if item != 0 : print('%02X '%item,end='') else : print('-- ',end='') if count%16 == 0 : print('') count += 1 print(' ') def print_usage(): print('Usage: {} file [option]'.format(__file__)) print(' Read GPT table from file.') print(' Option:') print(' --dd : file is a dd-dumped image. (default)') print(' --gdisk : file is a GPT table saved by gdisk.') print('') print(' Caution:') print(' gdisk may repair GPT header automatically.') print(' To get GPT header as is, use dd-dumped image.') def args_parser(): args = sys.argv if (len(args) == 2 ): return 'dd' # read GPT table from dd-dump (default) if (len(args) == 3): if ( args[2] == '--gdisk' ): return 'gdisk' # read GPT table saved by gdisk if ( args[2] == '--dd' ): return 'dd' print_usage() sys.exit() if __name__ == '__main__': # set file_type = 'dd' or 'gdisk' file_type = args_parser() filename = sys.argv[1] bsize = 512 ### read & print GPT header if ( file_type == 'gdisk' ): # read from gdisk file print('### 1st GPT Header ###') gpt_header1 = read_gpt(filename,bsize) print_gpt(gpt_header1) print('') print('### 2nd GPT Header ###') gpt_header2 = read_gpt(filename,2*bsize) print_gpt(gpt_header2) print('') print('### Partition Entry ###') entry_list = read_entry(filename,3*bsize) print('Number of entries = %d\n'%entry_list.size) print_entry(entry_list) print('### MBR ###') mbr = read_mbr(filename,0) print_mbr(mbr) else: # read from dd-dump file print('### 1st GPT Header ###') gpt_header1 = read_gpt(filename,bsize) print_gpt(gpt_header1) print('') print('### 2nd GPT Header ###') # escape from converting into float64 uint64_512 = np.array([512],dtype=np.uint64) uint64_loc = gpt_header1['loc_2nd_gpt'][0]*uint64_512[0] gpt_header2 = read_gpt(filename,uint64_loc) print_gpt(gpt_header2) print('') print('### Partition Entry ###') entry_list = read_entry(filename,2*bsize,128) print('Number of entries = %d\n'%entry_list.size) print_entry(entry_list) print('### MBR ###') mbr = read_mbr(filename,0) print_mbr(mbr) sys.exit()
付録B:read-gpt-table.pyの実行結果
### 1st GPT Header ### signature : EFI PART revision : 0x00000100 header size : 92 crc32(1) : 2997411310 reserved(2) : 0 1st GPT location : 1 2nd GPT location : 1953525167 first usable sector : 34 last usable sector : 1953525134 disk_guid : B45D2A5C-F9A4-478D-818B-D8E6280ABEA4 starting LBA : 2 number of entry : 128 size of entry : 128 crc32(2) : 3723135598 ### 2nd GPT Header ### signature : EFI PART revision : 0x00000100 header size : 92 crc32(1) : 288711948 reserved(2) : 0 1st GPT location : 1953525167 2nd GPT location : 1 first usable sector : 34 last usable sector : 1953525134 disk_guid : B45D2A5C-F9A4-478D-818B-D8E6280ABEA4 starting LBA : 1953525135 number of entry : 128 size of entry : 128 crc32(2) : 3723135598 ### Partition Entry ### Number of entries = 128 partition type guid : C12A7328-F81F-11D2-BA4B-00A0C93EC93B unique partition guid : CC2D4D60-A1FB-4D4E-9542-C603A9BA1CCD first_lba : 2048 last_lba : 534527 attribute flags : 0x8000000000000000 partition name : 'EFI system partition' partition type guid : E3C9E316-0B5C-4DB8-817D-F92DF00215AE unique partition guid : 91297EAC-45C5-4BDC-8F28-CC0978A7084F first_lba : 534528 last_lba : 567295 attribute flags : 0x8000000000000000 partition name : 'Microsoft reserved partition' partition type guid : EBD0A0A2-B9E5-4433-87C0-68B6B72699C7 unique partition guid : 79886FBF-71C1-4E37-9922-A2C336CA8720 first_lba : 567296 last_lba : 1926947658 attribute flags : 0x0000000000000000 partition name : 'Basic data partition' partition type guid : DE94BBA4-06D1-4D40-A16A-BFD50179D6AC unique partition guid : 6ABF2C3A-3420-48A2-968D-5F3FCBA7EF1B first_lba : 1926948864 last_lba : 1929021439 attribute flags : 0x8000000000000001 partition name : '' partition type guid : DE94BBA4-06D1-4D40-A16A-BFD50179D6AC unique partition guid : 932D4253-8484-440A-856C-2A8E03037CBE first_lba : 1929022863 last_lba : 1953525134 attribute flags : 0x8000000000000001 partition name : 'Basic data partition' Number of valid entries = 5 ### MBR ### -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 02 -- EE FF FF FF 01 -- -- -- FF FF FF FF -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 55 AA
*1:"The oversized 0xEE partition ...(snip)... could indicate something strange to do with disk size detection." (Rod Smith, gdiskの開発者)
https://superuser.com/questions/1023343/after-upgrading-from-windows-7-to-windows-10-system-thinks-gpt-partition-is-mbr#comment1424307_1023702
*2: GPTテーブルの構造については、「GUIパーティションテーブル」(wikipedia)やその英語版に解説があります。これを参考にしました。