虎之助の徒然記

GPTテーブルを読み取るプログラムを作って、Windowsディスクを調べた(in Python)

【概要】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)
MBRの第1パーティションの情報 (500GBのSSDの場合)。

 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)

関連記事

付録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)やその英語版に解説があります。これを参考にしました。